@effect-agent/storage-cloudflare 0.1.0-beta.11 → 0.1.0-beta.111

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 (74) hide show
  1. package/dist/DoMemoryStore.d.mts +29 -0
  2. package/dist/DoMemoryStore.mjs +55 -0
  3. package/dist/DoMemoryStore.mjs.map +1 -0
  4. package/dist/DoMessageDeliveryStore.d.mts +17 -0
  5. package/dist/DoMessageDeliveryStore.mjs +32 -0
  6. package/dist/DoMessageDeliveryStore.mjs.map +1 -0
  7. package/dist/DoScheduleStore.d.mts +36 -0
  8. package/dist/DoScheduleStore.mjs +388 -0
  9. package/dist/DoScheduleStore.mjs.map +1 -0
  10. package/dist/DoStorageConfig.d.mts +45 -0
  11. package/dist/DoStorageConfig.mjs +52 -0
  12. package/dist/DoStorageConfig.mjs.map +1 -0
  13. package/dist/DoStorageError-Cmhvl4TQ.d.mts +98 -0
  14. package/dist/DoStorageError.d.mts +2 -0
  15. package/dist/DoStorageError.mjs +164 -0
  16. package/dist/DoStorageError.mjs.map +1 -0
  17. package/dist/DoStorageFailpoint.d.mts +17 -0
  18. package/dist/DoStorageFailpoint.mjs +14 -0
  19. package/dist/DoStorageFailpoint.mjs.map +1 -0
  20. package/dist/DoStorageFailpointTesting.d.mts +33 -0
  21. package/dist/DoStorageFailpointTesting.mjs +35 -0
  22. package/dist/DoStorageFailpointTesting.mjs.map +1 -0
  23. package/dist/DoStorageVersion-Db6fjP-I.d.mts +12 -0
  24. package/dist/DoStorageVersion.d.mts +2 -0
  25. package/dist/DoStorageVersion.mjs +8 -0
  26. package/dist/DoStorageVersion.mjs.map +1 -0
  27. package/dist/DoSubmissionLedger.d.mts +23 -0
  28. package/dist/DoSubmissionLedger.mjs +1864 -0
  29. package/dist/DoSubmissionLedger.mjs.map +1 -0
  30. package/dist/DoSubscriptionStore.d.mts +25 -0
  31. package/dist/DoSubscriptionStore.mjs +183 -0
  32. package/dist/DoSubscriptionStore.mjs.map +1 -0
  33. package/dist/DoThreadStore.d.mts +59 -0
  34. package/dist/DoThreadStore.mjs +481 -0
  35. package/dist/DoThreadStore.mjs.map +1 -0
  36. package/dist/MemoryProtocol.d.mts +38160 -0
  37. package/dist/MemoryProtocol.mjs +219 -0
  38. package/dist/MemoryProtocol.mjs.map +1 -0
  39. package/dist/PortProtocol.d.mts +4933 -0
  40. package/dist/PortProtocol.mjs +193 -0
  41. package/dist/PortProtocol.mjs.map +1 -0
  42. package/dist/PortRouting.d.mts +86 -0
  43. package/dist/PortRouting.mjs +439 -0
  44. package/dist/PortRouting.mjs.map +1 -0
  45. package/dist/do-journal-THW8-ZeP.mjs +1041 -0
  46. package/dist/do-journal-THW8-ZeP.mjs.map +1 -0
  47. package/dist/index.d.mts +14 -1525
  48. package/dist/index.mjs +14 -3695
  49. package/dist/migrations-DZKTNU88.mjs +291 -0
  50. package/dist/migrations-DZKTNU88.mjs.map +1 -0
  51. package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
  52. package/package.json +1 -45
  53. package/src/DoMemoryStore.ts +56 -0
  54. package/src/DoMessageDeliveryStore.ts +53 -0
  55. package/src/DoScheduleStore.ts +622 -0
  56. package/src/{errors.ts → DoStorageError.ts} +10 -3
  57. package/src/DoStorageFailpoint.ts +20 -0
  58. package/src/{do-storage-failpoint.ts → DoStorageFailpointTesting.ts} +5 -18
  59. package/src/DoStorageVersion.ts +2 -0
  60. package/src/{do-ledger.ts → DoSubmissionLedger.ts} +960 -237
  61. package/src/DoSubscriptionStore.ts +329 -0
  62. package/src/DoThreadStore.ts +1039 -0
  63. package/src/MemoryProtocol.ts +351 -0
  64. package/src/{port-protocol.ts → PortProtocol.ts} +76 -38
  65. package/src/{routing.ts → PortRouting.ts} +374 -264
  66. package/src/index.ts +13 -32
  67. package/src/internal/do-journal.ts +1656 -0
  68. package/src/internal/message-delivery-schema.ts +24 -0
  69. package/src/{migrations.ts → internal/migrations.ts} +55 -35
  70. package/src/internal/recovery-checkpoint-schema.ts +17 -0
  71. package/dist/index.mjs.map +0 -1
  72. package/src/do-conversation-store.ts +0 -850
  73. package/src/do-journal.ts +0 -1078
  74. /package/src/{do-storage-config.ts → DoStorageConfig.ts} +0 -0
@@ -1,25 +1,31 @@
1
+ import { Context, Effect, Layer, Option, Predicate, Schema, Stream } from "effect";
2
+ import { MessageDeliveryStore, MessageDeliveryError } from "effect-agent/message-delivery";
1
3
  import {
2
4
  AdmissionIndeterminate,
3
- AppendConflict,
5
+ AdmissionConflict,
6
+ AdmissionPolicyError,
4
7
  ChildAttachmentSnapshot,
5
- ConversationMaterialization,
6
- ConversationNotMaterialized,
7
- ConversationStore,
8
- ConversationStoreError,
9
- FenceRejected,
10
8
  JoinedToHost,
11
9
  LedgerError,
12
10
  RecoverySnapshot,
13
11
  SettlementConflict,
14
12
  SubmissionLedger,
15
13
  SubmissionLookupById,
16
- type AdmissionConflict,
17
14
  type SubmissionLookupByKey,
18
15
  type SubmissionSnapshot,
19
- } from "@effect-agent/session";
20
- import { Context, Effect, Layer, Option, Schema, Stream } from "effect";
16
+ } from "effect-agent/submission-ledger";
17
+ import {
18
+ AppendConflict,
19
+ ThreadMaterialization,
20
+ ThreadNotMaterialized,
21
+ ThreadStore,
22
+ ThreadStoreError,
23
+ FenceRejected,
24
+ } from "effect-agent/thread-store";
21
25
 
22
26
  import {
27
+ MessageDeliveryListCall,
28
+ MessageDeliveryListResult,
23
29
  boundPortDiagnostic,
24
30
  decodePortRequest,
25
31
  decodePortResponse,
@@ -42,6 +48,8 @@ import {
42
48
  PortSucceeded,
43
49
  StoreAppendCall,
44
50
  StoreAppendResult,
51
+ StoreCountPeerMessagesCall,
52
+ StoreCountPeerMessagesResult,
45
53
  StoreExportCall,
46
54
  StoreExportResult,
47
55
  StoreInspectTailCall,
@@ -55,13 +63,13 @@ import {
55
63
  type PortRequestEnvelope,
56
64
  type PortResponse,
57
65
  type PortResult,
58
- } from "./port-protocol.ts";
66
+ } from "./PortProtocol.ts";
59
67
 
60
- type ConversationId = ConversationMaterialization["conversationId"];
68
+ type ThreadId = ThreadMaterialization["threadId"];
61
69
  type SubmissionId = SubmissionSnapshot["submissionId"];
62
70
 
63
- const ConversationIdSchema = ConversationMaterialization.fields.conversationId;
64
- const decodeConversationId = Schema.decodeUnknownEffect(ConversationIdSchema);
71
+ const ThreadIdSchema = ThreadMaterialization.fields.threadId;
72
+ const decodeThreadId = Schema.decodeUnknownEffect(ThreadIdSchema);
65
73
 
66
74
  /**
67
75
  * The ledger row bound routable Submission identities must respect (mirrors the local
@@ -76,7 +84,7 @@ const UUID_HEAD_PATTERN =
76
84
 
77
85
  /**
78
86
  * A transport could not deliver a port request to (or an answer from) the owning
79
- * Conversation's Durable Object. `retryable` carries the platform's own stub signal when one
87
+ * Thread's Durable Object. `retryable` carries the platform's own stub signal when one
80
88
  * exists. This error never crosses the wire — it is the CALLER-side evidence that the
81
89
  * authority was unreachable, which is exactly the case `AdmissionIndeterminate` was
82
90
  * specified for (SUB-031).
@@ -91,20 +99,37 @@ export class PortTransportError extends Schema.TaggedError<PortTransportError>()
91
99
  },
92
100
  ) {}
93
101
 
102
+ const safeTransportDiagnostic = (cause: unknown): string => {
103
+ try {
104
+ const message = cause instanceof Error ? cause.message : cause;
105
+
106
+ return boundPortDiagnostic(typeof message === "string" ? message : String(message));
107
+ } catch {
108
+ return "[unavailable transport diagnostic]";
109
+ }
110
+ };
111
+
112
+ const transportRetryableSignal = (cause: unknown): boolean | undefined => {
113
+ if (!Predicate.isObjectKeyword(cause)) return undefined;
114
+ try {
115
+ const signal = Reflect.get(cause, "retryable");
116
+
117
+ return typeof signal === "boolean" ? signal : undefined;
118
+ } catch {
119
+ return undefined;
120
+ }
121
+ };
122
+
94
123
  /**
95
124
  * Build a `PortTransportError` from an arbitrary thrown transport cause, preserving the
96
125
  * platform stub's own `retryable` signal when present.
97
126
  */
98
127
  export const portTransportFailure = (target: string, cause: unknown): PortTransportError => {
99
- const message = cause instanceof Error ? cause.message : String(cause);
100
- let retryable: boolean | undefined;
101
- if (typeof cause === "object" && cause !== null && "retryable" in cause) {
102
- const signal = cause.retryable;
103
- if (typeof signal === "boolean") retryable = signal;
104
- }
128
+ const retryable = transportRetryableSignal(cause);
129
+
105
130
  return PortTransportError.make({
106
131
  target,
107
- message: boundPortDiagnostic(message),
132
+ message: safeTransportDiagnostic(cause),
108
133
  ...(retryable === undefined ? {} : { retryable }),
109
134
  cause,
110
135
  });
@@ -112,42 +137,42 @@ export const portTransportFailure = (target: string, cause: unknown): PortTransp
112
137
 
113
138
  /**
114
139
  * Delivery of Schema-encoded port envelopes to the Durable Object that owns a FOREIGN
115
- * Conversation (plan §1.3, D-P6-3). The shipped implementation (platform-cloudflare, WP3)
140
+ * Thread (plan §1.3, D-P6-3). The shipped implementation (platform-cloudflare, WP3)
116
141
  * calls the owner's `portCall` over native Durable Object JS RPC via
117
- * `namespace.idFromName(conversationId)`; the protocol is transport-agnostic and any carrier
142
+ * `namespace.idFromName(threadId)`; the protocol is transport-agnostic and any carrier
118
143
  * that moves the encoded envelopes verbatim satisfies this service. Implementations MUST
119
144
  * surface every delivery problem as `PortTransportError` and must never fabricate an answer.
120
145
  */
121
- export class ConversationPortTransport extends Context.Service<
122
- ConversationPortTransport,
146
+ export class ThreadPortTransport extends Context.Service<
147
+ ThreadPortTransport,
123
148
  {
124
149
  readonly call: (
125
- conversationId: ConversationId,
150
+ threadId: ThreadId,
126
151
  request: PortRequestEnvelope,
127
152
  ) => Effect.Effect<unknown, PortTransportError>;
128
153
  }
129
- >()("@effect-agent/storage-cloudflare/ConversationPortTransport") {}
154
+ >()("@effect-agent/storage-cloudflare/ThreadPortTransport") {}
130
155
 
131
156
  /** Construction options shared by both routed port Layers. */
132
157
  export interface RoutedPortOptions {
133
158
  /**
134
- * The Conversation this Durable Object owns (the Object identity rule is
135
- * `namespace.idFromName(conversationId)`). Requests addressed here execute on the local
136
- * facet; requests addressed anywhere else route through the transport or fail fast typed.
159
+ * Decide whether this physical owner stores the addressed logical Thread. Local
160
+ * requests use the same SQLite facets; foreign requests use the transport or fail
161
+ * fast typed. The predicate determines placement, never caller authorization.
137
162
  */
138
- readonly localConversationId: ConversationId;
163
+ readonly ownsThread: (threadId: ThreadId) => boolean;
139
164
  }
140
165
 
141
166
  /** Where one port request must execute. */
142
167
  type RouteTarget =
143
168
  | { readonly _tag: "local" }
144
- | { readonly _tag: "foreign"; readonly conversationId: ConversationId };
169
+ | { readonly _tag: "foreign"; readonly threadId: ThreadId };
145
170
 
146
171
  const LOCAL: RouteTarget = { _tag: "local" };
147
172
 
148
173
  /**
149
- * Parse a DC-minted routable Submission identity — `{uuidv7}:{conversationId}`, split at the
150
- * FIRST `:` because the Conversation tail may itself contain colons (D-P6-5). This adapter
174
+ * Parse a DC-minted routable Submission identity — `{uuidv7}:{threadId}`, split at the
175
+ * FIRST `:` because the Thread tail may itself contain colons (D-P6-5). This adapter
151
176
  * minted the format at admission and is the ONLY component that parses it; identities that do
152
177
  * not carry the minted shape (no separator, non-UUID head, empty tail) fall back to the local
153
178
  * facet, which is the only authority this Object can consult without inventing an owner.
@@ -155,7 +180,7 @@ const LOCAL: RouteTarget = { _tag: "local" };
155
180
  * refused them at admission, so they cannot name any stored row anywhere.
156
181
  */
157
182
  const routableSubmissionTarget = (
158
- localConversationId: ConversationId,
183
+ ownsThread: RoutedPortOptions["ownsThread"],
159
184
  ): ((operation: string, submissionId: string) => Effect.Effect<RouteTarget, LedgerError>) =>
160
185
  Effect.fn("DoPortRouting.routableSubmissionTarget")(function* (
161
186
  operation: string,
@@ -171,63 +196,49 @@ const routableSubmissionTarget = (
171
196
  });
172
197
  }
173
198
  const separator = submissionId.indexOf(":");
199
+
174
200
  if (separator === -1) return LOCAL;
175
201
  if (!UUID_HEAD_PATTERN.test(submissionId.slice(0, separator))) return LOCAL;
176
202
  const tail = submissionId.slice(separator + 1);
177
- if (tail === localConversationId) return LOCAL;
178
- return yield* decodeConversationId(tail).pipe(
179
- Effect.map((conversationId): RouteTarget => ({ _tag: "foreign", conversationId })),
203
+
204
+ return yield* decodeThreadId(tail).pipe(
205
+ Effect.map((threadId): RouteTarget =>
206
+ ownsThread(threadId) ? LOCAL : { _tag: "foreign", threadId },
207
+ ),
180
208
  Effect.orElseSucceed(() => LOCAL),
181
209
  );
182
210
  });
183
211
 
184
- const isResultTag =
185
- <Tag extends PortResult["_tag"]>(tag: Tag) =>
186
- (result: PortResult): result is Extract<PortResult, { readonly _tag: Tag }> =>
187
- result._tag === tag;
188
-
189
- const isAdmitConflict = (failure: PortFailure): failure is AdmissionConflict =>
190
- failure._tag === "AdmissionConflict";
191
- const isAbortConflict = (failure: PortFailure): failure is SettlementConflict | JoinedToHost =>
192
- failure._tag === "SettlementConflict" || failure._tag === "JoinedToHost";
193
- const noExtraFailure = (_failure: PortFailure): _failure is never => false;
194
- const isFenceRejected = (failure: PortFailure): failure is FenceRejected =>
195
- failure._tag === "FenceRejected";
196
- const isAppendFailure = (
197
- failure: PortFailure,
198
- ): failure is ConversationNotMaterialized | AppendConflict | FenceRejected =>
199
- failure._tag === "ConversationNotMaterialized" ||
200
- failure._tag === "AppendConflict" ||
201
- failure._tag === "FenceRejected";
202
- const isNotMaterialized = (failure: PortFailure): failure is ConversationNotMaterialized =>
203
- failure._tag === "ConversationNotMaterialized";
212
+ const NoAdditionalPortFailure = Schema.Never;
213
+ const AbortPortFailure = Schema.Union([SettlementConflict, JoinedToHost]);
214
+ const AppendPortFailure = Schema.Union([ThreadNotMaterialized, AppendConflict, FenceRejected]);
204
215
 
205
216
  /**
206
217
  * The fail-fast refusal for any foreign operation OUTSIDE the closed route-capable subset
207
218
  * (plan §1.3): honesty over accidental distribution.
208
219
  */
209
- const crossConversationLedgerError = (operation: string, target: string): LedgerError =>
220
+ const crossThreadLedgerError = (operation: string, target: string): LedgerError =>
210
221
  LedgerError.make({
211
222
  operation,
212
223
  message:
213
- `${operation} addressed to foreign Conversation ${target} is not route-capable; the ` +
224
+ `${operation} addressed to foreign Thread ${target} is not route-capable; the ` +
214
225
  "closed cross-Object subset is admit, markReady, lookup, resolveAdmission, " +
215
226
  "requestAbort, and recordChildSettled. Every other ledger operation is lane-local by " +
216
- "construction and must execute inside the owning Conversation's Durable Object.",
227
+ "construction and must execute inside the owning Thread's Durable Object.",
217
228
  });
218
229
 
219
- const crossConversationStoreError = (operation: string, target: string): ConversationStoreError =>
220
- ConversationStoreError.make({
230
+ const crossThreadStoreError = (operation: string, target: string): ThreadStoreError =>
231
+ ThreadStoreError.make({
221
232
  operation,
222
233
  message:
223
- `${operation} addressed to foreign Conversation ${target} is not route-capable; the ` +
234
+ `${operation} addressed to foreign Thread ${target} is not route-capable; the ` +
224
235
  "closed cross-Object subset is materialize, append, read (paged), inspectTail, and " +
225
236
  "export. Observation and checkpoints are lane-local by construction and must execute " +
226
- "inside the owning Conversation's Durable Object.",
237
+ "inside the owning Thread's Durable Object.",
227
238
  });
228
239
 
229
- const makeTransportCall = (transport: ConversationPortTransport["Service"]) =>
230
- Effect.fn("DoPortRouting.transportCall")(function* (target: ConversationId, call: PortRequest) {
240
+ const makeTransportCall = (transport: ThreadPortTransport["Service"]) =>
241
+ Effect.fn("DoPortRouting.transportCall")(function* (target: ThreadId, call: PortRequest) {
231
242
  const encoded = yield* encodePortRequest(call).pipe(
232
243
  Effect.mapError((error) =>
233
244
  PortProtocolError.make({
@@ -235,7 +246,9 @@ const makeTransportCall = (transport: ConversationPortTransport["Service"]) =>
235
246
  }),
236
247
  ),
237
248
  );
249
+
238
250
  const raw = yield* transport.call(target, encoded);
251
+
239
252
  return yield* decodePortResponse(raw).pipe(
240
253
  Effect.mapError((error) =>
241
254
  PortProtocolError.make({
@@ -251,9 +264,9 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
251
264
  options: RoutedPortOptions,
252
265
  ) {
253
266
  const local = yield* SubmissionLedger;
254
- const transport = yield* ConversationPortTransport;
267
+ const transport = yield* ThreadPortTransport;
255
268
  const transportCall: TransportCall = makeTransportCall(transport);
256
- const submissionTarget = routableSubmissionTarget(options.localConversationId);
269
+ const submissionTarget = routableSubmissionTarget(options.ownsThread);
257
270
 
258
271
  const routeFailure =
259
272
  (operation: string, target: string) =>
@@ -261,7 +274,7 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
261
274
  LedgerError.make({
262
275
  operation,
263
276
  message: boundPortDiagnostic(
264
- `Routed ${operation} to the Conversation Object owning ${target} failed: ${error.message}`,
277
+ `Routed ${operation} to the Thread Object owning ${target} failed: ${error.message}`,
265
278
  ),
266
279
  cause: error,
267
280
  });
@@ -272,48 +285,50 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
272
285
  * any failure outside the operation's declared surface — including protocol anomalies — is
273
286
  * folded into a `LedgerError` naming the anomaly instead of being erased or re-thrown raw.
274
287
  */
275
- const foreignLedgerCall = <Tag extends PortResult["_tag"], ExpectedFailure extends PortFailure>(
288
+ const foreignLedgerCall = <ResultSchema extends Schema.Top, FailureSchema extends Schema.Top>(
276
289
  operation: string,
277
- target: ConversationId,
290
+ target: ThreadId,
278
291
  call: PortRequest,
279
- resultTag: Tag,
280
- isExpectedFailure: (failure: PortFailure) => failure is ExpectedFailure,
281
- ): Effect.Effect<Extract<PortResult, { readonly _tag: Tag }>, ExpectedFailure | LedgerError> =>
282
- transportCall(target, call).pipe(
292
+ resultSchema: ResultSchema,
293
+ failureSchema: FailureSchema,
294
+ ): Effect.Effect<ResultSchema["Type"], FailureSchema["Type"] | LedgerError> => {
295
+ const isExpectedResult = Schema.is(resultSchema);
296
+ const isExpectedFailure = Schema.is(failureSchema);
297
+
298
+ return transportCall(target, call).pipe(
283
299
  Effect.mapError(routeFailure(operation, target)),
284
300
  Effect.flatMap(
285
- (
286
- response,
287
- ): Effect.Effect<
288
- Extract<PortResult, { readonly _tag: Tag }>,
289
- ExpectedFailure | LedgerError
290
- > => {
301
+ (response): Effect.Effect<ResultSchema["Type"], FailureSchema["Type"] | LedgerError> => {
291
302
  if (response._tag === "PortFailed") {
292
303
  const failure = response.failure;
304
+
293
305
  if (isExpectedFailure(failure)) return Effect.fail(failure);
294
306
  if (failure._tag === "LedgerError") return Effect.fail(failure);
307
+
295
308
  return Effect.fail(
296
309
  LedgerError.make({
297
310
  operation,
298
311
  message: boundPortDiagnostic(
299
- `The Conversation Object owning ${target} answered ${operation} with the ` +
300
- `out-of-contract failure ${failure._tag}: ${failure.message}`,
312
+ `The Thread Object owning ${target} answered ${operation} with the ` +
313
+ `out-of-contract failure ${failure._tag}`,
301
314
  ),
302
315
  cause: failure,
303
316
  }),
304
317
  );
305
318
  }
306
319
  const result = response.result;
307
- if (!isResultTag(resultTag)(result)) {
320
+
321
+ if (!isExpectedResult(result)) {
308
322
  return Effect.fail(
309
323
  LedgerError.make({
310
324
  operation,
311
325
  message:
312
- `The Conversation Object owning ${target} answered ${operation} with the ` +
313
- `mismatched result ${result._tag}; expected ${resultTag}.`,
326
+ `The Thread Object owning ${target} answered ${operation} with the ` +
327
+ `mismatched result ${result._tag}.`,
314
328
  }),
315
329
  );
316
330
  }
331
+
317
332
  return Effect.succeed(result);
318
333
  },
319
334
  ),
@@ -321,6 +336,7 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
321
336
  attributes: { operation, target },
322
337
  }),
323
338
  );
339
+ };
324
340
 
325
341
  /**
326
342
  * Routed `resolveAdmission` — where the S2 tri-state becomes real (plan §1.3): when the
@@ -331,7 +347,7 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
331
347
  * reached and reported its own storage failure.
332
348
  */
333
349
  const resolveForeignAdmission = (
334
- target: ConversationId,
350
+ target: ThreadId,
335
351
  request: SubmissionLookupByKey,
336
352
  ): Effect.Effect<
337
353
  | AdmissionIndeterminate
@@ -342,10 +358,11 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
342
358
  Effect.flatMap((response) => {
343
359
  if (response._tag === "PortFailed") {
344
360
  if (response.failure._tag === "LedgerError") return Effect.fail(response.failure);
361
+
345
362
  return Effect.succeed(
346
363
  AdmissionIndeterminate.make({
347
364
  reason: boundPortDiagnostic(
348
- `The Conversation Object owning ${target} answered resolveAdmission with the ` +
365
+ `The Thread Object owning ${target} answered resolveAdmission with the ` +
349
366
  `out-of-contract failure ${response.failure._tag}: ${response.failure.message}`,
350
367
  ),
351
368
  }),
@@ -355,12 +372,13 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
355
372
  return Effect.succeed(
356
373
  AdmissionIndeterminate.make({
357
374
  reason: boundPortDiagnostic(
358
- `The Conversation Object owning ${target} answered resolveAdmission with the ` +
375
+ `The Thread Object owning ${target} answered resolveAdmission with the ` +
359
376
  `mismatched result ${response.result._tag}.`,
360
377
  ),
361
378
  }),
362
379
  );
363
380
  }
381
+
364
382
  return Effect.succeed(response.result.resolution);
365
383
  }),
366
384
  Effect.catchTags({
@@ -368,7 +386,7 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
368
386
  Effect.succeed(
369
387
  AdmissionIndeterminate.make({
370
388
  reason: boundPortDiagnostic(
371
- `The Conversation Object owning ${target} is unreachable: ${error.message}`,
389
+ `The Thread Object owning ${target} is unreachable: ${error.message}`,
372
390
  ),
373
391
  }),
374
392
  ),
@@ -376,7 +394,7 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
376
394
  Effect.succeed(
377
395
  AdmissionIndeterminate.make({
378
396
  reason: boundPortDiagnostic(
379
- `The answer of the Conversation Object owning ${target} could not be ` +
397
+ `The answer of the Thread Object owning ${target} could not be ` +
380
398
  `understood: ${error.message}`,
381
399
  ),
382
400
  }),
@@ -387,15 +405,15 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
387
405
 
388
406
  const foreignLookupById = (
389
407
  operation: string,
390
- target: ConversationId,
408
+ target: ThreadId,
391
409
  submissionId: SubmissionId,
392
410
  ): Effect.Effect<Option.Option<SubmissionSnapshot>, LedgerError> =>
393
411
  foreignLedgerCall(
394
412
  operation,
395
413
  target,
396
414
  LedgerLookupCall.make({ request: SubmissionLookupById.make({ submissionId }) }),
397
- "LedgerLookupResult",
398
- noExtraFailure,
415
+ LedgerLookupResult,
416
+ NoAdditionalPortFailure,
399
417
  ).pipe(
400
418
  Effect.map((result) =>
401
419
  result.submission === undefined ? Option.none() : Option.some(result.submission),
@@ -413,18 +431,24 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
413
431
  snapshot: RecoverySnapshot,
414
432
  ): Effect.fn.Return<RecoverySnapshot, LedgerError> {
415
433
  const operation = "ledger load recovery snapshot";
434
+
416
435
  const attachments = new Map(
417
436
  snapshot.childAttachments.map((attachment) => [attachment.childSubmissionId, attachment]),
418
437
  );
438
+
419
439
  let enriched = false;
440
+
420
441
  for (const reservation of snapshot.childReservations) {
421
442
  const childSubmissionId = reservation.childSubmissionId;
443
+
422
444
  if (childSubmissionId === undefined || attachments.has(childSubmissionId)) continue;
423
445
  const target = yield* submissionTarget(operation, childSubmissionId);
446
+
424
447
  // A local or opaque child identity was already answered authoritatively by the local
425
448
  // facet; absence there means the child admission never committed.
426
449
  if (target._tag !== "foreign") continue;
427
- const child = yield* foreignLookupById(operation, target.conversationId, childSubmissionId);
450
+ const child = yield* foreignLookupById(operation, target.threadId, childSubmissionId);
451
+
428
452
  if (Option.isNone(child)) continue;
429
453
  attachments.set(
430
454
  childSubmissionId,
@@ -442,11 +466,14 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
442
466
  if (!enriched) return snapshot;
443
467
  // Rebuild in reservation (parent Tool Call) order, the order the local facet documents.
444
468
  const ordered: Array<ChildAttachmentSnapshot> = [];
469
+
445
470
  for (const reservation of snapshot.childReservations) {
446
471
  if (reservation.childSubmissionId === undefined) continue;
447
472
  const attachment = attachments.get(reservation.childSubmissionId);
473
+
448
474
  if (attachment !== undefined) ordered.push(attachment);
449
475
  }
476
+
450
477
  return RecoverySnapshot.make({ ...snapshot, childAttachments: ordered });
451
478
  });
452
479
 
@@ -454,14 +481,14 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
454
481
  capabilities: local.capabilities,
455
482
 
456
483
  admit: (request) =>
457
- request.conversationId === options.localConversationId
484
+ options.ownsThread(request.threadId)
458
485
  ? local.admit(request)
459
486
  : foreignLedgerCall(
460
487
  "ledger admit",
461
- request.conversationId,
488
+ request.threadId,
462
489
  LedgerAdmitCall.make({ request }),
463
- "LedgerAdmitResult",
464
- isAdmitConflict,
490
+ LedgerAdmitResult,
491
+ Schema.Union([AdmissionConflict, AdmissionPolicyError]),
465
492
  ).pipe(Effect.map((reply) => reply.result)),
466
493
 
467
494
  markReady: (request) =>
@@ -471,10 +498,10 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
471
498
  ? local.markReady(request)
472
499
  : foreignLedgerCall(
473
500
  "ledger mark ready",
474
- target.conversationId,
501
+ target.threadId,
475
502
  LedgerMarkReadyCall.make({ request }),
476
- "LedgerMarkReadyResult",
477
- noExtraFailure,
503
+ LedgerMarkReadyResult,
504
+ NoAdditionalPortFailure,
478
505
  ).pipe(Effect.asVoid),
479
506
  ),
480
507
  ),
@@ -485,17 +512,17 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
485
512
  Effect.flatMap((target) =>
486
513
  target._tag === "local"
487
514
  ? local.lookup(request)
488
- : foreignLookupById("ledger lookup", target.conversationId, request.submissionId),
515
+ : foreignLookupById("ledger lookup", target.threadId, request.submissionId),
489
516
  ),
490
517
  )
491
- : request.conversationId === options.localConversationId
518
+ : options.ownsThread(request.threadId)
492
519
  ? local.lookup(request)
493
520
  : foreignLedgerCall(
494
521
  "ledger lookup",
495
- request.conversationId,
522
+ request.threadId,
496
523
  LedgerLookupCall.make({ request }),
497
- "LedgerLookupResult",
498
- noExtraFailure,
524
+ LedgerLookupResult,
525
+ NoAdditionalPortFailure,
499
526
  ).pipe(
500
527
  Effect.map((result) =>
501
528
  result.submission === undefined ? Option.none() : Option.some(result.submission),
@@ -503,9 +530,9 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
503
530
  ),
504
531
 
505
532
  resolveAdmission: (request) =>
506
- request.conversationId === options.localConversationId
533
+ options.ownsThread(request.threadId)
507
534
  ? local.resolveAdmission(request)
508
- : resolveForeignAdmission(request.conversationId, request),
535
+ : resolveForeignAdmission(request.threadId, request),
509
536
 
510
537
  requestAbort: (request) =>
511
538
  submissionTarget("ledger request abort", request.submissionId).pipe(
@@ -514,10 +541,10 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
514
541
  ? local.requestAbort(request)
515
542
  : foreignLedgerCall(
516
543
  "ledger request abort",
517
- target.conversationId,
544
+ target.threadId,
518
545
  LedgerRequestAbortCall.make({ request }),
519
- "LedgerRequestAbortResult",
520
- isAbortConflict,
546
+ LedgerRequestAbortResult,
547
+ AbortPortFailure,
521
548
  ).pipe(Effect.map((reply) => reply.intent)),
522
549
  ),
523
550
  ),
@@ -529,10 +556,10 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
529
556
  ? local.recordChildSettled(request)
530
557
  : foreignLedgerCall(
531
558
  "ledger record child settled",
532
- target.conversationId,
559
+ target.threadId,
533
560
  LedgerRecordChildSettledCall.make({ request }),
534
- "LedgerRecordChildSettledResult",
535
- noExtraFailure,
561
+ LedgerRecordChildSettledResult,
562
+ NoAdditionalPortFailure,
536
563
  ).pipe(Effect.map((reply) => reply.outcome)),
537
564
  ),
538
565
  ),
@@ -540,23 +567,21 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
540
567
  // Every operation below is lane-local by construction (plan §1.3): a foreign address is
541
568
  // an out-of-contract call and fails fast typed instead of being quietly distributed.
542
569
  claim: (request) =>
543
- request.conversationId === options.localConversationId
570
+ options.ownsThread(request.threadId)
544
571
  ? local.claim(request)
545
- : Effect.fail(crossConversationLedgerError("ledger claim", request.conversationId)),
572
+ : Effect.fail(crossThreadLedgerError("ledger claim", request.threadId)),
546
573
 
547
574
  claimJoining: (request) =>
548
- request.conversationId === options.localConversationId
575
+ options.ownsThread(request.threadId)
549
576
  ? local.claimJoining(request)
550
- : Effect.fail(crossConversationLedgerError("ledger claim joining", request.conversationId)),
577
+ : Effect.fail(crossThreadLedgerError("ledger claim joining", request.threadId)),
551
578
 
552
579
  renewOwnership: (request) =>
553
580
  submissionTarget("ledger renew ownership", request.submissionId).pipe(
554
581
  Effect.flatMap((target) =>
555
582
  target._tag === "local"
556
583
  ? local.renewOwnership(request)
557
- : Effect.fail(
558
- crossConversationLedgerError("ledger renew ownership", target.conversationId),
559
- ),
584
+ : Effect.fail(crossThreadLedgerError("ledger renew ownership", target.threadId)),
560
585
  ),
561
586
  ),
562
587
 
@@ -565,9 +590,7 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
565
590
  Effect.flatMap((target) =>
566
591
  target._tag === "local"
567
592
  ? local.releaseOwnership(request)
568
- : Effect.fail(
569
- crossConversationLedgerError("ledger release ownership", target.conversationId),
570
- ),
593
+ : Effect.fail(crossThreadLedgerError("ledger release ownership", target.threadId)),
571
594
  ),
572
595
  ),
573
596
 
@@ -576,9 +599,7 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
576
599
  Effect.flatMap((target) =>
577
600
  target._tag === "local"
578
601
  ? local.markInputApplied(request)
579
- : Effect.fail(
580
- crossConversationLedgerError("ledger mark input applied", target.conversationId),
581
- ),
602
+ : Effect.fail(crossThreadLedgerError("ledger mark input applied", target.threadId)),
582
603
  ),
583
604
  ),
584
605
 
@@ -587,9 +608,7 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
587
608
  Effect.flatMap((target) =>
588
609
  target._tag === "local"
589
610
  ? local.reserveSettlement(request)
590
- : Effect.fail(
591
- crossConversationLedgerError("ledger reserve settlement", target.conversationId),
592
- ),
611
+ : Effect.fail(crossThreadLedgerError("ledger reserve settlement", target.threadId)),
593
612
  ),
594
613
  ),
595
614
 
@@ -598,9 +617,7 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
598
617
  Effect.flatMap((target) =>
599
618
  target._tag === "local"
600
619
  ? local.finalizeSettlement(request)
601
- : Effect.fail(
602
- crossConversationLedgerError("ledger finalize settlement", target.conversationId),
603
- ),
620
+ : Effect.fail(crossThreadLedgerError("ledger finalize settlement", target.threadId)),
604
621
  ),
605
622
  ),
606
623
 
@@ -609,9 +626,7 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
609
626
  Effect.flatMap((target) =>
610
627
  target._tag === "local"
611
628
  ? local.markJoined(request)
612
- : Effect.fail(
613
- crossConversationLedgerError("ledger mark joined", target.conversationId),
614
- ),
629
+ : Effect.fail(crossThreadLedgerError("ledger mark joined", target.threadId)),
615
630
  ),
616
631
  ),
617
632
 
@@ -620,9 +635,7 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
620
635
  Effect.flatMap((target) =>
621
636
  target._tag === "local"
622
637
  ? local.revertJoining(request)
623
- : Effect.fail(
624
- crossConversationLedgerError("ledger revert joining", target.conversationId),
625
- ),
638
+ : Effect.fail(crossThreadLedgerError("ledger revert joining", target.threadId)),
626
639
  ),
627
640
  ),
628
641
 
@@ -631,7 +644,7 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
631
644
  Effect.flatMap((target) =>
632
645
  target._tag === "local"
633
646
  ? local.suspend(request)
634
- : Effect.fail(crossConversationLedgerError("ledger suspend", target.conversationId)),
647
+ : Effect.fail(crossThreadLedgerError("ledger suspend", target.threadId)),
635
648
  ),
636
649
  ),
637
650
 
@@ -641,10 +654,7 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
641
654
  target._tag === "local"
642
655
  ? local.recordApprovalDecision(command)
643
656
  : Effect.fail(
644
- crossConversationLedgerError(
645
- "ledger record approval decision",
646
- target.conversationId,
647
- ),
657
+ crossThreadLedgerError("ledger record approval decision", target.threadId),
648
658
  ),
649
659
  ),
650
660
  ),
@@ -654,9 +664,7 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
654
664
  Effect.flatMap((target) =>
655
665
  target._tag === "local"
656
666
  ? local.markUnknown(request)
657
- : Effect.fail(
658
- crossConversationLedgerError("ledger mark unknown", target.conversationId),
659
- ),
667
+ : Effect.fail(crossThreadLedgerError("ledger mark unknown", target.threadId)),
660
668
  ),
661
669
  ),
662
670
 
@@ -666,10 +674,7 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
666
674
  target._tag === "local"
667
675
  ? local.recordUnknownResolution(command)
668
676
  : Effect.fail(
669
- crossConversationLedgerError(
670
- "ledger record unknown resolution",
671
- target.conversationId,
672
- ),
677
+ crossThreadLedgerError("ledger record unknown resolution", target.threadId),
673
678
  ),
674
679
  ),
675
680
  ),
@@ -679,33 +684,35 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
679
684
  Effect.flatMap((target) =>
680
685
  target._tag === "local"
681
686
  ? local.reserveChildBudget(request)
682
- : Effect.fail(
683
- crossConversationLedgerError("ledger reserve child budget", target.conversationId),
684
- ),
687
+ : Effect.fail(crossThreadLedgerError("ledger reserve child budget", target.threadId)),
685
688
  ),
686
689
  ),
687
690
 
688
- // Reservation identities carry no Conversation address; the reservation row lives in the
691
+ // Reservation identities carry no Thread address; the reservation row lives in the
689
692
  // parent's own Object and these transitions are parent-lane-local by construction, so
690
693
  // they always execute on the local facet (which fails typed for an unknown row).
691
694
  attachChildToReservation: local.attachChildToReservation,
692
695
  beginChildBudgetRelease: local.beginChildBudgetRelease,
693
696
  releaseChildBudget: local.releaseChildBudget,
694
697
 
695
- // The local scan IS the whole worklist: one Conversation per Object (durability §5).
698
+ // The local scan IS the whole worklist: one Thread per Object (durability §5).
696
699
  scanNonterminal: local.scanNonterminal,
697
700
 
701
+ readAbortIntent: (request) =>
702
+ submissionTarget("ledger read abort intent", request.submissionId).pipe(
703
+ Effect.flatMap((target) =>
704
+ target._tag === "local"
705
+ ? local.readAbortIntent(request)
706
+ : Effect.fail(crossThreadLedgerError("ledger read abort intent", target.threadId)),
707
+ ),
708
+ ),
709
+
698
710
  loadRecoverySnapshot: (request) =>
699
711
  submissionTarget("ledger load recovery snapshot", request.submissionId).pipe(
700
712
  Effect.flatMap((target) =>
701
713
  target._tag === "local"
702
714
  ? local.loadRecoverySnapshot(request).pipe(Effect.flatMap(enrichChildAttachments))
703
- : Effect.fail(
704
- crossConversationLedgerError(
705
- "ledger load recovery snapshot",
706
- target.conversationId,
707
- ),
708
- ),
715
+ : Effect.fail(crossThreadLedgerError("ledger load recovery snapshot", target.threadId)),
709
716
  ),
710
717
  ),
711
718
  });
@@ -716,67 +723,70 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
716
723
  const makeRoutedStoreServices = Effect.fn("DoPortRouting.makeRoutedStoreServices")(function* (
717
724
  options: RoutedPortOptions,
718
725
  ) {
719
- const local = yield* ConversationStore;
720
- const transport = yield* ConversationPortTransport;
726
+ const local = yield* ThreadStore;
727
+ const checkpoints = local.checkpoints;
728
+ const recoveryCheckpoints = local.recoveryCheckpoints;
729
+ const transport = yield* ThreadPortTransport;
721
730
  const transportCall: TransportCall = makeTransportCall(transport);
722
731
 
723
732
  const routeFailure =
724
733
  (operation: string, target: string) =>
725
- (error: PortTransportError | PortProtocolError): ConversationStoreError =>
726
- ConversationStoreError.make({
734
+ (error: PortTransportError | PortProtocolError): ThreadStoreError =>
735
+ ThreadStoreError.make({
727
736
  operation,
728
737
  message: boundPortDiagnostic(
729
- `Routed ${operation} to the Conversation Object owning ${target} failed: ${error.message}`,
738
+ `Routed ${operation} to the Thread Object owning ${target} failed: ${error.message}`,
730
739
  ),
731
740
  cause: error,
732
741
  });
733
742
 
734
- /** The store twin of `foreignLedgerCall` with `ConversationStoreError` as the base error. */
735
- const foreignStoreCall = <Tag extends PortResult["_tag"], ExpectedFailure extends PortFailure>(
743
+ /** The store twin of `foreignLedgerCall` with `ThreadStoreError` as the base error. */
744
+ const foreignStoreCall = <ResultSchema extends Schema.Top, FailureSchema extends Schema.Top>(
736
745
  operation: string,
737
- target: ConversationId,
746
+ target: ThreadId,
738
747
  call: PortRequest,
739
- resultTag: Tag,
740
- isExpectedFailure: (failure: PortFailure) => failure is ExpectedFailure,
741
- ): Effect.Effect<
742
- Extract<PortResult, { readonly _tag: Tag }>,
743
- ExpectedFailure | ConversationStoreError
744
- > =>
745
- transportCall(target, call).pipe(
748
+ resultSchema: ResultSchema,
749
+ failureSchema: FailureSchema,
750
+ ): Effect.Effect<ResultSchema["Type"], FailureSchema["Type"] | ThreadStoreError> => {
751
+ const isExpectedResult = Schema.is(resultSchema);
752
+ const isExpectedFailure = Schema.is(failureSchema);
753
+
754
+ return transportCall(target, call).pipe(
746
755
  Effect.mapError(routeFailure(operation, target)),
747
756
  Effect.flatMap(
748
757
  (
749
758
  response,
750
- ): Effect.Effect<
751
- Extract<PortResult, { readonly _tag: Tag }>,
752
- ExpectedFailure | ConversationStoreError
753
- > => {
759
+ ): Effect.Effect<ResultSchema["Type"], FailureSchema["Type"] | ThreadStoreError> => {
754
760
  if (response._tag === "PortFailed") {
755
761
  const failure = response.failure;
762
+
756
763
  if (isExpectedFailure(failure)) return Effect.fail(failure);
757
- if (failure._tag === "ConversationStoreError") return Effect.fail(failure);
764
+ if (failure._tag === "ThreadStoreError") return Effect.fail(failure);
765
+
758
766
  return Effect.fail(
759
- ConversationStoreError.make({
767
+ ThreadStoreError.make({
760
768
  operation,
761
769
  message: boundPortDiagnostic(
762
- `The Conversation Object owning ${target} answered ${operation} with the ` +
763
- `out-of-contract failure ${failure._tag}: ${failure.message}`,
770
+ `The Thread Object owning ${target} answered ${operation} with the ` +
771
+ `out-of-contract failure ${failure._tag}`,
764
772
  ),
765
773
  cause: failure,
766
774
  }),
767
775
  );
768
776
  }
769
777
  const result = response.result;
770
- if (!isResultTag(resultTag)(result)) {
778
+
779
+ if (!isExpectedResult(result)) {
771
780
  return Effect.fail(
772
- ConversationStoreError.make({
781
+ ThreadStoreError.make({
773
782
  operation,
774
783
  message:
775
- `The Conversation Object owning ${target} answered ${operation} with the ` +
776
- `mismatched result ${result._tag}; expected ${resultTag}.`,
784
+ `The Thread Object owning ${target} answered ${operation} with the ` +
785
+ `mismatched result ${result._tag}.`,
777
786
  }),
778
787
  );
779
788
  }
789
+
780
790
  return Effect.succeed(result);
781
791
  },
782
792
  ),
@@ -784,120 +794,192 @@ const makeRoutedStoreServices = Effect.fn("DoPortRouting.makeRoutedStoreServices
784
794
  attributes: { operation, target },
785
795
  }),
786
796
  );
797
+ };
798
+
799
+ const routed = ThreadStore.of({
800
+ countPeerMessages: (request) =>
801
+ options.ownsThread(request.threadId)
802
+ ? local.countPeerMessages === undefined
803
+ ? Effect.fail(crossThreadStoreError("peer count unavailable", request.threadId))
804
+ : local.countPeerMessages(request)
805
+ : foreignStoreCall(
806
+ "thread countPeerMessages",
807
+ request.threadId,
808
+ StoreCountPeerMessagesCall.make({ request }),
809
+ StoreCountPeerMessagesResult,
810
+ ThreadNotMaterialized,
811
+ ).pipe(Effect.map((reply) => reply.count)),
787
812
 
788
- const routed = ConversationStore.of({
789
813
  materialize: (request) =>
790
- request.conversationId === options.localConversationId
814
+ options.ownsThread(request.threadId)
791
815
  ? local.materialize(request)
792
816
  : foreignStoreCall(
793
- "conversation materialize",
794
- request.conversationId,
817
+ "thread materialize",
818
+ request.threadId,
795
819
  StoreMaterializeCall.make({ request }),
796
- "StoreMaterializeResult",
797
- isFenceRejected,
820
+ StoreMaterializeResult,
821
+ FenceRejected,
798
822
  ).pipe(Effect.asVoid),
799
823
 
800
824
  append: (request) =>
801
- request.conversationId === options.localConversationId
825
+ options.ownsThread(request.threadId)
802
826
  ? local.append(request)
803
827
  : foreignStoreCall(
804
- "conversation append",
805
- request.conversationId,
828
+ "thread append",
829
+ request.threadId,
806
830
  StoreAppendCall.make({ request }),
807
- "StoreAppendResult",
808
- isAppendFailure,
831
+ StoreAppendResult,
832
+ AppendPortFailure,
809
833
  ).pipe(Effect.map((reply) => reply.result)),
810
834
 
811
835
  read: (request) =>
812
- request.conversationId === options.localConversationId
836
+ options.ownsThread(request.threadId)
813
837
  ? local.read(request)
814
838
  : Stream.unwrap(
815
839
  foreignStoreCall(
816
- "conversation read",
817
- request.conversationId,
840
+ "thread read",
841
+ request.threadId,
818
842
  StoreReadPageCall.make({ request }),
819
- "StoreReadPageResult",
820
- isNotMaterialized,
843
+ StoreReadPageResult,
844
+ ThreadNotMaterialized,
821
845
  ).pipe(Effect.map((reply) => Stream.fromIterable(reply.records))),
822
846
  ),
823
847
 
824
848
  inspectTail: (request) =>
825
- request.conversationId === options.localConversationId
849
+ options.ownsThread(request.threadId)
826
850
  ? local.inspectTail(request)
827
851
  : foreignStoreCall(
828
- "conversation inspect tail",
829
- request.conversationId,
852
+ "thread inspect tail",
853
+ request.threadId,
830
854
  StoreInspectTailCall.make({ request }),
831
- "StoreInspectTailResult",
832
- isNotMaterialized,
855
+ StoreInspectTailResult,
856
+ ThreadNotMaterialized,
833
857
  ).pipe(Effect.map((reply) => reply.tail)),
834
858
 
835
859
  export: (request) =>
836
- request.conversationId === options.localConversationId
860
+ options.ownsThread(request.threadId)
837
861
  ? local.export(request)
838
862
  : foreignStoreCall(
839
- "conversation export",
840
- request.conversationId,
863
+ "thread export",
864
+ request.threadId,
841
865
  StoreExportCall.make({ request }),
842
- "StoreExportResult",
843
- isNotMaterialized,
866
+ StoreExportResult,
867
+ ThreadNotMaterialized,
844
868
  ).pipe(Effect.map((reply) => reply.export)),
845
869
 
846
- // Observation and checkpoints are lane-local by construction (plan §1.3): the closed
847
- // route-capable store subset is materialize/append/read/inspectTail/export, and a
848
- // foreign address on anything else fails fast typed.
870
+ // Observation and checkpoints are lane-local: the closed route-capable store subset
871
+ // is materialize/append/read/inspectTail/export. Recovery cache misses can fall back
872
+ // to those canonical reads, including when the cache belongs to a foreign Object.
849
873
  observe: (request) =>
850
- request.conversationId === options.localConversationId
874
+ options.ownsThread(request.threadId)
851
875
  ? local.observe(request)
852
- : Stream.unwrap(
853
- Effect.fail(
854
- crossConversationStoreError("conversation observe", request.conversationId),
855
- ),
856
- ),
857
-
858
- saveCheckpoint: (request) =>
859
- request.checkpoint.conversationId === options.localConversationId
860
- ? local.saveCheckpoint(request)
861
- : Effect.fail(
862
- crossConversationStoreError(
863
- "conversation save checkpoint",
864
- request.checkpoint.conversationId,
865
- ),
866
- ),
867
-
868
- loadCheckpoint: (request) =>
869
- request.conversationId === options.localConversationId
870
- ? local.loadCheckpoint(request)
871
- : Effect.fail(
872
- crossConversationStoreError("conversation load checkpoint", request.conversationId),
873
- ),
876
+ : Stream.unwrap(Effect.fail(crossThreadStoreError("thread observe", request.threadId))),
877
+
878
+ ...(recoveryCheckpoints === undefined
879
+ ? {}
880
+ : {
881
+ recoveryCheckpoints: {
882
+ save: (request) =>
883
+ options.ownsThread(request.checkpoint.threadId)
884
+ ? recoveryCheckpoints.save(request)
885
+ : Effect.fail(
886
+ crossThreadStoreError(
887
+ "thread save recovery checkpoint",
888
+ request.checkpoint.threadId,
889
+ ),
890
+ ),
891
+ load: (request) =>
892
+ options.ownsThread(request.threadId)
893
+ ? recoveryCheckpoints.load(request)
894
+ : Effect.succeed(Option.none()),
895
+ },
896
+ }),
897
+ ...(checkpoints === undefined
898
+ ? {}
899
+ : {
900
+ checkpoints: {
901
+ save: (request) =>
902
+ options.ownsThread(request.checkpoint.threadId)
903
+ ? checkpoints.save(request)
904
+ : Effect.fail(
905
+ crossThreadStoreError("thread save checkpoint", request.checkpoint.threadId),
906
+ ),
907
+ load: (request) =>
908
+ options.ownsThread(request.threadId)
909
+ ? checkpoints.load(request)
910
+ : Effect.fail(crossThreadStoreError("thread load checkpoint", request.threadId)),
911
+ },
912
+ }),
874
913
  });
875
914
 
876
- return Context.make(ConversationStore, routed);
915
+ return Context.make(ThreadStore, routed);
877
916
  });
878
917
 
879
918
  /**
880
919
  * Routing decorator over the LOCAL `SubmissionLedger` facet (plan §1.3): a request addressing
881
- * this Object's Conversation executes locally; a route-capable request addressing another
882
- * Conversation is Schema-encoded onto the `ConversationPortTransport` and executed by the
920
+ * this Object's Thread executes locally; a route-capable request addressing another
921
+ * Thread is Schema-encoded onto the `ThreadPortTransport` and executed by the
883
922
  * owning Object's local facet; any other foreign request fails fast typed. Provide the WP1
884
923
  * local facet (`submissionLedgerLayer`/`ledgerLayer`) and a transport to close it.
885
924
  */
886
925
  export const routedSubmissionLedgerLayer = (
887
926
  options: RoutedPortOptions,
888
- ): Layer.Layer<SubmissionLedger, never, SubmissionLedger | ConversationPortTransport> =>
927
+ ): Layer.Layer<SubmissionLedger, never, SubmissionLedger | ThreadPortTransport> =>
889
928
  Layer.effectContext(makeRoutedLedgerServices(options));
890
929
 
891
930
  /**
892
- * Routing decorator over the LOCAL `ConversationStore` facet (plan §1.3): this-conversation
931
+ * Routing decorator over the LOCAL `ThreadStore` facet (plan §1.3): this-thread
893
932
  * requests execute locally; foreign materialize/append/read/inspectTail/export travel the
894
- * transport; foreign observation and checkpoints fail fast typed.
933
+ * transport. Foreign recovery-cache loads return none for canonical replay; other foreign
934
+ * checkpoint operations and observation fail fast typed.
895
935
  */
896
- export const routedConversationStoreLayer = (
936
+ export const routedThreadStoreLayer = (
897
937
  options: RoutedPortOptions,
898
- ): Layer.Layer<ConversationStore, never, ConversationStore | ConversationPortTransport> =>
938
+ ): Layer.Layer<ThreadStore, never, ThreadStore | ThreadPortTransport> =>
899
939
  Layer.effectContext(makeRoutedStoreServices(options));
900
940
 
941
+ /** Route the existing owner-scoped list; delivery mutations remain source local. */
942
+ export const routedMessageDeliveryStoreLayer = (options: RoutedPortOptions) =>
943
+ Layer.effect(
944
+ MessageDeliveryStore,
945
+ Effect.gen(function* () {
946
+ const local = yield* MessageDeliveryStore;
947
+ const call = makeTransportCall(yield* ThreadPortTransport);
948
+
949
+ return MessageDeliveryStore.of({
950
+ ...local,
951
+ list: (request) =>
952
+ options.ownsThread(request.ownerThreadId)
953
+ ? local.list(request)
954
+ : call(request.ownerThreadId, MessageDeliveryListCall.make({ request })).pipe(
955
+ Effect.mapError(() =>
956
+ MessageDeliveryError.make({
957
+ reason: "storage",
958
+ operation: "route delivery list",
959
+ }),
960
+ ),
961
+ Effect.flatMap((response) => {
962
+ if (
963
+ response._tag === "PortSucceeded" &&
964
+ response.result._tag === "MessageDeliveryListResult"
965
+ )
966
+ return Effect.succeed(response.result.page);
967
+
968
+ return Effect.fail(
969
+ response._tag === "PortFailed" &&
970
+ response.failure._tag === "MessageDeliveryError"
971
+ ? response.failure
972
+ : MessageDeliveryError.make({
973
+ reason: "storage",
974
+ operation: "route delivery list",
975
+ }),
976
+ );
977
+ }),
978
+ ),
979
+ });
980
+ }),
981
+ );
982
+
901
983
  // ---------------------------------------------------------------------------
902
984
  // Owner-side execution
903
985
  // ---------------------------------------------------------------------------
@@ -915,16 +997,37 @@ const capture = <Failure extends PortFailure>(
915
997
  * Execute one decoded port request against THIS Object's LOCAL facets — the owner-side half
916
998
  * of the routed ports (plan §1.3). Callers must provide the WP1 local facets, never the
917
999
  * routed decorators: the routing layer already established that this Object owns the
918
- * addressed Conversation, and re-routing here could bounce a request between Objects.
1000
+ * addressed Thread, and re-routing here could bounce a request between Objects.
919
1001
  * Failures never escape — every typed port failure becomes a `PortFailed` envelope that
920
1002
  * re-decodes on the caller side.
921
1003
  */
922
1004
  export const executePortRequest = Effect.fn("DoPortRouting.executePortRequest")(function* (
923
1005
  request: PortRequest,
924
- ): Effect.fn.Return<PortResponse, never, SubmissionLedger | ConversationStore> {
1006
+ ): Effect.fn.Return<PortResponse, never, SubmissionLedger | ThreadStore | MessageDeliveryStore> {
925
1007
  switch (request._tag) {
1008
+ case "MessageDeliveryList": {
1009
+ const store = yield* MessageDeliveryStore;
1010
+
1011
+ return yield* capture(
1012
+ store
1013
+ .list(request.request)
1014
+ .pipe(Effect.map((page) => MessageDeliveryListResult.make({ page }))),
1015
+ );
1016
+ }
1017
+ case "StoreCountPeerMessages": {
1018
+ const store = yield* ThreadStore;
1019
+
1020
+ return yield* capture(
1021
+ store.countPeerMessages === undefined
1022
+ ? Effect.fail(crossThreadStoreError("native read unavailable", request.request.threadId))
1023
+ : store
1024
+ .countPeerMessages(request.request)
1025
+ .pipe(Effect.map((count) => StoreCountPeerMessagesResult.make({ count }))),
1026
+ );
1027
+ }
926
1028
  case "LedgerAdmit": {
927
1029
  const ledger = yield* SubmissionLedger;
1030
+
928
1031
  return yield* capture(
929
1032
  ledger
930
1033
  .admit(request.request)
@@ -933,12 +1036,14 @@ export const executePortRequest = Effect.fn("DoPortRouting.executePortRequest")(
933
1036
  }
934
1037
  case "LedgerMarkReady": {
935
1038
  const ledger = yield* SubmissionLedger;
1039
+
936
1040
  return yield* capture(
937
1041
  ledger.markReady(request.request).pipe(Effect.map(() => LedgerMarkReadyResult.make({}))),
938
1042
  );
939
1043
  }
940
1044
  case "LedgerLookup": {
941
1045
  const ledger = yield* SubmissionLedger;
1046
+
942
1047
  return yield* capture(
943
1048
  ledger
944
1049
  .lookup(request.request)
@@ -953,6 +1058,7 @@ export const executePortRequest = Effect.fn("DoPortRouting.executePortRequest")(
953
1058
  }
954
1059
  case "LedgerResolveAdmission": {
955
1060
  const ledger = yield* SubmissionLedger;
1061
+
956
1062
  return yield* capture(
957
1063
  ledger
958
1064
  .resolveAdmission(request.request)
@@ -961,6 +1067,7 @@ export const executePortRequest = Effect.fn("DoPortRouting.executePortRequest")(
961
1067
  }
962
1068
  case "LedgerRequestAbort": {
963
1069
  const ledger = yield* SubmissionLedger;
1070
+
964
1071
  return yield* capture(
965
1072
  ledger
966
1073
  .requestAbort(request.request)
@@ -969,6 +1076,7 @@ export const executePortRequest = Effect.fn("DoPortRouting.executePortRequest")(
969
1076
  }
970
1077
  case "LedgerRecordChildSettled": {
971
1078
  const ledger = yield* SubmissionLedger;
1079
+
972
1080
  return yield* capture(
973
1081
  ledger
974
1082
  .recordChildSettled(request.request)
@@ -976,13 +1084,15 @@ export const executePortRequest = Effect.fn("DoPortRouting.executePortRequest")(
976
1084
  );
977
1085
  }
978
1086
  case "StoreMaterialize": {
979
- const store = yield* ConversationStore;
1087
+ const store = yield* ThreadStore;
1088
+
980
1089
  return yield* capture(
981
1090
  store.materialize(request.request).pipe(Effect.map(() => StoreMaterializeResult.make({}))),
982
1091
  );
983
1092
  }
984
1093
  case "StoreAppend": {
985
- const store = yield* ConversationStore;
1094
+ const store = yield* ThreadStore;
1095
+
986
1096
  return yield* capture(
987
1097
  store
988
1098
  .append(request.request)
@@ -990,7 +1100,8 @@ export const executePortRequest = Effect.fn("DoPortRouting.executePortRequest")(
990
1100
  );
991
1101
  }
992
1102
  case "StoreReadPage": {
993
- const store = yield* ConversationStore;
1103
+ const store = yield* ThreadStore;
1104
+
994
1105
  return yield* capture(
995
1106
  store.read(request.request).pipe(
996
1107
  Stream.runCollect,
@@ -999,7 +1110,8 @@ export const executePortRequest = Effect.fn("DoPortRouting.executePortRequest")(
999
1110
  );
1000
1111
  }
1001
1112
  case "StoreInspectTail": {
1002
- const store = yield* ConversationStore;
1113
+ const store = yield* ThreadStore;
1114
+
1003
1115
  return yield* capture(
1004
1116
  store
1005
1117
  .inspectTail(request.request)
@@ -1007,15 +1119,12 @@ export const executePortRequest = Effect.fn("DoPortRouting.executePortRequest")(
1007
1119
  );
1008
1120
  }
1009
1121
  case "StoreExport": {
1010
- const store = yield* ConversationStore;
1122
+ const store = yield* ThreadStore;
1123
+
1011
1124
  return yield* capture(
1012
1125
  store
1013
1126
  .export(request.request)
1014
- .pipe(
1015
- Effect.map((conversationExport) =>
1016
- StoreExportResult.make({ export: conversationExport }),
1017
- ),
1018
- ),
1127
+ .pipe(Effect.map((threadExport) => StoreExportResult.make({ export: threadExport }))),
1019
1128
  );
1020
1129
  }
1021
1130
  }
@@ -1041,7 +1150,7 @@ const encodedProtocolFailure = (message: string): unknown => ({
1041
1150
  export const handleEncodedPortRequest = Effect.fn("DoPortRouting.handleEncodedPortRequest")(
1042
1151
  function* (
1043
1152
  encoded: unknown,
1044
- ): Effect.fn.Return<unknown, never, SubmissionLedger | ConversationStore> {
1153
+ ): Effect.fn.Return<unknown, never, SubmissionLedger | ThreadStore | MessageDeliveryStore> {
1045
1154
  const response = yield* decodePortRequest(encoded).pipe(
1046
1155
  Effect.flatMap(executePortRequest),
1047
1156
  Effect.catch((error) =>
@@ -1056,6 +1165,7 @@ export const handleEncodedPortRequest = Effect.fn("DoPortRouting.handleEncodedPo
1056
1165
  ),
1057
1166
  ),
1058
1167
  );
1168
+
1059
1169
  return yield* encodePortResponse(response).pipe(
1060
1170
  Effect.catch((error) =>
1061
1171
  Effect.succeed(