@yielded/agent-storage-cloudflare 0.1.0-beta.166

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 (77) hide show
  1. package/LICENSE +21 -0
  2. package/dist/DoMemoryStore.d.mts +30 -0
  3. package/dist/DoMemoryStore.mjs +66 -0
  4. package/dist/DoMemoryStore.mjs.map +1 -0
  5. package/dist/DoMessageDeliveryStore.d.mts +17 -0
  6. package/dist/DoMessageDeliveryStore.mjs +30 -0
  7. package/dist/DoMessageDeliveryStore.mjs.map +1 -0
  8. package/dist/DoScheduleStore.d.mts +36 -0
  9. package/dist/DoScheduleStore.mjs +388 -0
  10. package/dist/DoScheduleStore.mjs.map +1 -0
  11. package/dist/DoStorageConfig.d.mts +45 -0
  12. package/dist/DoStorageConfig.mjs +52 -0
  13. package/dist/DoStorageConfig.mjs.map +1 -0
  14. package/dist/DoStorageError-CGuAfSwF.d.mts +101 -0
  15. package/dist/DoStorageError.d.mts +2 -0
  16. package/dist/DoStorageError.mjs +167 -0
  17. package/dist/DoStorageError.mjs.map +1 -0
  18. package/dist/DoStorageFailpoint.d.mts +17 -0
  19. package/dist/DoStorageFailpoint.mjs +14 -0
  20. package/dist/DoStorageFailpoint.mjs.map +1 -0
  21. package/dist/DoStorageFailpointTesting.d.mts +24 -0
  22. package/dist/DoStorageFailpointTesting.mjs +22 -0
  23. package/dist/DoStorageFailpointTesting.mjs.map +1 -0
  24. package/dist/DoStorageVersion-iNSyi3cJ.d.mts +12 -0
  25. package/dist/DoStorageVersion.d.mts +2 -0
  26. package/dist/DoStorageVersion.mjs +8 -0
  27. package/dist/DoStorageVersion.mjs.map +1 -0
  28. package/dist/DoSubmissionLedger.d.mts +23 -0
  29. package/dist/DoSubmissionLedger.mjs +1979 -0
  30. package/dist/DoSubmissionLedger.mjs.map +1 -0
  31. package/dist/DoSubscriptionStore.d.mts +25 -0
  32. package/dist/DoSubscriptionStore.mjs +186 -0
  33. package/dist/DoSubscriptionStore.mjs.map +1 -0
  34. package/dist/DoThreadStore-D_83OjCH.d.mts +72 -0
  35. package/dist/DoThreadStore.d.mts +2 -0
  36. package/dist/DoThreadStore.mjs +529 -0
  37. package/dist/DoThreadStore.mjs.map +1 -0
  38. package/dist/MemoryProtocol.d.mts +38160 -0
  39. package/dist/MemoryProtocol.mjs +219 -0
  40. package/dist/MemoryProtocol.mjs.map +1 -0
  41. package/dist/PortProtocol.d.mts +7726 -0
  42. package/dist/PortProtocol.mjs +234 -0
  43. package/dist/PortProtocol.mjs.map +1 -0
  44. package/dist/PortRouting.d.mts +97 -0
  45. package/dist/PortRouting.mjs +544 -0
  46. package/dist/PortRouting.mjs.map +1 -0
  47. package/dist/do-journal-DUhjZkOf.mjs +1352 -0
  48. package/dist/do-journal-DUhjZkOf.mjs.map +1 -0
  49. package/dist/index.d.mts +14 -0
  50. package/dist/index.mjs +14 -0
  51. package/dist/migrations-DtKz_Gxr.mjs +306 -0
  52. package/dist/migrations-DtKz_Gxr.mjs.map +1 -0
  53. package/dist/owned-state-DHm2aRXx.mjs +119 -0
  54. package/dist/owned-state-DHm2aRXx.mjs.map +1 -0
  55. package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
  56. package/package.json +1 -0
  57. package/src/DoMemoryStore.ts +73 -0
  58. package/src/DoMessageDeliveryStore.ts +39 -0
  59. package/src/DoScheduleStore.ts +622 -0
  60. package/src/DoStorageConfig.ts +53 -0
  61. package/src/DoStorageError.ts +180 -0
  62. package/src/DoStorageFailpoint.ts +20 -0
  63. package/src/DoStorageFailpointTesting.ts +41 -0
  64. package/src/DoStorageVersion.ts +2 -0
  65. package/src/DoSubmissionLedger.ts +4027 -0
  66. package/src/DoSubscriptionStore.ts +327 -0
  67. package/src/DoThreadStore.ts +1121 -0
  68. package/src/MemoryProtocol.ts +355 -0
  69. package/src/PortProtocol.ts +436 -0
  70. package/src/PortRouting.ts +1464 -0
  71. package/src/index.ts +13 -0
  72. package/src/internal/do-journal.ts +2282 -0
  73. package/src/internal/message-delivery-schema.ts +24 -0
  74. package/src/internal/migrations.ts +304 -0
  75. package/src/internal/owned-state.ts +228 -0
  76. package/src/internal/recovery-checkpoint-schema.ts +17 -0
  77. package/src/internal/storage-span.ts +44 -0
@@ -0,0 +1,1464 @@
1
+ import type { DurableRuntimeFailpoint } from "@yielded/agent/durable-failpoint";
2
+ import { LifecyclePublicationError } from "@yielded/agent/lifecycle-publication";
3
+ import { MessageDeliveryStore, MessageDeliveryError } from "@yielded/agent/message-delivery";
4
+ import {
5
+ AdmissionIndeterminate,
6
+ AdmissionConflict,
7
+ AdmissionPolicyError,
8
+ ChildAttachmentSnapshot,
9
+ JoinedToHost,
10
+ LedgerError,
11
+ RecoverySnapshot,
12
+ SettlementConflict,
13
+ SubmissionLedger,
14
+ SubmissionLookupById,
15
+ type SubmissionLookupByKey,
16
+ type SubmissionSnapshot,
17
+ } from "@yielded/agent/submission-ledger";
18
+ import {
19
+ AppendConflict,
20
+ ThreadMaterialization,
21
+ ThreadNotMaterialized,
22
+ ThreadStore,
23
+ ThreadStoreError,
24
+ FenceRejected,
25
+ } from "@yielded/agent/thread-store";
26
+ import type { WakeScheduler } from "@yielded/agent/wake-scheduler";
27
+ import {
28
+ admitWorker,
29
+ WorkerAdmissionPort,
30
+ WorkerAdmissionFailure,
31
+ } from "@yielded/agent/worker-admission";
32
+ import { Cause, Clock, Context, Effect, Layer, Option, Predicate, Schema, Stream } from "effect";
33
+
34
+ import {
35
+ WorkerAdmitCall,
36
+ WorkerAdmitResult,
37
+ MessageDeliveryListCall,
38
+ MessageDeliveryListResult,
39
+ MessageDeliveryCompleteCall,
40
+ MessageDeliveryCompleteResult,
41
+ boundPortDiagnostic,
42
+ decodePortRequest,
43
+ decodePortResponse,
44
+ encodePortRequest,
45
+ encodePortResponse,
46
+ LedgerAdmitCall,
47
+ LedgerAdmitResult,
48
+ LedgerLookupCall,
49
+ LedgerLookupResult,
50
+ LedgerMarkReadyCall,
51
+ LedgerMarkReadyResult,
52
+ LedgerRecordChildSettledCall,
53
+ LedgerRecordChildSettledResult,
54
+ LedgerRequestAbortCall,
55
+ LedgerStopWorkerCall,
56
+ LedgerInspectWorkerCall,
57
+ LedgerInspectWorkerResult,
58
+ LedgerStopWorkerResult,
59
+ LedgerRequestAbortResult,
60
+ LedgerResolveAdmissionCall,
61
+ LedgerResolveAdmissionResult,
62
+ PortFailed,
63
+ PortProtocolError,
64
+ PortSucceeded,
65
+ StoreAppendCall,
66
+ StoreAppendResult,
67
+ StoreCountPeerMessagesCall,
68
+ StoreCountPeerMessagesResult,
69
+ StoreExportCall,
70
+ StoreExportResult,
71
+ StoreInspectTailCall,
72
+ StoreInspectTailResult,
73
+ StoreReadIdentityCall,
74
+ StoreReadIdentityResult,
75
+ StoreMaterializeCall,
76
+ StoreMaterializeResult,
77
+ StoreReadPageCall,
78
+ StoreReadPageResult,
79
+ type PortFailure,
80
+ type PortRequest,
81
+ type PortRequestEnvelope,
82
+ type PortResponse,
83
+ type PortResult,
84
+ } from "./PortProtocol.ts";
85
+
86
+ type ThreadId = ThreadMaterialization["threadId"];
87
+ type SubmissionId = SubmissionSnapshot["submissionId"];
88
+
89
+ const ThreadIdSchema = ThreadMaterialization.fields.threadId;
90
+ const decodeThreadId = Schema.decodeUnknownEffect(ThreadIdSchema);
91
+
92
+ /**
93
+ * The ledger row bound routable Submission identities must respect (mirrors the local
94
+ * facet's `MAX_IDENTIFIER_LENGTH`; the minting side already refuses longer identities typed
95
+ * at admission, so a longer identity presented here cannot name any stored row).
96
+ */
97
+ const MAX_ROUTABLE_SUBMISSION_ID_LENGTH = 1_024;
98
+
99
+ /** The `{uuidv7}` head of a DC-minted routable Submission identity. */
100
+ const UUID_HEAD_PATTERN =
101
+ /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
102
+
103
+ /**
104
+ * A transport could not deliver a port request to (or an answer from) the owning
105
+ * Thread's Durable Object. `retryable` carries the platform's own stub signal when one
106
+ * exists. This error never crosses the wire — it is the CALLER-side evidence that the
107
+ * authority was unreachable, which is exactly the case `AdmissionIndeterminate` was
108
+ * specified for (SUB-031).
109
+ */
110
+ export class PortTransportError extends Schema.TaggedError<PortTransportError>()(
111
+ "PortTransportError",
112
+ {
113
+ target: Schema.String,
114
+ message: Schema.String,
115
+ retryable: Schema.optionalKey(Schema.Boolean),
116
+ cause: Schema.optionalKey(Schema.Defect()),
117
+ },
118
+ ) {}
119
+
120
+ const safeTransportDiagnostic = (cause: unknown): string => {
121
+ try {
122
+ const message = cause instanceof Error ? cause.message : cause;
123
+
124
+ return boundPortDiagnostic(typeof message === "string" ? message : String(message));
125
+ } catch {
126
+ return "[unavailable transport diagnostic]";
127
+ }
128
+ };
129
+
130
+ const transportRetryableSignal = (cause: unknown): boolean | undefined => {
131
+ if (!Predicate.isObjectKeyword(cause)) return undefined;
132
+ try {
133
+ const signal = Reflect.get(cause, "retryable");
134
+
135
+ return typeof signal === "boolean" ? signal : undefined;
136
+ } catch {
137
+ return undefined;
138
+ }
139
+ };
140
+
141
+ /**
142
+ * Build a `PortTransportError` from an arbitrary thrown transport cause, preserving the
143
+ * platform stub's own `retryable` signal when present.
144
+ */
145
+ export const portTransportFailure = (target: string, cause: unknown): PortTransportError => {
146
+ const retryable = transportRetryableSignal(cause);
147
+
148
+ return PortTransportError.make({
149
+ target,
150
+ message: safeTransportDiagnostic(cause),
151
+ ...(retryable === undefined ? {} : { retryable }),
152
+ cause,
153
+ });
154
+ };
155
+
156
+ /**
157
+ * Delivery of Schema-encoded port envelopes to the Durable Object that owns a FOREIGN
158
+ * Thread (plan §1.3, D-P6-3). The shipped implementation (platform-cloudflare, WP3)
159
+ * calls the owner's `portCall` over native Durable Object JS RPC via
160
+ * `namespace.idFromName(threadId)`; the protocol is transport-agnostic and any carrier
161
+ * that moves the encoded envelopes verbatim satisfies this service. Implementations MUST
162
+ * surface every delivery problem as `PortTransportError` and must never fabricate an answer.
163
+ */
164
+ export class ThreadPortTransport extends Context.Service<
165
+ ThreadPortTransport,
166
+ {
167
+ readonly call: (
168
+ threadId: ThreadId,
169
+ request: PortRequestEnvelope,
170
+ ) => Effect.Effect<unknown, PortTransportError>;
171
+ }
172
+ >()("@effect-agent/storage-cloudflare/ThreadPortTransport") {}
173
+
174
+ /** Construction options shared by both routed port Layers. */
175
+ export interface RoutedPortOptions {
176
+ /**
177
+ * Decide whether this physical owner stores the addressed logical Thread. Local
178
+ * requests use the same SQLite facets; foreign requests use the transport or fail
179
+ * fast typed. The predicate determines placement, never caller authorization.
180
+ */
181
+ readonly ownsThread: (threadId: ThreadId) => boolean;
182
+ }
183
+
184
+ /** Where one port request must execute. */
185
+ type RouteTarget =
186
+ | { readonly _tag: "local" }
187
+ | { readonly _tag: "foreign"; readonly threadId: ThreadId };
188
+
189
+ const LOCAL: RouteTarget = { _tag: "local" };
190
+
191
+ /**
192
+ * Parse a DC-minted routable Submission identity — `{uuidv7}:{threadId}`, split at the
193
+ * FIRST `:` because the Thread tail may itself contain colons (D-P6-5). This adapter
194
+ * minted the format at admission and is the ONLY component that parses it; identities that do
195
+ * not carry the minted shape (no separator, non-UUID head, empty tail) fall back to the local
196
+ * facet, which is the only authority this Object can consult without inventing an owner.
197
+ * Identities beyond the ledger's 1,024-character row bound fail typed: the minting side
198
+ * refused them at admission, so they cannot name any stored row anywhere.
199
+ */
200
+ const routableSubmissionTarget = (
201
+ ownsThread: RoutedPortOptions["ownsThread"],
202
+ ): ((operation: string, submissionId: string) => Effect.Effect<RouteTarget, LedgerError>) =>
203
+ Effect.fn("DoPortRouting.routableSubmissionTarget")(function* (
204
+ operation: string,
205
+ submissionId: string,
206
+ ): Effect.fn.Return<RouteTarget, LedgerError> {
207
+ if (submissionId.length > MAX_ROUTABLE_SUBMISSION_ID_LENGTH) {
208
+ return yield* LedgerError.make({
209
+ operation,
210
+ message:
211
+ `A Submission identity of ${submissionId.length} characters exceeds the ` +
212
+ `${MAX_ROUTABLE_SUBMISSION_ID_LENGTH}-character routable identity bound; admission ` +
213
+ "refuses such identities, so it cannot name any stored row.",
214
+ });
215
+ }
216
+ const separator = submissionId.indexOf(":");
217
+
218
+ if (separator === -1) return LOCAL;
219
+ if (!UUID_HEAD_PATTERN.test(submissionId.slice(0, separator))) return LOCAL;
220
+ const tail = submissionId.slice(separator + 1);
221
+
222
+ return yield* decodeThreadId(tail).pipe(
223
+ Effect.map((threadId): RouteTarget =>
224
+ ownsThread(threadId) ? LOCAL : { _tag: "foreign", threadId },
225
+ ),
226
+ Effect.orElseSucceed(() => LOCAL),
227
+ );
228
+ });
229
+
230
+ /**
231
+ * Read a Submission only from this physical owner. Encoded foreign identities fail before
232
+ * storage access; opaque identities still use local lookup. A returned row must also belong
233
+ * to an owned Thread. This is placement validation, not caller authorization.
234
+ */
235
+ export const makeLocalSubmissionLookup = (
236
+ options: RoutedPortOptions,
237
+ ): ((
238
+ submissionId: SubmissionId,
239
+ ) => Effect.Effect<Option.Option<SubmissionSnapshot>, LedgerError, SubmissionLedger>) => {
240
+ const submissionTarget = routableSubmissionTarget(options.ownsThread);
241
+
242
+ return Effect.fn("DoPortRouting.lookupLocalSubmission")(function* (submissionId: SubmissionId) {
243
+ const operation = "local Submission lookup";
244
+ const target = yield* submissionTarget(operation, submissionId);
245
+
246
+ if (target._tag === "foreign")
247
+ return yield* LedgerError.make({
248
+ operation,
249
+ message: "The Submission belongs to another Thread Object",
250
+ });
251
+
252
+ const local = yield* SubmissionLedger;
253
+ const submission = yield* local.lookup(SubmissionLookupById.make({ submissionId }));
254
+
255
+ if (Option.isSome(submission) && !options.ownsThread(submission.value.threadId))
256
+ return yield* LedgerError.make({
257
+ operation,
258
+ message: "The stored Submission belongs to another Thread Object",
259
+ });
260
+
261
+ return submission;
262
+ });
263
+ };
264
+
265
+ const NoAdditionalPortFailure = Schema.Never;
266
+ const AbortPortFailure = Schema.Union([SettlementConflict, JoinedToHost]);
267
+ const AppendPortFailure = Schema.Union([ThreadNotMaterialized, AppendConflict, FenceRejected]);
268
+
269
+ /**
270
+ * The fail-fast refusal for any foreign operation OUTSIDE the closed route-capable subset
271
+ * (plan §1.3): honesty over accidental distribution.
272
+ */
273
+ const crossThreadLedgerError = (operation: string, target: string): LedgerError =>
274
+ LedgerError.make({
275
+ operation,
276
+ message:
277
+ `${operation} addressed to foreign Thread ${target} is not route-capable; the ` +
278
+ "closed cross-Object subset is admit, markReady, lookup, resolveAdmission, " +
279
+ "requestAbort, and recordChildSettled. Every other ledger operation is lane-local by " +
280
+ "construction and must execute inside the owning Thread's Durable Object.",
281
+ });
282
+
283
+ const crossThreadStoreError = (operation: string, target: string): ThreadStoreError =>
284
+ ThreadStoreError.make({
285
+ operation,
286
+ message:
287
+ `${operation} addressed to foreign Thread ${target} is not route-capable; the ` +
288
+ "closed cross-Object subset is materialize, append, read (paged), inspectTail, and " +
289
+ "export. Observation and checkpoints are lane-local by construction and must execute " +
290
+ "inside the owning Thread's Durable Object.",
291
+ });
292
+
293
+ const makeTransportCall = (transport: ThreadPortTransport["Service"]) =>
294
+ Effect.fn("DoPortRouting.transportCall")(function* (target: ThreadId, call: PortRequest) {
295
+ const encoded = yield* encodePortRequest(call).pipe(
296
+ Effect.mapError((error) =>
297
+ PortProtocolError.make({
298
+ message: boundPortDiagnostic(`The port request could not be encoded: ${error.message}`),
299
+ }),
300
+ ),
301
+ );
302
+
303
+ const raw = yield* transport.call(target, encoded);
304
+
305
+ return yield* decodePortResponse(raw).pipe(
306
+ Effect.mapError((error) =>
307
+ PortProtocolError.make({
308
+ message: boundPortDiagnostic(`The port response could not be decoded: ${error.message}`),
309
+ }),
310
+ ),
311
+ );
312
+ });
313
+
314
+ type TransportCall = ReturnType<typeof makeTransportCall>;
315
+
316
+ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServices")(function* (
317
+ options: RoutedPortOptions,
318
+ ) {
319
+ const local = yield* SubmissionLedger;
320
+ const transport = yield* ThreadPortTransport;
321
+ const transportCall: TransportCall = makeTransportCall(transport);
322
+ const submissionTarget = routableSubmissionTarget(options.ownsThread);
323
+
324
+ const routeFailure =
325
+ (operation: string, target: string) =>
326
+ (error: PortTransportError | PortProtocolError): LedgerError =>
327
+ LedgerError.make({
328
+ operation,
329
+ message: boundPortDiagnostic(
330
+ `Routed ${operation} to the Thread Object owning ${target} failed: ${error.message}`,
331
+ ),
332
+ cause: error,
333
+ });
334
+
335
+ /**
336
+ * One routed ledger call: encode, deliver, decode, then narrow the uniform envelope to the
337
+ * operation's own result and failure surface. A foreign `LedgerError` is always in-channel;
338
+ * any failure outside the operation's declared surface — including protocol anomalies — is
339
+ * folded into a `LedgerError` naming the anomaly instead of being erased or re-thrown raw.
340
+ */
341
+ const foreignLedgerCall = <ResultSchema extends Schema.Top, FailureSchema extends Schema.Top>(
342
+ operation: string,
343
+ target: ThreadId,
344
+ call: PortRequest,
345
+ resultSchema: ResultSchema,
346
+ failureSchema: FailureSchema,
347
+ ): Effect.Effect<ResultSchema["Type"], FailureSchema["Type"] | LedgerError> => {
348
+ const isExpectedResult = Schema.is(resultSchema);
349
+ const isExpectedFailure = Schema.is(failureSchema);
350
+
351
+ return transportCall(target, call).pipe(
352
+ Effect.mapError(routeFailure(operation, target)),
353
+ Effect.flatMap(
354
+ (response): Effect.Effect<ResultSchema["Type"], FailureSchema["Type"] | LedgerError> => {
355
+ if (response._tag === "PortFailed") {
356
+ const failure = response.failure;
357
+
358
+ if (isExpectedFailure(failure)) return Effect.fail(failure);
359
+ if (failure._tag === "LedgerError") return Effect.fail(failure);
360
+
361
+ return Effect.fail(
362
+ LedgerError.make({
363
+ operation,
364
+ message: boundPortDiagnostic(
365
+ `The Thread Object owning ${target} answered ${operation} with the ` +
366
+ `out-of-contract failure ${failure._tag}`,
367
+ ),
368
+ cause: failure,
369
+ }),
370
+ );
371
+ }
372
+ const result = response.result;
373
+
374
+ if (!isExpectedResult(result)) {
375
+ return Effect.fail(
376
+ LedgerError.make({
377
+ operation,
378
+ message:
379
+ `The Thread Object owning ${target} answered ${operation} with the ` +
380
+ `mismatched result ${result._tag}.`,
381
+ }),
382
+ );
383
+ }
384
+
385
+ return Effect.succeed(result);
386
+ },
387
+ ),
388
+ Effect.withSpan("DoPortRouting.foreignLedgerCall", {
389
+ attributes: { operation, target },
390
+ }),
391
+ );
392
+ };
393
+
394
+ /**
395
+ * Routed `resolveAdmission` — where the S2 tri-state becomes real (plan §1.3): when the
396
+ * owning Object cannot be reached, or its answer cannot be understood, the routed adapter
397
+ * answers `AdmissionIndeterminate{reason}` and NEVER `NotAdmitted` — an unreachable
398
+ * authority proves nothing, and only `NotAdmitted` permits an admission attempt (SUB-031).
399
+ * A typed `LedgerError` answered BY the authority still fails typed: the authority was
400
+ * reached and reported its own storage failure.
401
+ */
402
+ const resolveForeignAdmission = (
403
+ target: ThreadId,
404
+ request: SubmissionLookupByKey,
405
+ ): Effect.Effect<
406
+ | AdmissionIndeterminate
407
+ | Extract<PortResult, { readonly _tag: "LedgerResolveAdmissionResult" }>["resolution"],
408
+ LedgerError
409
+ > =>
410
+ transportCall(target, LedgerResolveAdmissionCall.make({ request })).pipe(
411
+ Effect.flatMap((response) => {
412
+ if (response._tag === "PortFailed") {
413
+ if (response.failure._tag === "LedgerError") return Effect.fail(response.failure);
414
+
415
+ return Effect.succeed(
416
+ AdmissionIndeterminate.make({
417
+ reason: boundPortDiagnostic(
418
+ `The Thread Object owning ${target} answered resolveAdmission with the ` +
419
+ `out-of-contract failure ${response.failure._tag}: ${response.failure.message}`,
420
+ ),
421
+ }),
422
+ );
423
+ }
424
+ if (response.result._tag !== "LedgerResolveAdmissionResult") {
425
+ return Effect.succeed(
426
+ AdmissionIndeterminate.make({
427
+ reason: boundPortDiagnostic(
428
+ `The Thread Object owning ${target} answered resolveAdmission with the ` +
429
+ `mismatched result ${response.result._tag}.`,
430
+ ),
431
+ }),
432
+ );
433
+ }
434
+
435
+ return Effect.succeed(response.result.resolution);
436
+ }),
437
+ Effect.catchTags({
438
+ PortTransportError: (error) =>
439
+ Effect.succeed(
440
+ AdmissionIndeterminate.make({
441
+ reason: boundPortDiagnostic(
442
+ `The Thread Object owning ${target} is unreachable: ${error.message}`,
443
+ ),
444
+ }),
445
+ ),
446
+ PortProtocolError: (error) =>
447
+ Effect.succeed(
448
+ AdmissionIndeterminate.make({
449
+ reason: boundPortDiagnostic(
450
+ `The answer of the Thread Object owning ${target} could not be ` +
451
+ `understood: ${error.message}`,
452
+ ),
453
+ }),
454
+ ),
455
+ }),
456
+ Effect.withSpan("DoPortRouting.resolveForeignAdmission", { attributes: { target } }),
457
+ );
458
+
459
+ const foreignLookupById = (
460
+ operation: string,
461
+ target: ThreadId,
462
+ submissionId: SubmissionId,
463
+ ): Effect.Effect<Option.Option<SubmissionSnapshot>, LedgerError> =>
464
+ foreignLedgerCall(
465
+ operation,
466
+ target,
467
+ LedgerLookupCall.make({ request: SubmissionLookupById.make({ submissionId }) }),
468
+ LedgerLookupResult,
469
+ NoAdditionalPortFailure,
470
+ ).pipe(
471
+ Effect.map((result) =>
472
+ result.submission === undefined ? Option.none() : Option.some(result.submission),
473
+ ),
474
+ );
475
+
476
+ /**
477
+ * Enrich a LOCAL parent's recovery snapshot with the lane state of attached children whose
478
+ * rows live in other Durable Objects (plan §1.3): markers first (the local facet already
479
+ * consulted them), then a routed per-child `lookup` for any attached child that is neither
480
+ * local nor marker-settled. A transport failure surfaces as `LedgerError` so the alarm
481
+ * pass retries; the child's canonical Settlement remains the only authority (DUR-015).
482
+ */
483
+ const enrichChildAttachments = Effect.fn("DoPortRouting.enrichChildAttachments")(function* (
484
+ snapshot: RecoverySnapshot,
485
+ ): Effect.fn.Return<RecoverySnapshot, LedgerError> {
486
+ const operation = "ledger load recovery snapshot";
487
+
488
+ const attachments = new Map(
489
+ snapshot.childAttachments.map((attachment) => [attachment.childSubmissionId, attachment]),
490
+ );
491
+
492
+ let enriched = false;
493
+
494
+ for (const reservation of snapshot.childReservations) {
495
+ const childSubmissionId = reservation.childSubmissionId;
496
+
497
+ if (childSubmissionId === undefined || attachments.has(childSubmissionId)) continue;
498
+ const target = yield* submissionTarget(operation, childSubmissionId);
499
+
500
+ // A local or opaque child identity was already answered authoritatively by the local
501
+ // facet; absence there means the child admission never committed.
502
+ if (target._tag !== "foreign") continue;
503
+ const child = yield* foreignLookupById(operation, target.threadId, childSubmissionId);
504
+
505
+ if (Option.isNone(child)) continue;
506
+ attachments.set(
507
+ childSubmissionId,
508
+ ChildAttachmentSnapshot.make({
509
+ toolCallId: reservation.parentToolCallId,
510
+ childSubmissionId,
511
+ childState: child.value.state,
512
+ ...(child.value.settledOutcome === undefined
513
+ ? {}
514
+ : { childOutcome: child.value.settledOutcome }),
515
+ }),
516
+ );
517
+ enriched = true;
518
+ }
519
+ if (!enriched) return snapshot;
520
+ // Rebuild in reservation (parent Tool Call) order, the order the local facet documents.
521
+ const ordered: Array<ChildAttachmentSnapshot> = [];
522
+
523
+ for (const reservation of snapshot.childReservations) {
524
+ if (reservation.childSubmissionId === undefined) continue;
525
+ const attachment = attachments.get(reservation.childSubmissionId);
526
+
527
+ if (attachment !== undefined) ordered.push(attachment);
528
+ }
529
+
530
+ return RecoverySnapshot.make({ ...snapshot, childAttachments: ordered });
531
+ });
532
+
533
+ const routed = SubmissionLedger.of({
534
+ capabilities: local.capabilities,
535
+
536
+ admit: (request) =>
537
+ options.ownsThread(request.threadId)
538
+ ? local.admit(request)
539
+ : foreignLedgerCall(
540
+ "ledger admit",
541
+ request.threadId,
542
+ LedgerAdmitCall.make({ request }),
543
+ LedgerAdmitResult,
544
+ Schema.Union([AdmissionConflict, AdmissionPolicyError]),
545
+ ).pipe(Effect.map((reply) => reply.result)),
546
+
547
+ markReady: (request) =>
548
+ submissionTarget("ledger mark ready", request.submissionId).pipe(
549
+ Effect.flatMap((target) =>
550
+ target._tag === "local"
551
+ ? local.markReady(request)
552
+ : foreignLedgerCall(
553
+ "ledger mark ready",
554
+ target.threadId,
555
+ LedgerMarkReadyCall.make({ request }),
556
+ LedgerMarkReadyResult,
557
+ NoAdditionalPortFailure,
558
+ ).pipe(Effect.asVoid),
559
+ ),
560
+ ),
561
+
562
+ lookup: (request) =>
563
+ request._tag === "SubmissionLookupById"
564
+ ? submissionTarget("ledger lookup", request.submissionId).pipe(
565
+ Effect.flatMap((target) =>
566
+ target._tag === "local"
567
+ ? local.lookup(request)
568
+ : foreignLookupById("ledger lookup", target.threadId, request.submissionId),
569
+ ),
570
+ )
571
+ : options.ownsThread(request.threadId)
572
+ ? local.lookup(request)
573
+ : foreignLedgerCall(
574
+ "ledger lookup",
575
+ request.threadId,
576
+ LedgerLookupCall.make({ request }),
577
+ LedgerLookupResult,
578
+ NoAdditionalPortFailure,
579
+ ).pipe(
580
+ Effect.map((result) =>
581
+ result.submission === undefined ? Option.none() : Option.some(result.submission),
582
+ ),
583
+ ),
584
+
585
+ resolveAdmission: (request) =>
586
+ options.ownsThread(request.threadId)
587
+ ? local.resolveAdmission(request)
588
+ : resolveForeignAdmission(request.threadId, request),
589
+
590
+ inspectWorker: (threadId) =>
591
+ options.ownsThread(threadId)
592
+ ? local.inspectWorker === undefined
593
+ ? Effect.fail(
594
+ LedgerError.make({
595
+ operation: "inspectWorker",
596
+ message: "Worker inspection unavailable",
597
+ }),
598
+ )
599
+ : local.inspectWorker(threadId)
600
+ : foreignLedgerCall(
601
+ "inspect worker",
602
+ threadId,
603
+ LedgerInspectWorkerCall.make({ request: { threadId } }),
604
+ LedgerInspectWorkerResult,
605
+ NoAdditionalPortFailure,
606
+ ).pipe(Effect.map((reply) => reply.state)),
607
+
608
+ stopWorker: (request) =>
609
+ options.ownsThread(request.threadId)
610
+ ? local.stopWorker === undefined
611
+ ? Effect.fail(
612
+ LedgerError.make({ operation: "stopWorker", message: "Worker stop unavailable" }),
613
+ )
614
+ : local.stopWorker(request)
615
+ : foreignLedgerCall(
616
+ "ledger stop worker",
617
+ request.threadId,
618
+ LedgerStopWorkerCall.make({ request }),
619
+ LedgerStopWorkerResult,
620
+ NoAdditionalPortFailure,
621
+ ).pipe(Effect.map((reply) => reply.owned)),
622
+
623
+ requestAbort: (request) =>
624
+ submissionTarget("ledger request abort", request.submissionId).pipe(
625
+ Effect.flatMap((target) =>
626
+ target._tag === "local"
627
+ ? local.requestAbort(request)
628
+ : foreignLedgerCall(
629
+ "ledger request abort",
630
+ target.threadId,
631
+ LedgerRequestAbortCall.make({ request }),
632
+ LedgerRequestAbortResult,
633
+ AbortPortFailure,
634
+ ).pipe(Effect.map((reply) => reply.intent)),
635
+ ),
636
+ ),
637
+
638
+ recordChildSettled: (request) =>
639
+ submissionTarget("ledger record child settled", request.parentSubmissionId).pipe(
640
+ Effect.flatMap((target) =>
641
+ target._tag === "local"
642
+ ? local.recordChildSettled(request)
643
+ : foreignLedgerCall(
644
+ "ledger record child settled",
645
+ target.threadId,
646
+ LedgerRecordChildSettledCall.make({ request }),
647
+ LedgerRecordChildSettledResult,
648
+ NoAdditionalPortFailure,
649
+ ).pipe(Effect.map((reply) => reply.outcome)),
650
+ ),
651
+ ),
652
+
653
+ // Every operation below is lane-local by construction (plan §1.3): a foreign address is
654
+ // an out-of-contract call and fails fast typed instead of being quietly distributed.
655
+ claim: (request) =>
656
+ options.ownsThread(request.threadId)
657
+ ? local.claim(request)
658
+ : Effect.fail(crossThreadLedgerError("ledger claim", request.threadId)),
659
+
660
+ claimJoining: (request) =>
661
+ options.ownsThread(request.threadId)
662
+ ? local.claimJoining(request)
663
+ : Effect.fail(crossThreadLedgerError("ledger claim joining", request.threadId)),
664
+
665
+ renewOwnership: (request) =>
666
+ submissionTarget("ledger renew ownership", request.submissionId).pipe(
667
+ Effect.flatMap((target) =>
668
+ target._tag === "local"
669
+ ? local.renewOwnership(request)
670
+ : Effect.fail(crossThreadLedgerError("ledger renew ownership", target.threadId)),
671
+ ),
672
+ ),
673
+
674
+ releaseOwnership: (request) =>
675
+ submissionTarget("ledger release ownership", request.submissionId).pipe(
676
+ Effect.flatMap((target) =>
677
+ target._tag === "local"
678
+ ? local.releaseOwnership(request)
679
+ : Effect.fail(crossThreadLedgerError("ledger release ownership", target.threadId)),
680
+ ),
681
+ ),
682
+
683
+ markInputApplied: (request) =>
684
+ submissionTarget("ledger mark input applied", request.submissionId).pipe(
685
+ Effect.flatMap((target) =>
686
+ target._tag === "local"
687
+ ? local.markInputApplied(request)
688
+ : Effect.fail(crossThreadLedgerError("ledger mark input applied", target.threadId)),
689
+ ),
690
+ ),
691
+
692
+ reserveSettlement: (request) =>
693
+ submissionTarget("ledger reserve settlement", request.submissionId).pipe(
694
+ Effect.flatMap((target) =>
695
+ target._tag === "local"
696
+ ? local.reserveSettlement(request)
697
+ : Effect.fail(crossThreadLedgerError("ledger reserve settlement", target.threadId)),
698
+ ),
699
+ ),
700
+
701
+ finalizeSettlement: (request) =>
702
+ submissionTarget("ledger finalize settlement", request.submissionId).pipe(
703
+ Effect.flatMap((target) =>
704
+ target._tag === "local"
705
+ ? local.finalizeSettlement(request)
706
+ : Effect.fail(crossThreadLedgerError("ledger finalize settlement", target.threadId)),
707
+ ),
708
+ ),
709
+
710
+ markJoined: (request) =>
711
+ submissionTarget("ledger mark joined", request.submissionId).pipe(
712
+ Effect.flatMap((target) =>
713
+ target._tag === "local"
714
+ ? local.markJoined(request)
715
+ : Effect.fail(crossThreadLedgerError("ledger mark joined", target.threadId)),
716
+ ),
717
+ ),
718
+
719
+ revertJoining: (request) =>
720
+ submissionTarget("ledger revert joining", request.submissionId).pipe(
721
+ Effect.flatMap((target) =>
722
+ target._tag === "local"
723
+ ? local.revertJoining(request)
724
+ : Effect.fail(crossThreadLedgerError("ledger revert joining", target.threadId)),
725
+ ),
726
+ ),
727
+
728
+ suspend: (request) =>
729
+ submissionTarget("ledger suspend", request.submissionId).pipe(
730
+ Effect.flatMap((target) =>
731
+ target._tag === "local"
732
+ ? local.suspend(request)
733
+ : Effect.fail(crossThreadLedgerError("ledger suspend", target.threadId)),
734
+ ),
735
+ ),
736
+
737
+ recordApprovalDecision: (command) =>
738
+ submissionTarget("ledger record approval decision", command.submissionId).pipe(
739
+ Effect.flatMap((target) =>
740
+ target._tag === "local"
741
+ ? local.recordApprovalDecision(command)
742
+ : Effect.fail(
743
+ crossThreadLedgerError("ledger record approval decision", target.threadId),
744
+ ),
745
+ ),
746
+ ),
747
+
748
+ markUnknown: (request) =>
749
+ submissionTarget("ledger mark unknown", request.submissionId).pipe(
750
+ Effect.flatMap((target) =>
751
+ target._tag === "local"
752
+ ? local.markUnknown(request)
753
+ : Effect.fail(crossThreadLedgerError("ledger mark unknown", target.threadId)),
754
+ ),
755
+ ),
756
+
757
+ recordUnknownResolution: (command) =>
758
+ submissionTarget("ledger record unknown resolution", command.submissionId).pipe(
759
+ Effect.flatMap((target) =>
760
+ target._tag === "local"
761
+ ? local.recordUnknownResolution(command)
762
+ : Effect.fail(
763
+ crossThreadLedgerError("ledger record unknown resolution", target.threadId),
764
+ ),
765
+ ),
766
+ ),
767
+
768
+ reserveChildBudget: (request) =>
769
+ submissionTarget("ledger reserve child budget", request.parentSubmissionId).pipe(
770
+ Effect.flatMap((target) =>
771
+ target._tag === "local"
772
+ ? local.reserveChildBudget(request)
773
+ : Effect.fail(crossThreadLedgerError("ledger reserve child budget", target.threadId)),
774
+ ),
775
+ ),
776
+
777
+ // Reservation identities carry no Thread address; the reservation row lives in the
778
+ // parent's own Object and these transitions are parent-lane-local by construction, so
779
+ // they always execute on the local facet (which fails typed for an unknown row).
780
+ attachChildToReservation: local.attachChildToReservation,
781
+ beginChildBudgetRelease: local.beginChildBudgetRelease,
782
+ releaseChildBudget: local.releaseChildBudget,
783
+
784
+ // The local scan IS the whole worklist: one Thread per Object (durability §5).
785
+ scanNonterminal: local.scanNonterminal,
786
+
787
+ readAbortIntent: (request) =>
788
+ submissionTarget("ledger read abort intent", request.submissionId).pipe(
789
+ Effect.flatMap((target) =>
790
+ target._tag === "local"
791
+ ? local.readAbortIntent(request)
792
+ : Effect.fail(crossThreadLedgerError("ledger read abort intent", target.threadId)),
793
+ ),
794
+ ),
795
+
796
+ loadRecoverySnapshot: (request) =>
797
+ submissionTarget("ledger load recovery snapshot", request.submissionId).pipe(
798
+ Effect.flatMap((target) =>
799
+ target._tag === "local"
800
+ ? local.loadRecoverySnapshot(request).pipe(Effect.flatMap(enrichChildAttachments))
801
+ : Effect.fail(crossThreadLedgerError("ledger load recovery snapshot", target.threadId)),
802
+ ),
803
+ ),
804
+ });
805
+
806
+ return Context.make(SubmissionLedger, routed);
807
+ });
808
+
809
+ const makeRoutedStoreServices = Effect.fn("DoPortRouting.makeRoutedStoreServices")(function* (
810
+ options: RoutedPortOptions,
811
+ ) {
812
+ const local = yield* ThreadStore;
813
+ const checkpoints = local.checkpoints;
814
+ const recoveryCheckpoints = local.recoveryCheckpoints;
815
+ const lifecyclePublications = local.lifecyclePublications;
816
+ const acknowledgeMany = lifecyclePublications?.acknowledgeMany;
817
+ const transport = yield* ThreadPortTransport;
818
+ const transportCall: TransportCall = makeTransportCall(transport);
819
+
820
+ const routeFailure =
821
+ (operation: string, target: string) =>
822
+ (error: PortTransportError | PortProtocolError): ThreadStoreError =>
823
+ ThreadStoreError.make({
824
+ operation,
825
+ message: boundPortDiagnostic(
826
+ `Routed ${operation} to the Thread Object owning ${target} failed: ${error.message}`,
827
+ ),
828
+ cause: error,
829
+ });
830
+
831
+ /** The store twin of `foreignLedgerCall` with `ThreadStoreError` as the base error. */
832
+ const foreignStoreCall = <ResultSchema extends Schema.Top, FailureSchema extends Schema.Top>(
833
+ operation: string,
834
+ target: ThreadId,
835
+ call: PortRequest,
836
+ resultSchema: ResultSchema,
837
+ failureSchema: FailureSchema,
838
+ ): Effect.Effect<ResultSchema["Type"], FailureSchema["Type"] | ThreadStoreError> => {
839
+ const isExpectedResult = Schema.is(resultSchema);
840
+ const isExpectedFailure = Schema.is(failureSchema);
841
+
842
+ return transportCall(target, call).pipe(
843
+ Effect.mapError(routeFailure(operation, target)),
844
+ Effect.flatMap(
845
+ (
846
+ response,
847
+ ): Effect.Effect<ResultSchema["Type"], FailureSchema["Type"] | ThreadStoreError> => {
848
+ if (response._tag === "PortFailed") {
849
+ const failure = response.failure;
850
+
851
+ if (isExpectedFailure(failure)) return Effect.fail(failure);
852
+ if (failure._tag === "ThreadStoreError") return Effect.fail(failure);
853
+
854
+ return Effect.fail(
855
+ ThreadStoreError.make({
856
+ operation,
857
+ message: boundPortDiagnostic(
858
+ `The Thread Object owning ${target} answered ${operation} with the ` +
859
+ `out-of-contract failure ${failure._tag}`,
860
+ ),
861
+ cause: failure,
862
+ }),
863
+ );
864
+ }
865
+ const result = response.result;
866
+
867
+ if (!isExpectedResult(result)) {
868
+ return Effect.fail(
869
+ ThreadStoreError.make({
870
+ operation,
871
+ message:
872
+ `The Thread Object owning ${target} answered ${operation} with the ` +
873
+ `mismatched result ${result._tag}.`,
874
+ }),
875
+ );
876
+ }
877
+
878
+ return Effect.succeed(result);
879
+ },
880
+ ),
881
+ Effect.withSpan("DoPortRouting.foreignStoreCall", {
882
+ attributes: { operation, target },
883
+ }),
884
+ );
885
+ };
886
+
887
+ const routed = ThreadStore.of({
888
+ countPeerMessages: (request) =>
889
+ options.ownsThread(request.threadId)
890
+ ? local.countPeerMessages === undefined
891
+ ? Effect.fail(crossThreadStoreError("peer count unavailable", request.threadId))
892
+ : local.countPeerMessages(request)
893
+ : foreignStoreCall(
894
+ "thread countPeerMessages",
895
+ request.threadId,
896
+ StoreCountPeerMessagesCall.make({ request }),
897
+ StoreCountPeerMessagesResult,
898
+ ThreadNotMaterialized,
899
+ ).pipe(Effect.map((reply) => reply.count)),
900
+
901
+ materialize: (request) =>
902
+ options.ownsThread(request.threadId)
903
+ ? local.materialize(request)
904
+ : foreignStoreCall(
905
+ "thread materialize",
906
+ request.threadId,
907
+ StoreMaterializeCall.make({ request }),
908
+ StoreMaterializeResult,
909
+ FenceRejected,
910
+ ).pipe(Effect.asVoid),
911
+
912
+ append: (request) =>
913
+ options.ownsThread(request.threadId)
914
+ ? local.append(request)
915
+ : foreignStoreCall(
916
+ "thread append",
917
+ request.threadId,
918
+ StoreAppendCall.make({ request }),
919
+ StoreAppendResult,
920
+ AppendPortFailure,
921
+ ).pipe(Effect.map((reply) => reply.result)),
922
+
923
+ read: (request) =>
924
+ options.ownsThread(request.threadId)
925
+ ? local.read(request)
926
+ : Stream.unwrap(
927
+ foreignStoreCall(
928
+ "thread read",
929
+ request.threadId,
930
+ StoreReadPageCall.make({ request }),
931
+ StoreReadPageResult,
932
+ ThreadNotMaterialized,
933
+ ).pipe(Effect.map((reply) => Stream.fromIterable(reply.records))),
934
+ ),
935
+
936
+ inspectTail: (request) =>
937
+ options.ownsThread(request.threadId)
938
+ ? local.inspectTail(request)
939
+ : foreignStoreCall(
940
+ "thread inspect tail",
941
+ request.threadId,
942
+ StoreInspectTailCall.make({ request }),
943
+ StoreInspectTailResult,
944
+ ThreadNotMaterialized,
945
+ ).pipe(Effect.map((reply) => reply.tail)),
946
+
947
+ readIdentity: (request) =>
948
+ options.ownsThread(request.threadId)
949
+ ? local.readIdentity(request)
950
+ : foreignStoreCall(
951
+ "thread read identity",
952
+ request.threadId,
953
+ StoreReadIdentityCall.make({ request }),
954
+ StoreReadIdentityResult,
955
+ ThreadNotMaterialized,
956
+ ).pipe(
957
+ Effect.flatMap((reply) =>
958
+ reply.identity.threadId === request.threadId
959
+ ? Effect.succeed(reply.identity)
960
+ : Effect.fail(
961
+ ThreadStoreError.make({
962
+ operation: "thread read identity",
963
+ message: "The identity snapshot belongs to another Thread.",
964
+ }),
965
+ ),
966
+ ),
967
+ ),
968
+
969
+ export: (request) =>
970
+ options.ownsThread(request.threadId)
971
+ ? local.export(request)
972
+ : foreignStoreCall(
973
+ "thread export",
974
+ request.threadId,
975
+ StoreExportCall.make({ request }),
976
+ StoreExportResult,
977
+ ThreadNotMaterialized,
978
+ ).pipe(Effect.map((reply) => reply.export)),
979
+
980
+ // Observation and checkpoints are lane-local: the closed route-capable store subset
981
+ // is materialize/append/read/readIdentity/inspectTail/export. Recovery cache misses can fall back
982
+ // to those canonical reads, including when the cache belongs to a foreign Object.
983
+ observe: (request) =>
984
+ options.ownsThread(request.threadId)
985
+ ? local.observe(request)
986
+ : Stream.unwrap(Effect.fail(crossThreadStoreError("thread observe", request.threadId))),
987
+
988
+ // Publication obligations and operator retries stay with their native owner.
989
+ ...(lifecyclePublications === undefined
990
+ ? {}
991
+ : {
992
+ lifecyclePublications: {
993
+ ...lifecyclePublications,
994
+ ...(acknowledgeMany === undefined
995
+ ? {}
996
+ : {
997
+ acknowledgeMany: (batches) =>
998
+ batches.every((batch) =>
999
+ batch.every((publication) => options.ownsThread(publication.ownerThreadId)),
1000
+ )
1001
+ ? acknowledgeMany(batches)
1002
+ : Effect.fail(LifecyclePublicationError.make({ reason: "unavailable" })),
1003
+ }),
1004
+ acknowledge: (batch) =>
1005
+ batch.every((publication) => options.ownsThread(publication.ownerThreadId))
1006
+ ? lifecyclePublications.acknowledge(batch)
1007
+ : Effect.fail(LifecyclePublicationError.make({ reason: "unavailable" })),
1008
+ claim: (batch, nowMillis, timeoutMillis) =>
1009
+ batch.every((publication) => options.ownsThread(publication.ownerThreadId))
1010
+ ? lifecyclePublications.claim(batch, nowMillis, timeoutMillis)
1011
+ : Effect.fail(LifecyclePublicationError.make({ reason: "unavailable" })),
1012
+ retryParked: (ownerThreadId, nowMillis) =>
1013
+ options.ownsThread(ownerThreadId)
1014
+ ? lifecyclePublications.retryParked(ownerThreadId, nowMillis)
1015
+ : Effect.fail(LifecyclePublicationError.make({ reason: "unavailable" })),
1016
+ },
1017
+ }),
1018
+ ...(recoveryCheckpoints === undefined
1019
+ ? {}
1020
+ : {
1021
+ recoveryCheckpoints: {
1022
+ save: (request) =>
1023
+ options.ownsThread(request.checkpoint.threadId)
1024
+ ? recoveryCheckpoints.save(request)
1025
+ : Effect.fail(
1026
+ crossThreadStoreError(
1027
+ "thread save recovery checkpoint",
1028
+ request.checkpoint.threadId,
1029
+ ),
1030
+ ),
1031
+ load: (request) =>
1032
+ options.ownsThread(request.threadId)
1033
+ ? recoveryCheckpoints.load(request)
1034
+ : Effect.succeed(Option.none()),
1035
+ },
1036
+ }),
1037
+ ...(checkpoints === undefined
1038
+ ? {}
1039
+ : {
1040
+ checkpoints: {
1041
+ save: (request) =>
1042
+ options.ownsThread(request.checkpoint.threadId)
1043
+ ? checkpoints.save(request)
1044
+ : Effect.fail(
1045
+ crossThreadStoreError("thread save checkpoint", request.checkpoint.threadId),
1046
+ ),
1047
+ load: (request) =>
1048
+ options.ownsThread(request.threadId)
1049
+ ? checkpoints.load(request)
1050
+ : Effect.fail(crossThreadStoreError("thread load checkpoint", request.threadId)),
1051
+ },
1052
+ }),
1053
+ });
1054
+
1055
+ return Context.make(ThreadStore, routed);
1056
+ });
1057
+
1058
+ /**
1059
+ * Routing decorator over the LOCAL `SubmissionLedger` facet (plan §1.3): a request addressing
1060
+ * this Object's Thread executes locally; a route-capable request addressing another
1061
+ * Thread is Schema-encoded onto the `ThreadPortTransport` and executed by the
1062
+ * owning Object's local facet; any other foreign request fails fast typed. Provide the WP1
1063
+ * local facet (`submissionLedgerLayer`/`ledgerLayer`) and a transport to close it.
1064
+ */
1065
+ export const routedSubmissionLedgerLayer = (
1066
+ options: RoutedPortOptions,
1067
+ ): Layer.Layer<SubmissionLedger, never, SubmissionLedger | ThreadPortTransport> =>
1068
+ Layer.effectContext(makeRoutedLedgerServices(options));
1069
+
1070
+ /**
1071
+ * Routing decorator over the LOCAL `ThreadStore` facet (plan §1.3): this-thread
1072
+ * requests execute locally; foreign materialize/append/read/inspectTail/export travel the
1073
+ * transport. Foreign recovery-cache loads return none for canonical replay; other foreign
1074
+ * checkpoint operations and observation fail fast typed.
1075
+ */
1076
+ export const routedThreadStoreLayer = (
1077
+ options: RoutedPortOptions,
1078
+ ): Layer.Layer<ThreadStore, never, ThreadStore | ThreadPortTransport> =>
1079
+ Layer.effectContext(makeRoutedStoreServices(options));
1080
+
1081
+ /** Route validated worker admission as one operation; source policy hooks stay at the caller. */
1082
+ export const routedWorkerAdmissionLayer = (options: RoutedPortOptions) =>
1083
+ Layer.effect(
1084
+ WorkerAdmissionPort,
1085
+ Effect.gen(function* () {
1086
+ const call = makeTransportCall(yield* ThreadPortTransport);
1087
+
1088
+ return WorkerAdmissionPort.of({
1089
+ admit: (request) =>
1090
+ options.ownsThread(request.threadId)
1091
+ ? admitWorker(request)
1092
+ : call(request.threadId, WorkerAdmitCall.make({ request })).pipe(
1093
+ Effect.mapError((cause) =>
1094
+ LedgerError.make({
1095
+ operation: "admit worker",
1096
+ message: "Worker admission transport failed",
1097
+ cause,
1098
+ }),
1099
+ ),
1100
+ Effect.flatMap((response) => {
1101
+ if (
1102
+ response._tag === "PortSucceeded" &&
1103
+ response.result._tag === "WorkerAdmitResult"
1104
+ )
1105
+ return Effect.succeed(response.result.result);
1106
+
1107
+ return Effect.fail(
1108
+ response._tag === "PortFailed" &&
1109
+ Schema.is(WorkerAdmissionFailure)(response.failure)
1110
+ ? response.failure
1111
+ : LedgerError.make({
1112
+ operation: "admit worker",
1113
+ message: "Unexpected worker admission response",
1114
+ cause: response,
1115
+ }),
1116
+ );
1117
+ }),
1118
+ ),
1119
+ });
1120
+ }),
1121
+ );
1122
+
1123
+ /** Route the existing owner-scoped list; delivery mutations remain source local. */
1124
+ export const routedMessageDeliveryStoreLayer = (options: RoutedPortOptions) =>
1125
+ Layer.effect(
1126
+ MessageDeliveryStore,
1127
+ Effect.gen(function* () {
1128
+ const local = yield* MessageDeliveryStore;
1129
+ const call = makeTransportCall(yield* ThreadPortTransport);
1130
+
1131
+ return MessageDeliveryStore.of({
1132
+ ...local,
1133
+ change: (key, change) =>
1134
+ options.ownsThread(key.ownerThreadId) || change._tag !== "Complete"
1135
+ ? local.change(key, change)
1136
+ : call(
1137
+ key.ownerThreadId,
1138
+ MessageDeliveryCompleteCall.make({ key, completion: change }),
1139
+ ).pipe(
1140
+ Effect.catchCause((cause) =>
1141
+ Effect.failCause(
1142
+ Cause.map(cause, (failure) =>
1143
+ MessageDeliveryError.make({
1144
+ reason: "storage",
1145
+ operation: "route delivery completion",
1146
+ cause: failure,
1147
+ }),
1148
+ ),
1149
+ ),
1150
+ ),
1151
+ Effect.flatMap((response) => {
1152
+ if (
1153
+ response._tag === "PortSucceeded" &&
1154
+ response.result._tag === "MessageDeliveryCompleteResult"
1155
+ )
1156
+ return Effect.succeed(response.result.record);
1157
+ if (
1158
+ response._tag === "PortFailed" &&
1159
+ (response.failure._tag === "MessageDeliveryError" ||
1160
+ response.failure._tag === "MessageDeliveryFailpointError")
1161
+ )
1162
+ return Effect.fail(response.failure);
1163
+
1164
+ return Effect.fail(
1165
+ MessageDeliveryError.make({
1166
+ reason: "storage",
1167
+ operation: "route delivery completion",
1168
+ cause: response,
1169
+ }),
1170
+ );
1171
+ }),
1172
+ ),
1173
+ list: (request) =>
1174
+ options.ownsThread(request.ownerThreadId)
1175
+ ? local.list(request)
1176
+ : call(request.ownerThreadId, MessageDeliveryListCall.make({ request })).pipe(
1177
+ Effect.mapError(() =>
1178
+ MessageDeliveryError.make({
1179
+ reason: "storage",
1180
+ operation: "route delivery list",
1181
+ }),
1182
+ ),
1183
+ Effect.flatMap((response) => {
1184
+ if (
1185
+ response._tag === "PortSucceeded" &&
1186
+ response.result._tag === "MessageDeliveryListResult"
1187
+ )
1188
+ return Effect.succeed(response.result.page);
1189
+
1190
+ return Effect.fail(
1191
+ response._tag === "PortFailed" &&
1192
+ response.failure._tag === "MessageDeliveryError"
1193
+ ? response.failure
1194
+ : MessageDeliveryError.make({
1195
+ reason: "storage",
1196
+ operation: "route delivery list",
1197
+ }),
1198
+ );
1199
+ }),
1200
+ ),
1201
+ });
1202
+ }),
1203
+ );
1204
+
1205
+ // ---------------------------------------------------------------------------
1206
+ // Owner-side execution
1207
+ // ---------------------------------------------------------------------------
1208
+
1209
+ /** Fold one port operation's typed failures into the uniform response envelope. */
1210
+ const capture = <Failure extends PortFailure, R>(
1211
+ effect: Effect.Effect<PortResult, Failure, R>,
1212
+ ): Effect.Effect<PortResponse, never, R> =>
1213
+ effect.pipe(
1214
+ Effect.map((result): PortResponse => PortSucceeded.make({ result })),
1215
+ Effect.catch((failure) => Effect.succeed<PortResponse>(PortFailed.make({ failure }))),
1216
+ );
1217
+
1218
+ /**
1219
+ * Execute one decoded port request against THIS Object's LOCAL facets — the owner-side half
1220
+ * of the routed ports (plan §1.3). Callers must provide the WP1 local facets, never the
1221
+ * routed decorators: the routing layer already established that this Object owns the
1222
+ * addressed Thread, and re-routing here could bounce a request between Objects.
1223
+ * Failures never escape — every typed port failure becomes a `PortFailed` envelope that
1224
+ * re-decodes on the caller side.
1225
+ */
1226
+ export const executePortRequest = Effect.fn("DoPortRouting.executePortRequest")(function* (
1227
+ request: PortRequest,
1228
+ ): Effect.fn.Return<
1229
+ PortResponse,
1230
+ never,
1231
+ SubmissionLedger | ThreadStore | MessageDeliveryStore | WakeScheduler | DurableRuntimeFailpoint
1232
+ > {
1233
+ switch (request._tag) {
1234
+ case "MessageDeliveryComplete": {
1235
+ const store = yield* MessageDeliveryStore;
1236
+
1237
+ return yield* capture(
1238
+ store
1239
+ .change(request.key, {
1240
+ _tag: "Complete",
1241
+ ...request.completion,
1242
+ nowMillis: yield* Clock.currentTimeMillis,
1243
+ })
1244
+ .pipe(Effect.map((record) => MessageDeliveryCompleteResult.make({ record }))),
1245
+ );
1246
+ }
1247
+ case "WorkerAdmit":
1248
+ return yield* capture(
1249
+ admitWorker(request.request).pipe(
1250
+ Effect.map((result) => WorkerAdmitResult.make({ result })),
1251
+ ),
1252
+ );
1253
+ case "MessageDeliveryList": {
1254
+ const store = yield* MessageDeliveryStore;
1255
+
1256
+ return yield* capture(
1257
+ store
1258
+ .list(request.request)
1259
+ .pipe(Effect.map((page) => MessageDeliveryListResult.make({ page }))),
1260
+ );
1261
+ }
1262
+ case "StoreCountPeerMessages": {
1263
+ const store = yield* ThreadStore;
1264
+
1265
+ return yield* capture(
1266
+ store.countPeerMessages === undefined
1267
+ ? Effect.fail(crossThreadStoreError("native read unavailable", request.request.threadId))
1268
+ : store
1269
+ .countPeerMessages(request.request)
1270
+ .pipe(Effect.map((count) => StoreCountPeerMessagesResult.make({ count }))),
1271
+ );
1272
+ }
1273
+ case "LedgerAdmit": {
1274
+ const ledger = yield* SubmissionLedger;
1275
+
1276
+ return yield* capture(
1277
+ ledger
1278
+ .admit(request.request)
1279
+ .pipe(Effect.map((result) => LedgerAdmitResult.make({ result }))),
1280
+ );
1281
+ }
1282
+ case "LedgerMarkReady": {
1283
+ const ledger = yield* SubmissionLedger;
1284
+
1285
+ return yield* capture(
1286
+ ledger.markReady(request.request).pipe(Effect.map(() => LedgerMarkReadyResult.make({}))),
1287
+ );
1288
+ }
1289
+ case "LedgerLookup": {
1290
+ const ledger = yield* SubmissionLedger;
1291
+
1292
+ return yield* capture(
1293
+ ledger
1294
+ .lookup(request.request)
1295
+ .pipe(
1296
+ Effect.map((submission) =>
1297
+ Option.isSome(submission)
1298
+ ? LedgerLookupResult.make({ submission: submission.value })
1299
+ : LedgerLookupResult.make({}),
1300
+ ),
1301
+ ),
1302
+ );
1303
+ }
1304
+ case "LedgerResolveAdmission": {
1305
+ const ledger = yield* SubmissionLedger;
1306
+
1307
+ return yield* capture(
1308
+ ledger
1309
+ .resolveAdmission(request.request)
1310
+ .pipe(Effect.map((resolution) => LedgerResolveAdmissionResult.make({ resolution }))),
1311
+ );
1312
+ }
1313
+ case "LedgerInspectWorker": {
1314
+ const ledger = yield* SubmissionLedger;
1315
+
1316
+ return yield* capture(
1317
+ ledger.inspectWorker === undefined
1318
+ ? Effect.fail(
1319
+ LedgerError.make({
1320
+ operation: "inspectWorker",
1321
+ message: "Worker inspection unavailable",
1322
+ }),
1323
+ )
1324
+ : ledger
1325
+ .inspectWorker(request.request.threadId)
1326
+ .pipe(Effect.map((state) => LedgerInspectWorkerResult.make({ state }))),
1327
+ );
1328
+ }
1329
+ case "LedgerStopWorker": {
1330
+ const ledger = yield* SubmissionLedger;
1331
+
1332
+ return yield* capture(
1333
+ ledger.stopWorker === undefined
1334
+ ? Effect.fail(
1335
+ LedgerError.make({ operation: "stopWorker", message: "Worker stop unavailable" }),
1336
+ )
1337
+ : ledger
1338
+ .stopWorker(request.request)
1339
+ .pipe(Effect.map((owned) => LedgerStopWorkerResult.make({ owned }))),
1340
+ );
1341
+ }
1342
+ case "LedgerRequestAbort": {
1343
+ const ledger = yield* SubmissionLedger;
1344
+
1345
+ return yield* capture(
1346
+ ledger
1347
+ .requestAbort(request.request)
1348
+ .pipe(Effect.map((intent) => LedgerRequestAbortResult.make({ intent }))),
1349
+ );
1350
+ }
1351
+ case "LedgerRecordChildSettled": {
1352
+ const ledger = yield* SubmissionLedger;
1353
+
1354
+ return yield* capture(
1355
+ ledger
1356
+ .recordChildSettled(request.request)
1357
+ .pipe(Effect.map((outcome) => LedgerRecordChildSettledResult.make({ outcome }))),
1358
+ );
1359
+ }
1360
+ case "StoreMaterialize": {
1361
+ const store = yield* ThreadStore;
1362
+
1363
+ return yield* capture(
1364
+ store.materialize(request.request).pipe(Effect.map(() => StoreMaterializeResult.make({}))),
1365
+ );
1366
+ }
1367
+ case "StoreAppend": {
1368
+ const store = yield* ThreadStore;
1369
+
1370
+ return yield* capture(
1371
+ store
1372
+ .append(request.request)
1373
+ .pipe(Effect.map((result) => StoreAppendResult.make({ result }))),
1374
+ );
1375
+ }
1376
+ case "StoreReadPage": {
1377
+ const store = yield* ThreadStore;
1378
+
1379
+ return yield* capture(
1380
+ store.read(request.request).pipe(
1381
+ Stream.runCollect,
1382
+ Effect.map((records) => StoreReadPageResult.make({ records: [...records] })),
1383
+ ),
1384
+ );
1385
+ }
1386
+ case "StoreInspectTail": {
1387
+ const store = yield* ThreadStore;
1388
+
1389
+ return yield* capture(
1390
+ store
1391
+ .inspectTail(request.request)
1392
+ .pipe(Effect.map((tail) => StoreInspectTailResult.make({ tail }))),
1393
+ );
1394
+ }
1395
+ case "StoreReadIdentity": {
1396
+ const store = yield* ThreadStore;
1397
+
1398
+ return yield* capture(
1399
+ store
1400
+ .readIdentity(request.request)
1401
+ .pipe(Effect.map((identity) => StoreReadIdentityResult.make({ identity }))),
1402
+ );
1403
+ }
1404
+ case "StoreExport": {
1405
+ const store = yield* ThreadStore;
1406
+
1407
+ return yield* capture(
1408
+ store
1409
+ .export(request.request)
1410
+ .pipe(Effect.map((threadExport) => StoreExportResult.make({ export: threadExport }))),
1411
+ );
1412
+ }
1413
+ }
1414
+ });
1415
+
1416
+ /**
1417
+ * The last-resort wire fallback when even encoding a response fails: the literal encoded
1418
+ * form of `PortFailed(PortProtocolError)` — tagged classes of bounded strings encode to
1419
+ * exactly this shape, so no Schema round trip is needed to produce it.
1420
+ */
1421
+ const encodedProtocolFailure = (message: string): unknown => ({
1422
+ _tag: "PortFailed",
1423
+ failure: { _tag: "PortProtocolError", message: boundPortDiagnostic(message) },
1424
+ });
1425
+
1426
+ /**
1427
+ * The complete owner-side endpoint body for `portCall` (D-P6-3): decode the wire request,
1428
+ * execute it against this Object's LOCAL facets, and answer with the encoded response
1429
+ * envelope. Total by construction — a request that cannot be decoded, or a response that
1430
+ * cannot be encoded, answers `PortFailed(PortProtocolError)` instead of throwing, so the
1431
+ * transport never has to interpret exceptions as protocol answers.
1432
+ */
1433
+ export const handleEncodedPortRequest = Effect.fn("DoPortRouting.handleEncodedPortRequest")(
1434
+ function* (
1435
+ encoded: unknown,
1436
+ ): Effect.fn.Return<
1437
+ unknown,
1438
+ never,
1439
+ SubmissionLedger | ThreadStore | MessageDeliveryStore | WakeScheduler | DurableRuntimeFailpoint
1440
+ > {
1441
+ const response = yield* decodePortRequest(encoded).pipe(
1442
+ Effect.flatMap(executePortRequest),
1443
+ Effect.catch((error) =>
1444
+ Effect.succeed<PortResponse>(
1445
+ PortFailed.make({
1446
+ failure: PortProtocolError.make({
1447
+ message: boundPortDiagnostic(
1448
+ `The port request could not be decoded: ${error.message}`,
1449
+ ),
1450
+ }),
1451
+ }),
1452
+ ),
1453
+ ),
1454
+ );
1455
+
1456
+ return yield* encodePortResponse(response).pipe(
1457
+ Effect.catch((error) =>
1458
+ Effect.succeed(
1459
+ encodedProtocolFailure(`The port response could not be encoded: ${error.message}`),
1460
+ ),
1461
+ ),
1462
+ );
1463
+ },
1464
+ );