@effect-agent/storage-cloudflare 0.1.0-beta.9 → 0.1.0-beta.91

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