@effect-agent/storage-cloudflare 0.0.1-beta.0

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.
@@ -0,0 +1,53 @@
1
+ import { Context, Schema } from "effect";
2
+
3
+ const ObservationPollInterval = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0));
4
+ const OwnershipLeaseMillis = Schema.Int.check(Schema.isGreaterThan(0));
5
+
6
+ /**
7
+ * Default per-value byte bound, kept under the Durable Object platform's 2 MB SQLite value
8
+ * limit with a safety margin. This is the DC analogue of Node's 16 MB `BoundedStoredText`
9
+ * bound: both fail typed before mutating, only the threshold differs (a documented DN/DC
10
+ * behavioral difference; Travel Planner payloads sit orders of magnitude below both).
11
+ */
12
+ export const DEFAULT_MAX_STORED_VALUE_BYTES = 1_900_000;
13
+
14
+ /** The hard schema ceiling for the configurable bound: never at or above the platform limit. */
15
+ const MaxStoredValueBytes = Schema.Int.check(
16
+ Schema.isGreaterThan(0),
17
+ Schema.isLessThanOrEqualTo(2_000_000),
18
+ );
19
+
20
+ /**
21
+ * Validated construction configuration consumed by the Durable Object storage Layers. The
22
+ * storage identity itself belongs to the SqlClient Layer (built from `ctx.storage`);
23
+ * duplicating it here could silently diverge from the handle actually in use.
24
+ */
25
+ export class DoStorageConfigValue extends Schema.Class<DoStorageConfigValue>(
26
+ "@effect-agent/storage-cloudflare/DoStorageConfigValue",
27
+ )({
28
+ observationPollInterval: ObservationPollInterval,
29
+ /**
30
+ * Submission ownership lease duration in milliseconds (D5). Inside one Durable Object the
31
+ * object itself is the serialized owner, so the lease's primary DC role is fencing work
32
+ * across DO incarnations (an evicted incarnation's claim becomes reclaimable); correctness
33
+ * never depends on it because every canonical append is fenced by producer epoch.
34
+ */
35
+ ownershipLeaseDuration: OwnershipLeaseMillis,
36
+ /**
37
+ * Maximum bytes for any single stored text value (canonical batch/record JSON, admission
38
+ * input payload, checkpoint JSON). Enforced typed BEFORE any write; must stay under the
39
+ * platform's 2 MB per-value limit.
40
+ */
41
+ maxStoredValueBytes: MaxStoredValueBytes,
42
+ /**
43
+ * Re-verify every stored payload and digest chain while opening the store. Per-operation
44
+ * Schema decoding and the digest chain already fail clearly on corrupt rows, so the full
45
+ * scan is an explicit opt-in integrity audit rather than a startup requirement.
46
+ */
47
+ verifyOnOpen: Schema.Boolean,
48
+ }) {}
49
+
50
+ /** Explicit Durable Object storage configuration authority. */
51
+ export class DoStorageConfig extends Context.Service<DoStorageConfig, DoStorageConfigValue>()(
52
+ "@effect-agent/storage-cloudflare/DoStorageConfig",
53
+ ) {}
@@ -0,0 +1,87 @@
1
+ import { Context, Effect, Layer, Ref } from "effect";
2
+
3
+ import { DoStorageFailpointError, type DoStorageFailpointLocation } from "./errors.ts";
4
+
5
+ export type DoStorageFailpointHandler = (
6
+ location: DoStorageFailpointLocation,
7
+ ) => Effect.Effect<void, DoStorageFailpointError>;
8
+
9
+ const noFailpoint: DoStorageFailpointHandler = () => Effect.void;
10
+
11
+ /** Test-only control for replacing the active Durable Object failpoint handler. */
12
+ export class DoStorageFailpointTestControl extends Context.Service<
13
+ DoStorageFailpointTestControl,
14
+ {
15
+ readonly clear: Effect.Effect<void>;
16
+ readonly setHandler: (handler: DoStorageFailpointHandler) => Effect.Effect<void>;
17
+ }
18
+ >()("@effect-agent/storage-cloudflare/DoStorageFailpointTestControl") {}
19
+
20
+ /** Explicit fault-injection authority used at Durable Object storage operation boundaries. */
21
+ export class DoStorageFailpoint extends Context.Service<
22
+ DoStorageFailpoint,
23
+ {
24
+ readonly hit: DoStorageFailpointHandler;
25
+ }
26
+ >()("@effect-agent/storage-cloudflare/DoStorageFailpoint") {
27
+ /** Production default: no fault injection. */
28
+ static readonly layer = Layer.succeed(this)({ hit: noFailpoint });
29
+
30
+ /** Reusable test Layer with a control service backed by the same handler Ref. */
31
+ static readonly layerTest = Layer.effectContext(
32
+ Effect.gen(function* () {
33
+ const handler = yield* Ref.make<DoStorageFailpointHandler>(noFailpoint);
34
+ return Context.make(
35
+ DoStorageFailpoint,
36
+ DoStorageFailpoint.of({
37
+ hit: (location) => Ref.get(handler).pipe(Effect.flatMap((current) => current(location))),
38
+ }),
39
+ ).pipe(
40
+ Context.add(
41
+ DoStorageFailpointTestControl,
42
+ DoStorageFailpointTestControl.of({
43
+ clear: Ref.set(handler, noFailpoint),
44
+ setHandler: (next) => Ref.set(handler, next),
45
+ }),
46
+ ),
47
+ );
48
+ }),
49
+ );
50
+ }
51
+
52
+ /**
53
+ * The DC-specific eviction failpoint mode: instead of failing typed, an armed hit evicts the
54
+ * Durable Object through an injected `evict` thunk — in production-shaped harnesses that thunk
55
+ * is `() => ctx.abort()`, the platform's real failure mode. `ctx.abort()` never returns (it
56
+ * throws while destroying the in-memory instance and every in-flight implicit or explicit
57
+ * storage transaction rolls back), so an armed hit ends the current Attempt exactly like an
58
+ * unannounced platform eviction; DO storage — the only correctness-critical state — survives
59
+ * for the next incarnation, which the persisted alarm wakes without any incoming request.
60
+ *
61
+ * The handles stay injected: this package never imports `cloudflare:workers`, so the harness
62
+ * that owns a `DurableObjectState` supplies the thunk.
63
+ */
64
+ export const evictionFailpointHandler =
65
+ (options: {
66
+ readonly isArmed: (location: DoStorageFailpointLocation) => Effect.Effect<boolean>;
67
+ /** Kills the incarnation — e.g. `() => ctx.abort()`. Typed `void` because the platform
68
+ * declares `abort` as returning, but it throws while destroying the instance. */
69
+ readonly evict: (location: DoStorageFailpointLocation) => void;
70
+ }): DoStorageFailpointHandler =>
71
+ (location) =>
72
+ options.isArmed(location).pipe(
73
+ Effect.flatMap((armed) =>
74
+ armed
75
+ ? // `ctx.abort()` throws while destroying the instance; that throw surfaces as a
76
+ // defect in the (already dying) incarnation. The defensive throw below keeps the
77
+ // guarantee — nothing after an armed hit may observe in-memory state — even if a
78
+ // harness supplies an evict thunk that returns.
79
+ Effect.sync((): never => {
80
+ options.evict(location);
81
+ throw new Error(
82
+ `Durable Object eviction did not interrupt execution at ${location}.`,
83
+ );
84
+ })
85
+ : Effect.void,
86
+ ),
87
+ );
package/src/errors.ts ADDED
@@ -0,0 +1,170 @@
1
+ import { CanonicalSequence, ProducerEpoch } from "@effect-agent/session";
2
+ import { Schema } from "effect";
3
+
4
+ /** The Durable Object's SQLite storage uses a private-development format this adapter cannot read. */
5
+ export class DoStorageCompatibilityError extends Schema.TaggedError<DoStorageCompatibilityError>()(
6
+ "DoStorageCompatibilityError",
7
+ {
8
+ actualVersion: Schema.Int,
9
+ message: Schema.String,
10
+ supportedVersion: Schema.Int,
11
+ },
12
+ ) {}
13
+
14
+ /** Stored bytes failed the current Schema and cannot be used as recovery truth. */
15
+ export class DoStorageCorruptionError extends Schema.TaggedError<DoStorageCorruptionError>()(
16
+ "DoStorageCorruptionError",
17
+ {
18
+ message: Schema.String,
19
+ rowKey: Schema.String,
20
+ table: Schema.String,
21
+ },
22
+ ) {}
23
+
24
+ /** Durable Object SQLite infrastructure failed while opening or operating the store. */
25
+ export class DoStorageError extends Schema.TaggedError<DoStorageError>()("DoStorageError", {
26
+ cause: Schema.optionalKey(Schema.Defect()),
27
+ message: Schema.String,
28
+ operation: Schema.String,
29
+ }) {}
30
+
31
+ /**
32
+ * Durable Object SQLite infrastructure failed while operating the Submission Ledger. Surfaces
33
+ * at the SubmissionLedger port as the typed `LedgerError` with this error preserved as its
34
+ * cause, so the adapter-level tag is never erased.
35
+ */
36
+ export class DoLedgerError extends Schema.TaggedError<DoLedgerError>()("DoLedgerError", {
37
+ cause: Schema.optionalKey(Schema.Defect()),
38
+ message: Schema.String,
39
+ operation: Schema.String,
40
+ }) {}
41
+
42
+ /**
43
+ * A value to be stored exceeds the configured Durable Object per-value bound
44
+ * (`DoStorageConfigValue.maxStoredValueBytes`, kept under the platform's 2 MB SQLite value
45
+ * limit). The refusal happens typed BEFORE any durable mutation; no partial state is written.
46
+ * Payloads of this size are the designed overflow case for a future R2-backed AttachmentStore
47
+ * (deployment spec §3.1, deferred until a real attachment requirement exists).
48
+ */
49
+ export class DoValueBoundExceeded extends Schema.TaggedError<DoValueBoundExceeded>()(
50
+ "DoValueBoundExceeded",
51
+ {
52
+ actualBytes: Schema.Int,
53
+ maxBytes: Schema.Int,
54
+ operation: Schema.String,
55
+ },
56
+ ) {
57
+ override get message() {
58
+ return (
59
+ `A stored value of ${this.actualBytes} bytes exceeds the Durable Object per-value bound ` +
60
+ `of ${this.maxBytes} bytes during ${this.operation}. Nothing was written. Values of this ` +
61
+ "size are the designed R2 AttachmentStore overflow path (deferred, deployment spec §3.1)."
62
+ );
63
+ }
64
+ }
65
+
66
+ /**
67
+ * A canonical batch retry conflicts with existing append state. Tail conflicts carry the
68
+ * actual committed tail as a diagnostic resume hint.
69
+ */
70
+ export class DoAppendConflict extends Schema.TaggedError<DoAppendConflict>()("DoAppendConflict", {
71
+ message: Schema.String,
72
+ reason: Schema.Literals(["batch-digest", "record-identity", "tail"]),
73
+ actualTailSequence: Schema.optionalKey(CanonicalSequence),
74
+ actualTailDigest: Schema.optionalKey(Schema.String),
75
+ }) {}
76
+
77
+ /**
78
+ * A producer epoch does not match the Conversation's current writer registration. Appends
79
+ * require the exact registered epoch, so both older and newer unregistered epochs are fenced;
80
+ * a newer epoch takes over by materializing first.
81
+ */
82
+ export class DoFenceRejected extends Schema.TaggedError<DoFenceRejected>()("DoFenceRejected", {
83
+ actualEpoch: ProducerEpoch,
84
+ message: Schema.String,
85
+ producerEpoch: ProducerEpoch,
86
+ }) {}
87
+
88
+ /** A checkpoint conflicts with a previously stored checkpoint at the same offset. */
89
+ export class DoCheckpointConflict extends Schema.TaggedError<DoCheckpointConflict>()(
90
+ "DoCheckpointConflict",
91
+ {
92
+ message: Schema.String,
93
+ },
94
+ ) {}
95
+
96
+ /**
97
+ * Deterministic fault-injection locations at Durable Object storage operation boundaries.
98
+ *
99
+ * The string list is copied VERBATIM from `SqliteStorageFailpointLocation`
100
+ * (`packages/storage-sqlite/src/errors.ts`) so every crash-matrix row keeps the same name on
101
+ * both platforms — the DN process-kill evidence and the DC eviction evidence address identical
102
+ * locations. There is intentionally no Cloudflare-only location.
103
+ */
104
+ export const DoStorageFailpointLocation = Schema.Literals([
105
+ "materialize:before",
106
+ "materialize:after",
107
+ "append:before",
108
+ "append:after-batch-insert",
109
+ "append:after-record-insert",
110
+ "append:after-tail-update",
111
+ "append:after",
112
+ "export:after-conversation-read",
113
+ "save-checkpoint:before",
114
+ "save-checkpoint:after",
115
+ "ledger:admit:before",
116
+ "ledger:admit:after",
117
+ "ledger:mark-ready:before",
118
+ "ledger:mark-ready:after",
119
+ "ledger:claim:before",
120
+ "ledger:claim:after",
121
+ "ledger:mark-input-applied:before",
122
+ "ledger:mark-input-applied:after",
123
+ "ledger:renew:before",
124
+ "ledger:renew:after",
125
+ "ledger:reserve-settlement:before",
126
+ "ledger:reserve-settlement:after",
127
+ "ledger:finalize-settlement:before",
128
+ "ledger:finalize-settlement:after",
129
+ "ledger:request-abort:before",
130
+ "ledger:request-abort:after",
131
+ "ledger:release:before",
132
+ "ledger:release:after",
133
+ "ledger:claim-joining:before",
134
+ "ledger:claim-joining:after",
135
+ "ledger:mark-joined:before",
136
+ "ledger:mark-joined:after",
137
+ "ledger:revert-joining:before",
138
+ "ledger:revert-joining:after",
139
+ "ledger:suspend:before",
140
+ "ledger:suspend:after",
141
+ "ledger:approval-decision:before",
142
+ "ledger:approval-decision:after",
143
+ "ledger:mark-unknown:before",
144
+ "ledger:mark-unknown:after",
145
+ "ledger:unknown-resolution:before",
146
+ "ledger:unknown-resolution:after",
147
+ "ledger:child-reservation:before",
148
+ "ledger:child-reservation:after",
149
+ "ledger:child-attach:before",
150
+ "ledger:child-attach:after",
151
+ "ledger:child-release-pending:before",
152
+ "ledger:child-release-pending:after",
153
+ "ledger:child-release:before",
154
+ "ledger:child-release:after",
155
+ "ledger:child-settled:before",
156
+ "ledger:child-settled:after",
157
+ ]);
158
+ export type DoStorageFailpointLocation = typeof DoStorageFailpointLocation.Type;
159
+
160
+ /** Deterministic test-only fault or pause injected at a Durable Object storage boundary. */
161
+ export class DoStorageFailpointError extends Schema.TaggedError<DoStorageFailpointError>()(
162
+ "DoStorageFailpointError",
163
+ {
164
+ location: DoStorageFailpointLocation,
165
+ },
166
+ ) {
167
+ override get message() {
168
+ return `Injected Durable Object storage failure at ${this.location}.`;
169
+ }
170
+ }
package/src/index.ts ADDED
@@ -0,0 +1,32 @@
1
+ /**
2
+ * `@effect-agent/storage-cloudflare` — Durable Object SQLite adapters for the session ports
3
+ * (`ConversationStore`, `SubmissionLedger`).
4
+ *
5
+ * WP1 lands the LOCAL facets: the full port implementations against one Conversation Durable
6
+ * Object's private SQLite database, structurally mirroring the Node/SQLite adapters (same
7
+ * tables, same failpoint-location names, same conformance suites) with the DC-specific
8
+ * differences documented in each module — Durable Object storage-backed transactions instead
9
+ * of `BEGIN IMMEDIATE`, an `effect_agent_meta` exact-or-fresh version gate instead of
10
+ * `PRAGMA user_version`, a ~1.9 MB per-value bound instead of 16 MB, routable minted
11
+ * Submission identities, and the durable `effect_agent_child_settlements` cross-store
12
+ * notification marker.
13
+ *
14
+ * WP2 adds the cross-Object distribution seam: `port-protocol.ts` (the Schema
15
+ * request/response/failure envelopes for the CLOSED route-capable port subset) and
16
+ * `routing.ts` (the `ConversationPortTransport` service, the routed decorator Layers over
17
+ * the local facets — this-conversation → local, route-capable foreign → transport, anything
18
+ * else foreign → fail fast typed — and the owner-side `handleEncodedPortRequest` endpoint
19
+ * body for the Conversation Object's `portCall`).
20
+ *
21
+ * This package never imports the `cloudflare:workers` runtime module — Durable Object handles
22
+ * (`ctx.storage`) are injected as Layer construction values, and `@cloudflare/workers-types`
23
+ * stays a types-only devDependency.
24
+ */
25
+ export * from "./errors.ts";
26
+ export * from "./migrations.ts";
27
+ export * from "./do-conversation-store.ts";
28
+ export * from "./do-ledger.ts";
29
+ export * from "./do-storage-config.ts";
30
+ export * from "./do-storage-failpoint.ts";
31
+ export * from "./port-protocol.ts";
32
+ export * from "./routing.ts";
@@ -0,0 +1,267 @@
1
+ import { SqliteMigrator } from "@effect/sql-sqlite-do";
2
+ import { Effect } from "effect";
3
+ import * as SqlClient from "effect/unstable/sql/SqlClient";
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
+ */
11
+ export const CurrentDoStorageVersion = 1;
12
+
13
+ /**
14
+ * The Conversation 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:
18
+ *
19
+ * 1. `effect_agent_meta` replaces `PRAGMA user_version` as the exact-or-fresh version gate —
20
+ * a meta table is portable regardless of which PRAGMAs Durable Object SQL storage allows.
21
+ * 2. `effect_agent_child_settlements` is the durable cross-store notification marker the
22
+ * SubmissionLedger port contract mandates for cross-store adapters (`suspend`'s covering
23
+ * check and `recordChildSettled`'s wake both consult it): parent and child Conversations
24
+ * live in different Durable Objects, so a child settlement reported before the parent's
25
+ * suspend commits must be observable from the PARENT's own storage.
26
+ */
27
+ export const doMigrations = SqliteMigrator.fromRecord({
28
+ "1_current_cloudflare_conversation_object": Effect.gen(function* () {
29
+ const sql = yield* SqlClient.SqlClient;
30
+
31
+ yield* sql`
32
+ CREATE TABLE effect_agent_conversations (
33
+ conversation_id TEXT PRIMARY KEY NOT NULL,
34
+ created_at TEXT NOT NULL,
35
+ tail_sequence INTEGER NOT NULL,
36
+ tail_digest TEXT NOT NULL,
37
+ producer_epoch INTEGER NOT NULL
38
+ )
39
+ `.withoutTransform;
40
+
41
+ yield* sql`
42
+ CREATE TABLE effect_agent_canonical_batches (
43
+ conversation_id TEXT NOT NULL,
44
+ batch_id TEXT NOT NULL,
45
+ first_sequence INTEGER NOT NULL,
46
+ last_sequence INTEGER NOT NULL,
47
+ batch_digest TEXT NOT NULL,
48
+ tail_digest TEXT NOT NULL,
49
+ batch_json TEXT NOT NULL,
50
+ PRIMARY KEY (conversation_id, batch_id),
51
+ FOREIGN KEY (conversation_id)
52
+ REFERENCES effect_agent_conversations(conversation_id)
53
+ ON DELETE RESTRICT
54
+ )
55
+ `.withoutTransform;
56
+
57
+ yield* sql`
58
+ CREATE TABLE effect_agent_canonical_records (
59
+ conversation_id TEXT NOT NULL,
60
+ sequence INTEGER NOT NULL,
61
+ record_id TEXT NOT NULL,
62
+ batch_id TEXT NOT NULL,
63
+ record_json TEXT NOT NULL,
64
+ PRIMARY KEY (conversation_id, sequence),
65
+ UNIQUE (conversation_id, record_id),
66
+ FOREIGN KEY (conversation_id, batch_id)
67
+ REFERENCES effect_agent_canonical_batches(conversation_id, batch_id)
68
+ ON DELETE RESTRICT
69
+ )
70
+ `.withoutTransform;
71
+
72
+ yield* sql`
73
+ CREATE INDEX effect_agent_canonical_records_batch
74
+ ON effect_agent_canonical_records (conversation_id, batch_id, sequence)
75
+ `.withoutTransform;
76
+
77
+ yield* sql`
78
+ CREATE TABLE effect_agent_checkpoints (
79
+ conversation_id TEXT NOT NULL,
80
+ through_sequence INTEGER NOT NULL,
81
+ tail_digest TEXT NOT NULL,
82
+ checkpoint_json TEXT NOT NULL,
83
+ PRIMARY KEY (conversation_id, through_sequence),
84
+ FOREIGN KEY (conversation_id)
85
+ REFERENCES effect_agent_conversations(conversation_id)
86
+ ON DELETE RESTRICT
87
+ )
88
+ `.withoutTransform;
89
+
90
+ // Admission rows exist before Conversation materialization (durability §4), so
91
+ // conversation_id intentionally carries no foreign key into effect_agent_conversations.
92
+ yield* sql`
93
+ CREATE TABLE effect_agent_submissions (
94
+ submission_id TEXT PRIMARY KEY NOT NULL,
95
+ conversation_id TEXT NOT NULL,
96
+ queue_sequence INTEGER NOT NULL,
97
+ principal TEXT NOT NULL,
98
+ idempotency_key TEXT NOT NULL,
99
+ agent_id TEXT NOT NULL,
100
+ agent_digests_json TEXT NOT NULL,
101
+ deployment_id TEXT NOT NULL,
102
+ input_json TEXT NOT NULL,
103
+ input_digest TEXT NOT NULL,
104
+ receipt_id TEXT NOT NULL,
105
+ state TEXT NOT NULL,
106
+ settled_outcome TEXT,
107
+ created_at TEXT NOT NULL,
108
+ ready_at TEXT,
109
+ input_applied_record_id TEXT,
110
+ input_applied_sequence INTEGER,
111
+ joined_host_submission_id TEXT,
112
+ suspended_reason_json TEXT,
113
+ suspended_at TEXT,
114
+ unknown_reason TEXT,
115
+ unknown_tool_call_ids_json TEXT,
116
+ parent_submission_id TEXT,
117
+ parent_tool_call_id TEXT,
118
+ UNIQUE (conversation_id, principal, idempotency_key),
119
+ UNIQUE (conversation_id, queue_sequence)
120
+ )
121
+ `.withoutTransform;
122
+
123
+ yield* sql`
124
+ CREATE INDEX effect_agent_submissions_joined_host
125
+ ON effect_agent_submissions (joined_host_submission_id)
126
+ `.withoutTransform;
127
+
128
+ yield* sql`
129
+ CREATE INDEX effect_agent_submissions_parent
130
+ ON effect_agent_submissions (parent_submission_id)
131
+ `.withoutTransform;
132
+
133
+ yield* sql`
134
+ CREATE TABLE effect_agent_submission_ownership (
135
+ submission_id TEXT PRIMARY KEY NOT NULL,
136
+ attempt_id TEXT NOT NULL,
137
+ ownership_token TEXT NOT NULL,
138
+ producer_epoch INTEGER NOT NULL,
139
+ owner_producer_id TEXT NOT NULL,
140
+ lease_expires_at TEXT NOT NULL,
141
+ FOREIGN KEY (submission_id)
142
+ REFERENCES effect_agent_submissions(submission_id)
143
+ ON DELETE RESTRICT
144
+ )
145
+ `.withoutTransform;
146
+
147
+ yield* sql`
148
+ CREATE TABLE effect_agent_attempts (
149
+ attempt_id TEXT PRIMARY KEY NOT NULL,
150
+ submission_id TEXT NOT NULL,
151
+ conversation_id TEXT NOT NULL,
152
+ owner_producer_id TEXT NOT NULL,
153
+ producer_epoch INTEGER NOT NULL,
154
+ claimed_at TEXT NOT NULL,
155
+ FOREIGN KEY (submission_id)
156
+ REFERENCES effect_agent_submissions(submission_id)
157
+ ON DELETE RESTRICT
158
+ )
159
+ `.withoutTransform;
160
+
161
+ yield* sql`
162
+ CREATE TABLE effect_agent_settlement_reservations (
163
+ submission_id TEXT PRIMARY KEY NOT NULL,
164
+ settlement_id TEXT NOT NULL,
165
+ outcome TEXT NOT NULL,
166
+ record_id TEXT NOT NULL,
167
+ record_json TEXT NOT NULL,
168
+ record_digest TEXT NOT NULL,
169
+ reserved_at TEXT NOT NULL,
170
+ finalized_at TEXT,
171
+ FOREIGN KEY (submission_id)
172
+ REFERENCES effect_agent_submissions(submission_id)
173
+ ON DELETE RESTRICT
174
+ )
175
+ `.withoutTransform;
176
+
177
+ yield* sql`
178
+ CREATE TABLE effect_agent_abort_intents (
179
+ submission_id TEXT PRIMARY KEY NOT NULL,
180
+ author TEXT NOT NULL,
181
+ reason TEXT NOT NULL,
182
+ requested_at TEXT NOT NULL,
183
+ canonical_record_id TEXT,
184
+ FOREIGN KEY (submission_id)
185
+ REFERENCES effect_agent_submissions(submission_id)
186
+ ON DELETE RESTRICT
187
+ )
188
+ `.withoutTransform;
189
+
190
+ yield* sql`
191
+ CREATE TABLE effect_agent_approval_decisions (
192
+ submission_id TEXT NOT NULL,
193
+ tool_call_id TEXT NOT NULL,
194
+ decision TEXT NOT NULL,
195
+ resolver TEXT NOT NULL,
196
+ reason TEXT NOT NULL,
197
+ decided_at TEXT NOT NULL,
198
+ PRIMARY KEY (submission_id, tool_call_id),
199
+ FOREIGN KEY (submission_id)
200
+ REFERENCES effect_agent_submissions(submission_id)
201
+ ON DELETE RESTRICT
202
+ )
203
+ `.withoutTransform;
204
+
205
+ yield* sql`
206
+ CREATE TABLE effect_agent_unknown_resolutions (
207
+ submission_id TEXT NOT NULL,
208
+ tool_call_id TEXT NOT NULL,
209
+ author TEXT NOT NULL,
210
+ reason TEXT NOT NULL,
211
+ resolution_json TEXT NOT NULL,
212
+ resolved_at TEXT NOT NULL,
213
+ PRIMARY KEY (submission_id, tool_call_id),
214
+ FOREIGN KEY (submission_id)
215
+ REFERENCES effect_agent_submissions(submission_id)
216
+ ON DELETE RESTRICT
217
+ )
218
+ `.withoutTransform;
219
+
220
+ yield* sql`
221
+ CREATE TABLE effect_agent_child_reservations (
222
+ reservation_id TEXT PRIMARY KEY NOT NULL,
223
+ parent_submission_id TEXT NOT NULL,
224
+ parent_tool_call_id TEXT NOT NULL,
225
+ child_submission_id TEXT,
226
+ status TEXT NOT NULL,
227
+ allocation_json TEXT NOT NULL,
228
+ allocation_digest TEXT NOT NULL,
229
+ accounting_json TEXT,
230
+ reserved_at TEXT NOT NULL,
231
+ release_began_at TEXT,
232
+ released_at TEXT,
233
+ UNIQUE (parent_submission_id, parent_tool_call_id),
234
+ FOREIGN KEY (parent_submission_id)
235
+ REFERENCES effect_agent_submissions(submission_id)
236
+ ON DELETE RESTRICT
237
+ )
238
+ `.withoutTransform;
239
+
240
+ // Durable cross-store child-settlement notification marker (parent-side; the child's row
241
+ // lives in ANOTHER Durable Object). child_outcome is nullable: the notification command
242
+ // carries identities only, and the child's canonical Settlement stays the outcome
243
+ // authority (DUR-015). No foreign keys: the parent row is checked by the operation, and
244
+ // the child row is intentionally foreign.
245
+ yield* sql`
246
+ CREATE TABLE effect_agent_child_settlements (
247
+ parent_submission_id TEXT NOT NULL,
248
+ child_submission_id TEXT NOT NULL,
249
+ child_outcome TEXT,
250
+ recorded_at TEXT NOT NULL,
251
+ PRIMARY KEY (parent_submission_id, child_submission_id)
252
+ )
253
+ `.withoutTransform;
254
+
255
+ yield* sql`
256
+ CREATE TABLE effect_agent_meta (
257
+ key TEXT PRIMARY KEY NOT NULL,
258
+ value TEXT NOT NULL
259
+ )
260
+ `.withoutTransform;
261
+
262
+ yield* sql`
263
+ INSERT INTO effect_agent_meta (key, value)
264
+ VALUES ('storage_version', ${String(CurrentDoStorageVersion)})
265
+ `.withoutTransform;
266
+ }),
267
+ });