@byok-sdk/cloud-dataplane 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/runtime.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import pg from 'pg';
2
- import { DEDUP_RING_CAPACITY, NONCE_TTL_MS, validateActivityAppend, projectTimelineEvents, ByokCloudError, validateApprovalTimelineAppend, ApprovalObservationSchema, AllowAllRateLimiter, TruthCommitError, TruthCommitResponseSchema, truthRecordMetadata, TRUTH_REQUEST_ID_MAX_LENGTH, parseTimelineEvents, activityCursor, parseApprovalObservations, approvalTimelineCursor } from '@byok-sdk/cloud';
2
+ import { DEDUP_RING_CAPACITY, NONCE_TTL_MS, assertTaskAttemptListLimit, validateActivityAppend, projectTimelineEvents, activitySourceBatchState, ByokCloudError, validateApprovalTimelineAppend, ApprovalObservationSchema, validateApprovalTimelineResolve, AllowAllRateLimiter, TruthCommitError, TruthCommitResponseSchema, truthRecordMetadata, TRUTH_REQUEST_ID_MAX_LENGTH, parseTimelineEvents, activityCursor, parseApprovalObservations, approvalTimelineCursor } from '@byok-sdk/cloud';
3
3
  import { contentHash, ByokCoreError, assertCanonicalTimestamp, isContentHash, tenantObjectKey, objectKeyPrefix, CoreConflictError, isLegalBoardTransition, checkSkillPackManifest, checkSkillPackEntry, SKILL_PACK_ENTRY_PATH, SKILL_PACK_MANIFEST_SCHEMA_ID } from '@byok-sdk/core';
4
4
  import { AwsClient } from 'aws4fetch';
5
5
  import { XMLParser } from 'fast-xml-parser';
@@ -822,6 +822,57 @@ async function rollback(client) {
822
822
  } catch {
823
823
  }
824
824
  }
825
+ async function registerDeviceOnClient(client, tenant, input) {
826
+ if (input.machineId !== void 0) {
827
+ const superseded = await client.query(
828
+ `DELETE FROM device
829
+ WHERE tenant_id = $1 AND product_id = $2 AND machine_id = $3 AND device_id <> $4
830
+ RETURNING device_id`,
831
+ [tenant, input.productId, input.machineId, input.deviceId]
832
+ );
833
+ await deleteDeviceState(
834
+ client,
835
+ tenant,
836
+ superseded.rows.map((row) => row.device_id)
837
+ );
838
+ }
839
+ const result = await client.query(
840
+ `INSERT INTO device (
841
+ tenant_id, device_id, product_id, device_name, device_public_key,
842
+ proof_key_id, proof_key_epoch, revoked, machine_id
843
+ )
844
+ VALUES ($1, $2, $3, $4, $5, $6, $7, false, $8)
845
+ ON CONFLICT (tenant_id, device_id) DO UPDATE
846
+ SET product_id = EXCLUDED.product_id,
847
+ device_name = EXCLUDED.device_name,
848
+ device_public_key = EXCLUDED.device_public_key,
849
+ proof_key_id = EXCLUDED.proof_key_id,
850
+ proof_key_epoch = EXCLUDED.proof_key_epoch,
851
+ revoked = false,
852
+ machine_id = EXCLUDED.machine_id,
853
+ capabilities = NULL
854
+ RETURNING ${SELECT_COLUMNS}`,
855
+ [
856
+ tenant,
857
+ input.deviceId,
858
+ input.productId,
859
+ input.deviceName,
860
+ input.devicePublicKey,
861
+ input.proofKeyId,
862
+ input.proofKeyEpoch,
863
+ input.machineId ?? null
864
+ ]
865
+ );
866
+ return toRecord(result.rows[0]);
867
+ }
868
+ async function getDeviceOnClient(client, tenant, deviceId) {
869
+ const result = await client.query(
870
+ `SELECT ${SELECT_COLUMNS} FROM device WHERE tenant_id = $1 AND device_id = $2`,
871
+ [tenant, deviceId]
872
+ );
873
+ const row = result.rows[0];
874
+ return row === void 0 ? void 0 : toRecord(row);
875
+ }
825
876
  var PostgresDeviceDirectory = class {
826
877
  #pool;
827
878
  #clock;
@@ -833,48 +884,9 @@ var PostgresDeviceDirectory = class {
833
884
  const client = await this.#pool.connect();
834
885
  try {
835
886
  await client.query("BEGIN");
836
- if (input.machineId !== void 0) {
837
- const superseded = await client.query(
838
- `DELETE FROM device
839
- WHERE tenant_id = $1 AND product_id = $2 AND machine_id = $3 AND device_id <> $4
840
- RETURNING device_id`,
841
- [tenant, input.productId, input.machineId, input.deviceId]
842
- );
843
- await deleteDeviceState(
844
- client,
845
- tenant,
846
- superseded.rows.map((row) => row.device_id)
847
- );
848
- }
849
- const result = await client.query(
850
- `INSERT INTO device (
851
- tenant_id, device_id, product_id, device_name, device_public_key,
852
- proof_key_id, proof_key_epoch, revoked, machine_id
853
- )
854
- VALUES ($1, $2, $3, $4, $5, $6, $7, false, $8)
855
- ON CONFLICT (tenant_id, device_id) DO UPDATE
856
- SET product_id = EXCLUDED.product_id,
857
- device_name = EXCLUDED.device_name,
858
- device_public_key = EXCLUDED.device_public_key,
859
- proof_key_id = EXCLUDED.proof_key_id,
860
- proof_key_epoch = EXCLUDED.proof_key_epoch,
861
- revoked = false,
862
- machine_id = EXCLUDED.machine_id,
863
- capabilities = NULL
864
- RETURNING ${SELECT_COLUMNS}`,
865
- [
866
- tenant,
867
- input.deviceId,
868
- input.productId,
869
- input.deviceName,
870
- input.devicePublicKey,
871
- input.proofKeyId,
872
- input.proofKeyEpoch,
873
- input.machineId ?? null
874
- ]
875
- );
887
+ const record = await registerDeviceOnClient(client, tenant, input);
876
888
  await client.query("COMMIT");
877
- return toRecord(result.rows[0]);
889
+ return record;
878
890
  } catch (cause) {
879
891
  await rollback(client);
880
892
  throw cause;
@@ -883,12 +895,12 @@ var PostgresDeviceDirectory = class {
883
895
  }
884
896
  }
885
897
  async get(tenant, deviceId) {
886
- const result = await this.#pool.query(
887
- `SELECT ${SELECT_COLUMNS} FROM device WHERE tenant_id = $1 AND device_id = $2`,
888
- [tenant, deviceId]
889
- );
890
- const row = result.rows[0];
891
- return row === void 0 ? void 0 : toRecord(row);
898
+ const client = await this.#pool.connect();
899
+ try {
900
+ return await getDeviceOnClient(client, tenant, deviceId);
901
+ } finally {
902
+ client.release();
903
+ }
892
904
  }
893
905
  /**
894
906
  * Revocation removes the registration and the device-scoped state that only
@@ -1022,23 +1034,31 @@ var PostgresInboundDedupStore = class {
1022
1034
  this.#capacity = capacity;
1023
1035
  }
1024
1036
  async checkAndRecord(tenant, deviceId, envelopeId) {
1037
+ return this.#checkAndRecord(tenant, deviceId, "", "", envelopeId);
1038
+ }
1039
+ async checkAndRecordAgent(tenant, deviceId, agentRef, envelopeId) {
1040
+ return this.#checkAndRecord(tenant, deviceId, agentRef.agentId, agentRef.profileRevision, envelopeId);
1041
+ }
1042
+ async #checkAndRecord(tenant, deviceId, agentId, agentProfileRevision, envelopeId) {
1025
1043
  const inserted = await this.#pool.query(
1026
- `INSERT INTO inbound_dedup (tenant_id, device_id, envelope_id)
1027
- VALUES ($1, $2, $3)
1028
- ON CONFLICT (tenant_id, device_id, envelope_id) DO NOTHING
1044
+ `INSERT INTO inbound_dedup (tenant_id, device_id, agent_id, agent_profile_revision, envelope_id)
1045
+ VALUES ($1, $2, $3, $4, $5)
1046
+ ON CONFLICT (tenant_id, device_id, agent_id, agent_profile_revision, envelope_id) DO NOTHING
1029
1047
  RETURNING 1`,
1030
- [tenant, deviceId, envelopeId]
1048
+ [tenant, deviceId, agentId, agentProfileRevision, envelopeId]
1031
1049
  );
1032
1050
  if (inserted.rowCount === 0) return true;
1033
1051
  await this.#pool.query(
1034
1052
  `DELETE FROM inbound_dedup
1035
1053
  WHERE tenant_id = $1 AND device_id = $2
1054
+ AND agent_id = $3 AND agent_profile_revision = $4
1036
1055
  AND recorded_seq <= (
1037
1056
  SELECT recorded_seq FROM inbound_dedup
1038
1057
  WHERE tenant_id = $1 AND device_id = $2
1058
+ AND agent_id = $3 AND agent_profile_revision = $4
1039
1059
  ORDER BY recorded_seq DESC
1040
- OFFSET $3 LIMIT 1)`,
1041
- [tenant, deviceId, this.#capacity]
1060
+ OFFSET $5 LIMIT 1)`,
1061
+ [tenant, deviceId, agentId, agentProfileRevision, this.#capacity]
1042
1062
  );
1043
1063
  return false;
1044
1064
  }
@@ -1070,24 +1090,58 @@ var PostgresNonceStore = class {
1070
1090
  );
1071
1091
  return nonce;
1072
1092
  }
1073
- async validate(tenant, deviceId, nonce) {
1093
+ async consumeIfValid(tenant, deviceId, nonce) {
1074
1094
  const result = await this.#pool.query(
1075
- `SELECT 1 FROM auth_nonce
1076
- WHERE tenant_id = $1 AND device_id = $2 AND nonce = $3
1077
- AND used = false AND expires_at >= $4`,
1095
+ `UPDATE auth_nonce
1096
+ SET used = true
1097
+ WHERE tenant_id = $1 AND device_id = $2 AND nonce = $3
1098
+ AND used = false AND expires_at >= $4
1099
+ RETURNING nonce`,
1078
1100
  [tenant, deviceId, nonce, this.#clock.now().toISOString()]
1079
1101
  );
1080
1102
  return result.rowCount === 1;
1081
1103
  }
1082
- async markUsed(tenant, nonce) {
1083
- await this.#pool.query("UPDATE auth_nonce SET used = true WHERE tenant_id = $1 AND nonce = $2", [
1084
- tenant,
1085
- nonce
1086
- ]);
1087
- }
1088
1104
  };
1089
1105
 
1090
1106
  // src/stores/pairing-codes.ts
1107
+ function bindingOf(input) {
1108
+ return {
1109
+ deviceName: input.deviceName,
1110
+ devicePublicKey: input.devicePublicKey,
1111
+ proofKeyId: input.proofKeyId,
1112
+ proofKeyEpoch: input.proofKeyEpoch,
1113
+ ...input.machineId === void 0 ? {} : { machineId: input.machineId }
1114
+ };
1115
+ }
1116
+ async function completionKey(pairingCode) {
1117
+ const digest = new Uint8Array(await globalThis.crypto.subtle.digest(
1118
+ "SHA-256",
1119
+ new TextEncoder().encode(`byok-pairing-completion-v1\0${pairingCode}`)
1120
+ ));
1121
+ const hex = [...digest].map((byte) => byte.toString(16).padStart(2, "0")).join("");
1122
+ return `pairing-completion:v1:sha256:${hex}`;
1123
+ }
1124
+ function sameBinding(left, right) {
1125
+ return left.deviceName === right.deviceName && left.devicePublicKey === right.devicePublicKey && left.proofKeyId === right.proofKeyId && left.proofKeyEpoch === right.proofKeyEpoch && left.machineId === right.machineId;
1126
+ }
1127
+ function sameDevice(device, claims, binding) {
1128
+ return device.tenantId === claims.tenant_id && device.productId === claims.product_id && device.deviceName === binding.deviceName && device.devicePublicKey === binding.devicePublicKey && device.proofKeyId === binding.proofKeyId && device.proofKeyEpoch === binding.proofKeyEpoch && device.machineId === binding.machineId;
1129
+ }
1130
+ function parseCompletion(body) {
1131
+ try {
1132
+ const parsed = JSON.parse(body);
1133
+ if (typeof parsed.deviceId !== "string" || parsed.binding === void 0 || typeof parsed.binding.deviceName !== "string" || typeof parsed.binding.devicePublicKey !== "string" || typeof parsed.binding.proofKeyId !== "string" || typeof parsed.binding.proofKeyEpoch !== "number" || parsed.binding.machineId !== void 0 && typeof parsed.binding.machineId !== "string") return void 0;
1134
+ return parsed;
1135
+ } catch {
1136
+ return void 0;
1137
+ }
1138
+ }
1139
+ async function rollback2(client) {
1140
+ try {
1141
+ await client.query("ROLLBACK");
1142
+ } catch {
1143
+ }
1144
+ }
1091
1145
  var PostgresPairingCodeStore = class {
1092
1146
  #pool;
1093
1147
  #clock;
@@ -1096,30 +1150,102 @@ var PostgresPairingCodeStore = class {
1096
1150
  this.#clock = clock;
1097
1151
  }
1098
1152
  async issue(tenant, input) {
1099
- await this.#pool.query(
1100
- `INSERT INTO pairing_code (code, tenant_id, product_id, expires_at, redeemed_at)
1101
- VALUES ($1, $2, $3, $4, NULL)
1102
- ON CONFLICT (code) DO UPDATE
1103
- SET tenant_id = EXCLUDED.tenant_id,
1104
- product_id = EXCLUDED.product_id,
1105
- expires_at = EXCLUDED.expires_at,
1106
- redeemed_at = NULL`,
1107
- [input.code, tenant, input.productId, input.expiresAt]
1108
- );
1153
+ const client = await this.#pool.connect();
1154
+ try {
1155
+ await client.query("BEGIN");
1156
+ const previous = await client.query(
1157
+ "SELECT tenant_id FROM pairing_code WHERE code = $1 FOR UPDATE",
1158
+ [input.code]
1159
+ );
1160
+ const key = await completionKey(input.code);
1161
+ const previousTenant = previous.rows[0]?.tenant_id;
1162
+ if (previousTenant !== void 0) {
1163
+ await client.query(
1164
+ "DELETE FROM device_request_receipts WHERE tenant_id = $1 AND key = $2",
1165
+ [previousTenant, key]
1166
+ );
1167
+ }
1168
+ await client.query(
1169
+ `INSERT INTO pairing_code (code, tenant_id, product_id, expires_at, redeemed_at)
1170
+ VALUES ($1, $2, $3, $4, NULL)
1171
+ ON CONFLICT (code) DO UPDATE
1172
+ SET tenant_id = EXCLUDED.tenant_id,
1173
+ product_id = EXCLUDED.product_id,
1174
+ expires_at = EXCLUDED.expires_at,
1175
+ redeemed_at = NULL`,
1176
+ [input.code, tenant, input.productId, input.expiresAt]
1177
+ );
1178
+ await client.query("COMMIT");
1179
+ } catch (cause) {
1180
+ await rollback2(client);
1181
+ throw cause;
1182
+ } finally {
1183
+ client.release();
1184
+ }
1109
1185
  return { code: input.code, expiresAt: input.expiresAt };
1110
1186
  }
1111
- async redeem(code) {
1112
- const now = this.#clock.now().toISOString();
1113
- const result = await this.#pool.query(
1114
- `UPDATE pairing_code
1115
- SET redeemed_at = $2
1116
- WHERE code = $1 AND redeemed_at IS NULL AND expires_at >= $2
1117
- RETURNING tenant_id, product_id`,
1118
- [code, now]
1119
- );
1120
- const row = result.rows[0];
1121
- if (row === void 0) return void 0;
1122
- return { tenantId: row.tenant_id, productId: row.product_id };
1187
+ async redeemAndRegister(input) {
1188
+ const now = this.#clock.now();
1189
+ const client = await this.#pool.connect();
1190
+ try {
1191
+ await client.query("BEGIN");
1192
+ const selected = await client.query(
1193
+ `SELECT tenant_id, product_id, expires_at, redeemed_at
1194
+ FROM pairing_code WHERE code = $1 FOR UPDATE`,
1195
+ [input.pairingCode]
1196
+ );
1197
+ const claims = selected.rows[0];
1198
+ if (claims === void 0) {
1199
+ await client.query("COMMIT");
1200
+ return void 0;
1201
+ }
1202
+ const tenant = claims.tenant_id;
1203
+ const binding = bindingOf(input);
1204
+ if (claims.redeemed_at !== null) {
1205
+ const stored = await client.query(
1206
+ "SELECT body FROM device_request_receipts WHERE tenant_id = $1 AND key = $2",
1207
+ [tenant, await completionKey(input.pairingCode)]
1208
+ );
1209
+ const completion2 = stored.rows[0] === void 0 ? void 0 : parseCompletion(stored.rows[0].body);
1210
+ if (completion2 === void 0 || !sameBinding(completion2.binding, binding)) {
1211
+ await client.query("COMMIT");
1212
+ return void 0;
1213
+ }
1214
+ const device2 = await getDeviceOnClient(client, tenant, completion2.deviceId);
1215
+ await client.query("COMMIT");
1216
+ return device2 === void 0 || !sameDevice(device2, claims, binding) ? void 0 : device2;
1217
+ }
1218
+ if (claims.expires_at.getTime() < now.getTime()) {
1219
+ await client.query("COMMIT");
1220
+ return void 0;
1221
+ }
1222
+ await client.query(
1223
+ "UPDATE pairing_code SET redeemed_at = $2 WHERE code = $1 AND redeemed_at IS NULL",
1224
+ [input.pairingCode, now.toISOString()]
1225
+ );
1226
+ const device = await registerDeviceOnClient(client, tenant, {
1227
+ productId: claims.product_id,
1228
+ deviceId: input.deviceId,
1229
+ deviceName: input.deviceName,
1230
+ devicePublicKey: input.devicePublicKey,
1231
+ proofKeyId: input.proofKeyId,
1232
+ proofKeyEpoch: input.proofKeyEpoch,
1233
+ ...input.machineId === void 0 ? {} : { machineId: input.machineId }
1234
+ });
1235
+ const completion = { deviceId: device.deviceId, binding };
1236
+ await client.query(
1237
+ `INSERT INTO device_request_receipts (tenant_id, key, body, recorded_at)
1238
+ VALUES ($1, $2, $3, $4)`,
1239
+ [tenant, await completionKey(input.pairingCode), JSON.stringify(completion), now.toISOString()]
1240
+ );
1241
+ await client.query("COMMIT");
1242
+ return device;
1243
+ } catch (cause) {
1244
+ await rollback2(client);
1245
+ throw cause;
1246
+ } finally {
1247
+ client.release();
1248
+ }
1123
1249
  }
1124
1250
  };
1125
1251
 
@@ -1226,9 +1352,15 @@ var PostgresProofRequestReceiptStore = class {
1226
1352
  return row === void 0 ? void 0 : toReceipt2(row);
1227
1353
  }
1228
1354
  };
1229
-
1230
- // src/stores/task-attempts.ts
1231
- var TASK_SELECT_COLUMNS = "tenant_id, task_id, device_id, agent_id, agent_profile_revision, owner_device_id, status, terminal_cause, cancel_requested_at, cancel_reason, cancel_message_id, updated_at";
1355
+ var TASK_SELECT_COLUMNS = "tenant_id, task_id, device_id, agent_id, agent_profile_revision, owner_device_id, status, terminal_cause, cancel_requested_at, cancel_reason, cancel_message_id, claimed_runtime, claimed_runtime_capabilities, updated_at";
1356
+ var AGENT_MESSAGE_ADMISSION_SELECT_COLUMNS = "message_id, payload_body, terminal_body";
1357
+ function admissionRowToRecord(row) {
1358
+ return {
1359
+ messageId: row.message_id,
1360
+ payloadBody: row.payload_body,
1361
+ ...row.terminal_body === null ? {} : { terminalBody: row.terminal_body }
1362
+ };
1363
+ }
1232
1364
  function taskRowToAttempt(row) {
1233
1365
  return {
1234
1366
  tenantId: row.tenant_id,
@@ -1246,6 +1378,10 @@ function taskRowToAttempt(row) {
1246
1378
  // former.
1247
1379
  ...row.owner_device_id === null ? {} : { ownerDeviceId: row.owner_device_id },
1248
1380
  status: row.status,
1381
+ // `claimed_runtime` is written only by the guarded claim UPDATE below, so a
1382
+ // non-null value here is always the winning claim's own self-report.
1383
+ ...row.claimed_runtime === null ? {} : { claimedRuntime: row.claimed_runtime },
1384
+ ...row.claimed_runtime_capabilities === null ? {} : { claimedRuntimeCapabilities: row.claimed_runtime_capabilities },
1249
1385
  ...row.terminal_cause == null ? {} : { terminalCause: row.terminal_cause },
1250
1386
  ...row.cancel_requested_at === null ? {} : {
1251
1387
  cancellation: {
@@ -1304,6 +1440,80 @@ var PostgresTaskAttemptStore = class {
1304
1440
  if (existing === void 0) throw new Error(`task ${input.taskId} vanished during Agent offer reservation`);
1305
1441
  return { attempt: existing, created: false };
1306
1442
  }
1443
+ async reserveAgentMessage(tenant, input) {
1444
+ const client = await this.#pool.connect();
1445
+ try {
1446
+ await client.query("BEGIN");
1447
+ const selected = await client.query(
1448
+ `SELECT ${TASK_SELECT_COLUMNS} FROM task
1449
+ WHERE tenant_id = $1 AND task_id = $2 FOR UPDATE`,
1450
+ [tenant, input.taskId]
1451
+ );
1452
+ const attempt = selected.rows[0];
1453
+ const existing = await client.query(
1454
+ `SELECT message_id, payload_body FROM agent_message_admission
1455
+ WHERE tenant_id = $1 AND device_id = $2 AND task_id = $3`,
1456
+ [tenant, input.deviceId, input.taskId]
1457
+ );
1458
+ const admission = existing.rows[0];
1459
+ if (admission !== void 0) {
1460
+ await client.query("COMMIT");
1461
+ return admission.message_id === input.messageId && admission.payload_body === input.payloadBody ? "pending" : "rejected";
1462
+ }
1463
+ if (attempt === void 0 || attempt.device_id !== input.deviceId || attempt.cancel_requested_at !== null || !["offered", "claimed", "running"].includes(attempt.status)) {
1464
+ await client.query("COMMIT");
1465
+ return "rejected";
1466
+ }
1467
+ const inserted = await client.query(
1468
+ `INSERT INTO agent_message_admission (
1469
+ tenant_id, device_id, task_id, message_id, payload_body
1470
+ ) VALUES ($1, $2, $3, $4, $5)
1471
+ ON CONFLICT (tenant_id, device_id, task_id) DO NOTHING
1472
+ RETURNING payload_body`,
1473
+ [tenant, input.deviceId, input.taskId, input.messageId, input.payloadBody]
1474
+ );
1475
+ if (inserted.rows[0] !== void 0) {
1476
+ await client.query("COMMIT");
1477
+ return "reserved";
1478
+ }
1479
+ const raced = await client.query(
1480
+ `SELECT message_id, payload_body FROM agent_message_admission
1481
+ WHERE tenant_id = $1 AND device_id = $2 AND task_id = $3`,
1482
+ [tenant, input.deviceId, input.taskId]
1483
+ );
1484
+ const racedAdmission = raced.rows[0];
1485
+ await client.query("COMMIT");
1486
+ return racedAdmission?.message_id === input.messageId && racedAdmission.payload_body === input.payloadBody ? "pending" : "rejected";
1487
+ } catch (cause) {
1488
+ await client.query("ROLLBACK").catch(() => {
1489
+ });
1490
+ throw cause;
1491
+ } finally {
1492
+ client.release();
1493
+ }
1494
+ }
1495
+ async readAgentMessage(tenant, input) {
1496
+ const result = await this.#pool.query(
1497
+ `SELECT ${AGENT_MESSAGE_ADMISSION_SELECT_COLUMNS}
1498
+ FROM agent_message_admission
1499
+ WHERE tenant_id = $1 AND device_id = $2 AND task_id = $3`,
1500
+ [tenant, input.deviceId, input.taskId]
1501
+ );
1502
+ const row = result.rows[0];
1503
+ return row !== void 0 && row.message_id === input.messageId && row.payload_body === input.payloadBody ? admissionRowToRecord(row) : void 0;
1504
+ }
1505
+ async finalizeAgentMessage(tenant, input) {
1506
+ const terminalized = await this.#pool.query(
1507
+ `UPDATE agent_message_admission
1508
+ SET terminal_body = $6
1509
+ WHERE tenant_id = $1 AND device_id = $2 AND task_id = $3
1510
+ AND message_id = $4 AND payload_body = $5 AND terminal_body IS NULL
1511
+ RETURNING ${AGENT_MESSAGE_ADMISSION_SELECT_COLUMNS}`,
1512
+ [tenant, input.deviceId, input.taskId, input.messageId, input.payloadBody, input.terminalBody]
1513
+ );
1514
+ const row = terminalized.rows[0];
1515
+ return row === void 0 ? this.readAgentMessage(tenant, input) : admissionRowToRecord(row);
1516
+ }
1307
1517
  async get(tenant, taskId) {
1308
1518
  const result = await this.#pool.query(
1309
1519
  `SELECT ${TASK_SELECT_COLUMNS} FROM task WHERE tenant_id = $1 AND task_id = $2`,
@@ -1320,14 +1530,44 @@ var PostgresTaskAttemptStore = class {
1320
1530
  );
1321
1531
  return result.rows.map(taskRowToAttempt);
1322
1532
  }
1533
+ async list(tenant, query) {
1534
+ assertTaskAttemptListLimit(query.limit);
1535
+ const result = await this.#pool.query(
1536
+ `SELECT ${TASK_SELECT_COLUMNS}
1537
+ FROM task
1538
+ WHERE tenant_id = $1 AND ($2::text IS NULL OR task_id > $2)
1539
+ ORDER BY task_id
1540
+ LIMIT $3`,
1541
+ [tenant, query.cursor ?? null, query.limit + 1]
1542
+ );
1543
+ const rows = result.rows.slice(0, query.limit);
1544
+ const attempts = rows.map(taskRowToAttempt);
1545
+ const hasMore = result.rows.length > rows.length;
1546
+ const last = attempts.at(-1);
1547
+ return {
1548
+ attempts,
1549
+ ...hasMore && last !== void 0 ? { nextCursor: last.taskId } : {}
1550
+ };
1551
+ }
1323
1552
  async claim(tenant, input) {
1324
1553
  const claimed = await this.#pool.query(
1325
1554
  `UPDATE task
1326
- SET owner_device_id = $3, status = 'claimed', updated_at = $4
1555
+ SET owner_device_id = $3,
1556
+ status = 'claimed',
1557
+ claimed_runtime = $5,
1558
+ claimed_runtime_capabilities = $6::jsonb,
1559
+ updated_at = $4
1327
1560
  WHERE tenant_id = $1 AND task_id = $2 AND owner_device_id IS NULL
1328
1561
  AND cancel_requested_at IS NULL AND status = 'offered'
1329
1562
  RETURNING ${TASK_SELECT_COLUMNS}`,
1330
- [tenant, input.taskId, input.deviceId, this.#now()]
1563
+ [
1564
+ tenant,
1565
+ input.taskId,
1566
+ input.deviceId,
1567
+ this.#now(),
1568
+ input.runtime ?? null,
1569
+ input.capabilities === void 0 ? null : JSON.stringify(input.capabilities)
1570
+ ]
1331
1571
  );
1332
1572
  const won = claimed.rows[0];
1333
1573
  if (won !== void 0) return taskRowToAttempt(won);
@@ -1492,20 +1732,63 @@ var PostgresMailboxStore = class {
1492
1732
  // Nothing above was mutated, so an identical call replays the same page.
1493
1733
  // The returned position is a READ cursor and moves no ack.
1494
1734
  nextSeq: page.at(-1)?.seq ?? query.afterSeq,
1495
- hasMore: result.rows.length > page.length
1735
+ hasMore: result.rows.length > page.length,
1736
+ recoverableFrom: await this.#recoverableFrom(tenant, query.deviceId)
1496
1737
  };
1497
1738
  }
1739
+ /**
1740
+ * One past the highest row this device lost, which is exactly the highest
1741
+ * `expired` seq — `1` when it has lost nothing.
1742
+ *
1743
+ * No column and no migration, because `collectRetired` never deletes an
1744
+ * unacked row: §12.7.5 keeps the dead letter, so the loss is already durable
1745
+ * in `outbox` and a stored floor would be a second authority free to drift
1746
+ * from the sweep that moved it. Whatever eventually reaps dead letters (S4B,
1747
+ * O-009) is the change that has to carry this floor forward instead.
1748
+ *
1749
+ * `acked` rows are not counted: a consumed row is not a lost one.
1750
+ *
1751
+ * Read AFTER the page on purpose. A sweep landing between the two reads can
1752
+ * only report a floor HIGHER than the page it accompanies, which makes the
1753
+ * caller fail closed over a page that may already be missing rows; reading
1754
+ * the floor first would report one too low and serve that gap silently.
1755
+ */
1756
+ async #recoverableFrom(tenant, deviceId) {
1757
+ const result = await this.#pool.query(
1758
+ `SELECT (COALESCE(MAX(seq), 0) + 1)::bigint AS recoverable_from
1759
+ FROM outbox
1760
+ WHERE tenant_id = $1 AND device_id = $2 AND state = 'expired'`,
1761
+ [tenant, deviceId]
1762
+ );
1763
+ return Number(result.rows[0].recoverable_from);
1764
+ }
1498
1765
  async advanceCursor(tenant, input) {
1499
1766
  this.#requireDeviceId(input.deviceId);
1500
1767
  const now = this.#now();
1501
1768
  const moved = await this.#pool.query(
1502
- `WITH moved AS (
1503
- INSERT INTO device_stream (tenant_id, device_id, next_seq, acked_seq, acked_at)
1504
- VALUES ($1, $2, 1, $3::bigint, $4)
1769
+ `WITH moved_zero AS (
1770
+ INSERT INTO device_stream (
1771
+ tenant_id, device_id, next_seq, acked_seq, acked_at, delivered_seq
1772
+ )
1773
+ SELECT $1, $2, 1, 0, $4, 0
1774
+ WHERE $3::bigint = 0
1505
1775
  ON CONFLICT (tenant_id, device_id) DO UPDATE
1506
- SET acked_seq = EXCLUDED.acked_seq, acked_at = EXCLUDED.acked_at
1507
- WHERE device_stream.acked_seq <= EXCLUDED.acked_seq
1508
- RETURNING acked_seq, acked_at
1776
+ SET acked_seq = 0, acked_at = EXCLUDED.acked_at
1777
+ WHERE device_stream.acked_seq <= 0
1778
+ AND device_stream.delivered_seq >= 0
1779
+ RETURNING acked_seq, acked_at, delivered_seq
1780
+ ), moved_positive AS (
1781
+ UPDATE device_stream
1782
+ SET acked_seq = $3::bigint, acked_at = $4
1783
+ WHERE tenant_id = $1 AND device_id = $2
1784
+ AND $3::bigint > 0
1785
+ AND acked_seq <= $3::bigint
1786
+ AND delivered_seq >= $3::bigint
1787
+ RETURNING acked_seq, acked_at, delivered_seq
1788
+ ), moved AS (
1789
+ SELECT acked_seq, acked_at, delivered_seq FROM moved_zero
1790
+ UNION ALL
1791
+ SELECT acked_seq, acked_at, delivered_seq FROM moved_positive
1509
1792
  ), marked AS (
1510
1793
  UPDATE outbox
1511
1794
  SET state = 'acked'
@@ -1513,7 +1796,7 @@ var PostgresMailboxStore = class {
1513
1796
  AND seq <= (SELECT acked_seq FROM moved)
1514
1797
  RETURNING 1
1515
1798
  )
1516
- SELECT acked_seq, acked_at FROM moved`,
1799
+ SELECT acked_seq, acked_at, delivered_seq FROM moved`,
1517
1800
  [tenant, input.deviceId, input.ackedSeq, now]
1518
1801
  );
1519
1802
  const row = moved.rows[0];
@@ -1521,21 +1804,51 @@ var PostgresMailboxStore = class {
1521
1804
  return {
1522
1805
  tenantId: tenant,
1523
1806
  deviceId: input.deviceId,
1807
+ deliveredSeq: Number(row.delivered_seq),
1524
1808
  ackedSeq: Number(row.acked_seq),
1525
1809
  updatedAt: row.acked_at ?? now
1526
1810
  };
1527
1811
  }
1528
1812
  const current = await this.readCursor(tenant, input.deviceId);
1813
+ if (input.ackedSeq < current.ackedSeq) {
1814
+ throw new CoreConflictError(
1815
+ "mailbox_cursor_regression",
1816
+ `Cursor for device ${input.deviceId} is at ${current.ackedSeq}; refusing to move it back to ${input.ackedSeq}.`,
1817
+ current,
1818
+ this.#now()
1819
+ );
1820
+ }
1529
1821
  throw new CoreConflictError(
1530
- "mailbox_cursor_regression",
1531
- `Cursor for device ${input.deviceId} is at ${current.ackedSeq}; refusing to move it back to ${input.ackedSeq}.`,
1822
+ "mailbox_cursor_ahead_of_delivery",
1823
+ `Cursor for device ${input.deviceId} was delivered through ${current.deliveredSeq}; refusing to acknowledge future cursor ${input.ackedSeq}.`,
1532
1824
  current,
1533
1825
  this.#now()
1534
1826
  );
1535
1827
  }
1828
+ async recordDelivery(tenant, input) {
1829
+ this.#requireDeviceId(input.deviceId);
1830
+ const recorded = await this.#pool.query(
1831
+ `INSERT INTO device_stream (
1832
+ tenant_id, device_id, next_seq, acked_seq, acked_at, delivered_seq
1833
+ )
1834
+ VALUES ($1, $2, 1, 0, NULL, $3::bigint)
1835
+ ON CONFLICT (tenant_id, device_id) DO UPDATE
1836
+ SET delivered_seq = GREATEST(device_stream.delivered_seq, EXCLUDED.delivered_seq)
1837
+ RETURNING acked_seq, acked_at, delivered_seq`,
1838
+ [tenant, input.deviceId, input.deliveredSeq]
1839
+ );
1840
+ const row = recorded.rows[0];
1841
+ return {
1842
+ tenantId: tenant,
1843
+ deviceId: input.deviceId,
1844
+ deliveredSeq: Number(row.delivered_seq),
1845
+ ackedSeq: Number(row.acked_seq),
1846
+ updatedAt: row.acked_at ?? this.#now()
1847
+ };
1848
+ }
1536
1849
  async readCursor(tenant, deviceId) {
1537
1850
  const result = await this.#pool.query(
1538
- `SELECT acked_seq, acked_at FROM device_stream
1851
+ `SELECT acked_seq, acked_at, delivered_seq FROM device_stream
1539
1852
  WHERE tenant_id = $1 AND device_id = $2`,
1540
1853
  [tenant, deviceId]
1541
1854
  );
@@ -1543,6 +1856,7 @@ var PostgresMailboxStore = class {
1543
1856
  return {
1544
1857
  tenantId: tenant,
1545
1858
  deviceId,
1859
+ deliveredSeq: row === void 0 ? 0 : Number(row.delivered_seq),
1546
1860
  ackedSeq: row === void 0 ? 0 : Number(row.acked_seq),
1547
1861
  updatedAt: row?.acked_at ?? this.#now()
1548
1862
  };
@@ -1708,8 +2022,35 @@ var PostgresActivityStore = class {
1708
2022
  const receivedAt = now.toISOString();
1709
2023
  const incoming = projectTimelineEvents(input, receivedAt);
1710
2024
  const expiresAt = new Date(now.getTime() + input.ttlMs).toISOString();
1711
- const result = await this.pool.query(
1712
- `WITH incoming AS (
2025
+ const client = await this.pool.connect();
2026
+ try {
2027
+ await client.query("BEGIN");
2028
+ await client.query(
2029
+ `SELECT pg_advisory_xact_lock(hashtextextended($1 || E'\\x1f' || $2, 0))`,
2030
+ [tenant, input.taskId]
2031
+ );
2032
+ const locked = await client.query(
2033
+ `SELECT tenant_id, task_id, entries, dropped, capacity, expires_at
2034
+ FROM activity_tail
2035
+ WHERE tenant_id = $1 AND task_id = $2
2036
+ FOR UPDATE`,
2037
+ [tenant, input.taskId]
2038
+ );
2039
+ const stored = locked.rows[0];
2040
+ const live = stored !== void 0 && receivedAt < stored.expires_at ? toTail(stored) : void 0;
2041
+ const sourceState = activitySourceBatchState(live?.entries ?? [], input);
2042
+ if (sourceState === "same") {
2043
+ await client.query("COMMIT");
2044
+ return live;
2045
+ }
2046
+ if (sourceState === "conflict") {
2047
+ throw new ByokCloudError(
2048
+ "coordination_input_invalid",
2049
+ `Activity source envelope ${input.sourceEnvelopeId} already belongs to another canonical batch.`
2050
+ );
2051
+ }
2052
+ const result = await client.query(
2053
+ `WITH incoming AS (
1713
2054
  SELECT entry
1714
2055
  FROM jsonb_array_elements($4::jsonb) AS element(entry)
1715
2056
  ), trimmed AS (
@@ -1776,16 +2117,24 @@ var PostgresActivityStore = class {
1776
2117
  )
1777
2118
  )
1778
2119
  RETURNING tenant_id, task_id, entries, dropped, capacity, expires_at`,
1779
- [tenant, input.taskId, receivedAt, JSON.stringify(incoming), input.dropped, capacity, expiresAt]
1780
- );
1781
- const row = result.rows[0];
1782
- if (row === void 0) {
1783
- throw new ByokCloudError(
1784
- "coordination_input_invalid",
1785
- `Activity batch ${input.batchSeq} conflicts with the existing typed tail authority.`
2120
+ [tenant, input.taskId, receivedAt, JSON.stringify(incoming), input.dropped, capacity, expiresAt]
1786
2121
  );
2122
+ const row = result.rows[0];
2123
+ if (row === void 0) {
2124
+ throw new ByokCloudError(
2125
+ "coordination_input_invalid",
2126
+ `Activity batch ${input.batchSeq} conflicts with the existing typed tail authority.`
2127
+ );
2128
+ }
2129
+ await client.query("COMMIT");
2130
+ return toTail(row);
2131
+ } catch (caught) {
2132
+ await client.query("ROLLBACK").catch(() => {
2133
+ });
2134
+ throw caught;
2135
+ } finally {
2136
+ client.release();
1787
2137
  }
1788
- return toTail(row);
1789
2138
  }
1790
2139
  async read(tenant, taskId) {
1791
2140
  const result = await this.pool.query(
@@ -1904,6 +2253,79 @@ var PostgresApprovalTimelineStore = class {
1904
2253
  client.release();
1905
2254
  }
1906
2255
  }
2256
+ async resolvePending(tenant, input) {
2257
+ const { capacity, ttlMs, event } = validateApprovalTimelineResolve(input);
2258
+ const now = this.clock.now();
2259
+ const receivedAt = now.toISOString();
2260
+ const expiresAt = new Date(now.getTime() + ttlMs).toISOString();
2261
+ const client = await this.pool.connect();
2262
+ try {
2263
+ await client.query("BEGIN");
2264
+ await client.query(
2265
+ `SELECT pg_advisory_xact_lock(hashtextextended($1 || E'\\x1f' || $2, 0))`,
2266
+ [tenant, input.taskId]
2267
+ );
2268
+ const stored = await readLocked(client, tenant, input.taskId);
2269
+ const live = stored !== void 0 && receivedAt < stored.expires_at ? toTail2(stored) : void 0;
2270
+ if (live === void 0) {
2271
+ await client.query("COMMIT");
2272
+ return { status: "absent" };
2273
+ }
2274
+ const duplicate = live.entries.find((entry) => entry.sourceEnvelopeId === input.sourceEnvelopeId);
2275
+ if (duplicate !== void 0) {
2276
+ await client.query("COMMIT");
2277
+ return JSON.stringify(duplicate.event) === JSON.stringify(event) ? { status: "replayed", tail: live } : { status: "conflict", tail: live };
2278
+ }
2279
+ const pending = pendingObservation(live.entries);
2280
+ if (pending === void 0) {
2281
+ await client.query("COMMIT");
2282
+ return { status: "absent", tail: live };
2283
+ }
2284
+ if (pending.sourceEnvelopeId !== input.expectedSourceEnvelopeId || pending.revision !== input.expectedRevision) {
2285
+ await client.query("COMMIT");
2286
+ return { status: "superseded", tail: live };
2287
+ }
2288
+ const revision = Number(stored.next_revision);
2289
+ const expectedRevision = (live.cursor ?? 0) + 1;
2290
+ if (!Number.isSafeInteger(revision) || revision <= 0 || revision !== expectedRevision) {
2291
+ throw new ByokCloudError(
2292
+ "coordination_input_invalid",
2293
+ "Approval timeline revision authority is malformed."
2294
+ );
2295
+ }
2296
+ const observation = ApprovalObservationSchema.parse({
2297
+ taskId: input.taskId,
2298
+ sourceEnvelopeId: input.sourceEnvelopeId,
2299
+ revision,
2300
+ receivedAt,
2301
+ event
2302
+ });
2303
+ const allEntries = [...live.entries, observation];
2304
+ const evicted = Math.max(allEntries.length - capacity, 0);
2305
+ const entries = allEntries.slice(evicted);
2306
+ const dropped = live.dropped + evicted;
2307
+ const result = await client.query(
2308
+ `INSERT INTO approval_timeline_tail
2309
+ (tenant_id, task_id, entries, next_revision, dropped, capacity, expires_at)
2310
+ VALUES ($1, $2, $3::jsonb, $4, $5, $6, $7)
2311
+ ON CONFLICT (tenant_id, task_id) DO UPDATE
2312
+ SET entries = EXCLUDED.entries,
2313
+ next_revision = EXCLUDED.next_revision,
2314
+ dropped = EXCLUDED.dropped,
2315
+ capacity = EXCLUDED.capacity,
2316
+ expires_at = EXCLUDED.expires_at
2317
+ RETURNING tenant_id, task_id, entries, next_revision, dropped, capacity, expires_at`,
2318
+ [tenant, input.taskId, JSON.stringify(entries), revision + 1, dropped, capacity, expiresAt]
2319
+ );
2320
+ await client.query("COMMIT");
2321
+ return { status: "applied", tail: toTail2(result.rows[0]) };
2322
+ } catch (caught) {
2323
+ await client.query("ROLLBACK");
2324
+ throw caught;
2325
+ } finally {
2326
+ client.release();
2327
+ }
2328
+ }
1907
2329
  async read(tenant, taskId) {
1908
2330
  const result = await this.pool.query(
1909
2331
  `SELECT tenant_id, task_id, entries, next_revision, dropped, capacity, expires_at
@@ -1915,6 +2337,19 @@ var PostgresApprovalTimelineStore = class {
1915
2337
  return row === void 0 ? void 0 : toTail2(row);
1916
2338
  }
1917
2339
  };
2340
+ function pendingObservation(entries) {
2341
+ let pending;
2342
+ for (const entry of entries) {
2343
+ if (entry.event.type === "approval_requested") {
2344
+ pending = entry;
2345
+ continue;
2346
+ }
2347
+ if (pending !== void 0 && (pending.event.approvalId === void 0 || pending.event.approvalId === entry.event.approvalId)) {
2348
+ pending = void 0;
2349
+ }
2350
+ }
2351
+ return pending;
2352
+ }
1918
2353
  var SELECT_COLUMNS4 = [
1919
2354
  "tenant_id",
1920
2355
  "device_id",
@@ -1961,7 +2396,7 @@ var PostgresAgentEgressStore = class {
1961
2396
  const inserted = await this.pool.query(
1962
2397
  `INSERT INTO agent_egress_event (${SELECT_COLUMNS4})
1963
2398
  VALUES ($1, $2, $3::uuid, $4, $5, $6, $7, $8::bigint, $9::jsonb, $10, $11::integer, $12::uuid, $13)
1964
- ON CONFLICT (tenant_id, device_id, event_id) DO NOTHING
2399
+ ON CONFLICT (tenant_id, device_id, agent_id, agent_profile_revision, event_id) DO NOTHING
1965
2400
  RETURNING ${SELECT_COLUMNS4}`,
1966
2401
  [
1967
2402
  tenant,
@@ -1981,16 +2416,18 @@ var PostgresAgentEgressStore = class {
1981
2416
  );
1982
2417
  const row = inserted.rows[0];
1983
2418
  if (row !== void 0) return { record: toRecord2(row), created: true };
1984
- const existing = await this.get(tenant, input.deviceId, payload.eventId);
2419
+ const existing = await this.get(tenant, input.deviceId, payload.agentRef, payload.eventId);
1985
2420
  if (existing === void 0) throw new Error(`Agent egress ${payload.eventId} vanished during first-write record.`);
1986
2421
  return { record: existing, created: false };
1987
2422
  }
1988
- async get(tenant, deviceId, eventId) {
2423
+ async get(tenant, deviceId, agentRef, eventId) {
1989
2424
  const result = await this.pool.query(
1990
2425
  `SELECT ${SELECT_COLUMNS4}
1991
2426
  FROM agent_egress_event
1992
- WHERE tenant_id = $1 AND device_id = $2 AND event_id = $3::uuid`,
1993
- [tenant, deviceId, eventId]
2427
+ WHERE tenant_id = $1 AND device_id = $2
2428
+ AND agent_id = $3 AND agent_profile_revision = $4
2429
+ AND event_id = $5::uuid`,
2430
+ [tenant, deviceId, agentRef.agentId, agentRef.profileRevision, eventId]
1994
2431
  );
1995
2432
  const row = result.rows[0];
1996
2433
  return row === void 0 ? void 0 : toRecord2(row);
@@ -2147,7 +2584,7 @@ var PostgresAgentMemoryProjectionStore = class {
2147
2584
  }
2148
2585
  });
2149
2586
  } catch (cause) {
2150
- await rollback2(client);
2587
+ await rollback3(client);
2151
2588
  throw cause;
2152
2589
  } finally {
2153
2590
  client.release();
@@ -2184,7 +2621,7 @@ var PostgresAgentMemoryProjectionStore = class {
2184
2621
  await client.query("COMMIT");
2185
2622
  return AgentMemoryProjectionEraseResultSchema.parse({ nextWriterEpoch });
2186
2623
  } catch (cause) {
2187
- await rollback2(client);
2624
+ await rollback3(client);
2188
2625
  throw cause;
2189
2626
  } finally {
2190
2627
  client.release();
@@ -2321,7 +2758,7 @@ function toBase64(bytes) {
2321
2758
  function toBase64Url(bytes) {
2322
2759
  return toBase64(bytes).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
2323
2760
  }
2324
- async function rollback2(client) {
2761
+ async function rollback3(client) {
2325
2762
  try {
2326
2763
  await client.query("ROLLBACK");
2327
2764
  } catch {
@@ -2380,11 +2817,16 @@ var PostgresDeviceAssertionReplayAuthority = class {
2380
2817
  // src/stores/index.ts
2381
2818
  function createPostgresCloudStores(options) {
2382
2819
  const { pool, clock, crypto } = options;
2820
+ const devices = new PostgresDeviceDirectory(pool, clock);
2821
+ const pairing = new PostgresPairingCodeStore(pool, clock);
2383
2822
  return {
2384
2823
  activity: new PostgresActivityStore(pool, clock),
2385
2824
  approvals: new PostgresApprovalTimelineStore(pool, clock),
2386
- devices: new PostgresDeviceDirectory(pool, clock),
2387
- pairingCodes: new PostgresPairingCodeStore(pool, clock),
2825
+ devices,
2826
+ // One durable code authority, projected into issuance and enrollment ports
2827
+ // so consumers cannot reopen the old redeem-then-register sequence.
2828
+ pairingCodes: { issue: pairing.issue.bind(pairing) },
2829
+ pairing: { redeemAndRegister: pairing.redeemAndRegister.bind(pairing) },
2388
2830
  nonces: new PostgresNonceStore(pool, clock, crypto),
2389
2831
  dedup: new PostgresInboundDedupStore(pool),
2390
2832
  tasks: new PostgresTaskAttemptStore(pool, clock),
@@ -3673,7 +4115,7 @@ function toReceipt4(tenant, row) {
3673
4115
  recordedAt: row.recorded_at.toISOString()
3674
4116
  };
3675
4117
  }
3676
- function sameBinding(receipt, input) {
4118
+ function sameBinding2(receipt, input) {
3677
4119
  return receipt.operation === input.operation && receipt.resource === input.resource && receipt.bodySha256 === input.proofBodySha256 && receipt.bodySize === input.proofBodySize;
3678
4120
  }
3679
4121
  function bodyColumns2(body) {
@@ -3712,7 +4154,7 @@ var PostgresTruthCommitter = class {
3712
4154
  ]);
3713
4155
  const replay = await this.#readReceipt(client, tenant, input.deviceId, input.requestId);
3714
4156
  if (replay !== void 0) {
3715
- if (!sameBinding(replay, input)) {
4157
+ if (!sameBinding2(replay, input)) {
3716
4158
  throw new TruthCommitError(
3717
4159
  "proof_request_conflict",
3718
4160
  `Request ${input.requestId} was already used with a different binding.`