@yielded/agent-storage-memory 0.1.0-beta.166 → 0.1.0-beta.168
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/MemoryMessageDeliveryStore.d.mts +7 -3
- package/dist/MemoryMessageDeliveryStore.mjs +427 -64
- package/dist/MemoryMessageDeliveryStore.mjs.map +1 -1
- package/dist/MemoryScheduleStore.mjs +8 -8
- package/dist/MemoryScheduleStore.mjs.map +1 -1
- package/dist/MemorySemanticIndex.mjs +7 -9
- package/dist/MemorySemanticIndex.mjs.map +1 -1
- package/dist/MemorySubmissionLedger.d.mts +7 -4
- package/dist/MemorySubmissionLedger.mjs +761 -377
- package/dist/MemorySubmissionLedger.mjs.map +1 -1
- package/dist/MemorySubscriptionStore.mjs +23 -23
- package/dist/MemorySubscriptionStore.mjs.map +1 -1
- package/dist/MemoryThreadStore.d.mts +8 -4
- package/dist/MemoryThreadStore.mjs +1325 -277
- package/dist/MemoryThreadStore.mjs.map +1 -1
- package/dist/MemoryThreadStoreKernel-DYNDjlhO.d.mts +85 -0
- package/dist/WorkSelection-CmqTuws3.mjs +31 -0
- package/dist/WorkSelection-CmqTuws3.mjs.map +1 -0
- package/package.json +1 -1
- package/src/MemoryMessageDeliveryStore.ts +766 -230
- package/src/MemoryScheduleStore.ts +190 -200
- package/src/MemorySemanticIndex.ts +94 -95
- package/src/MemorySubmissionLedger.ts +1955 -1245
- package/src/MemorySubscriptionStore.ts +1122 -1117
- package/src/MemoryThreadStore.ts +2609 -628
- package/src/internal/MemoryThreadStoreKernel.ts +156 -0
- package/src/internal/WorkSelection.ts +29 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { t as MemoryThreadStoreKernel } from "./MemoryThreadStoreKernel-DYNDjlhO.mjs";
|
|
1
2
|
import { Layer } from "effect";
|
|
2
3
|
import { MessageDeliveryError, MessageDeliveryStore, MessageDeliveryStoreLimits } from "@yielded/agent/message-delivery";
|
|
3
4
|
declare namespace MemoryMessageDeliveryStore_d_exports {
|
|
@@ -7,9 +8,12 @@ export interface MemoryMessageDeliveryStoreOptions {
|
|
|
7
8
|
/** UTF-8 bound on the complete stored record, including a processed Settlement. */
|
|
8
9
|
readonly maxStoredValueBytes?: number;
|
|
9
10
|
}
|
|
10
|
-
/**
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Pair with MemoryThreadStoreLive so export/import sees every retained delivery obligation.
|
|
13
|
+
* Each retained value and live partition is bounded; completed deduplication evidence is never evicted.
|
|
14
|
+
*/
|
|
15
|
+
export declare const memoryMessageDeliveryStoreLayer: (limits?: MessageDeliveryStoreLimits, options?: MemoryMessageDeliveryStoreOptions) => Layer.Layer<MessageDeliveryStore, MessageDeliveryError, MemoryThreadStoreKernel>;
|
|
16
|
+
export declare const MemoryMessageDeliveryStoreLive: Layer.Layer<MessageDeliveryStore, MessageDeliveryError, MemoryThreadStoreKernel>;
|
|
13
17
|
//#endregion
|
|
14
18
|
export { MemoryMessageDeliveryStore_d_exports as t };
|
|
15
19
|
//# sourceMappingURL=MemoryMessageDeliveryStore.d.mts.map
|
|
@@ -1,14 +1,29 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
|
|
2
|
+
import { n as MemoryThreadStoreKernel, t as boundedWorkSelection } from "./WorkSelection-CmqTuws3.mjs";
|
|
2
3
|
import { ScheduleInstant } from "@yielded/agent/schedule";
|
|
3
|
-
import { Effect, Layer, Ref, Schema
|
|
4
|
+
import { Effect, Layer, MutableRef, Ref, Schema } from "effect";
|
|
4
5
|
import { ThreadId } from "@yielded/agent/identifiers";
|
|
5
6
|
import { MessageDeliveryChange, MessageDeliveryError, MessageDeliveryFailpoint, MessageDeliveryKey, MessageDeliveryPageRequest, MessageDeliveryRecord, MessageDeliveryStore, MessageDeliveryStoreLimits, applyMessageDeliveryChange, defaultMessageDeliveryStoreLimits, isWorkerUpdateDelivery, messageDeliveryCapacity, messageDeliveryDeadline, messageDeliveryKeyString, messageDeliveryUsesCapacity, sameMessageDeliveryIdentity, validateMessageDelivery } from "@yielded/agent/message-delivery";
|
|
7
|
+
import { validateCanonicalSettlement } from "@yielded/agent/settlement-publisher";
|
|
8
|
+
import { Settlement, settlementFailureFromRecord, submissionSettlementId, submissionSettlementRecordId } from "@yielded/agent/submission-ledger";
|
|
9
|
+
import { PreparedInput } from "@yielded/agent/subscription";
|
|
10
|
+
import { ThreadDeliveryImportReader, ThreadImportRejected, invalidThreadArchive } from "@yielded/agent/thread-import";
|
|
11
|
+
import { ThreadStoreError, makeTransferFactCheck } from "@yielded/agent/thread-store";
|
|
12
|
+
import { workId } from "@yielded/agent/thread-work";
|
|
6
13
|
//#region src/MemoryMessageDeliveryStore.ts
|
|
7
14
|
var MemoryMessageDeliveryStore_exports = /* @__PURE__ */ __exportAll({
|
|
8
15
|
MemoryMessageDeliveryStoreLive: () => MemoryMessageDeliveryStoreLive,
|
|
9
16
|
memoryMessageDeliveryStoreLayer: () => memoryMessageDeliveryStoreLayer
|
|
10
17
|
});
|
|
11
18
|
const codec = Schema.fromJsonString(MessageDeliveryRecord);
|
|
19
|
+
const deliveryFitsTransfer = makeTransferFactCheck(MessageDeliveryRecord);
|
|
20
|
+
const checkTransfer = (record) => deliveryFitsTransfer(record.key.ownerThreadId, {
|
|
21
|
+
...record,
|
|
22
|
+
leaseUntilMillis: null
|
|
23
|
+
}) ? Effect.void : Effect.fail(MessageDeliveryError.make({
|
|
24
|
+
reason: "capacity",
|
|
25
|
+
operation: "transfer-page-bytes"
|
|
26
|
+
}));
|
|
12
27
|
const decode = (text) => Schema.decodeEffect(codec)(text).pipe(Effect.mapError(() => MessageDeliveryError.make({
|
|
13
28
|
reason: "corrupt",
|
|
14
29
|
operation: "decode"
|
|
@@ -18,48 +33,186 @@ const Scan = Schema.Struct({
|
|
|
18
33
|
limit: Schema.Int.check(Schema.isGreaterThan(0), Schema.isLessThanOrEqualTo(100)),
|
|
19
34
|
ownerThreadId: Schema.optionalKey(ThreadId)
|
|
20
35
|
});
|
|
21
|
-
|
|
36
|
+
const position = (keys, key) => {
|
|
37
|
+
let low = 0;
|
|
38
|
+
let high = keys.length;
|
|
39
|
+
while (low < high) {
|
|
40
|
+
const mid = Math.floor((low + high) / 2);
|
|
41
|
+
if (keys[mid] < key) low = mid + 1;
|
|
42
|
+
else high = mid;
|
|
43
|
+
}
|
|
44
|
+
return low;
|
|
45
|
+
};
|
|
46
|
+
const retainKey = (keys, key) => {
|
|
47
|
+
const at = position(keys, key);
|
|
48
|
+
if (keys[at] !== key) keys.splice(at, 0, key);
|
|
49
|
+
};
|
|
50
|
+
const removeKey = (keys, key) => {
|
|
51
|
+
const at = position(keys, key);
|
|
52
|
+
if (keys[at] === key) keys.splice(at, 1);
|
|
53
|
+
};
|
|
54
|
+
const backend = () => ({
|
|
55
|
+
records: /* @__PURE__ */ new Map(),
|
|
56
|
+
work: /* @__PURE__ */ new Map(),
|
|
57
|
+
ordered: /* @__PURE__ */ new Map(),
|
|
58
|
+
pending: /* @__PURE__ */ new Map(),
|
|
59
|
+
counts: /* @__PURE__ */ new Map(),
|
|
60
|
+
metadata: /* @__PURE__ */ new Map(),
|
|
61
|
+
deadlines: /* @__PURE__ */ new Map(),
|
|
62
|
+
deadlineKeys: new Array(),
|
|
63
|
+
ownerDeadlineKeys: /* @__PURE__ */ new Map()
|
|
64
|
+
});
|
|
65
|
+
const ownerKey = (id) => JSON.stringify([id]);
|
|
66
|
+
const keysFor = (current, key) => {
|
|
67
|
+
let keys = current.ordered.get(key);
|
|
68
|
+
if (keys === void 0) current.ordered.set(key, keys = []);
|
|
69
|
+
return keys;
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Pair with MemoryThreadStoreLive so export/import sees every retained delivery obligation.
|
|
73
|
+
* Each retained value and live partition is bounded; completed deduplication evidence is never evicted.
|
|
74
|
+
*/
|
|
22
75
|
const memoryMessageDeliveryStoreLayer = (limits = defaultMessageDeliveryStoreLimits, options = {}) => Layer.effect(MessageDeliveryStore, Effect.gen(function* () {
|
|
76
|
+
const journal = yield* MemoryThreadStoreKernel;
|
|
23
77
|
const config = yield* validateMessageDelivery(MessageDeliveryStoreLimits, limits, "limits");
|
|
24
78
|
const maxStoredValueBytes = yield* validateMessageDelivery(Schema.Int.check(Schema.isGreaterThan(0)), options.maxStoredValueBytes ?? 16777216, "stored-value-limit");
|
|
25
|
-
const state = yield* Ref.make(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
79
|
+
const state = yield* Ref.make(backend());
|
|
80
|
+
yield* journal.registerDeliveryLookup((threadId, messageId) => MutableRef.get(state.ref).records.has(messageDeliveryKeyString({
|
|
81
|
+
ownerThreadId: threadId,
|
|
82
|
+
messageId
|
|
83
|
+
})));
|
|
84
|
+
yield* journal.registerWorkOwner("deliveries", {
|
|
85
|
+
threads: (request) => Ref.get(state).pipe(Effect.map((current) => {
|
|
86
|
+
const candidates = boundedWorkSelection(request.limit, (id) => id, request.afterThreadId);
|
|
87
|
+
for (const threadId of current.pending.keys()) candidates.add(threadId);
|
|
88
|
+
const threadIds = candidates.values;
|
|
89
|
+
const afterThreadId = threadIds.length === request.limit ? threadIds.at(-1) : void 0;
|
|
90
|
+
return {
|
|
91
|
+
threadIds,
|
|
92
|
+
...afterThreadId === void 0 ? {} : { afterThreadId }
|
|
93
|
+
};
|
|
94
|
+
})),
|
|
95
|
+
page: (threadId, after, limit) => Effect.gen(function* () {
|
|
96
|
+
const current = yield* Ref.get(state);
|
|
97
|
+
const candidates = boundedWorkSelection(limit + 1, (id) => id, after);
|
|
98
|
+
for (const id of current.pending.get(threadId) ?? []) candidates.add(id);
|
|
99
|
+
const ids = candidates.values;
|
|
100
|
+
const entries = [];
|
|
101
|
+
for (const messageId of ids.slice(0, limit)) {
|
|
102
|
+
const key = messageDeliveryKeyString({
|
|
103
|
+
ownerThreadId: threadId,
|
|
104
|
+
messageId
|
|
105
|
+
});
|
|
106
|
+
const entry = current.work.get(key);
|
|
107
|
+
if (entry === void 0 || !current.records.has(key)) return yield* ThreadStoreError.make({
|
|
108
|
+
operation: "delivery work",
|
|
109
|
+
message: "Pending delivery index is incomplete or corrupt"
|
|
110
|
+
});
|
|
111
|
+
entries.push(entry);
|
|
112
|
+
}
|
|
113
|
+
const next = ids.length > limit ? ids[limit - 1] : void 0;
|
|
114
|
+
return {
|
|
115
|
+
entries,
|
|
116
|
+
...next === void 0 ? {} : { after: next }
|
|
117
|
+
};
|
|
118
|
+
})
|
|
29
119
|
});
|
|
30
|
-
const
|
|
120
|
+
const commitInto = (current, record, encoded, publish) => {
|
|
31
121
|
const key = messageDeliveryKeyString(record.key);
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const pending =
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
122
|
+
const owner = record.key.ownerThreadId;
|
|
123
|
+
const old = current.metadata.get(key);
|
|
124
|
+
const update = isWorkerUpdateDelivery(record);
|
|
125
|
+
const pending = messageDeliveryUsesCapacity(record);
|
|
126
|
+
const peer = record.envelope.messageAdmission !== void 0 && "peerName" in record.envelope.messageAdmission;
|
|
127
|
+
let counts = current.counts.get(owner);
|
|
128
|
+
if (counts === void 0) current.counts.set(owner, counts = {
|
|
129
|
+
retained: 0,
|
|
130
|
+
ordinary: 0,
|
|
131
|
+
update: 0,
|
|
132
|
+
peer: 0
|
|
133
|
+
});
|
|
134
|
+
if (old === void 0) counts.retained++;
|
|
135
|
+
if (old?.pending) {
|
|
136
|
+
if (old.update) counts.update--;
|
|
137
|
+
else counts.ordinary--;
|
|
138
|
+
if (old.peer) counts.peer--;
|
|
139
|
+
}
|
|
140
|
+
if (pending) {
|
|
141
|
+
if (update) counts.update++;
|
|
142
|
+
else counts.ordinary++;
|
|
143
|
+
if (peer) counts.peer++;
|
|
144
|
+
}
|
|
145
|
+
let owned = current.pending.get(owner);
|
|
146
|
+
if (owned === void 0) current.pending.set(owner, owned = /* @__PURE__ */ new Set());
|
|
147
|
+
if (pending) owned.add(record.key.messageId);
|
|
148
|
+
else owned.delete(record.key.messageId);
|
|
149
|
+
if (owned.size === 0) current.pending.delete(owner);
|
|
150
|
+
retainKey(keysFor(current, ownerKey(owner)), record.key.messageId);
|
|
39
151
|
const admission = record.envelope.workerAdmission;
|
|
40
152
|
if (admission !== void 0) {
|
|
41
153
|
const worker = admission.origin.worker;
|
|
42
|
-
|
|
43
|
-
|
|
154
|
+
if (record.key.messageId === admission.origin.firstMessageId) retainKey(keysFor(current, JSON.stringify([
|
|
155
|
+
owner,
|
|
44
156
|
worker.delegationId,
|
|
45
157
|
worker.targetAgentId
|
|
46
|
-
]);
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
158
|
+
])), record.key.messageId);
|
|
159
|
+
const inputs = keysFor(current, JSON.stringify([owner, worker.threadId]));
|
|
160
|
+
if ((record.status === "pending" || record.status === "parked") && record.receipt === null) retainKey(inputs, record.key.messageId);
|
|
161
|
+
else removeKey(inputs, record.key.messageId);
|
|
162
|
+
}
|
|
163
|
+
const deadline = messageDeliveryDeadline(record);
|
|
164
|
+
let ownerDeadlines = current.ownerDeadlineKeys.get(owner);
|
|
165
|
+
if (ownerDeadlines === void 0) current.ownerDeadlineKeys.set(owner, ownerDeadlines = []);
|
|
166
|
+
if (old?.deadlineKey !== void 0) {
|
|
167
|
+
current.deadlines.delete(old.deadlineKey);
|
|
168
|
+
removeKey(current.deadlineKeys, old.deadlineKey);
|
|
169
|
+
removeKey(ownerDeadlines, old.deadlineKey);
|
|
170
|
+
}
|
|
171
|
+
const deadlineKey = deadline === null ? void 0 : `${String(deadline).padStart(16, "0")}:${key}`;
|
|
172
|
+
if (deadlineKey !== void 0 && deadline !== null) {
|
|
173
|
+
current.deadlines.set(deadlineKey, {
|
|
174
|
+
key: record.key,
|
|
175
|
+
deadline
|
|
176
|
+
});
|
|
177
|
+
retainKey(current.deadlineKeys, deadlineKey);
|
|
178
|
+
retainKey(ownerDeadlines, deadlineKey);
|
|
53
179
|
}
|
|
54
|
-
|
|
55
|
-
|
|
180
|
+
current.metadata.set(key, {
|
|
181
|
+
update,
|
|
56
182
|
pending,
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
183
|
+
peer,
|
|
184
|
+
...deadlineKey === void 0 ? {} : { deadlineKey }
|
|
185
|
+
});
|
|
186
|
+
if (pending) current.work.set(key, {
|
|
187
|
+
id: workId("delivery", record.key.messageId),
|
|
188
|
+
owner: {
|
|
189
|
+
_tag: "Delivery",
|
|
190
|
+
messageId: record.key.messageId
|
|
191
|
+
},
|
|
192
|
+
partition: update ? "update" : "ordinary",
|
|
193
|
+
stateReference: {
|
|
194
|
+
_tag: "Delivery",
|
|
195
|
+
messageId: record.key.messageId,
|
|
196
|
+
version: record.version
|
|
197
|
+
},
|
|
198
|
+
state: record.status === "pending" ? "ready" : "waiting",
|
|
199
|
+
...record.status === "parked" ? { wait: "parked" } : record.receipt !== null ? { wait: "destination" } : {},
|
|
200
|
+
...deadline === null ? {} : { notBeforeMillis: deadline },
|
|
201
|
+
...record.receipt === null ? {} : {
|
|
202
|
+
receiptId: record.receipt.receiptId,
|
|
203
|
+
queueSequence: record.receipt.queueSequence
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
else current.work.delete(key);
|
|
207
|
+
current.records.set(key, encoded);
|
|
208
|
+
if (publish) {
|
|
209
|
+
journal.initializeWork(owner);
|
|
210
|
+
journal.retainDelivery(owner, record.key.messageId);
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
const commit = (record, encoded) => Effect.sync(() => commitInto(MutableRef.get(state.ref), record, encoded, true));
|
|
61
214
|
const failpoint = yield* MessageDeliveryFailpoint;
|
|
62
|
-
const encode = Effect.
|
|
215
|
+
const encode = Effect.fnUntraced(function* (record) {
|
|
63
216
|
const text = yield* Schema.encodeEffect(codec)(record).pipe(Effect.mapError(() => MessageDeliveryError.make({
|
|
64
217
|
reason: "corrupt",
|
|
65
218
|
operation: "encode"
|
|
@@ -70,10 +223,7 @@ const memoryMessageDeliveryStoreLayer = (limits = defaultMessageDeliveryStoreLim
|
|
|
70
223
|
});
|
|
71
224
|
return text;
|
|
72
225
|
});
|
|
73
|
-
const
|
|
74
|
-
return yield* Effect.forEach((yield* Ref.get(state)).records.values(), decode);
|
|
75
|
-
});
|
|
76
|
-
const insert = Effect.fn("MemoryMessageDeliveryStore.insert")(function* (record) {
|
|
226
|
+
const insert = Effect.fnUntraced(function* (record) {
|
|
77
227
|
const encoded = yield* encode(record);
|
|
78
228
|
const input = yield* decode(encoded);
|
|
79
229
|
if (input.version !== 1 || input.status !== "pending" || input.leaseUntilMillis !== null || input.retry.attempts !== 0 || input.retry.generation !== 0 || input.retry.automaticAttempts !== 0 || input.retry.nextAttemptAtMillis !== input.createdAtMillis || input.retry.lastAttemptAtMillis !== null || input.retry.lastFailure !== null || input.deadlineAtMillis !== input.initialDeadlineAtMillis) return yield* MessageDeliveryError.make({
|
|
@@ -85,7 +235,11 @@ const memoryMessageDeliveryStoreLayer = (limits = defaultMessageDeliveryStoreLim
|
|
|
85
235
|
operation: "envelope-bytes"
|
|
86
236
|
});
|
|
87
237
|
yield* failpoint.hit("message-delivery:insert:before");
|
|
88
|
-
const inserted = yield*
|
|
238
|
+
const inserted = yield* journal.withMutation(Effect.uninterruptible(Effect.gen(function* () {
|
|
239
|
+
yield* journal.checkThreadCapacity(input.key.ownerThreadId).pipe(Effect.mapError(() => MessageDeliveryError.make({
|
|
240
|
+
reason: "capacity",
|
|
241
|
+
operation: "Thread capacity"
|
|
242
|
+
})));
|
|
89
243
|
const { records } = yield* Ref.get(state);
|
|
90
244
|
const key = messageDeliveryKeyString(input.key);
|
|
91
245
|
const existingText = records.get(key);
|
|
@@ -95,11 +249,14 @@ const memoryMessageDeliveryStoreLayer = (limits = defaultMessageDeliveryStoreLim
|
|
|
95
249
|
reason: "conflict",
|
|
96
250
|
operation: "insert"
|
|
97
251
|
});
|
|
252
|
+
journal.retainDelivery(existing.key.ownerThreadId, existing.key.messageId);
|
|
98
253
|
return existing;
|
|
99
254
|
}
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
|
|
255
|
+
yield* checkTransfer(input);
|
|
256
|
+
const update = isWorkerUpdateDelivery(input);
|
|
257
|
+
const counts = MutableRef.get(state.ref).counts.get(input.key.ownerThreadId);
|
|
258
|
+
const capacity = messageDeliveryCapacity(config, update);
|
|
259
|
+
if ((update ? counts?.update ?? 0 : counts?.ordinary ?? 0) >= capacity.pending) return yield* MessageDeliveryError.make({
|
|
103
260
|
reason: "capacity",
|
|
104
261
|
operation: "insert"
|
|
105
262
|
});
|
|
@@ -109,23 +266,25 @@ const memoryMessageDeliveryStoreLayer = (limits = defaultMessageDeliveryStoreLim
|
|
|
109
266
|
yield* failpoint.hit("message-delivery:insert:after");
|
|
110
267
|
return yield* decode(yield* encode(inserted));
|
|
111
268
|
});
|
|
112
|
-
const get = Effect.
|
|
269
|
+
const get = Effect.fnUntraced(function* (key) {
|
|
113
270
|
const input = yield* validateMessageDelivery(MessageDeliveryKey, key, "get");
|
|
114
271
|
const text = (yield* Ref.get(state)).records.get(messageDeliveryKeyString(input));
|
|
115
272
|
return text === void 0 ? null : yield* decode(text);
|
|
116
273
|
});
|
|
117
|
-
const change = Effect.
|
|
274
|
+
const change = Effect.fnUntraced(function* (key, change) {
|
|
118
275
|
const input = yield* validateMessageDelivery(MessageDeliveryChange, change, "change");
|
|
119
276
|
const decodedKey = yield* validateMessageDelivery(MessageDeliveryKey, key, "change");
|
|
120
277
|
const point = `message-delivery:${input._tag.toLowerCase()}`;
|
|
121
278
|
yield* failpoint.hit(`${point}:before`);
|
|
122
|
-
const changed = yield*
|
|
279
|
+
const changed = yield* journal.withMutation(Effect.uninterruptible(Effect.gen(function* () {
|
|
123
280
|
const existing = yield* get(decodedKey);
|
|
124
281
|
if (existing === null) return yield* MessageDeliveryError.make({
|
|
125
282
|
reason: "not-found",
|
|
126
283
|
operation: "change"
|
|
127
284
|
});
|
|
128
285
|
const next = yield* Effect.fromResult(applyMessageDeliveryChange(existing, input));
|
|
286
|
+
if (next === existing) return existing;
|
|
287
|
+
yield* checkTransfer(next);
|
|
129
288
|
const encoded = yield* encode(next);
|
|
130
289
|
yield* commit(next, encoded);
|
|
131
290
|
return next;
|
|
@@ -133,13 +292,196 @@ const memoryMessageDeliveryStoreLayer = (limits = defaultMessageDeliveryStoreLim
|
|
|
133
292
|
yield* failpoint.hit(`${point}:after`);
|
|
134
293
|
return yield* decode(yield* encode(changed));
|
|
135
294
|
});
|
|
295
|
+
const transferDecode = (text) => decode(text).pipe(Effect.mapError(() => ThreadStoreError.make({
|
|
296
|
+
operation: "delivery transfer",
|
|
297
|
+
message: "Invalid retained delivery wire"
|
|
298
|
+
})));
|
|
299
|
+
yield* journal.registerDeliveryTransfer({
|
|
300
|
+
record: (threadId, messageId) => {
|
|
301
|
+
const text = MutableRef.get(state.ref).records.get(messageDeliveryKeyString({
|
|
302
|
+
ownerThreadId: threadId,
|
|
303
|
+
messageId
|
|
304
|
+
}));
|
|
305
|
+
return text === void 0 ? Effect.succeed(void 0) : transferDecode(text);
|
|
306
|
+
},
|
|
307
|
+
snapshot: (threadId) => {
|
|
308
|
+
const counts = MutableRef.get(state.ref).counts.get(threadId);
|
|
309
|
+
return {
|
|
310
|
+
idle: (MutableRef.get(state.ref).pending.get(threadId)?.size ?? 0) === 0,
|
|
311
|
+
deliveries: counts?.retained ?? 0
|
|
312
|
+
};
|
|
313
|
+
},
|
|
314
|
+
pendingPeerCount: (threadId, limit) => Math.min(MutableRef.get(state.ref).counts.get(threadId)?.peer ?? 0, limit),
|
|
315
|
+
facts: Effect.fnUntraced(function* (threadId, after) {
|
|
316
|
+
const current = MutableRef.get(state.ref);
|
|
317
|
+
const keys = current.ordered.get(ownerKey(threadId)) ?? [];
|
|
318
|
+
const messageId = keys[after === void 0 ? 0 : position(keys, after) + Number(keys[position(keys, after)] === after)];
|
|
319
|
+
const record = messageId === void 0 ? void 0 : yield* transferDecode(current.records.get(messageDeliveryKeyString({
|
|
320
|
+
ownerThreadId: threadId,
|
|
321
|
+
messageId: Schema.decodeSync(MessageDeliveryKey.fields.messageId)(messageId)
|
|
322
|
+
})) ?? "");
|
|
323
|
+
return {
|
|
324
|
+
facts: {
|
|
325
|
+
admissions: [],
|
|
326
|
+
commands: {
|
|
327
|
+
aborts: [],
|
|
328
|
+
approvals: [],
|
|
329
|
+
resolutions: []
|
|
330
|
+
},
|
|
331
|
+
deliveries: record === void 0 ? [] : [MessageDeliveryRecord.make({
|
|
332
|
+
...record,
|
|
333
|
+
leaseUntilMillis: null
|
|
334
|
+
})]
|
|
335
|
+
},
|
|
336
|
+
after: messageId ?? after ?? ""
|
|
337
|
+
};
|
|
338
|
+
}),
|
|
339
|
+
startImport: Effect.fnUntraced(function* (threadId) {
|
|
340
|
+
const current = MutableRef.get(state.ref);
|
|
341
|
+
if ((current.counts.get(threadId)?.retained ?? 0) > 0) return yield* ThreadImportRejected.make({
|
|
342
|
+
threadId,
|
|
343
|
+
reason: "target-not-empty",
|
|
344
|
+
message: "Destination Thread owns retained deliveries"
|
|
345
|
+
});
|
|
346
|
+
const staged = backend();
|
|
347
|
+
return {
|
|
348
|
+
record: (messageId) => {
|
|
349
|
+
const text = staged.records.get(messageDeliveryKeyString({
|
|
350
|
+
ownerThreadId: threadId,
|
|
351
|
+
messageId
|
|
352
|
+
}));
|
|
353
|
+
return text === void 0 ? Effect.succeed(void 0) : transferDecode(text);
|
|
354
|
+
},
|
|
355
|
+
has: (messageId) => staged.records.has(messageDeliveryKeyString({
|
|
356
|
+
ownerThreadId: threadId,
|
|
357
|
+
messageId
|
|
358
|
+
})),
|
|
359
|
+
stage: Effect.fnUntraced(function* (page) {
|
|
360
|
+
for (const record of page.archive.deliveries) {
|
|
361
|
+
const key = messageDeliveryKeyString(record.key);
|
|
362
|
+
if (staged.records.has(key) || current.records.has(key)) return yield* invalidThreadArchive("Duplicate retained delivery", threadId);
|
|
363
|
+
const text = yield* encode(record).pipe(Effect.mapError(() => ThreadImportRejected.make({
|
|
364
|
+
threadId,
|
|
365
|
+
reason: "unsupported-capacity",
|
|
366
|
+
message: "Delivery wire exceeds destination stored-value bound"
|
|
367
|
+
})));
|
|
368
|
+
if (new TextEncoder().encode(JSON.stringify(yield* Schema.encodeEffect(PreparedInput)(record.envelope).pipe(Effect.mapError(() => invalidThreadArchive("Invalid delivery envelope", threadId))))).byteLength > config.maxEnvelopeBytes) return yield* ThreadImportRejected.make({
|
|
369
|
+
threadId,
|
|
370
|
+
reason: "unsupported-capacity",
|
|
371
|
+
message: "Delivery envelope exceeds destination bound"
|
|
372
|
+
});
|
|
373
|
+
const counts = staged.counts.get(threadId);
|
|
374
|
+
if (messageDeliveryUsesCapacity(record) && (isWorkerUpdateDelivery(record) ? counts?.update ?? 0 : counts?.ordinary ?? 0) >= messageDeliveryCapacity(config, isWorkerUpdateDelivery(record)).pending) return yield* ThreadImportRejected.make({
|
|
375
|
+
threadId,
|
|
376
|
+
reason: "unsupported-capacity",
|
|
377
|
+
message: "Pending delivery capacity exceeded"
|
|
378
|
+
});
|
|
379
|
+
commitInto(staged, record, text, false);
|
|
380
|
+
}
|
|
381
|
+
}),
|
|
382
|
+
prepareCommit: Effect.fnUntraced(function* () {
|
|
383
|
+
const reader = yield* ThreadDeliveryImportReader;
|
|
384
|
+
const read = (messageId) => {
|
|
385
|
+
const text = staged.records.get(messageDeliveryKeyString({
|
|
386
|
+
ownerThreadId: threadId,
|
|
387
|
+
messageId
|
|
388
|
+
}));
|
|
389
|
+
return text === void 0 ? Effect.fail(invalidThreadArchive("Missing delivery predecessor", threadId)) : transferDecode(text);
|
|
390
|
+
};
|
|
391
|
+
const visit = (messageId) => {
|
|
392
|
+
const metadata = staged.metadata.get(messageDeliveryKeyString({
|
|
393
|
+
ownerThreadId: threadId,
|
|
394
|
+
messageId
|
|
395
|
+
}));
|
|
396
|
+
return metadata === void 0 ? Effect.fail(invalidThreadArchive("Missing delivery predecessor metadata", threadId)) : Effect.succeed(metadata);
|
|
397
|
+
};
|
|
398
|
+
const verifyPredecessors = Effect.fnUntraced(function* (start) {
|
|
399
|
+
let cursor = start;
|
|
400
|
+
while (cursor !== void 0) {
|
|
401
|
+
const metadata = yield* visit(cursor);
|
|
402
|
+
if (metadata.importVisit === "verified") break;
|
|
403
|
+
if (metadata.importVisit === "visiting") return yield* invalidThreadArchive("Delivery predecessor cycle", threadId);
|
|
404
|
+
metadata.importVisit = "visiting";
|
|
405
|
+
const record = yield* read(cursor);
|
|
406
|
+
if (record.predecessor !== void 0 && (yield* read(record.predecessor)).createdAtMillis > record.createdAtMillis) return yield* invalidThreadArchive("Delivery predecessor has a later boundary", threadId);
|
|
407
|
+
cursor = record.predecessor;
|
|
408
|
+
}
|
|
409
|
+
cursor = start;
|
|
410
|
+
while (cursor !== void 0) {
|
|
411
|
+
const metadata = yield* visit(cursor);
|
|
412
|
+
if (metadata.importVisit === "verified") break;
|
|
413
|
+
metadata.importVisit = "verified";
|
|
414
|
+
cursor = (yield* read(cursor)).predecessor;
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
for (const text of staged.records.values()) {
|
|
418
|
+
const d = yield* transferDecode(text);
|
|
419
|
+
yield* verifyPredecessors(d.key.messageId);
|
|
420
|
+
if (d.receipt !== null) {
|
|
421
|
+
if (d.settlement !== null && d.settlement.settlementId !== submissionSettlementId(d.receipt.submissionId)) return yield* invalidThreadArchive("Delivery settlement identity is not canonical", threadId);
|
|
422
|
+
const foreign = d.receipt.threadId !== threadId;
|
|
423
|
+
if (foreign && d.status !== "processed") return yield* ThreadImportRejected.make({
|
|
424
|
+
threadId,
|
|
425
|
+
reason: "unsupported-obligations",
|
|
426
|
+
message: "Live foreign delivery receipt cannot be restored"
|
|
427
|
+
});
|
|
428
|
+
const destination = yield* reader.admission(d.receipt.threadId, d.receipt.submissionId);
|
|
429
|
+
if (destination === void 0) {
|
|
430
|
+
if (foreign && d.status === "processed") continue;
|
|
431
|
+
return yield* invalidThreadArchive("Destination delivery receipt evidence is missing", threadId);
|
|
432
|
+
}
|
|
433
|
+
const expected = PreparedInput.make({
|
|
434
|
+
schemaVersion: 1,
|
|
435
|
+
authorization: d.envelope.authorization,
|
|
436
|
+
threadId: destination.threadId,
|
|
437
|
+
deliveryPrincipal: destination.principal,
|
|
438
|
+
admissionKey: destination.idempotencyKey,
|
|
439
|
+
agentId: destination.agentId,
|
|
440
|
+
definitions: destination.agentDigests,
|
|
441
|
+
input: destination.inputPayload,
|
|
442
|
+
inputDigest: destination.inputDigest,
|
|
443
|
+
...destination.admissionGroup === void 0 ? {} : { admissionGroup: destination.admissionGroup },
|
|
444
|
+
...destination.admissionFence === void 0 ? {} : { admissionFence: destination.admissionFence },
|
|
445
|
+
...destination.workerAdmission === void 0 ? {} : { workerAdmission: destination.workerAdmission },
|
|
446
|
+
...destination.messageAdmission === void 0 ? {} : { messageAdmission: destination.messageAdmission }
|
|
447
|
+
});
|
|
448
|
+
if (d.receipt.receiptId !== destination.receiptId || d.receipt.queueSequence !== destination.queueSequence || !Schema.toEquivalence(PreparedInput)(d.envelope, expected)) return yield* invalidThreadArchive("Delivery envelope differs from destination admission", threadId);
|
|
449
|
+
if (d.settlement !== null) {
|
|
450
|
+
const record = yield* reader.record(destination.threadId, submissionSettlementRecordId(destination.submissionId));
|
|
451
|
+
if (record === void 0) {
|
|
452
|
+
if (foreign) continue;
|
|
453
|
+
return yield* invalidThreadArchive("Processed delivery has no destination settlement", threadId);
|
|
454
|
+
}
|
|
455
|
+
const settled = yield* validateCanonicalSettlement(record, destination).pipe(Effect.mapError(() => invalidThreadArchive("Invalid destination settlement", threadId)));
|
|
456
|
+
const failure = settlementFailureFromRecord(record);
|
|
457
|
+
const expected = Settlement.make({
|
|
458
|
+
submissionId: settled.submissionId,
|
|
459
|
+
receiptId: settled.receiptId,
|
|
460
|
+
settlementId: settled.settlementId,
|
|
461
|
+
outcome: settled.outcome,
|
|
462
|
+
settledAt: d.settlement.settledAt,
|
|
463
|
+
...failure === void 0 ? {} : { failure },
|
|
464
|
+
...d.settlement.runDisposition === void 0 || settled.runDisposition === void 0 ? {} : { runDisposition: settled.runDisposition },
|
|
465
|
+
...d.settlement.usageSummary === void 0 || settled.usageSummary === void 0 ? {} : { usageSummary: settled.usageSummary }
|
|
466
|
+
});
|
|
467
|
+
if (!Schema.toEquivalence(Settlement)(d.settlement, expected)) return yield* invalidThreadArchive("Delivery settlement differs from destination evidence", threadId);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return () => {
|
|
472
|
+
for (const text of staged.records.values()) commitInto(current, Schema.decodeSync(codec)(text), text, true);
|
|
473
|
+
};
|
|
474
|
+
})
|
|
475
|
+
};
|
|
476
|
+
})
|
|
477
|
+
});
|
|
136
478
|
return MessageDeliveryStore.of({
|
|
137
479
|
limits: config,
|
|
138
480
|
maxStoredValueBytes,
|
|
139
481
|
insert,
|
|
140
482
|
get,
|
|
141
483
|
change,
|
|
142
|
-
list: Effect.
|
|
484
|
+
list: Effect.fnUntraced(function* (request) {
|
|
143
485
|
const input = yield* validateMessageDelivery(MessageDeliveryPageRequest, request, "list");
|
|
144
486
|
const current = yield* Ref.get(state);
|
|
145
487
|
const workerKey = input.workerStarts !== void 0 ? JSON.stringify([
|
|
@@ -147,39 +489,60 @@ const memoryMessageDeliveryStoreLayer = (limits = defaultMessageDeliveryStoreLim
|
|
|
147
489
|
input.workerStarts.delegationId,
|
|
148
490
|
input.workerStarts.targetAgentId
|
|
149
491
|
]) : input.pendingWorker !== void 0 ? JSON.stringify([input.ownerThreadId, input.pendingWorker]) : void 0;
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
492
|
+
const keys = workerKey === void 0 ? input.pendingOnly ? Array.from(current.pending.get(input.ownerThreadId) ?? []).sort() : current.ordered.get(ownerKey(input.ownerThreadId)) ?? [] : current.ordered.get(workerKey) ?? [];
|
|
493
|
+
const start = input.after === void 0 ? 0 : position(keys, input.after) + Number(keys[position(keys, input.after)] === input.after);
|
|
494
|
+
const items = [];
|
|
495
|
+
let bytes = 0;
|
|
496
|
+
let at = start;
|
|
497
|
+
while (at < keys.length && items.length < input.limit) {
|
|
498
|
+
const text = current.records.get(messageDeliveryKeyString({
|
|
499
|
+
ownerThreadId: input.ownerThreadId,
|
|
500
|
+
messageId: Schema.decodeSync(MessageDeliveryKey.fields.messageId)(keys[at])
|
|
501
|
+
}));
|
|
502
|
+
if (text === void 0) return yield* MessageDeliveryError.make({
|
|
503
|
+
reason: "corrupt",
|
|
504
|
+
operation: "list locator"
|
|
505
|
+
});
|
|
506
|
+
const size = new TextEncoder().encode(text).byteLength;
|
|
507
|
+
if (size > 33554432) return yield* MessageDeliveryError.make({
|
|
508
|
+
reason: "capacity",
|
|
509
|
+
operation: "list byte bound"
|
|
510
|
+
});
|
|
511
|
+
if (bytes + size > 33554432) break;
|
|
512
|
+
items.push(yield* decode(text));
|
|
513
|
+
bytes += size;
|
|
514
|
+
at++;
|
|
515
|
+
}
|
|
155
516
|
return {
|
|
156
517
|
items,
|
|
157
|
-
next:
|
|
518
|
+
next: at < keys.length ? items.at(-1)?.key.messageId ?? null : null
|
|
158
519
|
};
|
|
159
520
|
}),
|
|
160
|
-
due: Effect.
|
|
521
|
+
due: Effect.fnUntraced(function* (nowMillis, limit, ownerThreadId) {
|
|
161
522
|
const input = yield* validateMessageDelivery(Scan, {
|
|
162
523
|
nowMillis,
|
|
163
524
|
limit,
|
|
164
525
|
...ownerThreadId === void 0 ? {} : { ownerThreadId }
|
|
165
526
|
}, "due");
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
527
|
+
const current = MutableRef.get(state.ref);
|
|
528
|
+
const keys = input.ownerThreadId === void 0 ? current.deadlineKeys : current.ownerDeadlineKeys.get(input.ownerThreadId) ?? [];
|
|
529
|
+
const selected = [];
|
|
530
|
+
for (const key of keys) {
|
|
531
|
+
const row = current.deadlines.get(key);
|
|
532
|
+
if (row === void 0) return yield* MessageDeliveryError.make({
|
|
533
|
+
reason: "corrupt",
|
|
534
|
+
operation: "deadline index"
|
|
535
|
+
});
|
|
536
|
+
if (row.deadline > input.nowMillis || selected.length === input.limit) break;
|
|
537
|
+
selected.push(row.key);
|
|
538
|
+
}
|
|
539
|
+
return selected;
|
|
173
540
|
}),
|
|
174
|
-
nextDeadline: Effect.
|
|
541
|
+
nextDeadline: Effect.fnUntraced(function* (ownerThreadId) {
|
|
175
542
|
if (ownerThreadId !== void 0) yield* validateMessageDelivery(ThreadId, ownerThreadId, "nextDeadline");
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const deadline = messageDeliveryDeadline(record);
|
|
180
|
-
if (deadline !== null && (next === null || deadline < next)) next = deadline;
|
|
181
|
-
}
|
|
182
|
-
return next;
|
|
543
|
+
const current = MutableRef.get(state.ref);
|
|
544
|
+
const first = (ownerThreadId === void 0 ? current.deadlineKeys : current.ownerDeadlineKeys.get(ownerThreadId))?.[0];
|
|
545
|
+
return first === void 0 ? null : current.deadlines.get(first)?.deadline ?? null;
|
|
183
546
|
})
|
|
184
547
|
});
|
|
185
548
|
}));
|