@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.
- package/dist/index.d.mts +1501 -0
- package/dist/index.mjs +3706 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +50 -0
- package/src/do-conversation-store.ts +850 -0
- package/src/do-journal.ts +1078 -0
- package/src/do-ledger.ts +2940 -0
- package/src/do-storage-config.ts +53 -0
- package/src/do-storage-failpoint.ts +87 -0
- package/src/errors.ts +170 -0
- package/src/index.ts +32 -0
- package/src/migrations.ts +267 -0
- package/src/port-protocol.ts +319 -0
- package/src/routing.ts +1083 -0
|
@@ -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
|
+
});
|