@effect-agent/storage-cloudflare 0.1.0-beta.39 → 0.1.0-beta.41

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effect-agent/storage-cloudflare",
3
- "version": "0.1.0-beta.39",
3
+ "version": "0.1.0-beta.41",
4
4
  "exports": {
5
5
  ".": {
6
6
  "types": "./dist/index.d.mts",
@@ -12,7 +12,8 @@
12
12
  }
13
13
  },
14
14
  "dependencies": {
15
- "@effect-agent/thread": "0.1.0-beta.39",
15
+ "@effect-agent/core": "0.1.0-beta.41",
16
+ "@effect-agent/thread": "0.1.0-beta.41",
16
17
  "@effect/platform-browser": "4.0.0-rc.111",
17
18
  "@effect/sql-sqlite-do": "4.0.0-rc.111"
18
19
  },
@@ -41,11 +42,11 @@
41
42
  "devDependencies": {
42
43
  "@cloudflare/vitest-pool-workers": "0.21.3",
43
44
  "@cloudflare/workers-types": "5.20260825.1",
44
- "@effect-agent/testing": "0.1.0-beta.38",
45
+ "@effect-agent/testing": "0.1.0-beta.40",
45
46
  "@effect/vitest": "4.0.0-rc.111",
46
47
  "effect": "4.0.0-rc.111",
47
48
  "typescript": "7.0.2",
48
- "vite-plus": "0.2.6",
49
- "vitest": "4.1.10"
49
+ "vite-plus": "0.3.0",
50
+ "vitest": "4.1.11"
50
51
  }
51
52
  }
@@ -0,0 +1,51 @@
1
+ import { MemoryStorageError, MemoryMutationFailpoint } from "@effect-agent/core";
2
+ import { memoryStoreLayerWithFailpoints, SqlMemoryLimits } from "@effect-agent/thread/sql-memory";
3
+ import { SqliteClient } from "@effect/sql-sqlite-do";
4
+ import { Effect, Layer, Schema } from "effect";
5
+
6
+ export class DoMemoryStorageLimits extends Schema.Class<DoMemoryStorageLimits>(
7
+ "@effect-agent/storage-cloudflare/DoMemoryStorageLimits",
8
+ )({
9
+ maxRowBytes: Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 1_900_000 })),
10
+ maxStorageBytes: Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 536_870_912 })),
11
+ maxDocuments: Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 100_000 })),
12
+ maxReceipts: Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 1_000_000 })),
13
+ }) {}
14
+
15
+ export const defaultDoMemoryStorageLimits = DoMemoryStorageLimits.make({
16
+ maxRowBytes: 1_900_000,
17
+ maxStorageBytes: 536_870_912,
18
+ maxDocuments: 10_000,
19
+ maxReceipts: 100_000,
20
+ });
21
+
22
+ /**
23
+ * Local memory only, without Thread tables. Keep one SQL client per owner and pass the
24
+ * full storage handle: sql-only handles cannot provide atomic receipts and revisions.
25
+ * Byte limits conservatively count encoded rows, not SQLite page/index overhead.
26
+ */
27
+ export const doMemoryStoreLayerWithFailpoints = (
28
+ storage: NonNullable<SqliteClient.SqliteClientConfig["storage"]>,
29
+ limits: DoMemoryStorageLimits = defaultDoMemoryStorageLimits,
30
+ ) =>
31
+ Layer.unwrap(
32
+ Schema.decodeUnknownEffect(DoMemoryStorageLimits)(limits).pipe(
33
+ Effect.mapError(() =>
34
+ MemoryStorageError.make({ operation: "memory storage limits", reason: "invalid-input" }),
35
+ ),
36
+ Effect.map((validated) =>
37
+ memoryStoreLayerWithFailpoints.pipe(
38
+ Layer.provide(Layer.succeed(SqlMemoryLimits, validated)),
39
+ Layer.provide(SqliteClient.layer({ storage })),
40
+ ),
41
+ ),
42
+ ),
43
+ );
44
+
45
+ export const doMemoryStoreLayer = (
46
+ storage: NonNullable<SqliteClient.SqliteClientConfig["storage"]>,
47
+ limits: DoMemoryStorageLimits = defaultDoMemoryStorageLimits,
48
+ ) =>
49
+ doMemoryStoreLayerWithFailpoints(storage, limits).pipe(
50
+ Layer.provide(MemoryMutationFailpoint.layer),
51
+ );
package/src/index.ts CHANGED
@@ -32,3 +32,5 @@ export * from "./do-storage-config.ts";
32
32
  export * from "./do-storage-failpoint.ts";
33
33
  export * from "./port-protocol.ts";
34
34
  export * from "./routing.ts";
35
+ export * from "./do-memory-store.ts";
36
+ export * from "./memory-protocol.ts";
@@ -0,0 +1,262 @@
1
+ import type { MemoryReader } from "@effect-agent/core";
2
+ import {
3
+ MemoryAccess,
4
+ MemoryConflict,
5
+ MemoryDocument,
6
+ MemoryLookup,
7
+ MemoryMutationFailure,
8
+ MemoryNamespace,
9
+ MemoryOperationConflict,
10
+ MemoryRecallError,
11
+ MemoryRecallLimits,
12
+ MemoryStorageError,
13
+ MemoryWithdrawn,
14
+ MemoryWrite,
15
+ MemoryWriter,
16
+ revalidateMemoryLookup,
17
+ MemoryIndexSearch,
18
+ MemoryIndexError,
19
+ SemanticMemoryError,
20
+ SemanticMemoryProfile,
21
+ SemanticCandidateLimits,
22
+ SemanticCandidateResult,
23
+ revalidateSemanticMemoryCandidates,
24
+ } from "@effect-agent/core";
25
+ import { Principal } from "@effect-agent/thread";
26
+ import { Clock, Context, Effect, Encoding, Schema } from "effect";
27
+
28
+ export class MemoryRpcError extends Schema.TaggedError<MemoryRpcError>()("MemoryRpcError", {
29
+ reason: Schema.Literals(["denied", "protocol", "budget", "timeout", "unavailable"]),
30
+ }) {}
31
+
32
+ export class MemoryRpcLimits extends Schema.Class<MemoryRpcLimits>(
33
+ "@effect-agent/storage-cloudflare/MemoryRpcLimits",
34
+ )({
35
+ maxRequestBytes: Schema.Int.check(Schema.isBetween({ minimum: 256, maximum: 4_194_304 })),
36
+ maxResponseBytes: Schema.Int.check(Schema.isBetween({ minimum: 256, maximum: 16_777_216 })),
37
+ maxSourceBytes: Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 67_108_864 })),
38
+ maxSources: MemoryRecallLimits.fields.maxSources,
39
+ timeoutMillis: MemoryRecallLimits.fields.timeoutMillis,
40
+ }) {}
41
+
42
+ export const defaultMemoryRpcLimits = MemoryRpcLimits.make({
43
+ maxRequestBytes: 1_048_576,
44
+ maxResponseBytes: 4_194_304,
45
+ maxSourceBytes: 16_777_216,
46
+ maxSources: 16,
47
+ timeoutMillis: 10_000,
48
+ });
49
+
50
+ const RequestFields = {
51
+ version: Schema.Literal(1),
52
+ access: MemoryAccess.Wire,
53
+ /** Host-authenticated identity, never copied from model input. The owner still authorizes it. */
54
+ principal: Principal,
55
+ deadlineMillis: Schema.Finite.check(Schema.isGreaterThanOrEqualTo(0)),
56
+ };
57
+
58
+ const RevalidateRequest = Schema.TaggedStruct("Revalidate", {
59
+ ...RequestFields,
60
+ lookup: MemoryLookup,
61
+ limits: MemoryRecallLimits,
62
+ });
63
+ const ChangeRequest = Schema.TaggedStruct("Change", { ...RequestFields, write: MemoryWrite.Wire });
64
+ const SemanticRequest: Schema.TaggedStruct<
65
+ "RevalidateSemantic",
66
+ typeof RequestFields & {
67
+ readonly found: typeof MemoryIndexSearch.Wire;
68
+ readonly profile: typeof SemanticMemoryProfile;
69
+ readonly limits: typeof SemanticCandidateLimits;
70
+ }
71
+ > = Schema.TaggedStruct("RevalidateSemantic", {
72
+ ...RequestFields,
73
+ found: MemoryIndexSearch.Wire,
74
+ profile: SemanticMemoryProfile,
75
+ limits: SemanticCandidateLimits,
76
+ });
77
+ export const MemoryOwnerRequest: Schema.Union<
78
+ [typeof RevalidateRequest, typeof ChangeRequest, typeof SemanticRequest]
79
+ > = Schema.Union([RevalidateRequest, ChangeRequest, SemanticRequest]);
80
+ export type MemoryOwnerRequest = typeof MemoryOwnerRequest.Type;
81
+
82
+ export const MemoryOwnerFailure = Schema.Union([
83
+ MemoryRpcError,
84
+ MemoryStorageError,
85
+ MemoryRecallError,
86
+ MemoryConflict,
87
+ MemoryWithdrawn,
88
+ MemoryOperationConflict,
89
+ MemoryMutationFailure,
90
+ MemoryIndexError,
91
+ SemanticMemoryError,
92
+ ]);
93
+ export type MemoryOwnerFailure = typeof MemoryOwnerFailure.Type;
94
+
95
+ export const MemoryOwnerResponse = Schema.Union([
96
+ Schema.TaggedStruct("Lookup", { access: MemoryAccess.Wire, lookup: MemoryLookup }),
97
+ Schema.TaggedStruct("Changed", { access: MemoryAccess.Wire, document: MemoryDocument.Wire }),
98
+ Schema.TaggedStruct("Semantic", { access: MemoryAccess.Wire, result: SemanticCandidateResult }),
99
+ Schema.TaggedStruct("Failed", { failure: MemoryOwnerFailure }),
100
+ ]);
101
+ export type MemoryOwnerResponse = typeof MemoryOwnerResponse.Type;
102
+
103
+ /** Fail-closed application policy. Authorize the namespace, principal, scope, and full command. */
104
+ export class MemoryOwnerAuthorizer extends Context.Service<
105
+ MemoryOwnerAuthorizer,
106
+ {
107
+ readonly authorize: (request: MemoryOwnerRequest) => Effect.Effect<void, MemoryRpcError>;
108
+ }
109
+ >()("@effect-agent/storage-cloudflare/MemoryOwnerAuthorizer") {}
110
+
111
+ /** Canonical namespace derived from this object's idFromName identity, never its request. */
112
+ export class MemoryOwnerIdentity extends Context.Service<
113
+ MemoryOwnerIdentity,
114
+ {
115
+ readonly namespace: MemoryNamespace.Any;
116
+ }
117
+ >()("@effect-agent/storage-cloudflare/MemoryOwnerIdentity") {}
118
+
119
+ export const memoryWireBytes = (text: string): number => Encoding.encodeHex(text).length / 2;
120
+
121
+ export const decodeMemoryWire = Effect.fn("decodeMemoryWire")(function* <A, I>(
122
+ schema: Schema.Codec<A, I, never>,
123
+ raw: unknown,
124
+ maxBytes: number,
125
+ ) {
126
+ const text = yield* Schema.decodeUnknownEffect(Schema.String)(raw).pipe(
127
+ Effect.mapError(() => MemoryRpcError.make({ reason: "protocol" })),
128
+ );
129
+ if (text.length > maxBytes || memoryWireBytes(text) > maxBytes)
130
+ return yield* MemoryRpcError.make({ reason: "budget" });
131
+ return yield* Schema.decodeEffect(Schema.fromJsonString(schema))(text).pipe(
132
+ Effect.mapError(() => MemoryRpcError.make({ reason: "protocol" })),
133
+ );
134
+ });
135
+
136
+ export const encodeMemoryWire = Effect.fn("encodeMemoryWire")(function* <A, I>(
137
+ schema: Schema.Codec<A, I, never, never>,
138
+ value: A,
139
+ maxBytes: number,
140
+ ) {
141
+ const encoded = yield* Schema.encodeEffect(Schema.fromJsonString(schema))(value).pipe(
142
+ Effect.mapError(() => MemoryRpcError.make({ reason: "protocol" })),
143
+ );
144
+ if (encoded.length > maxBytes || memoryWireBytes(encoded) > maxBytes)
145
+ return yield* MemoryRpcError.make({ reason: "budget" });
146
+ return encoded;
147
+ });
148
+
149
+ /** One local read per distinct candidate source, with no per-document network calls. */
150
+ export const handleMemoryOwnerRequest = Effect.fn("MemoryOwner.handleRequest")(function* (
151
+ raw: unknown,
152
+ limits: MemoryRpcLimits = defaultMemoryRpcLimits,
153
+ ): Effect.fn.Return<
154
+ string,
155
+ never,
156
+ MemoryOwnerIdentity | MemoryOwnerAuthorizer | MemoryReader | MemoryWriter
157
+ > {
158
+ const result = yield* Effect.gen(function* (): Effect.fn.Return<
159
+ MemoryOwnerResponse,
160
+ MemoryOwnerFailure,
161
+ MemoryOwnerIdentity | MemoryOwnerAuthorizer | MemoryReader | MemoryWriter
162
+ > {
163
+ limits = yield* Schema.decodeUnknownEffect(MemoryRpcLimits)(limits).pipe(
164
+ Effect.mapError(() => MemoryRpcError.make({ reason: "protocol" })),
165
+ );
166
+ const request = yield* decodeMemoryWire(MemoryOwnerRequest, raw, limits.maxRequestBytes);
167
+ const { namespace } = yield* MemoryOwnerIdentity;
168
+ if (
169
+ !MemoryNamespace.equals(namespace, request.access.namespace) ||
170
+ (request._tag === "Change" &&
171
+ !MemoryNamespace.equals(namespace, request.write.key.namespace)) ||
172
+ (request._tag === "RevalidateSemantic" &&
173
+ request.found.candidates.some(
174
+ (candidate) => !MemoryNamespace.equals(namespace, candidate.key.namespace),
175
+ ))
176
+ )
177
+ return yield* MemoryRpcError.make({ reason: "denied" });
178
+ const remaining = Math.min(
179
+ limits.timeoutMillis,
180
+ request.deadlineMillis - (yield* Clock.currentTimeMillis),
181
+ );
182
+ if (remaining <= 0) return yield* MemoryRpcError.make({ reason: "timeout" });
183
+ return yield* Effect.gen(function* (): Effect.fn.Return<
184
+ MemoryOwnerResponse,
185
+ MemoryOwnerFailure,
186
+ MemoryOwnerAuthorizer | MemoryReader | MemoryWriter
187
+ > {
188
+ const authorizer = yield* MemoryOwnerAuthorizer;
189
+ yield* authorizer.authorize(request);
190
+ if (request._tag === "Change") {
191
+ const writer = yield* MemoryWriter;
192
+ return {
193
+ _tag: "Changed",
194
+ access: request.access,
195
+ document: yield* writer.change(request.write),
196
+ };
197
+ }
198
+ if (request._tag === "RevalidateSemantic") {
199
+ if (
200
+ new Set(request.found.candidates.map((candidate) => candidate.key.id)).size >
201
+ limits.maxSources
202
+ )
203
+ return yield* MemoryRpcError.make({ reason: "budget" });
204
+ const result = yield* revalidateSemanticMemoryCandidates(
205
+ request.found,
206
+ request.access,
207
+ request.profile,
208
+ {
209
+ ...request.limits,
210
+ maxSourceBytes: Math.min(
211
+ limits.maxSourceBytes,
212
+ request.limits.maxSourceBytes ?? 16_777_216,
213
+ ),
214
+ maxOutputBytes: Math.min(
215
+ limits.maxResponseBytes,
216
+ request.limits.maxOutputBytes ?? 16_777_216,
217
+ ),
218
+ },
219
+ );
220
+ return { _tag: "Semantic", access: request.access, result };
221
+ }
222
+ const count =
223
+ request.lookup._tag === "Found"
224
+ ? new Set(request.lookup.passages.map((passage) => passage.source.id)).size
225
+ : 0;
226
+ if (count > Math.min(limits.maxSources, request.limits.maxSources))
227
+ return yield* MemoryRpcError.make({ reason: "budget" });
228
+ const lookup = yield* revalidateMemoryLookup(request.lookup, request.access, {
229
+ maxSourceBytes: limits.maxSourceBytes,
230
+ maxInputBytes: Math.min(limits.maxSourceBytes, request.limits.maxInputBytes ?? 16_777_216),
231
+ });
232
+ return { _tag: "Lookup", access: request.access, lookup };
233
+ }).pipe(
234
+ Effect.scoped,
235
+ Effect.timeoutOrElse({
236
+ duration: remaining,
237
+ orElse: () => Effect.fail(MemoryRpcError.make({ reason: "timeout" })),
238
+ }),
239
+ );
240
+ }).pipe(
241
+ Effect.flatMap((response) =>
242
+ encodeMemoryWire(MemoryOwnerResponse, response, limits.maxResponseBytes),
243
+ ),
244
+ Effect.result,
245
+ );
246
+ if (result._tag === "Success") return result.success;
247
+ return yield* encodeMemoryWire(
248
+ MemoryOwnerResponse,
249
+ {
250
+ _tag: "Failed",
251
+ failure: result.failure,
252
+ },
253
+ limits.maxResponseBytes,
254
+ ).pipe(
255
+ Effect.catch(() =>
256
+ Schema.encodeEffect(Schema.fromJsonString(MemoryOwnerResponse))({
257
+ _tag: "Failed",
258
+ failure: MemoryRpcError.make({ reason: "budget" }),
259
+ }).pipe(Effect.orDie),
260
+ ),
261
+ );
262
+ });
package/src/migrations.ts CHANGED
@@ -2,19 +2,12 @@ import { SqliteMigrator } from "@effect/sql-sqlite-do";
2
2
  import { Effect } from "effect";
3
3
  import * as SqlClient from "effect/unstable/sql/SqlClient";
4
4
 
5
- /**
6
- * The exact-or-fresh storage version recorded in `effect_agent_meta`. Cloudflare is a fresh
7
- * platform, so there is exactly ONE migration carrying the complete current schema — no
8
- * v1→v4 history to replay (deployment spec §9: no rolling data-version promise during
9
- * private development).
10
- */
5
+ /** The current storage version recorded in `effect_agent_meta`. */
11
6
  export const CurrentDoStorageVersion = 2;
12
7
 
13
8
  /**
14
- * The Thread Durable Object schema. Table names and columns mirror the Node/SQLite v4
15
- * schema byte-for-byte (`packages/storage-sqlite/src/migrations.ts`, migrations 1–4 collapsed
16
- * into their final shape) so the shared conformance suites and crash-matrix rows address
17
- * identical durable state. Two DC-specific additions:
9
+ * The Thread Durable Object schema shares its thread and ledger tables with Node/SQLite.
10
+ * Schedules and subscriptions use separate Durable Objects. Two DC-specific additions:
18
11
  *
19
12
  * 1. `effect_agent_meta` replaces `PRAGMA user_version` as the exact-or-fresh version gate —
20
13
  * a meta table is portable regardless of which PRAGMAs Durable Object SQL storage allows.