@effect-agent/storage-cloudflare 0.1.0-beta.12 → 0.1.0-beta.120

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