@spine-event-engine/storage-datastore 2.0.0-snapshot.22 → 2.0.0-snapshot.24

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.
Files changed (33) hide show
  1. package/README.md +8 -2
  2. package/REFERENCE.md +55 -11
  3. package/dist/datastore/agent-execution.d.ts +337 -0
  4. package/dist/datastore/agent-execution.d.ts.map +1 -0
  5. package/dist/datastore/agent-execution.js +789 -0
  6. package/dist/datastore/agent-execution.js.map +1 -0
  7. package/dist/datastore/agent-history.d.ts +108 -0
  8. package/dist/datastore/agent-history.d.ts.map +1 -0
  9. package/dist/datastore/agent-history.js +219 -0
  10. package/dist/datastore/agent-history.js.map +1 -0
  11. package/dist/datastore/entity-history.d.ts +22 -65
  12. package/dist/datastore/entity-history.d.ts.map +1 -1
  13. package/dist/datastore/entity-history.js +83 -247
  14. package/dist/datastore/entity-history.js.map +1 -1
  15. package/dist/datastore/record-storage.d.ts +24 -0
  16. package/dist/datastore/record-storage.d.ts.map +1 -1
  17. package/dist/datastore/record-storage.js +46 -7
  18. package/dist/datastore/record-storage.js.map +1 -1
  19. package/dist/datastore/storage-factory.d.ts +110 -6
  20. package/dist/datastore/storage-factory.d.ts.map +1 -1
  21. package/dist/datastore/storage-factory.js +261 -8
  22. package/dist/datastore/storage-factory.js.map +1 -1
  23. package/dist/datastore/tenant-catalog.d.ts +9 -1
  24. package/dist/datastore/tenant-catalog.d.ts.map +1 -1
  25. package/dist/datastore/tenant-catalog.js +225 -1
  26. package/dist/datastore/tenant-catalog.js.map +1 -1
  27. package/dist/tsconfig.tsbuildinfo +1 -1
  28. package/index.yaml +24 -1
  29. package/package.json +5 -5
  30. package/dist/datastore/delivery-cleanup.d.ts +0 -31
  31. package/dist/datastore/delivery-cleanup.d.ts.map +0 -1
  32. package/dist/datastore/delivery-cleanup.js +0 -111
  33. package/dist/datastore/delivery-cleanup.js.map +0 -1
@@ -0,0 +1,789 @@
1
+ /*
2
+ * Copyright 2026, CodeMatters. All rights reserved.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5
+ * in compliance with the License. You may obtain a copy of the License at
6
+ *
7
+ * https://www.apache.org/licenses/LICENSE-2.0
8
+ *
9
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
10
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11
+ * or implied. See the License for the specific language governing permissions and limitations under
12
+ * the License.
13
+ */
14
+ import { create, toBinary } from "@bufbuild/protobuf";
15
+ import { Time } from "@spine-event-engine/core";
16
+ import {} from "@spine-event-engine/proto";
17
+ import { AgentExecutionHeadSchema, AgentExecutionRecordSchema, AgentInvocationStatus, } from "@spine-event-engine/proto/generated/spine/server/agent/execution_record_pb.js";
18
+ import { AgentExecutionRecords, AgentExecutionSizes, AgentExecutionTransitions, AgentExecutionValues, AgentHistoryRecords, AgentHistoryKeys, } from "@spine-event-engine/storage/provider";
19
+ import { Datastore } from "@google-cloud/datastore";
20
+ import { AgentHistoryHash } from "./agent-history.js";
21
+ import { DatastoreRecordStorage, } from "./record-storage.js";
22
+ /**
23
+ * Persists Agent execution evidence through native namespace-scoped transactions.
24
+ *
25
+ * @typeParam I Typed Agent identifier.
26
+ * @typeParam S Generated Agent state.
27
+ */
28
+ export class DatastoreAgentExecution {
29
+ input;
30
+ rows;
31
+ /**
32
+ * Entity and staged-transaction limits for complete internal Protobuf wrappers.
33
+ */
34
+ capacity = Object.freeze({
35
+ executionRecordBytes: 1_000_000,
36
+ executionHeadBytes: 1_000_000,
37
+ historyRecordBytes: 1_000_000,
38
+ transactionPayloadBytes: 9 * 1024 * 1024,
39
+ });
40
+ #open = true;
41
+ #ready;
42
+ #staged = new WeakMap();
43
+ /**
44
+ * Binds one complete tenant and generated state type to native record kinds.
45
+ *
46
+ * @param input Requested fenced execution change.
47
+ * @param rows Provider record handles bound to the tenant.
48
+ */
49
+ constructor(input, rows) {
50
+ this.input = input;
51
+ this.rows = rows;
52
+ }
53
+ /**
54
+ * Persists source facts once after proving transaction-query capability.
55
+ *
56
+ * @param accepted Original accepted signal and selected bindings.
57
+ * @returns Persisted invocation with the original accepted facts.
58
+ */
59
+ async admit(accepted) {
60
+ const key = AgentExecutionValues.requiredKey(accepted.key, this.input.stateType);
61
+ await this.ready();
62
+ const record = create(AgentExecutionRecordSchema, {
63
+ accepted,
64
+ status: AgentInvocationStatus.AGENT_INVOCATION_ACCEPTED,
65
+ });
66
+ this.validateRecord(record);
67
+ return this.transaction(async (tx) => {
68
+ const head = (await this.head(tx, key)) ?? create(AgentExecutionHeadSchema, { scope: key.scope });
69
+ const prior = await this.invocation(tx, key);
70
+ if (prior !== undefined) {
71
+ if (!AgentExecutionValues.sameAccepted(prior.accepted, accepted))
72
+ throw new Error("Agent execution admission conflicts with immutable source facts.");
73
+ return prior;
74
+ }
75
+ this.offerCandidate(head, record);
76
+ this.save(tx, this.rows.head, head);
77
+ this.save(tx, this.rows.invocation, record);
78
+ return AgentExecutionValues.cloneRecord(record);
79
+ });
80
+ }
81
+ /**
82
+ * Reads a bounded native head page before a fixed provider-time cutoff.
83
+ *
84
+ * @param request Bounded pending-page request.
85
+ * @returns Bounded eligible work and continuation cursor.
86
+ */
87
+ async pending(request) {
88
+ this.requireOpen();
89
+ if (!Number.isSafeInteger(request.count) || request.count < 1)
90
+ throw new RangeError("Agent pending count must be a positive safe integer.");
91
+ if (request.after !== undefined && request.after.key.scope.stateType !== this.input.stateType)
92
+ throw new TypeError("Agent pending cursor belongs to another state type.");
93
+ const asOf = request.after?.asOf ?? Time.currentTime();
94
+ const page = await this.pendingHeads(request, asOf);
95
+ const observed = page.entries.slice(0, request.count).map((item) => item.record);
96
+ const records = [];
97
+ for (const head of observed) {
98
+ if (head.scope?.stateType !== this.input.stateType || head.pending === undefined)
99
+ throw new Error("Datastore Agent execution head scope or candidate is invalid.");
100
+ const record = await this.read(head.pending);
101
+ if (record !== undefined)
102
+ records.push(record);
103
+ }
104
+ const last = observed.at(-1);
105
+ return {
106
+ records,
107
+ hasMore: page.entries.length > request.count || page.hasMore,
108
+ ...(last?.scope === undefined || last.eligibleAt === undefined
109
+ ? {}
110
+ : { after: { asOf, key: { scope: last.scope, eligibleAt: last.eligibleAt } } }),
111
+ };
112
+ }
113
+ /**
114
+ * Reads no more than count plus one complete native head keys.
115
+ *
116
+ * @param asOf Fixed eligibility cutoff for the page.
117
+ * @param request Bounded pending-page request.
118
+ * @returns Bounded native page of eligible per-instance heads.
119
+ */
120
+ async pendingHeads(request, asOf) {
121
+ const filters = [
122
+ {
123
+ property: "state_digest",
124
+ operator: "=",
125
+ value: AgentHistoryHash.value(this.input.stateType),
126
+ },
127
+ { property: "pending_key", operator: "<", value: AgentExecutionRecords.pendingLower(asOf) },
128
+ ];
129
+ if (request.after !== undefined)
130
+ filters.push({
131
+ property: "pending_key",
132
+ operator: ">",
133
+ value: AgentExecutionRecords.pendingKey(request.after.key.eligibleAt, request.after.key.scope),
134
+ });
135
+ const entries = [];
136
+ const target = request.count === Number.MAX_SAFE_INTEGER ? request.count : request.count + 1;
137
+ let cursor;
138
+ for (;;) {
139
+ const page = await this.rows.head.queryProviderPage({
140
+ filters,
141
+ order: [{ property: "pending_key", direction: "asc" }],
142
+ ...(cursor === undefined ? {} : { cursor }),
143
+ limit: Math.min(128, target - entries.length),
144
+ });
145
+ entries.push(...page.entries);
146
+ if (entries.length >= target || !page.hasMore)
147
+ return { entries, hasMore: page.hasMore };
148
+ if (page.cursor === undefined)
149
+ throw new Error("Datastore pending page continuation is missing.");
150
+ cursor = page.cursor;
151
+ }
152
+ }
153
+ /**
154
+ * Reads one complete retained invocation after reconstruction.
155
+ *
156
+ * @param key Original Agent invocation identity.
157
+ * @returns Complete stored invocation when present.
158
+ */
159
+ async read(key) {
160
+ this.requireOpen();
161
+ const found = await this.rows.invocation.read(invocationId(AgentExecutionValues.requiredKey(key, this.input.stateType)));
162
+ checkIdentity(found, key);
163
+ return found;
164
+ }
165
+ /**
166
+ * Acquires the earliest same-instance candidate with a fresh transaction clock.
167
+ *
168
+ * @param expiresAt Requested future lease expiry.
169
+ * @param key Original Agent invocation identity.
170
+ * @param token Exclusive claim token.
171
+ * @returns Claimed work and preferences when eligible.
172
+ */
173
+ async claim(key, token, expiresAt) {
174
+ await this.ready();
175
+ return this.transaction((tx) => this.claimLocked(tx, key, token, expiresAt));
176
+ }
177
+ /**
178
+ * Checks the current head and invocation before writing one exclusive lease.
179
+ *
180
+ * @param expiresAt Requested future lease expiry.
181
+ * @param key Original Agent invocation identity.
182
+ * @param token Exclusive claim token.
183
+ * @param tx Native transaction that fences the change.
184
+ * @returns Claimed record and preferences when eligible.
185
+ */
186
+ async claimLocked(tx, key, token, expiresAt) {
187
+ const head = await this.head(tx, key);
188
+ const record = await this.invocation(tx, key);
189
+ const now = Time.currentTime();
190
+ if (!token || AgentExecutionValues.compareTime(expiresAt, now) <= 0)
191
+ throw new TypeError("Agent claim requires a future expiry and token.");
192
+ if (head === undefined || record === undefined || !AgentExecutionValues.isPending(record, now))
193
+ return undefined;
194
+ if (!(await this.eligibleClaim(tx, head, key, now)))
195
+ return undefined;
196
+ record.claimToken = token;
197
+ record.claimExpiresAt = expiresAt;
198
+ if (record.status === AgentInvocationStatus.AGENT_INVOCATION_ACCEPTED)
199
+ record.status = AgentInvocationStatus.AGENT_INVOCATION_ACTIVE;
200
+ head.active = key;
201
+ head.claimToken = token;
202
+ head.claimExpiresAt = expiresAt;
203
+ head.eligibleAt = expiresAt;
204
+ this.save(tx, this.rows.invocation, record);
205
+ this.save(tx, this.rows.head, head);
206
+ return {
207
+ record,
208
+ preferences: head.preferences.map((item) => item),
209
+ };
210
+ }
211
+ /**
212
+ * Checks the current per-instance head and earliest unresolved source.
213
+ *
214
+ * @param tx Native transaction that fences the claim.
215
+ * @param head Per-instance pending and lease record.
216
+ * @param key Original Agent invocation identity.
217
+ * @param now Fresh time read inside the transaction.
218
+ * @returns Whether this invocation may receive the next lease.
219
+ */
220
+ async eligibleClaim(tx, head, key, now) {
221
+ if (head.pending === undefined ||
222
+ AgentExecutionRecords.invocation(head.pending) !== AgentExecutionRecords.invocation(key) ||
223
+ (head.active !== undefined &&
224
+ head.claimExpiresAt !== undefined &&
225
+ AgentExecutionValues.compareTime(head.claimExpiresAt, now) > 0))
226
+ return false;
227
+ const first = await this.successor(tx, key);
228
+ return (first !== undefined &&
229
+ AgentExecutionRecords.invocation(AgentExecutionValues.requiredInvocation(first)) ===
230
+ AgentExecutionRecords.invocation(key));
231
+ }
232
+ /**
233
+ * Updates the same live token inside a fresh native transaction.
234
+ *
235
+ * @param expiresAt Requested future lease expiry.
236
+ * @param key Original Agent invocation identity.
237
+ * @param token Exclusive claim token.
238
+ * @returns Whether the current lease was extended.
239
+ */
240
+ async renew(key, token, expiresAt) {
241
+ await this.ready();
242
+ return this.transaction(async (tx) => {
243
+ const pair = await this.claimed(tx, key, token);
244
+ if (pair?.record.claimExpiresAt === undefined ||
245
+ AgentExecutionValues.compareTime(expiresAt, pair.record.claimExpiresAt) <= 0)
246
+ return false;
247
+ pair.record.claimExpiresAt = expiresAt;
248
+ pair.head.claimExpiresAt = expiresAt;
249
+ pair.head.eligibleAt = expiresAt;
250
+ this.save(tx, this.rows.invocation, pair.record);
251
+ this.save(tx, this.rows.head, pair.head);
252
+ return true;
253
+ });
254
+ }
255
+ /**
256
+ * Persists evidence and immutable history inside one native transaction.
257
+ *
258
+ * @param input Requested fenced execution change.
259
+ * @returns Completion after fenced evidence is persisted.
260
+ */
261
+ async update(input) {
262
+ await this.ready();
263
+ this.validateRecord(input.next);
264
+ await this.transaction(async (tx) => {
265
+ const pair = await this.expected(tx, input);
266
+ AgentExecutionTransitions.assertUpdate(pair.record, input.next);
267
+ await this.appendHistory(tx, input.key, input.historyEntries ?? []);
268
+ this.save(tx, this.rows.invocation, input.next);
269
+ if (input.next.status === AgentInvocationStatus.AGENT_INVOCATION_TERMINATED) {
270
+ await this.advance(tx, pair.head, input.key, AgentExecutionValues.requiredOrder(pair.record));
271
+ this.save(tx, this.rows.head, pair.head);
272
+ }
273
+ });
274
+ }
275
+ /**
276
+ * Commits Entity, history, preferences and outgoing IDs together.
277
+ *
278
+ * @param input Requested fenced execution change.
279
+ * @returns Completion after conditional Entity and evidence commit.
280
+ */
281
+ async complete(input) {
282
+ await this.ready();
283
+ this.validateRecord(input.next);
284
+ await this.transaction(async (tx) => {
285
+ const pair = await this.expected(tx, input);
286
+ const expectedVersion = this.validateCompletion(input, pair.record);
287
+ AgentExecutionTransitions.assertCompletion(input.next);
288
+ await this.completeEntity(tx, input, expectedVersion);
289
+ await this.appendHistory(tx, input.key, input.historyEntries ?? []);
290
+ pair.head.preferences = AgentExecutionValues.requiredCompletion(input.next).preferences.map((item) => item);
291
+ if (input.next.status === AgentInvocationStatus.AGENT_INVOCATION_COMPLETED)
292
+ await this.advance(tx, pair.head, input.key, AgentExecutionValues.requiredOrder(pair.record));
293
+ this.save(tx, this.rows.invocation, input.next);
294
+ this.save(tx, this.rows.head, pair.head);
295
+ });
296
+ }
297
+ /**
298
+ * Marks original output IDs delivered without rerunning completed handlers.
299
+ *
300
+ * @param expectedRecordBytes Exact prior execution record bytes.
301
+ * @param key Original Agent invocation identity.
302
+ * @param signals Original outgoing signal identities.
303
+ * @param token Exclusive claim token.
304
+ * @returns Completion after original output IDs are marked delivered.
305
+ */
306
+ async markDelivered(key, token, expectedRecordBytes, signals) {
307
+ await this.ready();
308
+ await this.transaction(async (tx) => {
309
+ const pair = await this.expected(tx, { key, token, expectedRecordBytes, next: create(AgentExecutionRecordSchema) }, false);
310
+ const completion = AgentExecutionValues.requiredCompletion(pair.record);
311
+ AgentExecutionTransitions.assertDelivery(pair.record, signals);
312
+ for (const signal of signals)
313
+ AgentExecutionValues.markSignal(completion, signal);
314
+ if (completion.commands.every((item) => item.delivered)) {
315
+ pair.record.status = AgentInvocationStatus.AGENT_INVOCATION_COMPLETED;
316
+ await this.advance(tx, pair.head, key, AgentExecutionValues.requiredOrder(pair.record));
317
+ }
318
+ this.save(tx, this.rows.invocation, pair.record);
319
+ this.save(tx, this.rows.head, pair.head);
320
+ });
321
+ }
322
+ /**
323
+ * Closes provider handles without deleting accepted work.
324
+ */
325
+ close() {
326
+ if (!this.#open)
327
+ return;
328
+ this.#open = false;
329
+ this.rows.invocation.close();
330
+ this.rows.head.close();
331
+ this.rows.history.close();
332
+ this.rows.current.close();
333
+ this.rows.states?.close();
334
+ this.rows.diagnostics?.close();
335
+ this.rows.events.close();
336
+ }
337
+ /**
338
+ * Rejects use after closure and probes native transaction-query support once.
339
+ *
340
+ * @returns Completion after provider capabilities are checked.
341
+ */
342
+ ready() {
343
+ this.requireOpen();
344
+ return (this.#ready ??= this.checkCapability().catch((error) => {
345
+ this.#ready = undefined;
346
+ throw error;
347
+ }));
348
+ }
349
+ /**
350
+ * Checks flat scoped successor and head queries before accepting a signal.
351
+ *
352
+ * @returns Completion after native query support is verified.
353
+ */
354
+ async checkCapability() {
355
+ const tx = this.rows.head.transaction();
356
+ try {
357
+ await tx.run();
358
+ await this.rows.invocation.queryTransactionSuccessors(tx, [
359
+ {
360
+ property: "scope_digest",
361
+ operator: "=",
362
+ value: AgentHistoryHash.value("capability-probe"),
363
+ },
364
+ { property: "status", operator: "=", value: "1" },
365
+ ], "order_key");
366
+ await this.checkHeadCapability();
367
+ }
368
+ finally {
369
+ await tx.rollback().catch(() => undefined);
370
+ }
371
+ }
372
+ /**
373
+ * Checks that native indexed head pagination is available.
374
+ *
375
+ * @returns Completion after the indexed query succeeds.
376
+ */
377
+ async checkHeadCapability() {
378
+ await this.rows.head.queryProviderPage({
379
+ filters: [
380
+ {
381
+ property: "state_digest",
382
+ operator: "=",
383
+ value: AgentHistoryHash.value(this.input.stateType),
384
+ },
385
+ {
386
+ property: "pending_key",
387
+ operator: "<",
388
+ value: AgentExecutionRecords.pendingLower(Time.currentTime()),
389
+ },
390
+ ],
391
+ order: [{ property: "pending_key", direction: "asc" }],
392
+ limit: 1,
393
+ });
394
+ }
395
+ /**
396
+ * Executes one namespace-scoped transaction and retries native contention.
397
+ *
398
+ * @param work Operation executed inside the transaction.
399
+ * @returns Callback result after the native transaction commits.
400
+ * @typeParam T Result returned by the transaction callback.
401
+ */
402
+ async transaction(work) {
403
+ this.requireOpen();
404
+ for (let attempt = 0; attempt < 3; attempt += 1) {
405
+ const tx = this.rows.head.transaction();
406
+ try {
407
+ await tx.run();
408
+ this.#staged.set(tx, []);
409
+ const result = await work(tx);
410
+ const staged = this.#staged.get(tx) ?? [];
411
+ validateTransactionRows(staged);
412
+ for (const row of staged)
413
+ tx.save(row);
414
+ await tx.commit();
415
+ return result;
416
+ }
417
+ catch (error) {
418
+ await tx.rollback().catch(() => undefined);
419
+ if (attempt < 2 && isAborted(error))
420
+ continue;
421
+ throw error;
422
+ }
423
+ finally {
424
+ this.#staged.delete(tx);
425
+ }
426
+ }
427
+ throw new Error("Datastore Agent transaction did not complete.");
428
+ }
429
+ /**
430
+ * Reads one head by complete scope inside the active transaction.
431
+ *
432
+ * @param key Original Agent invocation identity.
433
+ * @param tx Native transaction that fences the change.
434
+ * @returns Per-instance head when present.
435
+ */
436
+ async head(tx, key) {
437
+ const scope = AgentExecutionValues.requiredScope(key);
438
+ const found = await this.readRow(tx, this.rows.head, AgentHistoryHash.value(AgentExecutionRecords.scope(scope)));
439
+ if (found !== undefined &&
440
+ AgentExecutionRecords.scope(AgentExecutionValues.requiredHeadScope(found)) !==
441
+ AgentExecutionRecords.scope(scope))
442
+ throw new Error("Datastore Agent head physical ID conflicts with another scope.");
443
+ return found;
444
+ }
445
+ /**
446
+ * Reads one invocation by original source and complete scope.
447
+ *
448
+ * @param key Original Agent invocation identity.
449
+ * @param tx Native transaction that fences the change.
450
+ * @returns Stored invocation when present.
451
+ */
452
+ async invocation(tx, key) {
453
+ const found = await this.readRow(tx, this.rows.invocation, invocationId(key));
454
+ checkIdentity(found, key);
455
+ return found;
456
+ }
457
+ /**
458
+ * Reads a materialized key and decodes exact original Protobuf bytes.
459
+ *
460
+ * @param id Complete persisted record identity.
461
+ * @param storage Native record storage for this row.
462
+ * @param tx Native transaction that fences the change.
463
+ * @returns Stored typed row when present.
464
+ * @typeParam Id Typed record identifier.
465
+ * @typeParam R Typed persisted record.
466
+ */
467
+ async readRow(tx, storage, id) {
468
+ const raw = first((await tx.get(storage.transactionKey(id), { wrapNumbers: true })));
469
+ return raw === undefined ? undefined : storage.decodeTransactionEntity(raw);
470
+ }
471
+ /**
472
+ * Queues one validated native row for the current short transaction.
473
+ *
474
+ * @param record Persisted Agent execution record.
475
+ * @param storage Native record storage for this row.
476
+ * @param tx Native transaction that fences the change.
477
+ * @typeParam Id Typed record identifier.
478
+ * @typeParam R Typed persisted record.
479
+ */
480
+ save(tx, storage, record) {
481
+ if (record.$typeName === AgentExecutionRecordSchema.typeName)
482
+ this.validateRecord(record);
483
+ if (record.$typeName === AgentExecutionHeadSchema.typeName)
484
+ this.validateHead(record);
485
+ const row = storage.transactionEntity(record);
486
+ this.#staged.get(tx)?.push(row);
487
+ }
488
+ /**
489
+ * Checks fresh Time, exact token, and the persisted claim image.
490
+ *
491
+ * @param key Original Agent invocation identity.
492
+ * @param token Exclusive claim token.
493
+ * @param tx Native transaction that fences the change.
494
+ * @returns Current record and head if the token remains valid.
495
+ */
496
+ async claimed(tx, key, token) {
497
+ const head = await this.head(tx, key);
498
+ const record = await this.invocation(tx, key);
499
+ if (!token ||
500
+ head?.claimToken !== token ||
501
+ record?.claimToken !== token ||
502
+ head.active === undefined ||
503
+ AgentExecutionRecords.invocation(head.active) !== AgentExecutionRecords.invocation(key) ||
504
+ head.claimExpiresAt === undefined ||
505
+ AgentExecutionValues.compareTime(head.claimExpiresAt, Time.currentTime()) <= 0)
506
+ return undefined;
507
+ return { head, record };
508
+ }
509
+ /**
510
+ * Checks exact prior bytes and unchanged immutable source facts.
511
+ *
512
+ * @param checkNext Whether to validate the proposed next record.
513
+ * @param input Requested fenced execution change.
514
+ * @param tx Native transaction that fences the change.
515
+ * @returns Current fenced record and head.
516
+ */
517
+ async expected(tx, input, checkNext = true) {
518
+ const pair = await this.claimed(tx, input.key, input.token);
519
+ if (pair === undefined ||
520
+ !AgentExecutionValues.sameBytes(toBinary(AgentExecutionRecordSchema, pair.record), input.expectedRecordBytes))
521
+ throw new Error("Agent execution claim or expected record is no longer current.");
522
+ if (checkNext && !AgentExecutionValues.sameAccepted(pair.record.accepted, input.next.accepted))
523
+ throw new Error("Agent execution cannot change immutable accepted source facts.");
524
+ return pair;
525
+ }
526
+ /**
527
+ * Updates the pending head for one earlier accepted source without resetting existing eligibility.
528
+ *
529
+ * @param head Per-instance pending and lease record.
530
+ * @param record Persisted Agent execution record.
531
+ */
532
+ offerCandidate(head, record) {
533
+ AgentExecutionValues.offerCandidate(head, record);
534
+ this.validateHead(head);
535
+ }
536
+ /**
537
+ * Reads up to two same-instance rows per unresolved status inside the transaction.
538
+ *
539
+ * @param exclude Invocation omitted while selecting the successor.
540
+ * @param key Original Agent invocation identity.
541
+ * @param tx Native transaction that fences the change.
542
+ * @returns Earliest unresolved invocation when present.
543
+ */
544
+ async successor(tx, key, exclude) {
545
+ const scope = AgentExecutionValues.requiredScope(key);
546
+ const records = [];
547
+ for (const status of ["1", "2", "3"])
548
+ records.push(...(await this.successorsForStatus(tx, scope, status)));
549
+ return records
550
+ .filter((record) => AgentExecutionRecords.scope(AgentExecutionValues.requiredScope(AgentExecutionValues.requiredInvocation(record))) === AgentExecutionRecords.scope(scope) &&
551
+ (exclude === undefined ||
552
+ AgentExecutionRecords.invocation(AgentExecutionValues.requiredInvocation(record)) !==
553
+ AgentExecutionRecords.invocation(exclude)))
554
+ .sort((left, right) => AgentExecutionValues.compareText(AgentExecutionRecords.order(AgentExecutionValues.requiredOrder(left)), AgentExecutionRecords.order(AgentExecutionValues.requiredOrder(right))))[0];
555
+ }
556
+ /**
557
+ * Reads the first two rows of one unresolved status under the instance scope.
558
+ *
559
+ * @param tx Native transaction that fences successor selection.
560
+ * @param scope Generated state type and canonical Agent key.
561
+ * @param status Persisted unresolved invocation status.
562
+ * @returns Native rows in complete Inbox order.
563
+ */
564
+ successorsForStatus(tx, scope, status) {
565
+ return this.rows.invocation.queryTransactionSuccessors(tx, [
566
+ {
567
+ property: "scope_digest",
568
+ operator: "=",
569
+ value: AgentHistoryHash.value(AgentExecutionRecords.scope(scope)),
570
+ },
571
+ { property: "status", operator: "=", value: status },
572
+ ], "order_key");
573
+ }
574
+ /**
575
+ * Updates the next original Inbox source after terminal work.
576
+ *
577
+ * @param head Per-instance pending and lease record.
578
+ * @param key Original Agent invocation identity.
579
+ * @param order Original Inbox order.
580
+ * @param tx Native transaction that fences the change.
581
+ * @returns Completion after updating the next pending head.
582
+ */
583
+ async advance(tx, head, key, order) {
584
+ const next = await this.successor(tx, key, key);
585
+ AgentExecutionValues.advanceCandidate(head, order, next);
586
+ this.validateHead(head);
587
+ }
588
+ /**
589
+ * Reads immutable history keys before queuing any transaction writes.
590
+ *
591
+ * @param entries Immutable history entries to append.
592
+ * @param key Original Agent invocation identity.
593
+ * @param tx Native transaction that fences the change.
594
+ * @returns Completion after immutable history is appended.
595
+ */
596
+ async appendHistory(tx, key, entries) {
597
+ for (const entry of entries) {
598
+ if (AgentHistoryKeys.indexValue(AgentHistoryKeys.fromEntry(entry)).length > 1500)
599
+ throw new RangeError("Agent history order exceeds Datastore indexed-value capacity.");
600
+ const record = AgentHistoryRecords.record(AgentExecutionValues.requiredScope(key).stateType, AgentExecutionValues.requiredScope(key).agentKey, entry);
601
+ if (AgentExecutionSizes.history(AgentExecutionValues.requiredScope(key), entry) >
602
+ this.capacity.historyRecordBytes)
603
+ throw new RangeError("Agent history record exceeds Datastore entity payload capacity.");
604
+ await this.immutable(tx, this.rows.history, record);
605
+ }
606
+ }
607
+ /**
608
+ * Persists one missing immutable row or rejects divergent existing bytes.
609
+ *
610
+ * @param record Persisted Agent execution record.
611
+ * @param storage Native record storage for this row.
612
+ * @param tx Native transaction that fences the change.
613
+ * @returns Completion after immutable row insertion or exact-byte verification.
614
+ * @typeParam Id Typed record identifier.
615
+ * @typeParam R Typed persisted record.
616
+ */
617
+ async immutable(tx, storage, record) {
618
+ const raw = first((await tx.get(storage.transactionEntity(record).key, { wrapNumbers: true })));
619
+ if (raw === undefined) {
620
+ this.save(tx, storage, record);
621
+ return;
622
+ }
623
+ if (!storage.matchesTransactionEntity(raw, record))
624
+ throw new Error("Immutable Agent execution record has divergent content.");
625
+ }
626
+ /**
627
+ * Checks initial Version and stages Entity records in this transaction.
628
+ *
629
+ * @param expected Expected current Entity Version.
630
+ * @param input Requested fenced execution change.
631
+ * @param tx Native transaction that fences the change.
632
+ * @returns Completion after conditional Entity state and Version write.
633
+ */
634
+ async completeEntity(tx, input, expected) {
635
+ const recipient = AgentExecutionValues.requiredAccepted(input.next).recipientId;
636
+ const id = recipient === undefined ? undefined : this.input.entity.id.unpack(recipient);
637
+ if (id === undefined)
638
+ throw new TypeError("Agent completion recipient ID has the wrong type.");
639
+ const current = await this.readRow(tx, this.rows.current, id);
640
+ if ((current?.version?.number ?? 0) !== expected)
641
+ throw new Error("Agent execution initial Entity Version is no longer current.");
642
+ const commit = input.entityCommit;
643
+ if (commit === undefined)
644
+ return;
645
+ this.validateEntityCommit(commit, id);
646
+ for (const state of commit.states ?? [])
647
+ await this.immutable(tx, requiredStorage(this.rows.states), state);
648
+ for (const event of commit.diagnostics ?? [])
649
+ await this.immutable(tx, requiredStorage(this.rows.diagnostics), event);
650
+ for (const event of commit.events ?? [])
651
+ await this.immutable(tx, this.rows.events, event);
652
+ this.save(tx, this.rows.current, commit.next);
653
+ }
654
+ /**
655
+ * Rejects cross-Entity completion and disabled framework histories.
656
+ *
657
+ * @param commit Conditional Entity state and Version write.
658
+ * @param id Complete persisted record identity.
659
+ */
660
+ validateEntityCommit(commit, id) {
661
+ if (commit.entity.sourceType.typeName !== this.input.entity.sourceType.typeName ||
662
+ this.input.entity.id.key(commit.entityId) !== this.input.entity.id.key(id))
663
+ throw new TypeError("Agent Entity commit scope differs from accepted recipient.");
664
+ if (!this.input.entity.stateHistory && (commit.states?.length ?? 0) > 0)
665
+ throw new Error("Agent Entity state history is disabled.");
666
+ if (!this.input.entity.eventHistory && (commit.diagnostics?.length ?? 0) > 0)
667
+ throw new Error("Agent Entity diagnostic history is disabled.");
668
+ }
669
+ /**
670
+ * Checks exactly one Version advance or an unchanged state.
671
+ *
672
+ * @param input Requested fenced execution change.
673
+ * @param prior Previously persisted execution record.
674
+ * @returns Expected initial Entity Version.
675
+ */
676
+ validateCompletion(input, prior) {
677
+ const completion = input.next.completion;
678
+ if (completion?.initialVersion === undefined ||
679
+ completion.resultingVersion === undefined ||
680
+ input.next.accepted === undefined ||
681
+ !AgentExecutionValues.sameAccepted(prior.accepted, input.next.accepted))
682
+ throw new TypeError("Agent completion requires immutable acceptance and both Versions.");
683
+ if (prior.status !== AgentInvocationStatus.AGENT_INVOCATION_ACTIVE ||
684
+ ![
685
+ AgentInvocationStatus.AGENT_INVOCATION_COMPLETED,
686
+ AgentInvocationStatus.AGENT_INVOCATION_COMPLETED_PENDING_COMMANDS,
687
+ ].includes(input.next.status))
688
+ throw new Error("Agent completion requires active work and completed status.");
689
+ const initial = completion.initialVersion.number;
690
+ const final = completion.resultingVersion.number;
691
+ if (input.entityCommit === undefined
692
+ ? final !== initial
693
+ : final !== initial + 1 || input.entityCommit.next.version?.number !== final)
694
+ throw new Error("Agent completion must commit exactly one Version or a no-op.");
695
+ return initial;
696
+ }
697
+ /**
698
+ * Checks native indexed metadata and per-entity payload limits.
699
+ *
700
+ * @param record Persisted Agent execution record.
701
+ */
702
+ validateRecord(record) {
703
+ const accepted = AgentExecutionValues.requiredAccepted(record);
704
+ const scope = AgentExecutionValues.requiredScope(accepted.key);
705
+ if (AgentExecutionRecords.order(AgentExecutionValues.requiredOrder(record)).length > 1500 ||
706
+ Buffer.byteLength(scope.stateType, "utf8") > 1500 ||
707
+ Buffer.byteLength(scope.agentKey, "utf8") > 1500)
708
+ throw new RangeError("Agent execution indexed value exceeds Datastore capacity.");
709
+ if (AgentExecutionSizes.record(record) > this.capacity.executionRecordBytes)
710
+ throw new RangeError("Agent execution record exceeds Datastore entity payload capacity.");
711
+ }
712
+ /**
713
+ * Checks complete head discovery key and per-entity payload limits.
714
+ *
715
+ * @param head Per-instance pending and lease record.
716
+ */
717
+ validateHead(head) {
718
+ if (head.pending !== undefined &&
719
+ head.eligibleAt !== undefined &&
720
+ AgentExecutionRecords.pendingKey(head.eligibleAt, AgentExecutionValues.requiredHeadScope(head)).length > 1500)
721
+ throw new RangeError("Agent execution head key exceeds Datastore indexed-value capacity.");
722
+ if (AgentExecutionSizes.head(head) > this.capacity.executionHeadBytes)
723
+ throw new RangeError("Agent execution head exceeds Datastore entity payload capacity.");
724
+ }
725
+ /**
726
+ * Rejects provider calls after closure.
727
+ */
728
+ requireOpen() {
729
+ if (!this.#open)
730
+ throw new Error("Agent execution storage handle is closed.");
731
+ }
732
+ }
733
+ /**
734
+ * Maps one complete invocation identity to a fixed physical row ID.
735
+ */
736
+ function invocationId(key) {
737
+ return AgentHistoryHash.value(AgentExecutionRecords.invocation(key));
738
+ }
739
+ /**
740
+ * Rejects a physical ID collision across different invocation identities.
741
+ */
742
+ function checkIdentity(record, key) {
743
+ if (record !== undefined &&
744
+ AgentExecutionRecords.invocation(AgentExecutionValues.requiredInvocation(record)) !==
745
+ AgentExecutionRecords.invocation(key))
746
+ throw new Error("Agent invocation physical ID conflicts with another source.");
747
+ }
748
+ /**
749
+ * Returns the first native entity when Datastore returns an array.
750
+ */
751
+ function first(value) {
752
+ return Array.isArray(value) ? value[0] : value;
753
+ }
754
+ /**
755
+ * Requires an enabled native record handle for this Entity.
756
+ *
757
+ * @typeParam I Typed entity identifier.
758
+ * @typeParam R Typed persisted record.
759
+ */
760
+ function requiredStorage(storage) {
761
+ if (storage === undefined)
762
+ throw new Error("Agent Entity history storage is disabled.");
763
+ return storage;
764
+ }
765
+ /**
766
+ * Checks provider mutation, entity-group and payload budgets before committing.
767
+ */
768
+ function validateTransactionRows(rows) {
769
+ const keys = rows.map((row) => JSON.stringify(row.key));
770
+ if (rows.length > 500)
771
+ throw new Error("Agent execution exceeds Datastore transaction mutation limit.");
772
+ if (new Set(keys).size > 25)
773
+ throw new Error("Agent execution exceeds Datastore entity-group limit.");
774
+ const bytes = rows.reduce((total, row) => total +
775
+ Buffer.byteLength(JSON.stringify(row.data, (_key, value) => value instanceof Uint8Array
776
+ ? Buffer.from(value).toString("base64")
777
+ : typeof value === "bigint"
778
+ ? value.toString()
779
+ : value), "utf8"), 0);
780
+ if (bytes > 9 * 1024 * 1024)
781
+ throw new Error("Agent execution exceeds Datastore transaction payload limit.");
782
+ }
783
+ /**
784
+ * Identifies native write contention eligible for a bounded retry.
785
+ */
786
+ function isAborted(error) {
787
+ return typeof error === "object" && error !== null && Reflect.get(error, "code") === 10;
788
+ }
789
+ //# sourceMappingURL=agent-execution.js.map