@effect-agent/storage-memory 0.1.0-beta.56 → 0.1.0-beta.58

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.
@@ -7,8 +7,10 @@ import {
7
7
  type ThreadId,
8
8
  type SettlementId,
9
9
  } from "@effect-agent/core/Identifiers";
10
+ import { MessageAdmission } from "@effect-agent/core/Messaging";
10
11
  import {
11
12
  PersistedJson,
13
+ WorkerAdmission,
12
14
  ProducerEpoch,
13
15
  type DefinitionDigests,
14
16
  type DeploymentId,
@@ -147,6 +149,8 @@ interface SubmissionRow {
147
149
  readonly parentLinkage: ParentLinkage | undefined;
148
150
  readonly admissionGroup?: string;
149
151
  readonly admissionFence?: AdmissionRequest["admissionFence"];
152
+ readonly workerAdmissionJson?: string;
153
+ readonly messageAdmissionJson?: string;
150
154
  }
151
155
 
152
156
  interface StoredOwnership {
@@ -290,6 +294,20 @@ const toSnapshot = (row: SubmissionRow): SubmissionSnapshot =>
290
294
  createdAt: utc(row.createdAtMillis),
291
295
  ...(row.admissionGroup === undefined ? {} : { admissionGroup: row.admissionGroup }),
292
296
  ...(row.admissionFence === undefined ? {} : { admissionFence: row.admissionFence }),
297
+ ...(row.workerAdmissionJson === undefined
298
+ ? {}
299
+ : {
300
+ workerAdmission: Schema.decodeSync(Schema.fromJsonString(WorkerAdmission))(
301
+ row.workerAdmissionJson,
302
+ ),
303
+ }),
304
+ ...(row.messageAdmissionJson === undefined
305
+ ? {}
306
+ : {
307
+ messageAdmission: Schema.decodeSync(Schema.fromJsonString(MessageAdmission))(
308
+ row.messageAdmissionJson,
309
+ ),
310
+ }),
293
311
  ...(row.settledOutcome === undefined ? {} : { settledOutcome: row.settledOutcome }),
294
312
  ...(row.readyAtMillis === undefined ? {} : { readyAt: utc(row.readyAtMillis) }),
295
313
  ...(row.parentLinkage === undefined ? {} : { parentLinkage: row.parentLinkage }),
@@ -414,6 +432,24 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
414
432
  Effect.gen(function* () {
415
433
  const request = yield* validate(AdmissionRequest, "admit", unvalidated);
416
434
 
435
+ const workerAdmissionJson =
436
+ request.workerAdmission === undefined
437
+ ? undefined
438
+ : yield* Schema.encodeEffect(Schema.fromJsonString(WorkerAdmission))(
439
+ request.workerAdmission,
440
+ ).pipe(
441
+ Effect.mapError(() => ledgerError("admit", "Invalid worker admission metadata")),
442
+ );
443
+
444
+ const messageAdmissionJson =
445
+ request.messageAdmission === undefined
446
+ ? undefined
447
+ : yield* Schema.encodeEffect(Schema.fromJsonString(MessageAdmission))(
448
+ request.messageAdmission,
449
+ ).pipe(
450
+ Effect.mapError(() => ledgerError("admit", "Invalid message admission metadata")),
451
+ );
452
+
417
453
  const nowMillis = yield* Clock.currentTimeMillis;
418
454
  const services = yield* Effect.context<never>();
419
455
 
@@ -448,6 +484,22 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
448
484
  existing.row.inputDigest !== request.inputDigest ||
449
485
  !sameParentLinkage(existing.row.parentLinkage, request.parentLinkage) ||
450
486
  existing.row.admissionGroup !== request.admissionGroup ||
487
+ !Schema.toEquivalence(Schema.optional(WorkerAdmission))(
488
+ existing.row.workerAdmissionJson === undefined
489
+ ? undefined
490
+ : Schema.decodeSync(Schema.fromJsonString(WorkerAdmission))(
491
+ existing.row.workerAdmissionJson,
492
+ ),
493
+ request.workerAdmission,
494
+ ) ||
495
+ !Schema.toEquivalence(Schema.optional(MessageAdmission))(
496
+ existing.row.messageAdmissionJson === undefined
497
+ ? undefined
498
+ : Schema.decodeSync(Schema.fromJsonString(MessageAdmission))(
499
+ existing.row.messageAdmissionJson,
500
+ ),
501
+ request.messageAdmission,
502
+ ) ||
451
503
  !Schema.toEquivalence(Schema.optional(Schema.Json))(
452
504
  existing.row.admissionFence,
453
505
  request.admissionFence,
@@ -480,6 +532,36 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
480
532
  current,
481
533
  ];
482
534
  }
535
+
536
+ // A Thread's first admission fixes its worker origin before canonical materialization.
537
+ const first = [...current.submissions.values()].find(
538
+ ({ row }) => row.threadId === request.threadId,
539
+ );
540
+
541
+ if (first !== undefined) {
542
+ const previous =
543
+ first.row.workerAdmissionJson === undefined
544
+ ? undefined
545
+ : Schema.decodeSync(Schema.fromJsonString(WorkerAdmission))(
546
+ first.row.workerAdmissionJson,
547
+ );
548
+
549
+ if (
550
+ !Schema.toEquivalence(Schema.optional(WorkerAdmission.fields.origin))(
551
+ previous?.origin,
552
+ request.workerAdmission?.origin,
553
+ )
554
+ )
555
+ return [
556
+ failure(
557
+ AdmissionPolicyError.make({
558
+ reason: "refused",
559
+ code: "worker-origin-conflict",
560
+ }),
561
+ ),
562
+ current,
563
+ ];
564
+ }
483
565
  // A memory policy and the ledger mutation share one synchronous critical section.
484
566
  // An asynchronous policy cannot fence this Ref and therefore fails closed.
485
567
  const checked = Effect.runSyncExitWith(services)(admissionFence.check(request));
@@ -535,6 +617,8 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
535
617
  createdAtMillis: nowMillis,
536
618
  readyAtMillis: undefined,
537
619
  parentLinkage: request.parentLinkage,
620
+ ...(workerAdmissionJson === undefined ? {} : { workerAdmissionJson }),
621
+ ...(messageAdmissionJson === undefined ? {} : { messageAdmissionJson }),
538
622
  ...(request.admissionGroup === undefined
539
623
  ? {}
540
624
  : { admissionGroup: request.admissionGroup }),
@@ -30,12 +30,28 @@ import {
30
30
  LoadCheckpointRequest,
31
31
  SaveCheckpointRequest,
32
32
  } from "@effect-agent/thread/ThreadStore";
33
- import { Crypto, Effect, Encoding, Layer, Option, PubSub, Ref, Schema, Stream } from "effect";
33
+ import {
34
+ Context,
35
+ Crypto,
36
+ Effect,
37
+ Encoding,
38
+ Layer,
39
+ Option,
40
+ PubSub,
41
+ Ref,
42
+ Schema,
43
+ Stream,
44
+ } from "effect";
34
45
 
35
46
  const MAX_THREADS = 256;
36
47
  const MAX_RECORDS_PER_THREAD = 65_536;
37
48
  const MAX_CHECKPOINTS_PER_THREAD = 1_024;
38
49
 
50
+ const ThreadCapacity = Context.Reference<number>(
51
+ "@effect-agent/storage-memory/MemoryThreadStore/ThreadCapacity",
52
+ { defaultValue: () => MAX_THREADS },
53
+ );
54
+
39
55
  interface StoredBatch {
40
56
  readonly digest: Digest;
41
57
  readonly result: AppendResult;
@@ -155,6 +171,7 @@ const validateCheckpointVersion = Effect.fn("MemoryThreadStore.validateCheckpoin
155
171
  );
156
172
 
157
173
  const makeThreadStore = Effect.gen(function* () {
174
+ const maxThreads = yield* ThreadCapacity;
158
175
  const crypto = yield* Crypto.Crypto;
159
176
  const state = yield* Ref.make<MemoryState>({ threads: new Map() });
160
177
  const updates = yield* PubSub.sliding<void>(1);
@@ -198,11 +215,11 @@ const makeThreadStore = Effect.gen(function* () {
198
215
 
199
216
  return [{ _tag: "success" }, { threads }];
200
217
  }
201
- if (current.threads.size >= MAX_THREADS) {
218
+ if (current.threads.size >= maxThreads) {
202
219
  return [
203
220
  {
204
221
  _tag: "failure",
205
- error: storeError("materialize", `In-memory thread limit ${MAX_THREADS} exceeded`),
222
+ error: storeError("materialize", `In-memory thread limit ${maxThreads} exceeded`),
206
223
  },
207
224
  current,
208
225
  ];
@@ -630,3 +647,15 @@ const makeThreadStore = Effect.gen(function* () {
630
647
  * SubmissionLedger port; this Layer deliberately provides only the ThreadStore.
631
648
  */
632
649
  export const MemoryThreadStoreLive = Layer.effect(ThreadStore, makeThreadStore);
650
+
651
+ /** Configure a finite retained Thread capacity. Invalid construction options throw immediately. */
652
+ export const memoryThreadStoreLayer = (options: { readonly maxThreads?: number } = {}) =>
653
+ MemoryThreadStoreLive.pipe(
654
+ Layer.provide(
655
+ Layer.succeed(ThreadCapacity)(
656
+ Schema.decodeSync(
657
+ Schema.Int.check(Schema.isGreaterThan(0), Schema.isLessThanOrEqualTo(65_536)),
658
+ )(options.maxThreads ?? MAX_THREADS),
659
+ ),
660
+ ),
661
+ );
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * as MemoryScheduleStore from "./MemoryScheduleStore.ts";
2
+ export * as MemoryMessageDeliveryStore from "./MemoryMessageDeliveryStore.ts";
2
3
  export * as MemorySemanticIndex from "./MemorySemanticIndex.ts";
3
4
  export * as MemorySubmissionLedger from "./MemorySubmissionLedger.ts";
4
5
  export * as MemorySubscriptionStore from "./MemorySubscriptionStore.ts";