@spine-event-engine/storage-postgres 2.0.0-snapshot.23 → 2.0.0-snapshot.25

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 (53) hide show
  1. package/README.md +34 -16
  2. package/REFERENCE.md +66 -26
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js.map +1 -1
  6. package/dist/postgres/agent-history.d.ts +60 -18
  7. package/dist/postgres/agent-history.d.ts.map +1 -1
  8. package/dist/postgres/agent-history.js +114 -37
  9. package/dist/postgres/agent-history.js.map +1 -1
  10. package/dist/postgres/column-mapping.d.ts.map +1 -1
  11. package/dist/postgres/column-mapping.js +6 -3
  12. package/dist/postgres/column-mapping.js.map +1 -1
  13. package/dist/postgres/entity-commit.d.ts +90 -56
  14. package/dist/postgres/entity-commit.d.ts.map +1 -1
  15. package/dist/postgres/entity-commit.js +439 -188
  16. package/dist/postgres/entity-commit.js.map +1 -1
  17. package/dist/postgres/entity-history.d.ts +4 -2
  18. package/dist/postgres/entity-history.d.ts.map +1 -1
  19. package/dist/postgres/entity-history.js +163 -262
  20. package/dist/postgres/entity-history.js.map +1 -1
  21. package/dist/postgres/errors.d.ts +1 -1
  22. package/dist/postgres/errors.d.ts.map +1 -1
  23. package/dist/postgres/errors.js +5 -3
  24. package/dist/postgres/errors.js.map +1 -1
  25. package/dist/postgres/id-column.d.ts.map +1 -1
  26. package/dist/postgres/id-column.js +6 -3
  27. package/dist/postgres/id-column.js.map +1 -1
  28. package/dist/postgres/record-storage.d.ts +228 -117
  29. package/dist/postgres/record-storage.d.ts.map +1 -1
  30. package/dist/postgres/record-storage.js +350 -204
  31. package/dist/postgres/record-storage.js.map +1 -1
  32. package/dist/postgres/storage-factory.d.ts +267 -30
  33. package/dist/postgres/storage-factory.d.ts.map +1 -1
  34. package/dist/postgres/storage-factory.js +786 -145
  35. package/dist/postgres/storage-factory.js.map +1 -1
  36. package/dist/postgres/table-initializer.d.ts +7 -4
  37. package/dist/postgres/table-initializer.d.ts.map +1 -1
  38. package/dist/postgres/table-initializer.js +21 -15
  39. package/dist/postgres/table-initializer.js.map +1 -1
  40. package/dist/postgres/table-resolver.d.ts +15 -1
  41. package/dist/postgres/table-resolver.d.ts.map +1 -1
  42. package/dist/postgres/table-resolver.js +36 -7
  43. package/dist/postgres/table-resolver.js.map +1 -1
  44. package/dist/tsconfig.tsbuildinfo +1 -1
  45. package/package.json +5 -5
  46. package/dist/postgres/agent-execution.d.ts +0 -284
  47. package/dist/postgres/agent-execution.d.ts.map +0 -1
  48. package/dist/postgres/agent-execution.js +0 -648
  49. package/dist/postgres/agent-execution.js.map +0 -1
  50. package/dist/postgres/delivery-cleanup.d.ts +0 -76
  51. package/dist/postgres/delivery-cleanup.d.ts.map +0 -1
  52. package/dist/postgres/delivery-cleanup.js +0 -224
  53. package/dist/postgres/delivery-cleanup.js.map +0 -1
@@ -1,648 +0,0 @@
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 { AgentExecutionHeadSchema, AgentExecutionRecordSchema, AgentInvocationStatus, } from "@spine-event-engine/proto/generated/spine/server/agent/execution_record_pb.js";
17
- import { AgentExecutionRecords, AgentExecutionTransitions, AgentExecutionValues, AgentHistoryRecords, } from "@spine-event-engine/storage/provider";
18
- import { AgentHistoryHash } from "./agent-history.js";
19
- import { PostgresRecordStorage } from "./record-storage.js";
20
- import { PostgresEntityCommitStorage } from "./entity-commit.js";
21
- /**
22
- * Persists accepted Agent work, exclusive claims and deterministic evidence.
23
- *
24
- * @typeParam I Typed Agent identifier.
25
- * @typeParam S Generated Agent state.
26
- */
27
- export class PostgresAgentExecution {
28
- input;
29
- invocation;
30
- head;
31
- history;
32
- current;
33
- commits;
34
- #open = true;
35
- #ready;
36
- /**
37
- * PostgreSQL has no tighter fixed row limit in this provider contract.
38
- */
39
- capacity = Object.freeze({});
40
- /**
41
- * Binds prepared record families to one Agent repository and tenant boundary.
42
- *
43
- * @param input Agent repository and current Entity layout.
44
- * @param invocation Mutable invocation records.
45
- * @param head Per-instance claim and current preference records.
46
- * @param history Immutable Agent conversation, System and domain entries.
47
- * @param current Existing Entity current records used for no-op version checks.
48
- * @param commits Opens the existing conditional Entity commit coordinator.
49
- */
50
- constructor(input, invocation, head, history, current, commits) {
51
- this.input = input;
52
- this.invocation = invocation;
53
- this.head = head;
54
- this.history = history;
55
- this.current = current;
56
- this.commits = commits;
57
- }
58
- /**
59
- * Persists original accepted work idempotently under its per-instance lock.
60
- *
61
- * @param accepted Complete source signal and selected bindings.
62
- * @returns Detached stored invocation.
63
- */
64
- async admit(accepted) {
65
- this.requireOpen();
66
- const rows = await this.ready();
67
- const key = AgentExecutionValues.requiredKey(accepted.key, this.input.stateType);
68
- return rows.head.transaction(async (client) => {
69
- await this.lockHead(client, rows, key);
70
- const prior = await this.existing(client, rows, key);
71
- if (prior !== undefined) {
72
- if (!AgentExecutionValues.sameAccepted(prior.accepted, accepted))
73
- throw new Error("Agent execution admission conflicts with immutable source facts.");
74
- return prior;
75
- }
76
- const record = create(AgentExecutionRecordSchema, {
77
- accepted,
78
- status: AgentInvocationStatus.AGENT_INVOCATION_ACCEPTED,
79
- });
80
- const head = (await this.currentHead(client, rows, key)) ??
81
- create(AgentExecutionHeadSchema, { scope: key.scope });
82
- this.offerCandidate(head, record);
83
- await rows.head.write(client, head);
84
- await rows.invocation.appendImmutable(client, record);
85
- return AgentExecutionValues.cloneRecord(record);
86
- });
87
- }
88
- /**
89
- * Reads a bounded indexed page of eligible accepted or interrupted work.
90
- *
91
- * @param request Complete continuation and page count.
92
- * @returns Detached records and continuation state.
93
- */
94
- async pending(request) {
95
- this.requireOpen();
96
- const rows = await this.ready();
97
- if (!Number.isSafeInteger(request.count) || request.count < 1)
98
- throw new RangeError("Agent pending count must be a positive safe integer.");
99
- const asOf = request.after?.asOf ?? Time.currentTime();
100
- const { sql, values } = this.pendingQuery(request, asOf, rows.head.table());
101
- return rows.head.using(async (client) => {
102
- const found = await rows.head.query(client, sql, values);
103
- const observed = found.slice(0, request.count);
104
- const records = [];
105
- for (const head of observed) {
106
- if (head.scope?.stateType !== this.input.stateType || head.pending === undefined)
107
- throw new Error("PostgreSQL Agent execution head scope or candidate is invalid.");
108
- const record = await this.existing(client, rows, head.pending);
109
- if (record !== undefined)
110
- records.push(record);
111
- }
112
- const last = observed.at(-1);
113
- return {
114
- records,
115
- hasMore: found.length > request.count,
116
- ...(last?.scope === undefined || last.eligibleAt === undefined
117
- ? {}
118
- : { after: { asOf, key: { scope: last.scope, eligibleAt: last.eligibleAt } } }),
119
- };
120
- });
121
- }
122
- /**
123
- * Builds one binary-collated native pending-head query.
124
- *
125
- * @param request Bounded pending-page request.
126
- * @param asOf Fixed provider-time eligibility cutoff.
127
- * @param table Resolved head table in the current schema.
128
- * @returns Native SQL and exact bound values.
129
- */
130
- pendingQuery(request, asOf, table) {
131
- const values = [
132
- AgentHistoryHash.value(this.input.stateType),
133
- this.input.stateType,
134
- AgentExecutionRecords.pendingLower(asOf),
135
- ];
136
- const where = ['"state_digest" = $1', '"state_type" = $2', '"pending_key" COLLATE "C" < $3'];
137
- if (request.after !== undefined) {
138
- values.push(AgentExecutionRecords.pendingKey(request.after.key.eligibleAt, request.after.key.scope));
139
- where.push(`"pending_key" COLLATE "C" > $${String(values.length)}`);
140
- }
141
- values.push(request.count + 1);
142
- const sql = `SELECT "bytes" FROM ${table} WHERE ${where.join(" AND ")} ` +
143
- `ORDER BY "pending_key" COLLATE "C" LIMIT $${String(values.length)}`;
144
- return { sql, values };
145
- }
146
- /**
147
- * Reads one complete persisted invocation.
148
- *
149
- * @param key Original source and Agent instance identity.
150
- * @returns Detached record if present.
151
- */
152
- async read(key) {
153
- this.requireOpen();
154
- const rows = await this.ready();
155
- AgentExecutionValues.requiredKey(key, this.input.stateType);
156
- return rows.invocation.using(async (client) => {
157
- const found = await this.existing(client, rows, key);
158
- return found;
159
- });
160
- }
161
- /**
162
- * Acquires only earliest unresolved same-instance work under a fresh transaction clock.
163
- *
164
- * @param key Original source and Agent instance identity.
165
- * @param token New exclusive token.
166
- * @param expiresAt Requested future expiry.
167
- * @returns Claimed record and current preferences, when eligible.
168
- */
169
- async claim(key, token, expiresAt) {
170
- this.requireOpen();
171
- const rows = await this.ready();
172
- AgentExecutionValues.requiredKey(key, this.input.stateType);
173
- return rows.head.transaction(async (client) => {
174
- await this.lockHead(client, rows, key);
175
- return this.claimLocked(client, rows, key, token, expiresAt);
176
- });
177
- }
178
- /**
179
- * Checks the locked instance and persists one exclusive lease.
180
- *
181
- * @param client Native client in the current transaction.
182
- * @param rows Tenant record executors.
183
- * @param key Original Agent invocation identity.
184
- * @param token New exclusive token.
185
- * @param expiresAt Requested future lease expiry.
186
- * @returns Claimed record and preferences when eligible.
187
- */
188
- async claimLocked(client, rows, key, token, expiresAt) {
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
- const head = await this.currentHead(client, rows, key);
193
- const record = await this.existing(client, rows, key);
194
- if (head === undefined || record === undefined || !AgentExecutionValues.isPending(record, now))
195
- return undefined;
196
- if (!(await this.eligibleClaim(client, rows, head, key, now)))
197
- return undefined;
198
- record.claimToken = token;
199
- record.claimExpiresAt = expiresAt;
200
- if (record.status === AgentInvocationStatus.AGENT_INVOCATION_ACCEPTED)
201
- record.status = AgentInvocationStatus.AGENT_INVOCATION_ACTIVE;
202
- head.active = key;
203
- head.claimToken = token;
204
- head.claimExpiresAt = expiresAt;
205
- head.eligibleAt = expiresAt;
206
- await rows.invocation.write(client, record);
207
- await rows.head.write(client, head);
208
- return {
209
- record,
210
- preferences: head.preferences.map((item) => item),
211
- };
212
- }
213
- /**
214
- * Checks the current head and earliest same-instance source.
215
- *
216
- * @param client Native client in the current transaction.
217
- * @param rows Tenant record executors.
218
- * @param head Per-instance pending and lease record.
219
- * @param key Original Agent invocation identity.
220
- * @param now Fresh time read under the per-instance lock.
221
- * @returns Whether this invocation may receive the next lease.
222
- */
223
- async eligibleClaim(client, rows, head, key, now) {
224
- if (head.active !== undefined &&
225
- head.claimExpiresAt !== undefined &&
226
- AgentExecutionValues.compareTime(head.claimExpiresAt, now) > 0)
227
- return false;
228
- return (head.pending !== undefined &&
229
- AgentExecutionRecords.invocation(head.pending) === AgentExecutionRecords.invocation(key) &&
230
- (await this.earliest(client, rows, key)));
231
- }
232
- /**
233
- * Updates one still-current lease under the per-instance transaction lock.
234
- *
235
- * @param key Original source and Agent instance identity.
236
- * @param token Current exclusive token.
237
- * @param expiresAt Requested later expiry.
238
- * @returns Whether renewal succeeded.
239
- */
240
- async renew(key, token, expiresAt) {
241
- this.requireOpen();
242
- const rows = await this.ready();
243
- return rows.head.transaction(async (client) => {
244
- await this.lockHead(client, rows, key);
245
- const pair = await this.claimed(client, rows, key, token);
246
- if (pair?.record.claimExpiresAt === undefined ||
247
- AgentExecutionValues.compareTime(expiresAt, pair.record.claimExpiresAt) <= 0)
248
- return false;
249
- pair.record.claimExpiresAt = expiresAt;
250
- pair.head.claimExpiresAt = expiresAt;
251
- pair.head.eligibleAt = expiresAt;
252
- await rows.invocation.write(client, pair.record);
253
- await rows.head.write(client, pair.head);
254
- return true;
255
- });
256
- }
257
- /**
258
- * Persists one execution update and its immutable history rows in the same transaction.
259
- *
260
- * @param input Exact read image, next execution record and history entries.
261
- * @returns Completion after fenced evidence is persisted.
262
- */
263
- async update(input) {
264
- this.requireOpen();
265
- const rows = await this.ready();
266
- await rows.head.transaction(async (client) => {
267
- await this.lockHead(client, rows, input.key);
268
- const pair = await this.expected(client, rows, input);
269
- AgentExecutionTransitions.assertUpdate(pair.record, input.next);
270
- for (const entry of input.historyEntries ?? [])
271
- await rows.history.assertImmutable(client, historyRecord(input.key, entry));
272
- for (const entry of input.historyEntries ?? [])
273
- await rows.history.appendImmutable(client, historyRecord(input.key, entry));
274
- await rows.invocation.write(client, input.next);
275
- if (input.next.status === AgentInvocationStatus.AGENT_INVOCATION_TERMINATED) {
276
- await this.advanceCandidate(client, rows, pair.head, input.key, AgentExecutionValues.requiredOrder(pair.record));
277
- await rows.head.write(client, pair.head);
278
- }
279
- });
280
- }
281
- /**
282
- * Commits Entity state, history, preferences and outgoing obligations together.
283
- *
284
- * @param input Complete conditional transition.
285
- * @returns Completion after conditional Entity and evidence commit.
286
- */
287
- async complete(input) {
288
- this.requireOpen();
289
- const rows = await this.ready();
290
- const coordinator = input.entityCommit === undefined ? undefined : this.commits();
291
- const prepared = input.entityCommit === undefined
292
- ? undefined
293
- : await AgentExecutionValues.require(coordinator).prepareConditional(input.entityCommit);
294
- try {
295
- await rows.head.transaction((client) => this.completeLocked(client, rows, input, prepared));
296
- }
297
- finally {
298
- prepared?.close();
299
- coordinator?.close();
300
- }
301
- }
302
- /**
303
- * Applies every conditional completion row on one transaction client.
304
- *
305
- * @param client Native database client in the current transaction.
306
- * @param input Requested fenced execution change.
307
- * @param prepared Prepared Entity state and Version write.
308
- * @param rows Provider record handles bound to the tenant.
309
- * @returns Completion after Entity and execution evidence commit.
310
- */
311
- async completeLocked(client, rows, input, prepared) {
312
- await this.lockHead(client, rows, input.key);
313
- const pair = await this.expected(client, rows, input);
314
- const expectedVersion = this.validateCompletion(input, pair.record);
315
- AgentExecutionTransitions.assertCompletion(input.next);
316
- await this.completeEntity(client, input, expectedVersion, prepared);
317
- for (const entry of input.historyEntries ?? [])
318
- await rows.history.appendImmutable(client, historyRecord(input.key, entry));
319
- await rows.invocation.write(client, input.next);
320
- pair.head.preferences = AgentExecutionValues.require(input.next.completion).preferences.map((item) => item);
321
- if (input.next.status === AgentInvocationStatus.AGENT_INVOCATION_COMPLETED)
322
- await this.advanceCandidate(client, rows, pair.head, input.key, AgentExecutionValues.requiredOrder(pair.record));
323
- await rows.head.write(client, pair.head);
324
- }
325
- /**
326
- * Validates one Version increment or no-op and a nonterminal prior phase.
327
- *
328
- * @param input Requested fenced execution change.
329
- * @param prior Previously persisted execution record.
330
- * @returns Expected initial Entity Version.
331
- */
332
- validateCompletion(input, prior) {
333
- const completion = input.next.completion;
334
- if (completion?.initialVersion === undefined ||
335
- completion.resultingVersion === undefined ||
336
- input.next.accepted === undefined ||
337
- !AgentExecutionValues.sameAccepted(prior.accepted, input.next.accepted))
338
- throw new TypeError("Agent completion requires immutable acceptance and both Versions.");
339
- if (prior.status !== AgentInvocationStatus.AGENT_INVOCATION_ACTIVE ||
340
- ![
341
- AgentInvocationStatus.AGENT_INVOCATION_COMPLETED,
342
- AgentInvocationStatus.AGENT_INVOCATION_COMPLETED_PENDING_DELIVERY,
343
- ].includes(input.next.status))
344
- throw new Error("Agent completion requires active work and completed status.");
345
- const initial = completion.initialVersion.number;
346
- const final = completion.resultingVersion.number;
347
- if (input.entityCommit === undefined
348
- ? final !== initial
349
- : final !== initial + 1 || input.entityCommit.next.version?.number !== final)
350
- throw new Error("Agent completion must commit exactly one Version or a no-op.");
351
- return initial;
352
- }
353
- /**
354
- * Marks original outgoing IDs accepted by normal delivery.
355
- *
356
- * @param key Original source and Agent instance identity.
357
- * @param token Current exclusive token.
358
- * @param expectedRecordBytes Exact previous record bytes.
359
- * @param signals Original accepted output IDs.
360
- * @returns Completion after original output IDs are marked delivered.
361
- */
362
- async markDelivered(key, token, expectedRecordBytes, signals) {
363
- this.requireOpen();
364
- const rows = await this.ready();
365
- await rows.head.transaction(async (client) => {
366
- await this.lockHead(client, rows, key);
367
- await this.deliverLocked(client, rows, key, token, expectedRecordBytes, signals);
368
- });
369
- }
370
- /**
371
- * Marks original output IDs while the per-instance native lock is held.
372
- *
373
- * @param client Native client in the current transaction.
374
- * @param rows Tenant record executors.
375
- * @param key Original Agent invocation identity.
376
- * @param token Current exclusive claim token.
377
- * @param expectedRecordBytes Exact prior execution record bytes.
378
- * @param signals Original accepted output IDs.
379
- * @returns Completion after persisted output status is updated.
380
- */
381
- async deliverLocked(client, rows, key, token, expectedRecordBytes, signals) {
382
- const pair = await this.expected(client, rows, { key, token, expectedRecordBytes, next: create(AgentExecutionRecordSchema) }, false);
383
- if (pair.record.completion === undefined)
384
- throw new Error("Agent execution has no completed output.");
385
- AgentExecutionTransitions.assertDelivery(pair.record, signals);
386
- for (const signal of signals)
387
- AgentExecutionValues.markSignal(AgentExecutionValues.requiredCompletion(pair.record), signal);
388
- if (pair.record.completion.outgoing.every((item) => item.delivered)) {
389
- pair.record.status = AgentInvocationStatus.AGENT_INVOCATION_COMPLETED;
390
- await this.advanceCandidate(client, rows, pair.head, key, AgentExecutionValues.requiredOrder(pair.record));
391
- }
392
- await rows.invocation.write(client, pair.record);
393
- await rows.head.write(client, pair.head);
394
- }
395
- /**
396
- * Closes all provider record handles without deleting persisted work.
397
- */
398
- close() {
399
- if (!this.#open)
400
- return;
401
- this.#open = false;
402
- this.invocation.close();
403
- this.head.close();
404
- this.history.close();
405
- this.current.close();
406
- }
407
- /**
408
- * Rejects calls after handle closure.
409
- */
410
- requireOpen() {
411
- if (!this.#open)
412
- throw new Error("Agent execution storage handle is closed.");
413
- }
414
- /**
415
- * Prepares three existing record families and required native indexes once.
416
- *
417
- * @returns Completion after provider capabilities are checked.
418
- */
419
- ready() {
420
- this.requireOpen();
421
- return (this.#ready ??= this.initialize()).then(() => ({
422
- invocation: this.invocation.historyExecutor(),
423
- head: this.head.historyExecutor(),
424
- history: this.history.historyExecutor(),
425
- }));
426
- }
427
- /**
428
- * Ensures full scoped order and expiry indexes before accepted work.
429
- *
430
- * @returns Completion after native tables and indexes are prepared.
431
- */
432
- async initialize() {
433
- const rows = [this.invocation, this.head, this.history, this.current].map((storage) => storage.historyExecutor());
434
- for (const row of rows)
435
- await row.prepare();
436
- const invocation = this.invocation.historyExecutor();
437
- await invocation.using(async (client) => {
438
- await ensureIndex(client, this.head.historyExecutor().table(), "agent_execution_pending", 'state_digest, pending_key COLLATE "C"');
439
- await ensureIndex(client, invocation.table(), "agent_execution_instance", 'scope_digest, status, order_key COLLATE "C"');
440
- });
441
- }
442
- /**
443
- * Acquires a transaction-scoped lock even when the head row is absent.
444
- *
445
- * @param client Native database client in the current transaction.
446
- * @param key Original Agent invocation identity.
447
- * @param rows Provider record handles bound to the tenant.
448
- * @returns Completion after locking the per-instance head.
449
- */
450
- async lockHead(client, rows, key) {
451
- const scope = AgentExecutionRecords.scope(AgentExecutionValues.require(AgentExecutionValues.requiredKey(key, this.input.stateType).scope));
452
- await client.query("SELECT pg_advisory_xact_lock($1)", [rows.head.lock("agent-head", scope)]);
453
- }
454
- /**
455
- * Reads one head and rejects a physical digest collision.
456
- *
457
- * @param client Native database client in the current transaction.
458
- * @param key Original Agent invocation identity.
459
- * @param rows Provider record handles bound to the tenant.
460
- * @returns Per-instance head when present.
461
- */
462
- async currentHead(client, rows, key) {
463
- const scope = AgentExecutionValues.require(AgentExecutionValues.requiredKey(key, this.input.stateType).scope);
464
- const found = await rows.head.read(client, AgentHistoryHash.value(AgentExecutionRecords.scope(scope)), "for-update");
465
- if (found !== undefined &&
466
- AgentExecutionRecords.scope(AgentExecutionValues.require(found.scope)) !==
467
- AgentExecutionRecords.scope(scope))
468
- throw new Error("Agent execution head physical ID conflicts with another scope.");
469
- return found;
470
- }
471
- /**
472
- * Reads one invocation and rejects a physical digest collision.
473
- *
474
- * @param client Native database client in the current transaction.
475
- * @param key Original Agent invocation identity.
476
- * @param rows Provider record handles bound to the tenant.
477
- * @returns Stored invocation when present.
478
- */
479
- async existing(client, rows, key) {
480
- const id = AgentHistoryHash.value(AgentExecutionRecords.invocation(AgentExecutionValues.requiredKey(key, this.input.stateType)));
481
- const found = await rows.invocation.read(client, id, "for-update");
482
- if (found !== undefined &&
483
- AgentExecutionRecords.invocation(AgentExecutionValues.requiredInvocation(found)) !==
484
- AgentExecutionRecords.invocation(key))
485
- throw new Error("Agent execution physical ID conflicts with another invocation.");
486
- return found;
487
- }
488
- /**
489
- * Checks fresh Time, exact token and complete previous record bytes.
490
- *
491
- * @param checkNext Whether to validate the proposed next record.
492
- * @param client Native database client in the current transaction.
493
- * @param input Requested fenced execution change.
494
- * @param rows Provider record handles bound to the tenant.
495
- * @returns Current fenced record and head.
496
- */
497
- async expected(client, rows, input, checkNext = true) {
498
- const pair = await this.claimed(client, rows, input.key, input.token);
499
- if (pair === undefined ||
500
- !AgentExecutionValues.sameBytes(toBinary(AgentExecutionRecordSchema, pair.record), input.expectedRecordBytes))
501
- throw new Error("Agent execution claim or expected record is no longer current.");
502
- if (checkNext && !AgentExecutionValues.sameAccepted(pair.record.accepted, input.next.accepted))
503
- throw new Error("Agent execution cannot change immutable accepted source facts.");
504
- return pair;
505
- }
506
- /**
507
- * Reads claimed rows under the already acquired per-instance lock.
508
- *
509
- * @param client Native database client in the current transaction.
510
- * @param key Original Agent invocation identity.
511
- * @param rows Provider record handles bound to the tenant.
512
- * @param token Exclusive claim token.
513
- * @returns Current record and head if the token remains valid.
514
- */
515
- async claimed(client, rows, key, token) {
516
- const head = await this.currentHead(client, rows, key);
517
- const record = await this.existing(client, rows, key);
518
- const now = Time.currentTime();
519
- if (!token ||
520
- head?.claimToken !== token ||
521
- record?.claimToken !== token ||
522
- head.claimExpiresAt === undefined ||
523
- AgentExecutionValues.compareTime(head.claimExpiresAt, now) <= 0)
524
- return undefined;
525
- return { head, record };
526
- }
527
- /**
528
- * Checks no earlier unresolved signal in the same full Agent scope.
529
- *
530
- * @param client Native database client in the current transaction.
531
- * @param key Original Agent invocation identity.
532
- * @param rows Provider record handles bound to the tenant.
533
- * @returns Whether this invocation is earliest unresolved work.
534
- */
535
- async earliest(client, rows, key) {
536
- const first = await this.successor(client, rows, key);
537
- return (first !== undefined &&
538
- AgentExecutionRecords.invocation(AgentExecutionValues.requiredInvocation(first)) ===
539
- AgentExecutionRecords.invocation(key));
540
- }
541
- /**
542
- * Adds an earlier accepted source without resetting an existing eligibility.
543
- *
544
- * @param head Per-instance pending and lease record.
545
- * @param record Persisted Agent execution record.
546
- */
547
- offerCandidate(head, record) {
548
- AgentExecutionValues.offerCandidate(head, record);
549
- }
550
- /**
551
- * Updates the head to one bounded native successor before terminal publication.
552
- *
553
- * @param client Native database client in the current transaction.
554
- * @param head Per-instance pending and lease record.
555
- * @param key Original Agent invocation identity.
556
- * @param order Original Inbox order.
557
- * @param rows Provider record handles bound to the tenant.
558
- * @returns Completion after updating the next pending head.
559
- */
560
- async advanceCandidate(client, rows, head, key, order) {
561
- const next = await this.successor(client, rows, key, key);
562
- AgentExecutionValues.advanceCandidate(head, order, next);
563
- }
564
- /**
565
- * Reads at most two rows per unresolved status in original Inbox order.
566
- *
567
- * @param client Native database client in the current transaction.
568
- * @param exclude Invocation omitted while selecting the successor.
569
- * @param key Original Agent invocation identity.
570
- * @param rows Provider record handles bound to the tenant.
571
- * @returns Earliest unresolved invocation when present.
572
- */
573
- async successor(client, rows, key, exclude) {
574
- const scope = AgentExecutionValues.require(key.scope);
575
- const candidates = [];
576
- for (const status of ["1", "2", "3"]) {
577
- const sql = this.successorSql(rows.invocation.table());
578
- candidates.push(...(await rows.invocation.query(client, sql, [
579
- AgentHistoryHash.value(AgentExecutionRecords.scope(scope)),
580
- scope.stateType,
581
- scope.agentKey,
582
- status,
583
- ])));
584
- }
585
- return candidates
586
- .filter((item) => item.accepted?.key !== undefined &&
587
- (exclude === undefined ||
588
- AgentExecutionRecords.invocation(item.accepted.key) !==
589
- AgentExecutionRecords.invocation(exclude)))
590
- .sort((left, right) => AgentExecutionValues.compareText(AgentExecutionRecords.order(AgentExecutionValues.requiredOrder(left)), AgentExecutionRecords.order(AgentExecutionValues.requiredOrder(right))))[0];
591
- }
592
- /**
593
- * Builds the bounded native query for one unresolved invocation status.
594
- *
595
- * @param table Resolved invocation table in the current schema.
596
- * @returns Complete indexed successor query.
597
- */
598
- successorSql(table) {
599
- return (`SELECT "bytes" FROM ${table} WHERE "scope_digest"=$1 ` +
600
- 'AND "state_type"=$2 AND "agent_key"=$3 AND "status"=$4 ' +
601
- 'ORDER BY "order_key" COLLATE "C" LIMIT 2');
602
- }
603
- /**
604
- * Validates current Version and applies optional Entity rows in this transaction.
605
- *
606
- * @param client Native database client in the current transaction.
607
- * @param expectedVersion Expected current Entity Version.
608
- * @param input Requested fenced execution change.
609
- * @param prepared Prepared Entity state and Version write.
610
- * @returns Completion after conditional Entity state and Version write.
611
- */
612
- async completeEntity(client, input, expectedVersion, prepared) {
613
- if (prepared !== undefined) {
614
- await prepared.apply(client, expectedVersion);
615
- return;
616
- }
617
- const recipient = AgentExecutionValues.require(input.next.accepted).recipientId;
618
- const id = recipient === undefined ? undefined : this.input.entity.id.unpack(recipient);
619
- if (id === undefined)
620
- throw new TypeError("Agent completion recipient ID has the wrong type.");
621
- const current = await this.current.historyExecutor().read(client, id, "for-update");
622
- if ((current?.version?.number ?? 0) !== expectedVersion)
623
- throw new Error("Agent execution initial Entity Version is no longer current.");
624
- }
625
- }
626
- /**
627
- * Wraps original Agent history without changing its category or identity.
628
- */
629
- function historyRecord(key, entry) {
630
- return AgentHistoryRecords.record(AgentExecutionValues.require(key.scope).stateType, AgentExecutionValues.require(key.scope).agentKey, entry);
631
- }
632
- /**
633
- * Creates and validates one required fixed PostgreSQL composite index.
634
- */
635
- async function ensureIndex(client, table, name, columns) {
636
- await client.query(`CREATE INDEX IF NOT EXISTS "${name}" ON ${table} (${columns})`);
637
- const result = await client.query("SELECT pg_get_indexdef(i.indexrelid) AS definition, i.indisvalid, i.indisready " +
638
- "FROM pg_index i JOIN pg_class c ON c.oid=i.indexrelid " +
639
- "WHERE i.indrelid=to_regclass($1) AND c.relname=$2", [table, name]);
640
- const index = result.rows[0];
641
- if (!index?.indisvalid ||
642
- !index.indisready ||
643
- !index.definition.includes(`(${columns})`) ||
644
- !index.definition.includes(" USING btree ") ||
645
- index.definition.includes(" WHERE "))
646
- throw new Error("PostgreSQL Agent execution index is incompatible.");
647
- }
648
- //# sourceMappingURL=agent-execution.js.map