@byok-sdk/cloud-dataplane 0.12.0 → 0.14.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';
@@ -1034,23 +1034,31 @@ var PostgresInboundDedupStore = class {
1034
1034
  this.#capacity = capacity;
1035
1035
  }
1036
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) {
1037
1043
  const inserted = await this.#pool.query(
1038
- `INSERT INTO inbound_dedup (tenant_id, device_id, envelope_id)
1039
- VALUES ($1, $2, $3)
1040
- 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
1041
1047
  RETURNING 1`,
1042
- [tenant, deviceId, envelopeId]
1048
+ [tenant, deviceId, agentId, agentProfileRevision, envelopeId]
1043
1049
  );
1044
1050
  if (inserted.rowCount === 0) return true;
1045
1051
  await this.#pool.query(
1046
1052
  `DELETE FROM inbound_dedup
1047
1053
  WHERE tenant_id = $1 AND device_id = $2
1054
+ AND agent_id = $3 AND agent_profile_revision = $4
1048
1055
  AND recorded_seq <= (
1049
1056
  SELECT recorded_seq FROM inbound_dedup
1050
1057
  WHERE tenant_id = $1 AND device_id = $2
1058
+ AND agent_id = $3 AND agent_profile_revision = $4
1051
1059
  ORDER BY recorded_seq DESC
1052
- OFFSET $3 LIMIT 1)`,
1053
- [tenant, deviceId, this.#capacity]
1060
+ OFFSET $5 LIMIT 1)`,
1061
+ [tenant, deviceId, agentId, agentProfileRevision, this.#capacity]
1054
1062
  );
1055
1063
  return false;
1056
1064
  }
@@ -1344,7 +1352,7 @@ var PostgresProofRequestReceiptStore = class {
1344
1352
  return row === void 0 ? void 0 : toReceipt2(row);
1345
1353
  }
1346
1354
  };
1347
- 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";
1348
1356
  var AGENT_MESSAGE_ADMISSION_SELECT_COLUMNS = "message_id, payload_body, terminal_body";
1349
1357
  function admissionRowToRecord(row) {
1350
1358
  return {
@@ -1370,6 +1378,10 @@ function taskRowToAttempt(row) {
1370
1378
  // former.
1371
1379
  ...row.owner_device_id === null ? {} : { ownerDeviceId: row.owner_device_id },
1372
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 },
1373
1385
  ...row.terminal_cause == null ? {} : { terminalCause: row.terminal_cause },
1374
1386
  ...row.cancel_requested_at === null ? {} : {
1375
1387
  cancellation: {
@@ -1438,6 +1450,16 @@ var PostgresTaskAttemptStore = class {
1438
1450
  [tenant, input.taskId]
1439
1451
  );
1440
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
+ }
1441
1463
  if (attempt === void 0 || attempt.device_id !== input.deviceId || attempt.cancel_requested_at !== null || !["offered", "claimed", "running"].includes(attempt.status)) {
1442
1464
  await client.query("COMMIT");
1443
1465
  return "rejected";
@@ -1454,14 +1476,14 @@ var PostgresTaskAttemptStore = class {
1454
1476
  await client.query("COMMIT");
1455
1477
  return "reserved";
1456
1478
  }
1457
- const existing = await client.query(
1479
+ const raced = await client.query(
1458
1480
  `SELECT message_id, payload_body FROM agent_message_admission
1459
1481
  WHERE tenant_id = $1 AND device_id = $2 AND task_id = $3`,
1460
1482
  [tenant, input.deviceId, input.taskId]
1461
1483
  );
1462
- const admission = existing.rows[0];
1484
+ const racedAdmission = raced.rows[0];
1463
1485
  await client.query("COMMIT");
1464
- return admission?.message_id === input.messageId && admission.payload_body === input.payloadBody ? "pending" : "rejected";
1486
+ return racedAdmission?.message_id === input.messageId && racedAdmission.payload_body === input.payloadBody ? "pending" : "rejected";
1465
1487
  } catch (cause) {
1466
1488
  await client.query("ROLLBACK").catch(() => {
1467
1489
  });
@@ -1508,14 +1530,44 @@ var PostgresTaskAttemptStore = class {
1508
1530
  );
1509
1531
  return result.rows.map(taskRowToAttempt);
1510
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
+ }
1511
1552
  async claim(tenant, input) {
1512
1553
  const claimed = await this.#pool.query(
1513
1554
  `UPDATE task
1514
- SET owner_device_id = $3, status = 'claimed', updated_at = $4
1515
- WHERE tenant_id = $1 AND task_id = $2 AND owner_device_id IS NULL
1555
+ SET owner_device_id = $3,
1556
+ status = 'claimed',
1557
+ claimed_runtime = $5,
1558
+ claimed_runtime_capabilities = $6::jsonb,
1559
+ updated_at = $4
1560
+ WHERE tenant_id = $1 AND task_id = $2 AND device_id = $3 AND owner_device_id IS NULL
1516
1561
  AND cancel_requested_at IS NULL AND status = 'offered'
1517
1562
  RETURNING ${TASK_SELECT_COLUMNS}`,
1518
- [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
+ ]
1519
1571
  );
1520
1572
  const won = claimed.rows[0];
1521
1573
  if (won !== void 0) return taskRowToAttempt(won);
@@ -1680,9 +1732,36 @@ var PostgresMailboxStore = class {
1680
1732
  // Nothing above was mutated, so an identical call replays the same page.
1681
1733
  // The returned position is a READ cursor and moves no ack.
1682
1734
  nextSeq: page.at(-1)?.seq ?? query.afterSeq,
1683
- hasMore: result.rows.length > page.length
1735
+ hasMore: result.rows.length > page.length,
1736
+ recoverableFrom: await this.#recoverableFrom(tenant, query.deviceId)
1684
1737
  };
1685
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
+ }
1686
1765
  async advanceCursor(tenant, input) {
1687
1766
  this.#requireDeviceId(input.deviceId);
1688
1767
  const now = this.#now();
@@ -1943,8 +2022,35 @@ var PostgresActivityStore = class {
1943
2022
  const receivedAt = now.toISOString();
1944
2023
  const incoming = projectTimelineEvents(input, receivedAt);
1945
2024
  const expiresAt = new Date(now.getTime() + input.ttlMs).toISOString();
1946
- const result = await this.pool.query(
1947
- `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 (
1948
2054
  SELECT entry
1949
2055
  FROM jsonb_array_elements($4::jsonb) AS element(entry)
1950
2056
  ), trimmed AS (
@@ -2011,16 +2117,24 @@ var PostgresActivityStore = class {
2011
2117
  )
2012
2118
  )
2013
2119
  RETURNING tenant_id, task_id, entries, dropped, capacity, expires_at`,
2014
- [tenant, input.taskId, receivedAt, JSON.stringify(incoming), input.dropped, capacity, expiresAt]
2015
- );
2016
- const row = result.rows[0];
2017
- if (row === void 0) {
2018
- throw new ByokCloudError(
2019
- "coordination_input_invalid",
2020
- `Activity batch ${input.batchSeq} conflicts with the existing typed tail authority.`
2120
+ [tenant, input.taskId, receivedAt, JSON.stringify(incoming), input.dropped, capacity, expiresAt]
2021
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();
2022
2137
  }
2023
- return toTail(row);
2024
2138
  }
2025
2139
  async read(tenant, taskId) {
2026
2140
  const result = await this.pool.query(
@@ -2139,6 +2253,79 @@ var PostgresApprovalTimelineStore = class {
2139
2253
  client.release();
2140
2254
  }
2141
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
+ }
2142
2329
  async read(tenant, taskId) {
2143
2330
  const result = await this.pool.query(
2144
2331
  `SELECT tenant_id, task_id, entries, next_revision, dropped, capacity, expires_at
@@ -2150,6 +2337,19 @@ var PostgresApprovalTimelineStore = class {
2150
2337
  return row === void 0 ? void 0 : toTail2(row);
2151
2338
  }
2152
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
+ }
2153
2353
  var SELECT_COLUMNS4 = [
2154
2354
  "tenant_id",
2155
2355
  "device_id",
@@ -2196,7 +2396,7 @@ var PostgresAgentEgressStore = class {
2196
2396
  const inserted = await this.pool.query(
2197
2397
  `INSERT INTO agent_egress_event (${SELECT_COLUMNS4})
2198
2398
  VALUES ($1, $2, $3::uuid, $4, $5, $6, $7, $8::bigint, $9::jsonb, $10, $11::integer, $12::uuid, $13)
2199
- 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
2200
2400
  RETURNING ${SELECT_COLUMNS4}`,
2201
2401
  [
2202
2402
  tenant,
@@ -2216,16 +2416,18 @@ var PostgresAgentEgressStore = class {
2216
2416
  );
2217
2417
  const row = inserted.rows[0];
2218
2418
  if (row !== void 0) return { record: toRecord2(row), created: true };
2219
- const existing = await this.get(tenant, input.deviceId, payload.eventId);
2419
+ const existing = await this.get(tenant, input.deviceId, payload.agentRef, payload.eventId);
2220
2420
  if (existing === void 0) throw new Error(`Agent egress ${payload.eventId} vanished during first-write record.`);
2221
2421
  return { record: existing, created: false };
2222
2422
  }
2223
- async get(tenant, deviceId, eventId) {
2423
+ async get(tenant, deviceId, agentRef, eventId) {
2224
2424
  const result = await this.pool.query(
2225
2425
  `SELECT ${SELECT_COLUMNS4}
2226
2426
  FROM agent_egress_event
2227
- WHERE tenant_id = $1 AND device_id = $2 AND event_id = $3::uuid`,
2228
- [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]
2229
2431
  );
2230
2432
  const row = result.rows[0];
2231
2433
  return row === void 0 ? void 0 : toRecord2(row);