@byok-sdk/cloud-dataplane 0.12.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/index.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, tenantId, SKILL_PACK_MANIFEST_SCHEMA_ID } from '@byok-sdk/core';
4
4
  import { AwsClient } from 'aws4fetch';
5
5
  import { XMLParser } from 'fast-xml-parser';
@@ -1038,23 +1038,31 @@ var PostgresInboundDedupStore = class {
1038
1038
  this.#capacity = capacity;
1039
1039
  }
1040
1040
  async checkAndRecord(tenant, deviceId, envelopeId) {
1041
+ return this.#checkAndRecord(tenant, deviceId, "", "", envelopeId);
1042
+ }
1043
+ async checkAndRecordAgent(tenant, deviceId, agentRef, envelopeId) {
1044
+ return this.#checkAndRecord(tenant, deviceId, agentRef.agentId, agentRef.profileRevision, envelopeId);
1045
+ }
1046
+ async #checkAndRecord(tenant, deviceId, agentId, agentProfileRevision, envelopeId) {
1041
1047
  const inserted = await this.#pool.query(
1042
- `INSERT INTO inbound_dedup (tenant_id, device_id, envelope_id)
1043
- VALUES ($1, $2, $3)
1044
- ON CONFLICT (tenant_id, device_id, envelope_id) DO NOTHING
1048
+ `INSERT INTO inbound_dedup (tenant_id, device_id, agent_id, agent_profile_revision, envelope_id)
1049
+ VALUES ($1, $2, $3, $4, $5)
1050
+ ON CONFLICT (tenant_id, device_id, agent_id, agent_profile_revision, envelope_id) DO NOTHING
1045
1051
  RETURNING 1`,
1046
- [tenant, deviceId, envelopeId]
1052
+ [tenant, deviceId, agentId, agentProfileRevision, envelopeId]
1047
1053
  );
1048
1054
  if (inserted.rowCount === 0) return true;
1049
1055
  await this.#pool.query(
1050
1056
  `DELETE FROM inbound_dedup
1051
1057
  WHERE tenant_id = $1 AND device_id = $2
1058
+ AND agent_id = $3 AND agent_profile_revision = $4
1052
1059
  AND recorded_seq <= (
1053
1060
  SELECT recorded_seq FROM inbound_dedup
1054
1061
  WHERE tenant_id = $1 AND device_id = $2
1062
+ AND agent_id = $3 AND agent_profile_revision = $4
1055
1063
  ORDER BY recorded_seq DESC
1056
- OFFSET $3 LIMIT 1)`,
1057
- [tenant, deviceId, this.#capacity]
1064
+ OFFSET $5 LIMIT 1)`,
1065
+ [tenant, deviceId, agentId, agentProfileRevision, this.#capacity]
1058
1066
  );
1059
1067
  return false;
1060
1068
  }
@@ -1348,7 +1356,7 @@ var PostgresProofRequestReceiptStore = class {
1348
1356
  return row === void 0 ? void 0 : toReceipt2(row);
1349
1357
  }
1350
1358
  };
1351
- 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";
1359
+ 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";
1352
1360
  var AGENT_MESSAGE_ADMISSION_SELECT_COLUMNS = "message_id, payload_body, terminal_body";
1353
1361
  function admissionRowToRecord(row) {
1354
1362
  return {
@@ -1374,6 +1382,10 @@ function taskRowToAttempt(row) {
1374
1382
  // former.
1375
1383
  ...row.owner_device_id === null ? {} : { ownerDeviceId: row.owner_device_id },
1376
1384
  status: row.status,
1385
+ // `claimed_runtime` is written only by the guarded claim UPDATE below, so a
1386
+ // non-null value here is always the winning claim's own self-report.
1387
+ ...row.claimed_runtime === null ? {} : { claimedRuntime: row.claimed_runtime },
1388
+ ...row.claimed_runtime_capabilities === null ? {} : { claimedRuntimeCapabilities: row.claimed_runtime_capabilities },
1377
1389
  ...row.terminal_cause == null ? {} : { terminalCause: row.terminal_cause },
1378
1390
  ...row.cancel_requested_at === null ? {} : {
1379
1391
  cancellation: {
@@ -1442,6 +1454,16 @@ var PostgresTaskAttemptStore = class {
1442
1454
  [tenant, input.taskId]
1443
1455
  );
1444
1456
  const attempt = selected.rows[0];
1457
+ const existing = await client.query(
1458
+ `SELECT message_id, payload_body FROM agent_message_admission
1459
+ WHERE tenant_id = $1 AND device_id = $2 AND task_id = $3`,
1460
+ [tenant, input.deviceId, input.taskId]
1461
+ );
1462
+ const admission = existing.rows[0];
1463
+ if (admission !== void 0) {
1464
+ await client.query("COMMIT");
1465
+ return admission.message_id === input.messageId && admission.payload_body === input.payloadBody ? "pending" : "rejected";
1466
+ }
1445
1467
  if (attempt === void 0 || attempt.device_id !== input.deviceId || attempt.cancel_requested_at !== null || !["offered", "claimed", "running"].includes(attempt.status)) {
1446
1468
  await client.query("COMMIT");
1447
1469
  return "rejected";
@@ -1458,14 +1480,14 @@ var PostgresTaskAttemptStore = class {
1458
1480
  await client.query("COMMIT");
1459
1481
  return "reserved";
1460
1482
  }
1461
- const existing = await client.query(
1483
+ const raced = await client.query(
1462
1484
  `SELECT message_id, payload_body FROM agent_message_admission
1463
1485
  WHERE tenant_id = $1 AND device_id = $2 AND task_id = $3`,
1464
1486
  [tenant, input.deviceId, input.taskId]
1465
1487
  );
1466
- const admission = existing.rows[0];
1488
+ const racedAdmission = raced.rows[0];
1467
1489
  await client.query("COMMIT");
1468
- return admission?.message_id === input.messageId && admission.payload_body === input.payloadBody ? "pending" : "rejected";
1490
+ return racedAdmission?.message_id === input.messageId && racedAdmission.payload_body === input.payloadBody ? "pending" : "rejected";
1469
1491
  } catch (cause) {
1470
1492
  await client.query("ROLLBACK").catch(() => {
1471
1493
  });
@@ -1512,14 +1534,44 @@ var PostgresTaskAttemptStore = class {
1512
1534
  );
1513
1535
  return result.rows.map(taskRowToAttempt);
1514
1536
  }
1537
+ async list(tenant, query) {
1538
+ assertTaskAttemptListLimit(query.limit);
1539
+ const result = await this.#pool.query(
1540
+ `SELECT ${TASK_SELECT_COLUMNS}
1541
+ FROM task
1542
+ WHERE tenant_id = $1 AND ($2::text IS NULL OR task_id > $2)
1543
+ ORDER BY task_id
1544
+ LIMIT $3`,
1545
+ [tenant, query.cursor ?? null, query.limit + 1]
1546
+ );
1547
+ const rows = result.rows.slice(0, query.limit);
1548
+ const attempts = rows.map(taskRowToAttempt);
1549
+ const hasMore = result.rows.length > rows.length;
1550
+ const last = attempts.at(-1);
1551
+ return {
1552
+ attempts,
1553
+ ...hasMore && last !== void 0 ? { nextCursor: last.taskId } : {}
1554
+ };
1555
+ }
1515
1556
  async claim(tenant, input) {
1516
1557
  const claimed = await this.#pool.query(
1517
1558
  `UPDATE task
1518
- SET owner_device_id = $3, status = 'claimed', updated_at = $4
1559
+ SET owner_device_id = $3,
1560
+ status = 'claimed',
1561
+ claimed_runtime = $5,
1562
+ claimed_runtime_capabilities = $6::jsonb,
1563
+ updated_at = $4
1519
1564
  WHERE tenant_id = $1 AND task_id = $2 AND owner_device_id IS NULL
1520
1565
  AND cancel_requested_at IS NULL AND status = 'offered'
1521
1566
  RETURNING ${TASK_SELECT_COLUMNS}`,
1522
- [tenant, input.taskId, input.deviceId, this.#now()]
1567
+ [
1568
+ tenant,
1569
+ input.taskId,
1570
+ input.deviceId,
1571
+ this.#now(),
1572
+ input.runtime ?? null,
1573
+ input.capabilities === void 0 ? null : JSON.stringify(input.capabilities)
1574
+ ]
1523
1575
  );
1524
1576
  const won = claimed.rows[0];
1525
1577
  if (won !== void 0) return taskRowToAttempt(won);
@@ -1684,9 +1736,36 @@ var PostgresMailboxStore = class {
1684
1736
  // Nothing above was mutated, so an identical call replays the same page.
1685
1737
  // The returned position is a READ cursor and moves no ack.
1686
1738
  nextSeq: page.at(-1)?.seq ?? query.afterSeq,
1687
- hasMore: result.rows.length > page.length
1739
+ hasMore: result.rows.length > page.length,
1740
+ recoverableFrom: await this.#recoverableFrom(tenant, query.deviceId)
1688
1741
  };
1689
1742
  }
1743
+ /**
1744
+ * One past the highest row this device lost, which is exactly the highest
1745
+ * `expired` seq — `1` when it has lost nothing.
1746
+ *
1747
+ * No column and no migration, because `collectRetired` never deletes an
1748
+ * unacked row: §12.7.5 keeps the dead letter, so the loss is already durable
1749
+ * in `outbox` and a stored floor would be a second authority free to drift
1750
+ * from the sweep that moved it. Whatever eventually reaps dead letters (S4B,
1751
+ * O-009) is the change that has to carry this floor forward instead.
1752
+ *
1753
+ * `acked` rows are not counted: a consumed row is not a lost one.
1754
+ *
1755
+ * Read AFTER the page on purpose. A sweep landing between the two reads can
1756
+ * only report a floor HIGHER than the page it accompanies, which makes the
1757
+ * caller fail closed over a page that may already be missing rows; reading
1758
+ * the floor first would report one too low and serve that gap silently.
1759
+ */
1760
+ async #recoverableFrom(tenant, deviceId) {
1761
+ const result = await this.#pool.query(
1762
+ `SELECT (COALESCE(MAX(seq), 0) + 1)::bigint AS recoverable_from
1763
+ FROM outbox
1764
+ WHERE tenant_id = $1 AND device_id = $2 AND state = 'expired'`,
1765
+ [tenant, deviceId]
1766
+ );
1767
+ return Number(result.rows[0].recoverable_from);
1768
+ }
1690
1769
  async advanceCursor(tenant, input) {
1691
1770
  this.#requireDeviceId(input.deviceId);
1692
1771
  const now = this.#now();
@@ -1947,8 +2026,35 @@ var PostgresActivityStore = class {
1947
2026
  const receivedAt = now.toISOString();
1948
2027
  const incoming = projectTimelineEvents(input, receivedAt);
1949
2028
  const expiresAt = new Date(now.getTime() + input.ttlMs).toISOString();
1950
- const result = await this.pool.query(
1951
- `WITH incoming AS (
2029
+ const client = await this.pool.connect();
2030
+ try {
2031
+ await client.query("BEGIN");
2032
+ await client.query(
2033
+ `SELECT pg_advisory_xact_lock(hashtextextended($1 || E'\\x1f' || $2, 0))`,
2034
+ [tenant, input.taskId]
2035
+ );
2036
+ const locked = await client.query(
2037
+ `SELECT tenant_id, task_id, entries, dropped, capacity, expires_at
2038
+ FROM activity_tail
2039
+ WHERE tenant_id = $1 AND task_id = $2
2040
+ FOR UPDATE`,
2041
+ [tenant, input.taskId]
2042
+ );
2043
+ const stored = locked.rows[0];
2044
+ const live = stored !== void 0 && receivedAt < stored.expires_at ? toTail(stored) : void 0;
2045
+ const sourceState = activitySourceBatchState(live?.entries ?? [], input);
2046
+ if (sourceState === "same") {
2047
+ await client.query("COMMIT");
2048
+ return live;
2049
+ }
2050
+ if (sourceState === "conflict") {
2051
+ throw new ByokCloudError(
2052
+ "coordination_input_invalid",
2053
+ `Activity source envelope ${input.sourceEnvelopeId} already belongs to another canonical batch.`
2054
+ );
2055
+ }
2056
+ const result = await client.query(
2057
+ `WITH incoming AS (
1952
2058
  SELECT entry
1953
2059
  FROM jsonb_array_elements($4::jsonb) AS element(entry)
1954
2060
  ), trimmed AS (
@@ -2015,16 +2121,24 @@ var PostgresActivityStore = class {
2015
2121
  )
2016
2122
  )
2017
2123
  RETURNING tenant_id, task_id, entries, dropped, capacity, expires_at`,
2018
- [tenant, input.taskId, receivedAt, JSON.stringify(incoming), input.dropped, capacity, expiresAt]
2019
- );
2020
- const row = result.rows[0];
2021
- if (row === void 0) {
2022
- throw new ByokCloudError(
2023
- "coordination_input_invalid",
2024
- `Activity batch ${input.batchSeq} conflicts with the existing typed tail authority.`
2124
+ [tenant, input.taskId, receivedAt, JSON.stringify(incoming), input.dropped, capacity, expiresAt]
2025
2125
  );
2126
+ const row = result.rows[0];
2127
+ if (row === void 0) {
2128
+ throw new ByokCloudError(
2129
+ "coordination_input_invalid",
2130
+ `Activity batch ${input.batchSeq} conflicts with the existing typed tail authority.`
2131
+ );
2132
+ }
2133
+ await client.query("COMMIT");
2134
+ return toTail(row);
2135
+ } catch (caught) {
2136
+ await client.query("ROLLBACK").catch(() => {
2137
+ });
2138
+ throw caught;
2139
+ } finally {
2140
+ client.release();
2026
2141
  }
2027
- return toTail(row);
2028
2142
  }
2029
2143
  async read(tenant, taskId) {
2030
2144
  const result = await this.pool.query(
@@ -2143,6 +2257,79 @@ var PostgresApprovalTimelineStore = class {
2143
2257
  client.release();
2144
2258
  }
2145
2259
  }
2260
+ async resolvePending(tenant, input) {
2261
+ const { capacity, ttlMs, event } = validateApprovalTimelineResolve(input);
2262
+ const now = this.clock.now();
2263
+ const receivedAt = now.toISOString();
2264
+ const expiresAt = new Date(now.getTime() + ttlMs).toISOString();
2265
+ const client = await this.pool.connect();
2266
+ try {
2267
+ await client.query("BEGIN");
2268
+ await client.query(
2269
+ `SELECT pg_advisory_xact_lock(hashtextextended($1 || E'\\x1f' || $2, 0))`,
2270
+ [tenant, input.taskId]
2271
+ );
2272
+ const stored = await readLocked(client, tenant, input.taskId);
2273
+ const live = stored !== void 0 && receivedAt < stored.expires_at ? toTail2(stored) : void 0;
2274
+ if (live === void 0) {
2275
+ await client.query("COMMIT");
2276
+ return { status: "absent" };
2277
+ }
2278
+ const duplicate = live.entries.find((entry) => entry.sourceEnvelopeId === input.sourceEnvelopeId);
2279
+ if (duplicate !== void 0) {
2280
+ await client.query("COMMIT");
2281
+ return JSON.stringify(duplicate.event) === JSON.stringify(event) ? { status: "replayed", tail: live } : { status: "conflict", tail: live };
2282
+ }
2283
+ const pending = pendingObservation(live.entries);
2284
+ if (pending === void 0) {
2285
+ await client.query("COMMIT");
2286
+ return { status: "absent", tail: live };
2287
+ }
2288
+ if (pending.sourceEnvelopeId !== input.expectedSourceEnvelopeId || pending.revision !== input.expectedRevision) {
2289
+ await client.query("COMMIT");
2290
+ return { status: "superseded", tail: live };
2291
+ }
2292
+ const revision = Number(stored.next_revision);
2293
+ const expectedRevision = (live.cursor ?? 0) + 1;
2294
+ if (!Number.isSafeInteger(revision) || revision <= 0 || revision !== expectedRevision) {
2295
+ throw new ByokCloudError(
2296
+ "coordination_input_invalid",
2297
+ "Approval timeline revision authority is malformed."
2298
+ );
2299
+ }
2300
+ const observation = ApprovalObservationSchema.parse({
2301
+ taskId: input.taskId,
2302
+ sourceEnvelopeId: input.sourceEnvelopeId,
2303
+ revision,
2304
+ receivedAt,
2305
+ event
2306
+ });
2307
+ const allEntries = [...live.entries, observation];
2308
+ const evicted = Math.max(allEntries.length - capacity, 0);
2309
+ const entries = allEntries.slice(evicted);
2310
+ const dropped = live.dropped + evicted;
2311
+ const result = await client.query(
2312
+ `INSERT INTO approval_timeline_tail
2313
+ (tenant_id, task_id, entries, next_revision, dropped, capacity, expires_at)
2314
+ VALUES ($1, $2, $3::jsonb, $4, $5, $6, $7)
2315
+ ON CONFLICT (tenant_id, task_id) DO UPDATE
2316
+ SET entries = EXCLUDED.entries,
2317
+ next_revision = EXCLUDED.next_revision,
2318
+ dropped = EXCLUDED.dropped,
2319
+ capacity = EXCLUDED.capacity,
2320
+ expires_at = EXCLUDED.expires_at
2321
+ RETURNING tenant_id, task_id, entries, next_revision, dropped, capacity, expires_at`,
2322
+ [tenant, input.taskId, JSON.stringify(entries), revision + 1, dropped, capacity, expiresAt]
2323
+ );
2324
+ await client.query("COMMIT");
2325
+ return { status: "applied", tail: toTail2(result.rows[0]) };
2326
+ } catch (caught) {
2327
+ await client.query("ROLLBACK");
2328
+ throw caught;
2329
+ } finally {
2330
+ client.release();
2331
+ }
2332
+ }
2146
2333
  async read(tenant, taskId) {
2147
2334
  const result = await this.pool.query(
2148
2335
  `SELECT tenant_id, task_id, entries, next_revision, dropped, capacity, expires_at
@@ -2154,6 +2341,19 @@ var PostgresApprovalTimelineStore = class {
2154
2341
  return row === void 0 ? void 0 : toTail2(row);
2155
2342
  }
2156
2343
  };
2344
+ function pendingObservation(entries) {
2345
+ let pending;
2346
+ for (const entry of entries) {
2347
+ if (entry.event.type === "approval_requested") {
2348
+ pending = entry;
2349
+ continue;
2350
+ }
2351
+ if (pending !== void 0 && (pending.event.approvalId === void 0 || pending.event.approvalId === entry.event.approvalId)) {
2352
+ pending = void 0;
2353
+ }
2354
+ }
2355
+ return pending;
2356
+ }
2157
2357
  var SELECT_COLUMNS4 = [
2158
2358
  "tenant_id",
2159
2359
  "device_id",
@@ -2200,7 +2400,7 @@ var PostgresAgentEgressStore = class {
2200
2400
  const inserted = await this.pool.query(
2201
2401
  `INSERT INTO agent_egress_event (${SELECT_COLUMNS4})
2202
2402
  VALUES ($1, $2, $3::uuid, $4, $5, $6, $7, $8::bigint, $9::jsonb, $10, $11::integer, $12::uuid, $13)
2203
- ON CONFLICT (tenant_id, device_id, event_id) DO NOTHING
2403
+ ON CONFLICT (tenant_id, device_id, agent_id, agent_profile_revision, event_id) DO NOTHING
2204
2404
  RETURNING ${SELECT_COLUMNS4}`,
2205
2405
  [
2206
2406
  tenant,
@@ -2220,16 +2420,18 @@ var PostgresAgentEgressStore = class {
2220
2420
  );
2221
2421
  const row = inserted.rows[0];
2222
2422
  if (row !== void 0) return { record: toRecord2(row), created: true };
2223
- const existing = await this.get(tenant, input.deviceId, payload.eventId);
2423
+ const existing = await this.get(tenant, input.deviceId, payload.agentRef, payload.eventId);
2224
2424
  if (existing === void 0) throw new Error(`Agent egress ${payload.eventId} vanished during first-write record.`);
2225
2425
  return { record: existing, created: false };
2226
2426
  }
2227
- async get(tenant, deviceId, eventId) {
2427
+ async get(tenant, deviceId, agentRef, eventId) {
2228
2428
  const result = await this.pool.query(
2229
2429
  `SELECT ${SELECT_COLUMNS4}
2230
2430
  FROM agent_egress_event
2231
- WHERE tenant_id = $1 AND device_id = $2 AND event_id = $3::uuid`,
2232
- [tenant, deviceId, eventId]
2431
+ WHERE tenant_id = $1 AND device_id = $2
2432
+ AND agent_id = $3 AND agent_profile_revision = $4
2433
+ AND event_id = $5::uuid`,
2434
+ [tenant, deviceId, agentRef.agentId, agentRef.profileRevision, eventId]
2233
2435
  );
2234
2436
  const row = result.rows[0];
2235
2437
  return row === void 0 ? void 0 : toRecord2(row);