@byok-sdk/cloud-dataplane 0.5.0 → 0.6.1

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,8 +1,9 @@
1
1
  import pg from 'pg';
2
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';
3
- import { ByokCoreError, assertCanonicalTimestamp, contentHash, isContentHash, tenantObjectKey, objectKeyPrefix, CoreConflictError, isLegalBoardTransition, checkSkillPackManifest, checkSkillPackEntry, SKILL_PACK_ENTRY_PATH, SKILL_PACK_MANIFEST_SCHEMA_ID } from '@byok-sdk/core';
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';
6
+ import { AgentEgressReliablePayloadSchema } from '@byok-sdk/protocol';
6
7
 
7
8
  // src/pool.ts
8
9
  var defaultTypeParser = pg.types.getTypeParser;
@@ -591,7 +592,7 @@ var R2ObjectMaintenanceStore = class {
591
592
  `A ListObjectsV2 page limit of ${String(limit)} is not a whole number in [1, 1000].`
592
593
  );
593
594
  }
594
- const prefix = `${tenant}/sha256/`;
595
+ const prefix = tenantObjectKey(tenant, LIST_PREFIX_HASH, this.#keyPrefix).slice(0, -LIST_HASH_HEX_LENGTH);
595
596
  const url = new URL(`${this.#origin}/${this.#bucket}`);
596
597
  url.searchParams.set("list-type", "2");
597
598
  url.searchParams.set("prefix", prefix);
@@ -775,6 +776,8 @@ function parseListObjectsV2(xml, prefix, attempts) {
775
776
  return { objects, nextContinuationToken };
776
777
  }
777
778
  var HASH_KEY_SUFFIX = /^[0-9a-f]{64}$/;
779
+ var LIST_HASH_HEX_LENGTH = 64;
780
+ var LIST_PREFIX_HASH = contentHash(`sha256:${"0".repeat(LIST_HASH_HEX_LENGTH)}`);
778
781
  function asRecord(value) {
779
782
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
780
783
  }
@@ -786,8 +789,6 @@ function requiredText(record, field, attempts) {
786
789
  attempts
787
790
  );
788
791
  }
789
-
790
- // src/stores/devices.ts
791
792
  function toRecord(row) {
792
793
  return {
793
794
  tenantId: row.tenant_id,
@@ -797,14 +798,17 @@ function toRecord(row) {
797
798
  devicePublicKey: row.device_public_key,
798
799
  proofKeyId: row.proof_key_id,
799
800
  proofKeyEpoch: row.proof_key_epoch,
800
- revoked: row.revoked
801
+ revoked: row.revoked,
802
+ ...row.capabilities == null ? {} : { capabilities: Object.freeze([...row.capabilities]) }
801
803
  };
802
804
  }
803
- var SELECT_COLUMNS = "tenant_id, device_id, product_id, device_name, device_public_key, proof_key_id, proof_key_epoch, revoked";
805
+ var SELECT_COLUMNS = "tenant_id, device_id, product_id, device_name, device_public_key, proof_key_id, proof_key_epoch, revoked, capabilities";
804
806
  var PostgresDeviceDirectory = class {
805
807
  #pool;
806
- constructor(pool) {
808
+ #clock;
809
+ constructor(pool, clock) {
807
810
  this.#pool = pool;
811
+ this.#clock = clock;
808
812
  }
809
813
  async register(tenant, input) {
810
814
  const result = await this.#pool.query(
@@ -819,7 +823,8 @@ var PostgresDeviceDirectory = class {
819
823
  device_public_key = EXCLUDED.device_public_key,
820
824
  proof_key_id = EXCLUDED.proof_key_id,
821
825
  proof_key_epoch = EXCLUDED.proof_key_epoch,
822
- revoked = false
826
+ revoked = false,
827
+ capabilities = NULL
823
828
  RETURNING ${SELECT_COLUMNS}`,
824
829
  [
825
830
  tenant,
@@ -847,6 +852,17 @@ var PostgresDeviceDirectory = class {
847
852
  deviceId
848
853
  ]);
849
854
  }
855
+ async recordCapabilities(tenant, input) {
856
+ const result = await this.#pool.query(
857
+ `UPDATE device
858
+ SET capabilities = $3::jsonb
859
+ WHERE tenant_id = $1 AND device_id = $2 AND revoked = false
860
+ RETURNING ${SELECT_COLUMNS}`,
861
+ [tenant, input.deviceId, JSON.stringify([...input.capabilities])]
862
+ );
863
+ const row = result.rows[0];
864
+ return row === void 0 ? void 0 : toRecord(row);
865
+ }
850
866
  async list(tenant) {
851
867
  const result = await this.#pool.query(
852
868
  `SELECT ${SELECT_COLUMNS} FROM device WHERE tenant_id = $1 ORDER BY device_id`,
@@ -854,6 +870,73 @@ var PostgresDeviceDirectory = class {
854
870
  );
855
871
  return result.rows.map(toRecord);
856
872
  }
873
+ async readiness(tenant, _presence) {
874
+ const result = await this.#pool.query(
875
+ `SELECT
876
+ d.device_id,
877
+ d.product_id,
878
+ d.device_name,
879
+ d.revoked,
880
+ CASE WHEN NOT d.revoked THEN p.level END AS presence_level,
881
+ CASE WHEN NOT d.revoked THEN p.detail END AS presence_detail,
882
+ CASE WHEN NOT d.revoked THEN p.configured_toolsets END AS presence_configured_toolsets,
883
+ CASE WHEN NOT d.revoked THEN p.client_version END AS presence_client_version,
884
+ CASE WHEN NOT d.revoked THEN p.protocol_versions END AS presence_protocol_versions,
885
+ CASE WHEN NOT d.revoked THEN p.runtimes END AS presence_runtimes,
886
+ CASE WHEN NOT d.revoked THEN p.observed_at END AS presence_observed_at,
887
+ CASE WHEN NOT d.revoked THEN p.expires_at END AS presence_expires_at,
888
+ (COUNT(*) FILTER (WHERE NOT d.revoked) OVER ())::int AS active_paired_device_count,
889
+ (COUNT(*) FILTER (WHERE d.revoked) OVER ())::int AS revoked_device_count,
890
+ (COUNT(*) FILTER (WHERE NOT d.revoked AND p.device_id IS NOT NULL) OVER ())::int AS observed_presence_count,
891
+ (COUNT(*) FILTER (WHERE NOT d.revoked AND p.level = 'online') OVER ())::int AS observed_online_count,
892
+ (COUNT(*) FILTER (WHERE NOT d.revoked AND p.level = 'thinking') OVER ())::int AS observed_thinking_count,
893
+ (COUNT(*) FILTER (WHERE NOT d.revoked AND p.level = 'working') OVER ())::int AS observed_working_count,
894
+ (COUNT(*) FILTER (WHERE NOT d.revoked AND p.level = 'error') OVER ())::int AS observed_error_count,
895
+ (COUNT(*) FILTER (WHERE NOT d.revoked AND p.level = 'offline') OVER ())::int AS observed_offline_count
896
+ FROM device d
897
+ LEFT JOIN device_presence p
898
+ ON p.tenant_id = d.tenant_id
899
+ AND p.device_id = d.device_id
900
+ AND p.expires_at > $2
901
+ WHERE d.tenant_id = $1
902
+ ORDER BY d.device_id`,
903
+ [tenant, this.#clock?.now().toISOString() ?? (/* @__PURE__ */ new Date()).toISOString()]
904
+ );
905
+ const row = result.rows[0];
906
+ const count = (value) => Number(value);
907
+ const devices = result.rows.map((device) => ({
908
+ deviceId: device.device_id,
909
+ productId: device.product_id,
910
+ deviceName: device.device_name,
911
+ revoked: device.revoked,
912
+ ...device.presence_level === null ? {} : {
913
+ presence: {
914
+ level: device.presence_level,
915
+ ...device.presence_detail === null ? {} : { detail: device.presence_detail },
916
+ ...device.presence_configured_toolsets === null ? {} : { configuredToolsets: Object.freeze([...device.presence_configured_toolsets]) },
917
+ ...device.presence_client_version === null ? {} : { clientVersion: device.presence_client_version },
918
+ ...device.presence_protocol_versions === null ? {} : { protocolVersions: Object.freeze([...device.presence_protocol_versions]) },
919
+ ...device.presence_runtimes === null ? {} : { runtimes: Object.freeze(device.presence_runtimes.map((runtime) => Object.freeze({ ...runtime }))) },
920
+ observedAt: device.presence_observed_at,
921
+ expiresAt: device.presence_expires_at
922
+ }
923
+ }
924
+ }));
925
+ return {
926
+ tenantId: tenant,
927
+ activePairedDeviceCount: count(row?.active_paired_device_count ?? 0),
928
+ revokedDeviceCount: count(row?.revoked_device_count ?? 0),
929
+ observedPresenceCount: count(row?.observed_presence_count ?? 0),
930
+ observedPresenceByLevel: {
931
+ online: count(row?.observed_online_count ?? 0),
932
+ thinking: count(row?.observed_thinking_count ?? 0),
933
+ working: count(row?.observed_working_count ?? 0),
934
+ error: count(row?.observed_error_count ?? 0),
935
+ offline: count(row?.observed_offline_count ?? 0)
936
+ },
937
+ devices
938
+ };
939
+ }
857
940
  async resolveByDeviceId(deviceId) {
858
941
  const result = await this.#pool.query(
859
942
  `SELECT ${SELECT_COLUMNS} FROM device WHERE device_id = $1`,
@@ -1077,18 +1160,31 @@ var PostgresProofRequestReceiptStore = class {
1077
1160
  };
1078
1161
 
1079
1162
  // src/stores/task-attempts.ts
1080
- var SELECT_COLUMNS4 = "tenant_id, task_id, device_id, owner_device_id, status, updated_at";
1081
- function toAttempt(row) {
1163
+ 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";
1164
+ function taskRowToAttempt(row) {
1082
1165
  return {
1083
1166
  tenantId: row.tenant_id,
1084
1167
  taskId: row.task_id,
1085
1168
  deviceId: row.device_id,
1169
+ ...row.agent_id == null || row.agent_profile_revision == null ? {} : {
1170
+ agentRef: {
1171
+ agentId: row.agent_id,
1172
+ profileRevision: row.agent_profile_revision
1173
+ }
1174
+ },
1086
1175
  // `exactOptionalPropertyTypes` is off here, but an explicit absent key is
1087
1176
  // still what the in-memory reference produces for an unclaimed attempt, and
1088
1177
  // `toEqual` in the suite treats `undefined` and absent alike only for the
1089
1178
  // former.
1090
1179
  ...row.owner_device_id === null ? {} : { ownerDeviceId: row.owner_device_id },
1091
1180
  status: row.status,
1181
+ ...row.terminal_cause == null ? {} : { terminalCause: row.terminal_cause },
1182
+ ...row.cancel_requested_at === null ? {} : {
1183
+ cancellation: {
1184
+ requestedAt: row.cancel_requested_at.toISOString(),
1185
+ ...row.cancel_reason === null ? {} : { reason: row.cancel_reason }
1186
+ }
1187
+ },
1092
1188
  updatedAt: row.updated_at.toISOString()
1093
1189
  };
1094
1190
  }
@@ -1101,53 +1197,423 @@ var PostgresTaskAttemptStore = class {
1101
1197
  }
1102
1198
  async open(tenant, input) {
1103
1199
  const inserted = await this.#pool.query(
1104
- `INSERT INTO task (tenant_id, task_id, device_id, owner_device_id, status, updated_at)
1105
- VALUES ($1, $2, $3, NULL, 'offered', $4)
1200
+ `INSERT INTO task (
1201
+ tenant_id, task_id, device_id, agent_id, agent_profile_revision,
1202
+ owner_device_id, status, updated_at
1203
+ )
1204
+ VALUES ($1, $2, $3, $4, $5, NULL, 'offered', $6)
1106
1205
  ON CONFLICT (tenant_id, task_id) DO NOTHING
1107
- RETURNING ${SELECT_COLUMNS4}`,
1108
- [tenant, input.taskId, input.deviceId, this.#now()]
1206
+ RETURNING ${TASK_SELECT_COLUMNS}`,
1207
+ [
1208
+ tenant,
1209
+ input.taskId,
1210
+ input.deviceId,
1211
+ input.agentRef?.agentId ?? null,
1212
+ input.agentRef?.profileRevision ?? null,
1213
+ this.#now()
1214
+ ]
1109
1215
  );
1110
1216
  const created = inserted.rows[0];
1111
- if (created !== void 0) return toAttempt(created);
1217
+ if (created !== void 0) return taskRowToAttempt(created);
1112
1218
  const existing = await this.get(tenant, input.taskId);
1113
1219
  if (existing === void 0) throw new Error(`task ${input.taskId} vanished during open`);
1114
1220
  return existing;
1115
1221
  }
1222
+ async reserveAgentOffer(tenant, input) {
1223
+ const inserted = await this.#pool.query(
1224
+ `INSERT INTO task (
1225
+ tenant_id, task_id, device_id, agent_id, agent_profile_revision,
1226
+ owner_device_id, status, updated_at
1227
+ )
1228
+ VALUES ($1, $2, $3, $4, $5, NULL, 'offered', $6)
1229
+ ON CONFLICT (tenant_id, task_id) DO NOTHING
1230
+ RETURNING ${TASK_SELECT_COLUMNS}`,
1231
+ [tenant, input.taskId, input.deviceId, input.agentRef.agentId, input.agentRef.profileRevision, this.#now()]
1232
+ );
1233
+ const created = inserted.rows[0];
1234
+ if (created !== void 0) return { attempt: taskRowToAttempt(created), created: true };
1235
+ const existing = await this.get(tenant, input.taskId);
1236
+ if (existing === void 0) throw new Error(`task ${input.taskId} vanished during Agent offer reservation`);
1237
+ return { attempt: existing, created: false };
1238
+ }
1116
1239
  async get(tenant, taskId) {
1117
1240
  const result = await this.#pool.query(
1118
- `SELECT ${SELECT_COLUMNS4} FROM task WHERE tenant_id = $1 AND task_id = $2`,
1241
+ `SELECT ${TASK_SELECT_COLUMNS} FROM task WHERE tenant_id = $1 AND task_id = $2`,
1119
1242
  [tenant, taskId]
1120
1243
  );
1121
1244
  const row = result.rows[0];
1122
- return row === void 0 ? void 0 : toAttempt(row);
1245
+ return row === void 0 ? void 0 : taskRowToAttempt(row);
1246
+ }
1247
+ async getMany(tenant, taskIds) {
1248
+ if (taskIds.length === 0) return [];
1249
+ const result = await this.#pool.query(
1250
+ `SELECT ${TASK_SELECT_COLUMNS} FROM task WHERE tenant_id = $1 AND task_id = ANY($2::text[])`,
1251
+ [tenant, [...new Set(taskIds)]]
1252
+ );
1253
+ return result.rows.map(taskRowToAttempt);
1123
1254
  }
1124
1255
  async claim(tenant, input) {
1125
1256
  const claimed = await this.#pool.query(
1126
1257
  `UPDATE task
1127
1258
  SET owner_device_id = $3, status = 'claimed', updated_at = $4
1128
1259
  WHERE tenant_id = $1 AND task_id = $2 AND owner_device_id IS NULL
1129
- RETURNING ${SELECT_COLUMNS4}`,
1260
+ AND cancel_requested_at IS NULL AND status = 'offered'
1261
+ RETURNING ${TASK_SELECT_COLUMNS}`,
1130
1262
  [tenant, input.taskId, input.deviceId, this.#now()]
1131
1263
  );
1132
1264
  const won = claimed.rows[0];
1133
- if (won !== void 0) return toAttempt(won);
1265
+ if (won !== void 0) return taskRowToAttempt(won);
1134
1266
  return this.get(tenant, input.taskId);
1135
1267
  }
1136
1268
  async recordStatus(tenant, input) {
1137
1269
  const result = await this.#pool.query(
1138
1270
  `UPDATE task
1139
- SET status = $3, updated_at = $4
1271
+ SET status = $3,
1272
+ terminal_cause = COALESCE($7, terminal_cause),
1273
+ updated_at = $4
1140
1274
  WHERE tenant_id = $1 AND task_id = $2
1141
- RETURNING ${SELECT_COLUMNS4}`,
1142
- [tenant, input.taskId, input.status, this.#now()]
1275
+ AND (
1276
+ (agent_id IS NULL AND agent_profile_revision IS NULL AND $5::text IS NULL AND $6::text IS NULL)
1277
+ OR (agent_id = $5 AND agent_profile_revision = $6)
1278
+ )
1279
+ AND (
1280
+ (cancel_requested_at IS NULL AND status NOT IN ('complete', 'failed', 'cancelled'))
1281
+ OR (cancel_requested_at IS NOT NULL AND $3 = 'cancelled' AND status <> 'cancelled')
1282
+ )
1283
+ RETURNING ${TASK_SELECT_COLUMNS}`,
1284
+ [
1285
+ tenant,
1286
+ input.taskId,
1287
+ input.status,
1288
+ this.#now(),
1289
+ input.agentRef?.agentId ?? null,
1290
+ input.agentRef?.profileRevision ?? null,
1291
+ input.terminalCause ?? null
1292
+ ]
1293
+ );
1294
+ const row = result.rows[0];
1295
+ return row === void 0 ? this.get(tenant, input.taskId) : taskRowToAttempt(row);
1296
+ }
1297
+ #now() {
1298
+ return this.#clock.now().toISOString();
1299
+ }
1300
+ };
1301
+
1302
+ // src/stores/core/mailbox-sequence.ts
1303
+ async function allocateMailboxSequence(client, tenant, deviceId, now) {
1304
+ const allocation = await client.query(
1305
+ `INSERT INTO device_stream (tenant_id, device_id, next_seq, acked_seq, acked_at)
1306
+ VALUES ($1, $2, 2, 0, $3)
1307
+ ON CONFLICT (tenant_id, device_id) DO UPDATE
1308
+ SET next_seq = device_stream.next_seq + 1
1309
+ RETURNING next_seq - 1 AS seq`,
1310
+ [tenant, deviceId, now]
1311
+ );
1312
+ return Number(allocation.rows[0].seq);
1313
+ }
1314
+
1315
+ // src/stores/core/mailbox.ts
1316
+ var DEFAULT_READ_LIMIT = 50;
1317
+ var OUTBOX_COLUMNS = "tenant_id, device_id, seq, message_id, body, body_hash, byte_size, state, appended_at";
1318
+ function toMailboxMessage(row) {
1319
+ return {
1320
+ tenantId: row.tenant_id,
1321
+ deviceId: row.device_id,
1322
+ // `seq` is bigint in the column and `number` on the port, because it is the
1323
+ // envelope `seq` on the wire. The column is wide so the counter cannot wrap
1324
+ // into a redelivery bug; the narrowing happens once, here.
1325
+ seq: Number(row.seq),
1326
+ messageId: row.message_id,
1327
+ body: row.body,
1328
+ bodyHash: row.body_hash,
1329
+ byteSize: row.byte_size,
1330
+ state: row.state,
1331
+ appendedAt: row.appended_at
1332
+ };
1333
+ }
1334
+ var PostgresMailboxStore = class {
1335
+ #pool;
1336
+ #clock;
1337
+ constructor(pool, clock) {
1338
+ this.#pool = pool;
1339
+ this.#clock = clock;
1340
+ }
1341
+ async append(tenant, input) {
1342
+ this.#requireDeviceId(input.deviceId);
1343
+ const client = await this.#pool.connect();
1344
+ try {
1345
+ await client.query("BEGIN");
1346
+ const existing = await client.query(
1347
+ `SELECT ${OUTBOX_COLUMNS} FROM outbox
1348
+ WHERE tenant_id = $1 AND device_id = $2 AND message_id = $3`,
1349
+ [tenant, input.deviceId, input.messageId]
1350
+ );
1351
+ const replayed = existing.rows[0];
1352
+ if (replayed !== void 0) {
1353
+ await client.query("COMMIT");
1354
+ return toMailboxMessage(replayed);
1355
+ }
1356
+ const seq = await allocateMailboxSequence(client, tenant, input.deviceId, this.#now());
1357
+ const serializedExisting = await client.query(
1358
+ `SELECT ${OUTBOX_COLUMNS} FROM outbox
1359
+ WHERE tenant_id = $1 AND device_id = $2 AND message_id = $3`,
1360
+ [tenant, input.deviceId, input.messageId]
1361
+ );
1362
+ const winnerAfterLock = serializedExisting.rows[0];
1363
+ if (winnerAfterLock !== void 0) {
1364
+ await client.query("ROLLBACK");
1365
+ return toMailboxMessage(winnerAfterLock);
1366
+ }
1367
+ const materialized = await input.materialize(seq);
1368
+ const now = this.#now();
1369
+ const inserted = await client.query(
1370
+ `INSERT INTO outbox (${OUTBOX_COLUMNS})
1371
+ VALUES ($1, $2, $3::bigint, $4, $5, $6, $7::bigint, 'pending', $8)
1372
+ ON CONFLICT (tenant_id, device_id, message_id) DO NOTHING
1373
+ RETURNING ${OUTBOX_COLUMNS}`,
1374
+ [
1375
+ tenant,
1376
+ input.deviceId,
1377
+ seq,
1378
+ input.messageId,
1379
+ materialized.body,
1380
+ materialized.bodyHash,
1381
+ materialized.byteSize,
1382
+ now
1383
+ ]
1384
+ );
1385
+ const row = inserted.rows[0];
1386
+ if (row !== void 0) {
1387
+ await client.query("COMMIT");
1388
+ return toMailboxMessage(row);
1389
+ }
1390
+ const winner = await client.query(
1391
+ `SELECT ${OUTBOX_COLUMNS} FROM outbox
1392
+ WHERE tenant_id = $1 AND device_id = $2 AND message_id = $3`,
1393
+ [tenant, input.deviceId, input.messageId]
1394
+ );
1395
+ await client.query("ROLLBACK");
1396
+ const won = winner.rows[0];
1397
+ if (won === void 0) {
1398
+ throw new ByokCoreError(
1399
+ "mailbox_message_not_found",
1400
+ `Message ${input.messageId} vanished during an idempotent append.`
1401
+ );
1402
+ }
1403
+ return toMailboxMessage(won);
1404
+ } catch (cause) {
1405
+ await client.query("ROLLBACK").catch(() => {
1406
+ });
1407
+ throw cause;
1408
+ } finally {
1409
+ client.release();
1410
+ }
1411
+ }
1412
+ async readAfter(tenant, query) {
1413
+ const limit = query.limit ?? DEFAULT_READ_LIMIT;
1414
+ const result = await this.#pool.query(
1415
+ `SELECT ${OUTBOX_COLUMNS} FROM outbox
1416
+ WHERE tenant_id = $1 AND device_id = $2 AND state = 'pending' AND seq > $3::bigint
1417
+ ORDER BY seq
1418
+ LIMIT $4`,
1419
+ [tenant, query.deviceId, query.afterSeq, limit + 1]
1420
+ );
1421
+ const page = result.rows.slice(0, limit).map(toMailboxMessage);
1422
+ return {
1423
+ messages: page,
1424
+ // Nothing above was mutated, so an identical call replays the same page.
1425
+ // The returned position is a READ cursor and moves no ack.
1426
+ nextSeq: page.at(-1)?.seq ?? query.afterSeq,
1427
+ hasMore: result.rows.length > page.length
1428
+ };
1429
+ }
1430
+ async advanceCursor(tenant, input) {
1431
+ this.#requireDeviceId(input.deviceId);
1432
+ const now = this.#now();
1433
+ const moved = await this.#pool.query(
1434
+ `WITH moved AS (
1435
+ INSERT INTO device_stream (tenant_id, device_id, next_seq, acked_seq, acked_at)
1436
+ VALUES ($1, $2, 1, $3::bigint, $4)
1437
+ ON CONFLICT (tenant_id, device_id) DO UPDATE
1438
+ SET acked_seq = EXCLUDED.acked_seq, acked_at = EXCLUDED.acked_at
1439
+ WHERE device_stream.acked_seq <= EXCLUDED.acked_seq
1440
+ RETURNING acked_seq, acked_at
1441
+ ), marked AS (
1442
+ UPDATE outbox
1443
+ SET state = 'acked'
1444
+ WHERE tenant_id = $1 AND device_id = $2 AND state = 'pending'
1445
+ AND seq <= (SELECT acked_seq FROM moved)
1446
+ RETURNING 1
1447
+ )
1448
+ SELECT acked_seq, acked_at FROM moved`,
1449
+ [tenant, input.deviceId, input.ackedSeq, now]
1450
+ );
1451
+ const row = moved.rows[0];
1452
+ if (row !== void 0) {
1453
+ return {
1454
+ tenantId: tenant,
1455
+ deviceId: input.deviceId,
1456
+ ackedSeq: Number(row.acked_seq),
1457
+ updatedAt: row.acked_at ?? now
1458
+ };
1459
+ }
1460
+ const current = await this.readCursor(tenant, input.deviceId);
1461
+ throw new CoreConflictError(
1462
+ "mailbox_cursor_regression",
1463
+ `Cursor for device ${input.deviceId} is at ${current.ackedSeq}; refusing to move it back to ${input.ackedSeq}.`,
1464
+ current,
1465
+ this.#now()
1466
+ );
1467
+ }
1468
+ async readCursor(tenant, deviceId) {
1469
+ const result = await this.#pool.query(
1470
+ `SELECT acked_seq, acked_at FROM device_stream
1471
+ WHERE tenant_id = $1 AND device_id = $2`,
1472
+ [tenant, deviceId]
1143
1473
  );
1144
1474
  const row = result.rows[0];
1145
- return row === void 0 ? void 0 : toAttempt(row);
1475
+ return {
1476
+ tenantId: tenant,
1477
+ deviceId,
1478
+ ackedSeq: row === void 0 ? 0 : Number(row.acked_seq),
1479
+ updatedAt: row?.acked_at ?? this.#now()
1480
+ };
1481
+ }
1482
+ async collectRetired(tenant, input) {
1483
+ assertCanonicalTimestamp(input.ackedBefore, "ackedBefore");
1484
+ assertCanonicalTimestamp(input.expireUnackedBefore, "expireUnackedBefore");
1485
+ const swept = await this.#pool.query(
1486
+ `WITH deleted AS (
1487
+ DELETE FROM outbox
1488
+ WHERE tenant_id = $1
1489
+ AND ($2::text IS NULL OR device_id = $2::text)
1490
+ AND state = 'acked'
1491
+ AND appended_at < $3
1492
+ RETURNING byte_size
1493
+ ), expired AS (
1494
+ UPDATE outbox
1495
+ SET state = 'expired'
1496
+ WHERE tenant_id = $1
1497
+ AND ($2::text IS NULL OR device_id = $2::text)
1498
+ AND state = 'pending'
1499
+ AND appended_at < $4
1500
+ RETURNING 1
1501
+ )
1502
+ SELECT (SELECT count(*) FROM deleted) AS deleted_count,
1503
+ (SELECT count(*) FROM expired) AS expired_count,
1504
+ (SELECT COALESCE(SUM(byte_size), 0) FROM deleted)::bigint AS released_bytes`,
1505
+ [tenant, input.deviceId ?? null, input.ackedBefore, input.expireUnackedBefore]
1506
+ );
1507
+ const row = swept.rows[0];
1508
+ return {
1509
+ deletedCount: Number(row.deleted_count),
1510
+ expiredCount: Number(row.expired_count),
1511
+ releasedBytes: row.released_bytes
1512
+ };
1513
+ }
1514
+ /**
1515
+ * The in-memory reference refuses an empty device id rather than opening a
1516
+ * mailbox nothing can address. Kept here so the two compositions answer the
1517
+ * same way; the table itself would happily store the row.
1518
+ */
1519
+ #requireDeviceId(deviceId) {
1520
+ if (deviceId.length === 0) {
1521
+ throw new ByokCoreError("mailbox_message_not_found", "Device id must not be empty.");
1522
+ }
1146
1523
  }
1147
1524
  #now() {
1148
1525
  return this.#clock.now().toISOString();
1149
1526
  }
1150
1527
  };
1528
+
1529
+ // src/stores/task-cancellations.ts
1530
+ var PostgresTaskCancellationStore = class {
1531
+ #pool;
1532
+ #clock;
1533
+ constructor(pool, clock) {
1534
+ this.#pool = pool;
1535
+ this.#clock = clock;
1536
+ }
1537
+ async request(tenant, input) {
1538
+ const client = await this.#pool.connect();
1539
+ try {
1540
+ await client.query("BEGIN");
1541
+ const selected = await client.query(
1542
+ `SELECT ${TASK_SELECT_COLUMNS} FROM task
1543
+ WHERE tenant_id = $1 AND task_id = $2
1544
+ FOR UPDATE`,
1545
+ [tenant, input.taskId]
1546
+ );
1547
+ const current = selected.rows[0];
1548
+ if (current === void 0) {
1549
+ await client.query("ROLLBACK");
1550
+ return void 0;
1551
+ }
1552
+ if (current.cancel_requested_at === null && (current.status === "complete" || current.status === "failed" || current.status === "cancelled")) {
1553
+ await client.query("COMMIT");
1554
+ return { attempt: taskRowToAttempt(current) };
1555
+ }
1556
+ if (current.cancel_message_id !== null) {
1557
+ const replayed = await client.query(
1558
+ `SELECT ${OUTBOX_COLUMNS} FROM outbox
1559
+ WHERE tenant_id = $1 AND device_id = $2 AND message_id = $3`,
1560
+ [tenant, current.device_id, current.cancel_message_id]
1561
+ );
1562
+ const message = replayed.rows[0];
1563
+ if (message === void 0) {
1564
+ if (current.status === "cancelled") {
1565
+ await client.query("COMMIT");
1566
+ return { attempt: taskRowToAttempt(current) };
1567
+ }
1568
+ throw new Error(`Cancellation delivery ${current.cancel_message_id} is missing for task ${input.taskId}`);
1569
+ }
1570
+ await client.query("COMMIT");
1571
+ return { attempt: taskRowToAttempt(current), message: toMailboxMessage(message) };
1572
+ }
1573
+ const now = this.#clock.now().toISOString();
1574
+ const messageId = input.proposedMessageId;
1575
+ const seq = await allocateMailboxSequence(client, tenant, current.device_id, now);
1576
+ const materialized = await input.materialize(seq, messageId);
1577
+ const insertedMessage = await client.query(
1578
+ `INSERT INTO outbox (${OUTBOX_COLUMNS})
1579
+ VALUES ($1, $2, $3::bigint, $4, $5, $6, $7::bigint, 'pending', $8)
1580
+ RETURNING ${OUTBOX_COLUMNS}`,
1581
+ [
1582
+ tenant,
1583
+ current.device_id,
1584
+ seq,
1585
+ messageId,
1586
+ materialized.body,
1587
+ materialized.bodyHash,
1588
+ materialized.byteSize,
1589
+ now
1590
+ ]
1591
+ );
1592
+ const updated = await client.query(
1593
+ `UPDATE task
1594
+ SET status = CASE WHEN owner_device_id IS NULL THEN 'cancelled' ELSE 'cancel_requested' END,
1595
+ cancel_requested_at = $3,
1596
+ cancel_reason = $4,
1597
+ cancel_message_id = $5,
1598
+ updated_at = $3
1599
+ WHERE tenant_id = $1 AND task_id = $2
1600
+ RETURNING ${TASK_SELECT_COLUMNS}`,
1601
+ [tenant, input.taskId, now, input.reason ?? null, messageId]
1602
+ );
1603
+ await client.query("COMMIT");
1604
+ return {
1605
+ attempt: taskRowToAttempt(updated.rows[0]),
1606
+ message: toMailboxMessage(insertedMessage.rows[0])
1607
+ };
1608
+ } catch (cause) {
1609
+ await client.query("ROLLBACK").catch(() => {
1610
+ });
1611
+ throw cause;
1612
+ } finally {
1613
+ client.release();
1614
+ }
1615
+ }
1616
+ };
1151
1617
  function toTail(row) {
1152
1618
  const entries = parseTimelineEvents(row.entries);
1153
1619
  const cursor = activityCursor(entries);
@@ -1381,6 +1847,87 @@ var PostgresApprovalTimelineStore = class {
1381
1847
  return row === void 0 ? void 0 : toTail2(row);
1382
1848
  }
1383
1849
  };
1850
+ var SELECT_COLUMNS4 = [
1851
+ "tenant_id",
1852
+ "device_id",
1853
+ "event_id",
1854
+ "agent_id",
1855
+ "agent_profile_revision",
1856
+ "session_ref",
1857
+ "policy_revision",
1858
+ "cursor",
1859
+ "payload_json",
1860
+ "content_hash",
1861
+ "byte_count",
1862
+ "receipt_id",
1863
+ "recorded_at"
1864
+ ].join(", ");
1865
+ function toRecord2(row) {
1866
+ const payload = AgentEgressReliablePayloadSchema.parse({
1867
+ agentRef: { agentId: row.agent_id, profileRevision: row.agent_profile_revision },
1868
+ sessionRef: row.session_ref,
1869
+ policyRevision: row.policy_revision,
1870
+ eventId: row.event_id,
1871
+ cursor: Number(row.cursor),
1872
+ payload: row.payload_json,
1873
+ contentHash: row.content_hash,
1874
+ byteCount: row.byte_count
1875
+ });
1876
+ return {
1877
+ tenantId: row.tenant_id,
1878
+ deviceId: row.device_id,
1879
+ payload,
1880
+ receiptId: row.receipt_id,
1881
+ recordedAt: row.recorded_at.toISOString()
1882
+ };
1883
+ }
1884
+ var PostgresAgentEgressStore = class {
1885
+ constructor(pool, clock) {
1886
+ this.pool = pool;
1887
+ this.clock = clock;
1888
+ }
1889
+ pool;
1890
+ clock;
1891
+ async record(tenant, input) {
1892
+ const payload = AgentEgressReliablePayloadSchema.parse(input.payload);
1893
+ const inserted = await this.pool.query(
1894
+ `INSERT INTO agent_egress_event (${SELECT_COLUMNS4})
1895
+ VALUES ($1, $2, $3::uuid, $4, $5, $6, $7, $8::bigint, $9::jsonb, $10, $11::integer, $12::uuid, $13)
1896
+ ON CONFLICT (tenant_id, device_id, event_id) DO NOTHING
1897
+ RETURNING ${SELECT_COLUMNS4}`,
1898
+ [
1899
+ tenant,
1900
+ input.deviceId,
1901
+ payload.eventId,
1902
+ payload.agentRef.agentId,
1903
+ payload.agentRef.profileRevision,
1904
+ payload.sessionRef,
1905
+ payload.policyRevision,
1906
+ payload.cursor,
1907
+ JSON.stringify(payload.payload),
1908
+ payload.contentHash,
1909
+ payload.byteCount,
1910
+ input.receiptId,
1911
+ this.clock.now().toISOString()
1912
+ ]
1913
+ );
1914
+ const row = inserted.rows[0];
1915
+ if (row !== void 0) return { record: toRecord2(row), created: true };
1916
+ const existing = await this.get(tenant, input.deviceId, payload.eventId);
1917
+ if (existing === void 0) throw new Error(`Agent egress ${payload.eventId} vanished during first-write record.`);
1918
+ return { record: existing, created: false };
1919
+ }
1920
+ async get(tenant, deviceId, eventId) {
1921
+ const result = await this.pool.query(
1922
+ `SELECT ${SELECT_COLUMNS4}
1923
+ FROM agent_egress_event
1924
+ WHERE tenant_id = $1 AND device_id = $2 AND event_id = $3::uuid`,
1925
+ [tenant, deviceId, eventId]
1926
+ );
1927
+ const row = result.rows[0];
1928
+ return row === void 0 ? void 0 : toRecord2(row);
1929
+ }
1930
+ };
1384
1931
 
1385
1932
  // src/stores/device-assertion-replay.ts
1386
1933
  var PostgresDeviceAssertionReplayAuthority = class {
@@ -1437,12 +1984,14 @@ function createPostgresCloudStores(options) {
1437
1984
  return {
1438
1985
  activity: new PostgresActivityStore(pool, clock),
1439
1986
  approvals: new PostgresApprovalTimelineStore(pool, clock),
1440
- devices: new PostgresDeviceDirectory(pool),
1987
+ devices: new PostgresDeviceDirectory(pool, clock),
1441
1988
  pairingCodes: new PostgresPairingCodeStore(pool, clock),
1442
1989
  nonces: new PostgresNonceStore(pool, clock, crypto),
1443
1990
  dedup: new PostgresInboundDedupStore(pool),
1444
1991
  tasks: new PostgresTaskAttemptStore(pool, clock),
1992
+ cancellations: new PostgresTaskCancellationStore(pool, clock),
1445
1993
  receipts: new PostgresRequestReceiptStore(pool, clock),
1994
+ egress: new PostgresAgentEgressStore(pool, clock),
1446
1995
  proofReceipts: new PostgresProofRequestReceiptStore(pool, clock),
1447
1996
  // A second `PostgresObjectStore` instance, not a shared one: it is a
1448
1997
  // stateless wrapper over the pool, so the two read and write the same rows
@@ -1455,7 +2004,7 @@ function createPostgresCloudStores(options) {
1455
2004
  rateLimiter: new AllowAllRateLimiter()
1456
2005
  };
1457
2006
  }
1458
- var PRESENCE_COLUMNS = "tenant_id, device_id, level, detail, configured_toolsets, observed_at, expires_at";
2007
+ var PRESENCE_COLUMNS = "tenant_id, device_id, level, detail, configured_toolsets, client_version, protocol_versions, runtimes, observed_at, expires_at";
1459
2008
  function toHint(row) {
1460
2009
  return {
1461
2010
  tenantId: row.tenant_id,
@@ -1463,6 +2012,19 @@ function toHint(row) {
1463
2012
  level: row.level,
1464
2013
  ...row.detail === null ? {} : { detail: row.detail },
1465
2014
  ...row.configured_toolsets === null ? {} : { configuredToolsets: Object.freeze([...row.configured_toolsets]) },
2015
+ ...row.client_version === null ? {} : { clientVersion: row.client_version },
2016
+ ...row.protocol_versions === null ? {} : { protocolVersions: Object.freeze([...row.protocol_versions]) },
2017
+ ...row.runtimes === null ? {} : {
2018
+ runtimes: Object.freeze(
2019
+ row.runtimes.map(
2020
+ (runtime) => Object.freeze({
2021
+ id: runtime.id,
2022
+ ...runtime.version === void 0 ? {} : { version: runtime.version },
2023
+ ...runtime.authPresent === void 0 ? {} : { authPresent: runtime.authPresent }
2024
+ })
2025
+ )
2026
+ )
2027
+ },
1466
2028
  observedAt: row.observed_at,
1467
2029
  expiresAt: row.expires_at
1468
2030
  };
@@ -1498,15 +2060,18 @@ var PostgresPresenceStore = class {
1498
2060
  const allowedBefore = new Date(now.getTime() - input.minimumIntervalMs).toISOString();
1499
2061
  const result = await this.#pool.query(
1500
2062
  `INSERT INTO device_presence (${PRESENCE_COLUMNS})
1501
- VALUES ($1, $2, $3, $4, $5::jsonb, $6, $7)
2063
+ VALUES ($1, $2, $3, $4, $5::jsonb, $6, $7::jsonb, $8::jsonb, $9, $10)
1502
2064
  ON CONFLICT (tenant_id, device_id) DO UPDATE
1503
2065
  SET level = EXCLUDED.level,
1504
2066
  detail = EXCLUDED.detail,
1505
2067
  configured_toolsets = EXCLUDED.configured_toolsets,
2068
+ client_version = EXCLUDED.client_version,
2069
+ protocol_versions = EXCLUDED.protocol_versions,
2070
+ runtimes = EXCLUDED.runtimes,
1506
2071
  observed_at = EXCLUDED.observed_at,
1507
2072
  expires_at = EXCLUDED.expires_at
1508
2073
  WHERE device_presence.expires_at <= EXCLUDED.observed_at
1509
- OR device_presence.observed_at <= $8
2074
+ OR device_presence.observed_at <= $11
1510
2075
  RETURNING ${PRESENCE_COLUMNS}`,
1511
2076
  [
1512
2077
  tenant,
@@ -1514,6 +2079,9 @@ var PostgresPresenceStore = class {
1514
2079
  input.level,
1515
2080
  input.detail ?? null,
1516
2081
  input.configuredToolsets === void 0 ? null : JSON.stringify(input.configuredToolsets),
2082
+ input.clientVersion ?? null,
2083
+ input.protocolVersions === void 0 ? null : JSON.stringify(input.protocolVersions),
2084
+ input.runtimes === void 0 ? null : JSON.stringify(input.runtimes),
1517
2085
  observedAt,
1518
2086
  new Date(now.getTime() + input.ttlMs).toISOString(),
1519
2087
  allowedBefore
@@ -1781,233 +2349,6 @@ var PostgresBoardStore = class {
1781
2349
  return this.#clock.now().toISOString();
1782
2350
  }
1783
2351
  };
1784
-
1785
- // src/stores/core/mailbox-sequence.ts
1786
- async function allocateMailboxSequence(client, tenant, deviceId, now) {
1787
- const allocation = await client.query(
1788
- `INSERT INTO device_stream (tenant_id, device_id, next_seq, acked_seq, acked_at)
1789
- VALUES ($1, $2, 2, 0, $3)
1790
- ON CONFLICT (tenant_id, device_id) DO UPDATE
1791
- SET next_seq = device_stream.next_seq + 1
1792
- RETURNING next_seq - 1 AS seq`,
1793
- [tenant, deviceId, now]
1794
- );
1795
- return Number(allocation.rows[0].seq);
1796
- }
1797
-
1798
- // src/stores/core/mailbox.ts
1799
- var DEFAULT_READ_LIMIT = 50;
1800
- var OUTBOX_COLUMNS = "tenant_id, device_id, seq, message_id, body, body_hash, byte_size, state, appended_at";
1801
- function toMessage(row) {
1802
- return {
1803
- tenantId: row.tenant_id,
1804
- deviceId: row.device_id,
1805
- // `seq` is bigint in the column and `number` on the port, because it is the
1806
- // envelope `seq` on the wire. The column is wide so the counter cannot wrap
1807
- // into a redelivery bug; the narrowing happens once, here.
1808
- seq: Number(row.seq),
1809
- messageId: row.message_id,
1810
- body: row.body,
1811
- bodyHash: row.body_hash,
1812
- byteSize: row.byte_size,
1813
- state: row.state,
1814
- appendedAt: row.appended_at
1815
- };
1816
- }
1817
- var PostgresMailboxStore = class {
1818
- #pool;
1819
- #clock;
1820
- constructor(pool, clock) {
1821
- this.#pool = pool;
1822
- this.#clock = clock;
1823
- }
1824
- async append(tenant, input) {
1825
- this.#requireDeviceId(input.deviceId);
1826
- const client = await this.#pool.connect();
1827
- try {
1828
- await client.query("BEGIN");
1829
- const existing = await client.query(
1830
- `SELECT ${OUTBOX_COLUMNS} FROM outbox
1831
- WHERE tenant_id = $1 AND device_id = $2 AND message_id = $3`,
1832
- [tenant, input.deviceId, input.messageId]
1833
- );
1834
- const replayed = existing.rows[0];
1835
- if (replayed !== void 0) {
1836
- await client.query("COMMIT");
1837
- return toMessage(replayed);
1838
- }
1839
- const seq = await allocateMailboxSequence(client, tenant, input.deviceId, this.#now());
1840
- const serializedExisting = await client.query(
1841
- `SELECT ${OUTBOX_COLUMNS} FROM outbox
1842
- WHERE tenant_id = $1 AND device_id = $2 AND message_id = $3`,
1843
- [tenant, input.deviceId, input.messageId]
1844
- );
1845
- const winnerAfterLock = serializedExisting.rows[0];
1846
- if (winnerAfterLock !== void 0) {
1847
- await client.query("ROLLBACK");
1848
- return toMessage(winnerAfterLock);
1849
- }
1850
- const materialized = await input.materialize(seq);
1851
- const now = this.#now();
1852
- const inserted = await client.query(
1853
- `INSERT INTO outbox (${OUTBOX_COLUMNS})
1854
- VALUES ($1, $2, $3::bigint, $4, $5, $6, $7::bigint, 'pending', $8)
1855
- ON CONFLICT (tenant_id, device_id, message_id) DO NOTHING
1856
- RETURNING ${OUTBOX_COLUMNS}`,
1857
- [
1858
- tenant,
1859
- input.deviceId,
1860
- seq,
1861
- input.messageId,
1862
- materialized.body,
1863
- materialized.bodyHash,
1864
- materialized.byteSize,
1865
- now
1866
- ]
1867
- );
1868
- const row = inserted.rows[0];
1869
- if (row !== void 0) {
1870
- await client.query("COMMIT");
1871
- return toMessage(row);
1872
- }
1873
- const winner = await client.query(
1874
- `SELECT ${OUTBOX_COLUMNS} FROM outbox
1875
- WHERE tenant_id = $1 AND device_id = $2 AND message_id = $3`,
1876
- [tenant, input.deviceId, input.messageId]
1877
- );
1878
- await client.query("ROLLBACK");
1879
- const won = winner.rows[0];
1880
- if (won === void 0) {
1881
- throw new ByokCoreError(
1882
- "mailbox_message_not_found",
1883
- `Message ${input.messageId} vanished during an idempotent append.`
1884
- );
1885
- }
1886
- return toMessage(won);
1887
- } catch (cause) {
1888
- await client.query("ROLLBACK").catch(() => {
1889
- });
1890
- throw cause;
1891
- } finally {
1892
- client.release();
1893
- }
1894
- }
1895
- async readAfter(tenant, query) {
1896
- const limit = query.limit ?? DEFAULT_READ_LIMIT;
1897
- const result = await this.#pool.query(
1898
- `SELECT ${OUTBOX_COLUMNS} FROM outbox
1899
- WHERE tenant_id = $1 AND device_id = $2 AND state = 'pending' AND seq > $3::bigint
1900
- ORDER BY seq
1901
- LIMIT $4`,
1902
- [tenant, query.deviceId, query.afterSeq, limit + 1]
1903
- );
1904
- const page = result.rows.slice(0, limit).map(toMessage);
1905
- return {
1906
- messages: page,
1907
- // Nothing above was mutated, so an identical call replays the same page.
1908
- // The returned position is a READ cursor and moves no ack.
1909
- nextSeq: page.at(-1)?.seq ?? query.afterSeq,
1910
- hasMore: result.rows.length > page.length
1911
- };
1912
- }
1913
- async advanceCursor(tenant, input) {
1914
- this.#requireDeviceId(input.deviceId);
1915
- const now = this.#now();
1916
- const moved = await this.#pool.query(
1917
- `WITH moved AS (
1918
- INSERT INTO device_stream (tenant_id, device_id, next_seq, acked_seq, acked_at)
1919
- VALUES ($1, $2, 1, $3::bigint, $4)
1920
- ON CONFLICT (tenant_id, device_id) DO UPDATE
1921
- SET acked_seq = EXCLUDED.acked_seq, acked_at = EXCLUDED.acked_at
1922
- WHERE device_stream.acked_seq <= EXCLUDED.acked_seq
1923
- RETURNING acked_seq, acked_at
1924
- ), marked AS (
1925
- UPDATE outbox
1926
- SET state = 'acked'
1927
- WHERE tenant_id = $1 AND device_id = $2 AND state = 'pending'
1928
- AND seq <= (SELECT acked_seq FROM moved)
1929
- RETURNING 1
1930
- )
1931
- SELECT acked_seq, acked_at FROM moved`,
1932
- [tenant, input.deviceId, input.ackedSeq, now]
1933
- );
1934
- const row = moved.rows[0];
1935
- if (row !== void 0) {
1936
- return {
1937
- tenantId: tenant,
1938
- deviceId: input.deviceId,
1939
- ackedSeq: Number(row.acked_seq),
1940
- updatedAt: row.acked_at ?? now
1941
- };
1942
- }
1943
- const current = await this.readCursor(tenant, input.deviceId);
1944
- throw new CoreConflictError(
1945
- "mailbox_cursor_regression",
1946
- `Cursor for device ${input.deviceId} is at ${current.ackedSeq}; refusing to move it back to ${input.ackedSeq}.`,
1947
- current,
1948
- this.#now()
1949
- );
1950
- }
1951
- async readCursor(tenant, deviceId) {
1952
- const result = await this.#pool.query(
1953
- `SELECT acked_seq, acked_at FROM device_stream
1954
- WHERE tenant_id = $1 AND device_id = $2`,
1955
- [tenant, deviceId]
1956
- );
1957
- const row = result.rows[0];
1958
- return {
1959
- tenantId: tenant,
1960
- deviceId,
1961
- ackedSeq: row === void 0 ? 0 : Number(row.acked_seq),
1962
- updatedAt: row?.acked_at ?? this.#now()
1963
- };
1964
- }
1965
- async collectRetired(tenant, input) {
1966
- assertCanonicalTimestamp(input.ackedBefore, "ackedBefore");
1967
- assertCanonicalTimestamp(input.expireUnackedBefore, "expireUnackedBefore");
1968
- const swept = await this.#pool.query(
1969
- `WITH deleted AS (
1970
- DELETE FROM outbox
1971
- WHERE tenant_id = $1
1972
- AND ($2::text IS NULL OR device_id = $2::text)
1973
- AND state = 'acked'
1974
- AND appended_at < $3
1975
- RETURNING byte_size
1976
- ), expired AS (
1977
- UPDATE outbox
1978
- SET state = 'expired'
1979
- WHERE tenant_id = $1
1980
- AND ($2::text IS NULL OR device_id = $2::text)
1981
- AND state = 'pending'
1982
- AND appended_at < $4
1983
- RETURNING 1
1984
- )
1985
- SELECT (SELECT count(*) FROM deleted) AS deleted_count,
1986
- (SELECT count(*) FROM expired) AS expired_count,
1987
- (SELECT COALESCE(SUM(byte_size), 0) FROM deleted)::bigint AS released_bytes`,
1988
- [tenant, input.deviceId ?? null, input.ackedBefore, input.expireUnackedBefore]
1989
- );
1990
- const row = swept.rows[0];
1991
- return {
1992
- deletedCount: Number(row.deleted_count),
1993
- expiredCount: Number(row.expired_count),
1994
- releasedBytes: row.released_bytes
1995
- };
1996
- }
1997
- /**
1998
- * The in-memory reference refuses an empty device id rather than opening a
1999
- * mailbox nothing can address. Kept here so the two compositions answer the
2000
- * same way; the table itself would happily store the row.
2001
- */
2002
- #requireDeviceId(deviceId) {
2003
- if (deviceId.length === 0) {
2004
- throw new ByokCoreError("mailbox_message_not_found", "Device id must not be empty.");
2005
- }
2006
- }
2007
- #now() {
2008
- return this.#clock.now().toISOString();
2009
- }
2010
- };
2011
2352
  var WARNING_NUMERATOR = 80n;
2012
2353
  var WARNING_DENOMINATOR = 100n;
2013
2354
  var ENTITLEMENT_COLUMNS = "tenant_id, version, hard_limit_bytes, max_object_bytes, max_inline_bytes, mailbox_limit_bytes, retention_policy_id, downgrade_grace_until";
@@ -2727,7 +3068,7 @@ var RECORD_COLUMNS = "tenant_id, kind, subject_id, rev, content_hash, byte_size,
2727
3068
  function toBody(row) {
2728
3069
  return row.body_kind === "inline" ? { kind: "inline", body: row.body_inline ?? "" } : { kind: "object", hash: row.body_object_hash ?? "" };
2729
3070
  }
2730
- function toRecord2(row) {
3071
+ function toRecord3(row) {
2731
3072
  return {
2732
3073
  tenantId: row.tenant_id,
2733
3074
  kind: row.kind,
@@ -2772,7 +3113,7 @@ var PostgresTruthStore = class {
2772
3113
  ]
2773
3114
  );
2774
3115
  const row = inserted.rows[0];
2775
- if (row !== void 0) return toRecord2(row);
3116
+ if (row !== void 0) return toRecord3(row);
2776
3117
  const existing = await this.getRecord(tenant, {
2777
3118
  kind: "task.terminal",
2778
3119
  recordKey: input.taskId
@@ -2838,7 +3179,7 @@ var PostgresTruthStore = class {
2838
3179
  ]
2839
3180
  );
2840
3181
  const row = written.rows[0];
2841
- if (row !== void 0) return toRecord2(row);
3182
+ if (row !== void 0) return toRecord3(row);
2842
3183
  const current = await this.getRecord(tenant, {
2843
3184
  kind: input.kind,
2844
3185
  recordKey: input.recordKey
@@ -2857,7 +3198,7 @@ var PostgresTruthStore = class {
2857
3198
  [tenant, selector.kind, selector.recordKey]
2858
3199
  );
2859
3200
  const row = result.rows[0];
2860
- return row === void 0 ? void 0 : toRecord2(row);
3201
+ return row === void 0 ? void 0 : toRecord3(row);
2861
3202
  }
2862
3203
  async listManifest(tenant, query) {
2863
3204
  const result = await this.#pool.query(
@@ -2905,7 +3246,7 @@ var RECEIPT_COLUMNS = "tenant_id, device_id, request_id, operation, resource, bo
2905
3246
  function toBody2(row) {
2906
3247
  return row.body_kind === "inline" ? { kind: "inline", body: row.body_inline ?? "" } : { kind: "object", hash: row.body_object_hash ?? "" };
2907
3248
  }
2908
- function toRecord3(tenant, row) {
3249
+ function toRecord4(tenant, row) {
2909
3250
  return {
2910
3251
  tenantId: tenant,
2911
3252
  kind: row.kind,
@@ -3082,7 +3423,7 @@ var PostgresTruthCommitter = class {
3082
3423
  );
3083
3424
  current.set(
3084
3425
  writeKey(write),
3085
- result.rows[0] === void 0 ? void 0 : toRecord3(tenant, result.rows[0])
3426
+ result.rows[0] === void 0 ? void 0 : toRecord4(tenant, result.rows[0])
3086
3427
  );
3087
3428
  }
3088
3429
  return current;
@@ -3295,7 +3636,7 @@ var PostgresTruthCommitter = class {
3295
3636
  applied.push({
3296
3637
  input: write,
3297
3638
  before,
3298
- record: toRecord3(tenant, result.rows[0]),
3639
+ record: toRecord4(tenant, result.rows[0]),
3299
3640
  mutated: true
3300
3641
  });
3301
3642
  }
@@ -3353,6 +3694,6 @@ var PostgresTruthCommitter = class {
3353
3694
  }
3354
3695
  };
3355
3696
 
3356
- export { DEFAULT_MAX_ATTEMPTS, DEFAULT_PRESIGN_TTL_SECONDS, DEFAULT_RETRY_DELAY_MS, MAX_PRESIGN_TTL_SECONDS, MIN_PRESIGN_TTL_SECONDS, ObjectStoreRequestError, PostgresActivityStore, PostgresApprovalTimelineStore, PostgresBoardStore, PostgresDeviceAssertionReplayAuthority, PostgresDeviceDirectory, PostgresInboundDedupStore, PostgresMailboxStore, PostgresNonceStore, PostgresObjectStore, PostgresPairingCodeStore, PostgresPresenceStore, PostgresProofRequestReceiptStore, PostgresQuotaStore, PostgresRequestReceiptStore, PostgresSkillPackStore, PostgresTaskAttemptStore, PostgresTruthCommitter, PostgresTruthStore, R2BlobStoreError, R2CloudBlobStore, R2ObjectMaintenanceStore, R2_BLOB_ERROR_CODES, createByokPool, createPostgresCloudStores, createPostgresCoreStores };
3697
+ export { DEFAULT_MAX_ATTEMPTS, DEFAULT_PRESIGN_TTL_SECONDS, DEFAULT_RETRY_DELAY_MS, MAX_PRESIGN_TTL_SECONDS, MIN_PRESIGN_TTL_SECONDS, ObjectStoreRequestError, PostgresActivityStore, PostgresApprovalTimelineStore, PostgresBoardStore, PostgresDeviceAssertionReplayAuthority, PostgresDeviceDirectory, PostgresInboundDedupStore, PostgresMailboxStore, PostgresNonceStore, PostgresObjectStore, PostgresPairingCodeStore, PostgresPresenceStore, PostgresProofRequestReceiptStore, PostgresQuotaStore, PostgresRequestReceiptStore, PostgresSkillPackStore, PostgresTaskAttemptStore, PostgresTaskCancellationStore, PostgresTruthCommitter, PostgresTruthStore, R2BlobStoreError, R2CloudBlobStore, R2ObjectMaintenanceStore, R2_BLOB_ERROR_CODES, createByokPool, createPostgresCloudStores, createPostgresCoreStores };
3357
3698
  //# sourceMappingURL=runtime.js.map
3358
3699
  //# sourceMappingURL=runtime.js.map