@byok-sdk/cloud 0.4.2 → 0.6.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,6 +1,6 @@
1
- import { BOARD_STATUSES, PRESENCE_LEVELS, CapabilityDeclarationSchema, NONCE_SIGNING_DOMAIN, hasCapability, isTenantId, tenantId, principalTenant, parseDeviceProofEnvelope, deviceProofSigningInput, parseCapabilityDeclaration, ByokCoreError, tenantKey, createInMemoryCoreStores, assertCapability, contentHash, isCoreConflictError, isCoreError, DEVICE_PROOF_HEADER, TRUTH_RECORD_KINDS, STORAGE_ERROR_CODES, STORAGE_ERROR_HTTP_STATUS } from '@byok-sdk/core';
1
+ import { BOARD_STATUSES, CapabilityDeclarationSchema, NONCE_SIGNING_DOMAIN, hasCapability, isTenantId, tenantId, principalTenant, parseDeviceProofEnvelope, deviceProofSigningInput, contentHash, parseCapabilityDeclaration, ByokCoreError, tenantKey, PRESENCE_LEVELS, createInMemoryCoreStores, authenticateDeviceAssertion, assertCapability, isCoreConflictError, isCoreError, DEVICE_PROOF_HEADER, TRUTH_RECORD_KINDS, STORAGE_ERROR_CODES, STORAGE_ERROR_HTTP_STATUS } from '@byok-sdk/core';
2
2
  export { DEVICE_PROOF_HEADER, NONCE_SIGNING_DOMAIN, isTenantId, tenantId } from '@byok-sdk/core';
3
- import { AgentEventOrUnknownSchema, ConfiguredToolsetsSchema, DAEMON_TO_SERVER_TYPES, encodeEnvelope, decodeEnvelope, BYOK_PAIR_PATH, BYOK_CHALLENGE_PATH, BYOK_TOKEN_PATH, BYOK_CAPABILITIES_PATH, BYOK_EVENTS_PATH, BYOK_MESSAGES_PATH, BYOK_BOARD_PATH, BYOK_BOARD_CLAIM_ROUTE, BYOK_BOARD_UNCLAIM_ROUTE, BYOK_BOARD_STATUS_ROUTE, BYOK_BOARD_STREAM_PATH, BYOK_PRESENCE_PATH, BYOK_ACTIVITY_PATH, BYOK_RECORDS_PATH, BYOK_RECORD_ROUTE, BYOK_SKILL_PACKS_PATH, BYOK_SKILL_PACK_FILE_ROUTE, BYOK_BLOBS_PATH, BYOK_BLOB_FINALIZE_ROUTE, BYOK_BLOB_URL_ROUTE, BYOK_BLOB_CONTENT_ROUTE, createEnvelope, PairRequestSchema, ChallengeRequestSchema, TokenRequestSchema, MessagesSendRequestSchema, CreateBlobRequestSchema, byokBlobContentPath } from '@byok-sdk/protocol';
3
+ import { AgentEventOrUnknownSchema, DAEMON_TO_SERVER_TYPES, encodeEnvelope, decodeEnvelope, BYOK_PAIR_PATH, BYOK_CHALLENGE_PATH, BYOK_TOKEN_PATH, BYOK_CAPABILITIES_PATH, BYOK_EVENTS_PATH, BYOK_MESSAGES_PATH, BYOK_BOARD_PATH, BYOK_BOARD_CLAIM_ROUTE, BYOK_BOARD_UNCLAIM_ROUTE, BYOK_BOARD_STATUS_ROUTE, BYOK_BOARD_STREAM_PATH, BYOK_PRESENCE_PATH, BYOK_ACTIVITY_PATH, BYOK_RECORDS_PATH, BYOK_RECORD_ROUTE, BYOK_SKILL_PACKS_PATH, BYOK_SKILL_PACK_FILE_ROUTE, BYOK_BLOBS_PATH, BYOK_BLOB_FINALIZE_ROUTE, BYOK_BLOB_URL_ROUTE, BYOK_BLOB_CONTENT_ROUTE, createEnvelope, PairRequestSchema, ChallengeRequestSchema, TokenRequestSchema, MessagesSendRequestSchema, PresencePublishRequestSchema, CreateBlobRequestSchema, byokBlobContentPath } from '@byok-sdk/protocol';
4
4
  import { z } from 'zod';
5
5
  import { Hono } from 'hono';
6
6
 
@@ -317,7 +317,9 @@ var CLOUD_ERROR_CODES = {
317
317
  */
318
318
  terminal_receipt_unreadable: "terminal_receipt_unreadable",
319
319
  /** A progress/activity batch exceeded the configured event or byte ceiling. */
320
- activity_batch_too_large: "activity_batch_too_large"
320
+ activity_batch_too_large: "activity_batch_too_large",
321
+ /** Host control-plane task lookup is tenant-closed and found no task. */
322
+ task_not_found: "task_not_found"
321
323
  };
322
324
  var ByokCloudError = class extends Error {
323
325
  code;
@@ -524,14 +526,19 @@ function tenantStoresFor(principal, root) {
524
526
  devices: {
525
527
  get: (deviceId) => cloud.devices.get(tenant, deviceId),
526
528
  list: () => cloud.devices.list(tenant),
527
- revoke: (deviceId) => cloud.devices.revoke(tenant, deviceId)
529
+ revoke: (deviceId) => cloud.devices.revoke(tenant, deviceId),
530
+ readiness: () => cloud.devices.readiness(tenant, core.presence)
528
531
  },
529
532
  tasks: {
530
533
  open: (input) => cloud.tasks.open(tenant, input),
531
534
  get: (taskId) => cloud.tasks.get(tenant, taskId),
535
+ getMany: (taskIds) => cloud.tasks.getMany(tenant, taskIds),
532
536
  claim: (input) => cloud.tasks.claim(tenant, input),
533
537
  recordStatus: (input) => cloud.tasks.recordStatus(tenant, input)
534
538
  },
539
+ cancellations: {
540
+ request: (input) => cloud.cancellations.request(tenant, input)
541
+ },
535
542
  dedup: {
536
543
  checkAndRecord: (deviceId, envelopeId) => cloud.dedup.checkAndRecord(tenant, deviceId, envelopeId)
537
544
  },
@@ -774,13 +781,30 @@ function eventsHandler(deps) {
774
781
  }
775
782
  const attempts = Math.max(1, Math.ceil(deps.longPollHoldMs / deps.longPollIntervalMs));
776
783
  for (let attempt = 0; attempt < attempts; attempt += 1) {
777
- const page = await stores.mailbox.readAfter({
778
- deviceId: device.deviceId,
779
- afterSeq: cursor,
780
- limit: deps.pageLimit
781
- });
782
- if (page.messages.length > 0) {
783
- const events = page.messages.map((message) => decodeEnvelope(message.body));
784
+ let scanCursor = cursor;
785
+ while (true) {
786
+ const page = await stores.mailbox.readAfter({
787
+ deviceId: device.deviceId,
788
+ afterSeq: scanCursor,
789
+ limit: deps.pageLimit
790
+ });
791
+ if (page.messages.length === 0) break;
792
+ const decoded = page.messages.map((message) => decodeEnvelope(message.body));
793
+ const offeredTaskIds = decoded.flatMap(
794
+ (event) => (event.type === "task.offer" || event.type === "task.offer_with_toolsets") && event.task_id !== void 0 ? [event.task_id] : []
795
+ );
796
+ const attemptsByTaskId = Object.fromEntries(
797
+ (await stores.tasks.getMany(offeredTaskIds)).map((attempt2) => [attempt2.taskId, attempt2])
798
+ );
799
+ const events = decoded.filter((event) => {
800
+ if (event.type !== "task.offer" && event.type !== "task.offer_with_toolsets") return true;
801
+ return event.task_id === void 0 || attemptsByTaskId[event.task_id]?.cancellation === void 0;
802
+ });
803
+ if (events.length === 0 && page.hasMore) {
804
+ scanCursor = page.nextSeq;
805
+ continue;
806
+ }
807
+ if (events.length === 0) break;
784
808
  const response2 = {
785
809
  events,
786
810
  cursor: page.nextSeq,
@@ -999,12 +1023,20 @@ function approvalTimelineAppendInput(taskId, envelope) {
999
1023
  }
1000
1024
  }
1001
1025
  async function recordTerminal(stores, taskId, envelope, status) {
1026
+ const attempt = await stores.tasks.get(taskId);
1002
1027
  const { created } = await stores.receipts.record({
1003
1028
  key: terminalReceiptKey(taskId),
1004
1029
  body: encodeEnvelope(envelope)
1005
1030
  });
1031
+ if (attempt?.cancellation !== void 0) {
1032
+ if (status === "cancelled") {
1033
+ await stores.tasks.recordStatus({ taskId, status: "cancelled" });
1034
+ }
1035
+ return;
1036
+ }
1006
1037
  if (!created) return;
1007
- await stores.tasks.recordStatus({ taskId, status });
1038
+ const recordedAttempt = await stores.tasks.recordStatus({ taskId, status });
1039
+ if (recordedAttempt?.status !== status || recordedAttempt.cancellation !== void 0) return;
1008
1040
  await projectTerminalToReview(stores.board, taskId);
1009
1041
  }
1010
1042
 
@@ -1271,11 +1303,7 @@ function boardFailure(c, caught) {
1271
1303
  if (isCoreError(caught, "board_not_held")) return c.json({ error: caught.code }, 409);
1272
1304
  throw caught;
1273
1305
  }
1274
- var PresenceBodySchema = z.object({
1275
- level: z.enum(PRESENCE_LEVELS),
1276
- detail: z.string().optional(),
1277
- configuredToolsets: ConfiguredToolsetsSchema.optional()
1278
- });
1306
+ var PresenceBodySchema = PresencePublishRequestSchema;
1279
1307
  function presencePublishHandler(deps) {
1280
1308
  return async (c) => {
1281
1309
  const authenticated = await authenticateDevice(c, deps);
@@ -1292,6 +1320,15 @@ function presencePublishHandler(deps) {
1292
1320
  level: parsed.data.level,
1293
1321
  ...parsed.data.detail === void 0 ? {} : { detail: parsed.data.detail },
1294
1322
  ...parsed.data.configuredToolsets === void 0 ? {} : { configuredToolsets: parsed.data.configuredToolsets },
1323
+ ...parsed.data.clientVersion === void 0 ? {} : { clientVersion: parsed.data.clientVersion },
1324
+ ...parsed.data.protocolVersions === void 0 ? {} : { protocolVersions: parsed.data.protocolVersions },
1325
+ ...parsed.data.runtimes === void 0 ? {} : {
1326
+ runtimes: parsed.data.runtimes.map(({ id, version, authPresent }) => ({
1327
+ id,
1328
+ ...version === void 0 ? {} : { version },
1329
+ ...authPresent === void 0 ? {} : { authPresent }
1330
+ }))
1331
+ },
1295
1332
  ttlMs: deps.ttlMs,
1296
1333
  minimumIntervalMs: deps.minimumIntervalMs
1297
1334
  }),
@@ -1792,6 +1829,7 @@ function projectTerminalResult(taskId, receipt) {
1792
1829
  sessionRef: envelope.payload.sessionRef,
1793
1830
  ...envelope.payload.artifactRefs !== void 0 ? { artifactRefs: envelope.payload.artifactRefs } : {},
1794
1831
  ...envelope.payload.document !== void 0 ? { document: envelope.payload.document } : {},
1832
+ ...envelope.payload.usage !== void 0 ? { usage: envelope.payload.usage } : {},
1795
1833
  recordedAt: receipt.recordedAt
1796
1834
  };
1797
1835
  case "task.fail":
@@ -1800,6 +1838,7 @@ function projectTerminalResult(taskId, receipt) {
1800
1838
  state: "failed",
1801
1839
  reason: envelope.payload.reason,
1802
1840
  ...envelope.payload.retryable !== void 0 ? { retryable: envelope.payload.retryable } : {},
1841
+ ...envelope.payload.usage !== void 0 ? { usage: envelope.payload.usage } : {},
1803
1842
  recordedAt: receipt.recordedAt
1804
1843
  };
1805
1844
  case "task.cancelled":
@@ -1807,6 +1846,7 @@ function projectTerminalResult(taskId, receipt) {
1807
1846
  taskId,
1808
1847
  state: "cancelled",
1809
1848
  ...envelope.payload.reason !== void 0 ? { reason: envelope.payload.reason } : {},
1849
+ ...envelope.payload.usage !== void 0 ? { usage: envelope.payload.usage } : {},
1810
1850
  recordedAt: receipt.recordedAt
1811
1851
  };
1812
1852
  default:
@@ -2041,6 +2081,42 @@ function createByokCloud(options) {
2041
2081
  (taskId, seq, messageId) => createEnvelope("task.offer_with_toolsets", input.payload, { id: messageId, taskId, seq })
2042
2082
  );
2043
2083
  },
2084
+ async cancelTask(tenant, taskId, reason) {
2085
+ const stores = tenantStoresFor(controlPlane(tenant), root);
2086
+ const proposedMessageId = options.crypto.randomUuid();
2087
+ const mutation = await stores.cancellations.request({
2088
+ taskId,
2089
+ proposedMessageId,
2090
+ ...reason === void 0 ? {} : { reason },
2091
+ materialize: async (seq, messageId) => {
2092
+ const envelope = createEnvelope("task.cancel", reason === void 0 ? {} : { reason }, {
2093
+ id: messageId,
2094
+ taskId,
2095
+ seq
2096
+ });
2097
+ const body = encodeEnvelope(envelope);
2098
+ const bytes = new TextEncoder().encode(body);
2099
+ return {
2100
+ body,
2101
+ bodyHash: contentHash(await options.crypto.sha256(bytes)),
2102
+ byteSize: BigInt(bytes.length)
2103
+ };
2104
+ }
2105
+ });
2106
+ if (mutation === void 0) {
2107
+ throw new ByokCloudError("task_not_found", `Task ${taskId} was not found for this tenant.`);
2108
+ }
2109
+ if (mutation.message !== void 0) {
2110
+ const envelope = decodeEnvelope(mutation.message.body);
2111
+ if (envelope.seq !== mutation.message.seq || envelope.type !== "task.cancel") {
2112
+ throw new ByokCloudError(
2113
+ "mailbox_seq_mismatch",
2114
+ `Mailbox stored cancellation seq ${String(envelope.seq)} at row ${mutation.message.seq}.`
2115
+ );
2116
+ }
2117
+ }
2118
+ return mutation.attempt;
2119
+ },
2044
2120
  readTaskAttempt(tenant, taskId) {
2045
2121
  return tenantStoresFor(controlPlane(tenant), root).tasks.get(taskId);
2046
2122
  },
@@ -2048,7 +2124,17 @@ function createByokCloud(options) {
2048
2124
  return tenantStoresFor(controlPlane(tenant), root).receipts.get(terminalReceiptKey(taskId));
2049
2125
  },
2050
2126
  async readTaskResult(tenant, taskId) {
2051
- const receipt = await tenantStoresFor(controlPlane(tenant), root).receipts.get(
2127
+ const stores = tenantStoresFor(controlPlane(tenant), root);
2128
+ const attempt = await stores.tasks.get(taskId);
2129
+ if (attempt?.cancellation !== void 0) {
2130
+ return {
2131
+ taskId,
2132
+ state: "cancelled",
2133
+ ...attempt.cancellation.reason === void 0 ? {} : { reason: attempt.cancellation.reason },
2134
+ recordedAt: attempt.cancellation.requestedAt
2135
+ };
2136
+ }
2137
+ const receipt = await stores.receipts.get(
2052
2138
  terminalReceiptKey(taskId)
2053
2139
  );
2054
2140
  return receipt === void 0 ? void 0 : projectTerminalResult(taskId, receipt);
@@ -2079,6 +2165,9 @@ function createByokCloud(options) {
2079
2165
  listPresence(tenant) {
2080
2166
  return tenantStoresFor(controlPlane(tenant), root).presence.list();
2081
2167
  },
2168
+ readTenantReadiness(tenant) {
2169
+ return tenantStoresFor(controlPlane(tenant), root).devices.readiness();
2170
+ },
2082
2171
  readActivity(tenant, taskId) {
2083
2172
  return tenantStoresFor(controlPlane(tenant), root).activity.read(taskId);
2084
2173
  },
@@ -2307,6 +2396,52 @@ var InMemoryDeviceDirectory = class {
2307
2396
  const prefix = tenantKey(tenant, "");
2308
2397
  return [...this.#byTenant.entries()].filter(([key]) => key.startsWith(prefix)).map(([, record]) => record);
2309
2398
  }
2399
+ async readiness(tenant, presence) {
2400
+ const devices = await this.list(tenant);
2401
+ const livePresence = await presence.list(tenant);
2402
+ const presenceByDevice = new Map(livePresence.map((hint) => [hint.deviceId, hint]));
2403
+ const activeDeviceIds = new Set(
2404
+ devices.filter((device) => !device.revoked).map((device) => device.deviceId)
2405
+ );
2406
+ const observedPresenceByLevel = Object.fromEntries(
2407
+ PRESENCE_LEVELS.map((level) => [level, 0])
2408
+ );
2409
+ for (const hint of livePresence) {
2410
+ if (!activeDeviceIds.has(hint.deviceId)) continue;
2411
+ observedPresenceByLevel[hint.level] += 1;
2412
+ }
2413
+ return {
2414
+ tenantId: tenant,
2415
+ activePairedDeviceCount: devices.filter((device) => !device.revoked).length,
2416
+ revokedDeviceCount: devices.filter((device) => device.revoked).length,
2417
+ observedPresenceCount: Object.values(observedPresenceByLevel).reduce(
2418
+ (total, count) => total + count,
2419
+ 0
2420
+ ),
2421
+ observedPresenceByLevel,
2422
+ devices: [...devices].sort((left, right) => left.deviceId.localeCompare(right.deviceId)).map((device) => {
2423
+ const hint = device.revoked ? void 0 : presenceByDevice.get(device.deviceId);
2424
+ return {
2425
+ deviceId: device.deviceId,
2426
+ productId: device.productId,
2427
+ deviceName: device.deviceName,
2428
+ revoked: device.revoked,
2429
+ ...hint === void 0 ? {} : {
2430
+ presence: {
2431
+ level: hint.level,
2432
+ ...hint.detail === void 0 ? {} : { detail: hint.detail },
2433
+ ...hint.configuredToolsets === void 0 ? {} : { configuredToolsets: hint.configuredToolsets },
2434
+ ...hint.clientVersion === void 0 ? {} : { clientVersion: hint.clientVersion },
2435
+ ...hint.protocolVersions === void 0 ? {} : { protocolVersions: hint.protocolVersions },
2436
+ ...hint.runtimes === void 0 ? {} : { runtimes: hint.runtimes },
2437
+ observedAt: hint.observedAt,
2438
+ expiresAt: hint.expiresAt
2439
+ }
2440
+ }
2441
+ };
2442
+ })
2443
+ };
2444
+ }
2310
2445
  async resolveByDeviceId(deviceId) {
2311
2446
  const key = this.#byDeviceId.get(deviceId);
2312
2447
  return key === void 0 ? void 0 : this.#byTenant.get(key);
@@ -2449,53 +2584,143 @@ var InMemoryProofRequestReceiptStore = class {
2449
2584
  }
2450
2585
  };
2451
2586
  var InMemoryTaskAttemptStore = class {
2452
- #attempts = /* @__PURE__ */ new Map();
2453
- #clock;
2454
- constructor(clock) {
2455
- this.#clock = clock;
2587
+ #state;
2588
+ constructor(clock, state = new InMemoryTaskAttemptState(clock)) {
2589
+ this.#state = state;
2456
2590
  }
2457
2591
  async open(tenant, input) {
2458
2592
  const key = tenantKey(tenant, input.taskId);
2459
- const existing = this.#attempts.get(key);
2460
- if (existing !== void 0) return existing;
2461
- const attempt = {
2462
- tenantId: tenant,
2463
- taskId: input.taskId,
2464
- deviceId: input.deviceId,
2465
- status: "offered",
2466
- updatedAt: this.#now()
2467
- };
2468
- this.#attempts.set(key, attempt);
2469
- return attempt;
2593
+ return this.#state.mutate(key, () => {
2594
+ const existing = this.#state.attempts.get(key);
2595
+ if (existing !== void 0) return existing;
2596
+ const attempt = {
2597
+ tenantId: tenant,
2598
+ taskId: input.taskId,
2599
+ deviceId: input.deviceId,
2600
+ status: "offered",
2601
+ updatedAt: this.#now()
2602
+ };
2603
+ this.#state.attempts.set(key, attempt);
2604
+ return attempt;
2605
+ });
2470
2606
  }
2471
2607
  async get(tenant, taskId) {
2472
- return this.#attempts.get(tenantKey(tenant, taskId));
2608
+ return this.#state.attempts.get(tenantKey(tenant, taskId));
2609
+ }
2610
+ async getMany(tenant, taskIds) {
2611
+ return taskIds.flatMap((taskId) => {
2612
+ const attempt = this.#state.attempts.get(tenantKey(tenant, taskId));
2613
+ return attempt === void 0 ? [] : [attempt];
2614
+ });
2473
2615
  }
2474
2616
  async claim(tenant, input) {
2475
2617
  const key = tenantKey(tenant, input.taskId);
2476
- const existing = this.#attempts.get(key);
2477
- if (existing === void 0) return void 0;
2478
- if (existing.ownerDeviceId !== void 0) return existing;
2479
- const claimed = {
2480
- ...existing,
2481
- ownerDeviceId: input.deviceId,
2482
- status: "claimed",
2483
- updatedAt: this.#now()
2484
- };
2485
- this.#attempts.set(key, claimed);
2486
- return claimed;
2618
+ return this.#state.mutate(key, () => {
2619
+ const existing = this.#state.attempts.get(key);
2620
+ if (existing === void 0) return void 0;
2621
+ if (existing.ownerDeviceId !== void 0 || existing.cancellation !== void 0 || existing.status !== "offered") return existing;
2622
+ const claimed = {
2623
+ ...existing,
2624
+ ownerDeviceId: input.deviceId,
2625
+ status: "claimed",
2626
+ updatedAt: this.#now()
2627
+ };
2628
+ this.#state.attempts.set(key, claimed);
2629
+ return claimed;
2630
+ });
2487
2631
  }
2488
2632
  async recordStatus(tenant, input) {
2489
2633
  const key = tenantKey(tenant, input.taskId);
2490
- const existing = this.#attempts.get(key);
2491
- if (existing === void 0) return void 0;
2492
- const updated = { ...existing, status: input.status, updatedAt: this.#now() };
2493
- this.#attempts.set(key, updated);
2494
- return updated;
2634
+ return this.#state.mutate(key, () => {
2635
+ const existing = this.#state.attempts.get(key);
2636
+ if (existing === void 0) return void 0;
2637
+ if (existing.cancellation !== void 0) {
2638
+ if (input.status !== "cancelled" || existing.status === "cancelled") return existing;
2639
+ } else if (existing.status === "complete" || existing.status === "failed" || existing.status === "cancelled") {
2640
+ return existing;
2641
+ }
2642
+ const updated = { ...existing, status: input.status, updatedAt: this.#now() };
2643
+ this.#state.attempts.set(key, updated);
2644
+ return updated;
2645
+ });
2495
2646
  }
2496
2647
  #now() {
2648
+ return this.#state.now();
2649
+ }
2650
+ };
2651
+ var InMemoryTaskAttemptState = class {
2652
+ attempts = /* @__PURE__ */ new Map();
2653
+ #clock;
2654
+ #mutationTails = /* @__PURE__ */ new Map();
2655
+ constructor(clock) {
2656
+ this.#clock = clock;
2657
+ }
2658
+ now() {
2497
2659
  return this.#clock.now().toISOString();
2498
2660
  }
2661
+ /** Serialize every state-changing operation for one tenant/task key. */
2662
+ async mutate(key, operation) {
2663
+ const previous = this.#mutationTails.get(key) ?? Promise.resolve();
2664
+ const result = previous.then(operation);
2665
+ const tail = result.then(
2666
+ () => void 0,
2667
+ () => void 0
2668
+ );
2669
+ this.#mutationTails.set(key, tail);
2670
+ try {
2671
+ return await result;
2672
+ } finally {
2673
+ if (this.#mutationTails.get(key) === tail) this.#mutationTails.delete(key);
2674
+ }
2675
+ }
2676
+ };
2677
+ var InMemoryTaskCancellationStore = class {
2678
+ #state;
2679
+ #mailbox;
2680
+ #messageIds = /* @__PURE__ */ new Map();
2681
+ #inFlight = /* @__PURE__ */ new Map();
2682
+ constructor(state, mailbox) {
2683
+ this.#state = state;
2684
+ this.#mailbox = mailbox;
2685
+ }
2686
+ request(tenant, input) {
2687
+ const key = tenantKey(tenant, input.taskId);
2688
+ const running = this.#inFlight.get(key);
2689
+ if (running !== void 0) return running;
2690
+ const operation = this.#request(tenant, key, input).finally(() => {
2691
+ this.#inFlight.delete(key);
2692
+ });
2693
+ this.#inFlight.set(key, operation);
2694
+ return operation;
2695
+ }
2696
+ async #request(tenant, key, input) {
2697
+ return this.#state.mutate(key, async () => {
2698
+ const existing = this.#state.attempts.get(key);
2699
+ if (existing === void 0) return void 0;
2700
+ if (existing.cancellation === void 0 && (existing.status === "complete" || existing.status === "failed" || existing.status === "cancelled")) {
2701
+ return { attempt: existing };
2702
+ }
2703
+ const messageId = this.#messageIds.get(key) ?? input.proposedMessageId;
2704
+ const message = await this.#mailbox.append(tenant, {
2705
+ deviceId: existing.deviceId,
2706
+ messageId,
2707
+ materialize: (seq) => input.materialize(seq, messageId)
2708
+ });
2709
+ const requestedAt = this.#state.now();
2710
+ const attempt = existing.cancellation === void 0 ? {
2711
+ ...existing,
2712
+ status: existing.ownerDeviceId === void 0 ? "cancelled" : "cancel_requested",
2713
+ cancellation: {
2714
+ requestedAt,
2715
+ ...input.reason === void 0 ? {} : { reason: input.reason }
2716
+ },
2717
+ updatedAt: requestedAt
2718
+ } : existing;
2719
+ this.#state.attempts.set(key, attempt);
2720
+ this.#messageIds.set(key, messageId);
2721
+ return { attempt, message };
2722
+ });
2723
+ }
2499
2724
  };
2500
2725
 
2501
2726
  // src/stores/in-memory/activity.ts
@@ -2616,8 +2841,10 @@ var InMemoryApprovalTimelineStore = class {
2616
2841
  };
2617
2842
 
2618
2843
  // src/stores/in-memory/index.ts
2619
- function createInMemoryCloudStores(clock, crypto, objects) {
2844
+ function createInMemoryCloudStores(clock, crypto, objects, mailbox) {
2620
2845
  const blobs = createInMemoryBlobs(clock, crypto, objects);
2846
+ const taskState = new InMemoryTaskAttemptState(clock);
2847
+ const tasks = new InMemoryTaskAttemptStore(clock, taskState);
2621
2848
  return {
2622
2849
  stores: {
2623
2850
  activity: new InMemoryActivityStore(clock),
@@ -2626,7 +2853,8 @@ function createInMemoryCloudStores(clock, crypto, objects) {
2626
2853
  pairingCodes: new InMemoryPairingCodeStore(clock),
2627
2854
  nonces: new InMemoryNonceStore(clock, crypto),
2628
2855
  dedup: new InMemoryInboundDedupStore(),
2629
- tasks: new InMemoryTaskAttemptStore(clock),
2856
+ tasks,
2857
+ cancellations: new InMemoryTaskCancellationStore(taskState, mailbox),
2630
2858
  receipts: new InMemoryRequestReceiptStore(clock),
2631
2859
  proofReceipts: new InMemoryProofRequestReceiptStore(clock),
2632
2860
  blobs: blobs.blobs,
@@ -2645,7 +2873,12 @@ function createInMemoryByokCloud(options = {}) {
2645
2873
  const clock = options.clock ?? systemClock();
2646
2874
  const crypto = options.crypto ?? createWebCrypto();
2647
2875
  const core = createInMemoryCoreStores({ clock }).stores;
2648
- const { stores, blobContentProxy } = createInMemoryCloudStores(clock, crypto, core.objects);
2876
+ const { stores, blobContentProxy } = createInMemoryCloudStores(
2877
+ clock,
2878
+ crypto,
2879
+ core.objects,
2880
+ core.mailbox
2881
+ );
2649
2882
  const tokenSigner = options.tokenSigner ?? createHmacTokenSigner(globalThis.crypto.getRandomValues(new Uint8Array(TOKEN_SECRET_BYTES)), clock);
2650
2883
  const cloud = createByokCloud({
2651
2884
  core,
@@ -2686,6 +2919,28 @@ function createInMemoryByokCloud(options = {}) {
2686
2919
  });
2687
2920
  return { cloud, core, stores, blobContentProxy, clock, crypto };
2688
2921
  }
2922
+ function authenticateHostedDeviceAssertion(input, deps) {
2923
+ return authenticateDeviceAssertion(input, {
2924
+ verifier: {
2925
+ verify: ({ publicKey, signingInput, signature }) => deps.crypto.verifyEd25519(publicKey, signingInput, signature)
2926
+ },
2927
+ lookupDevice: async (deviceId) => {
2928
+ const row = await deps.devices.resolveByDeviceId(deviceId);
2929
+ if (row === void 0) return void 0;
2930
+ return {
2931
+ tenantId: row.tenantId,
2932
+ productId: row.productId,
2933
+ deviceId: row.deviceId,
2934
+ publicKeyJwkX: row.devicePublicKey,
2935
+ revoked: row.revoked
2936
+ };
2937
+ },
2938
+ replay: deps.replay,
2939
+ expected: deps.expected,
2940
+ now: deps.clock.now(),
2941
+ ...deps.maxLifetimeMs === void 0 ? {} : { maxLifetimeMs: deps.maxLifetimeMs }
2942
+ });
2943
+ }
2689
2944
  var BoardFeedItemSchema = z.object({
2690
2945
  tenantId: z.string(),
2691
2946
  itemId: z.string(),
@@ -2825,6 +3080,7 @@ var TASK_ATTEMPT_STATUSES = [
2825
3080
  "offered",
2826
3081
  "claimed",
2827
3082
  "running",
3083
+ "cancel_requested",
2828
3084
  "complete",
2829
3085
  "failed",
2830
3086
  "cancelled"
@@ -2837,6 +3093,7 @@ var CLOUD_STORE_NAMES = [
2837
3093
  "nonces",
2838
3094
  "dedup",
2839
3095
  "tasks",
3096
+ "cancellations",
2840
3097
  "receipts",
2841
3098
  "proofReceipts",
2842
3099
  "blobs",
@@ -2847,11 +3104,12 @@ var CLOUD_STORE_NAMES = [
2847
3104
  var CLOUD_PORT_METHODS = {
2848
3105
  activity: ["append", "read"],
2849
3106
  approvals: ["append", "read"],
2850
- devices: ["register", "get", "revoke", "list", "resolveByDeviceId"],
3107
+ devices: ["register", "get", "revoke", "list", "readiness", "resolveByDeviceId"],
2851
3108
  pairingCodes: ["issue", "redeem"],
2852
3109
  nonces: ["issue", "validate", "markUsed"],
2853
3110
  dedup: ["checkAndRecord"],
2854
- tasks: ["open", "get", "claim", "recordStatus"],
3111
+ tasks: ["open", "get", "getMany", "claim", "recordStatus"],
3112
+ cancellations: ["request"],
2855
3113
  receipts: ["record", "get"],
2856
3114
  proofReceipts: ["record", "get"],
2857
3115
  // Three methods, not six: the byte-proxy trio moved to `BlobContentProxy`,
@@ -2868,12 +3126,13 @@ var CLOUD_PORT_INTERFACES = {
2868
3126
  nonces: "NonceStore",
2869
3127
  dedup: "InboundDedupStore",
2870
3128
  tasks: "TaskAttemptStore",
3129
+ cancellations: "TaskCancellationStore",
2871
3130
  receipts: "RequestReceiptStore",
2872
3131
  proofReceipts: "ProofRequestReceiptStore",
2873
3132
  blobs: "CloudBlobStore",
2874
3133
  rateLimiter: "InboundRateLimiter"
2875
3134
  };
2876
3135
 
2877
- export { ACCESS_TOKEN_TTL_SECONDS, APPROVAL_SUMMARY_MAX_BYTES, ActivityAppendRequestSchema, AllowAllRateLimiter, ApprovalObservationSchema, ApprovalTimelineEventSchema, BLOB_URL_TTL_MS, BoardFeedClient, BoardFeedRetryableError, BoardFeedStoppedError, ByokCloudError, CLOUD_CAPABILITIES, CLOUD_ERROR_CODES, CLOUD_PORT_INTERFACES, CLOUD_PORT_METHODS, CLOUD_STORE_NAMES, CapabilitiesResponseSchema, CloudRouteRegistry, DEDUP_RING_CAPACITY, DEFAULT_ACTIVITY_BOUNDS, DEFAULT_ACTIVITY_CAPACITY, DEFAULT_ACTIVITY_MAX_BYTES, DEFAULT_ACTIVITY_MAX_EVENTS, DEFAULT_ACTIVITY_TTL_MS, DEFAULT_APPROVAL_TIMELINE_CAPACITY, DEFAULT_APPROVAL_TIMELINE_TTL_MS, DEFAULT_BOARD_CHANNEL_MAX_BYTES, DEFAULT_BOARD_PAGE_LIMIT, DEFAULT_BOARD_STREAM_HEARTBEAT_INTERVAL_MS, DEFAULT_BOARD_STREAM_QUERY_INTERVAL_MS, DEFAULT_BOARD_STREAM_RECONCILIATION_INTERVAL_MS, DEFAULT_BOARD_TITLE_MAX_BYTES, DEFAULT_DEVICE_PROOF_CLOCK_SKEW_MS, DEFAULT_DEVICE_PROOF_MAX_LIFETIME_MS, DEFAULT_EVENTS_PAGE_LIMIT, DEFAULT_LONG_POLL_HOLD_MS, DEFAULT_LONG_POLL_INTERVAL_MS, DEFAULT_MAX_BLOB_SIZE_BYTES, DEFAULT_MAX_TRUTH_REQUEST_BYTES, DEFAULT_PRESENCE_DETAIL_MAX_BYTES, DEFAULT_PRESENCE_MINIMUM_INTERVAL_MS, DEFAULT_PRESENCE_TTL_MS, DEFAULT_SKILL_PACK_PAGE_LIMIT, DEVICE_IDENTITY_PROOF_KEY_EPOCH, DEVICE_IDENTITY_PROOF_KEY_ID, InMemoryActivityStore, InMemoryBlobContentProxy, InMemoryCloudBlobStore, InMemoryDeviceDirectory, InMemoryInboundDedupStore, InMemoryNonceStore, InMemoryPairingCodeStore, InMemoryProofRequestReceiptStore, InMemoryRequestReceiptStore, InMemoryTaskAttemptStore, MAX_DEVICE_PROOF_CLOCK_SKEW_MS, MAX_DEVICE_PROOF_HEADER_BYTES, MAX_DEVICE_PROOF_MAX_LIFETIME_MS, NONCE_TTL_MS, PAIRING_CODE_TTL_MS, ROUTE_CLASSES, ROUTE_METHODS, TASK_ATTEMPT_STATUSES, TRUTH_BATCH_MAX_RECORDS, TRUTH_INLINE_CONTENT_TYPE, TRUTH_LABEL_MAX_LENGTH, TRUTH_MANIFEST_MAX_LIMIT, TRUTH_RECORD_CAPABILITY, TRUTH_RECORD_KEY_MAX_LENGTH, TRUTH_REQUEST_ID_MAX_LENGTH, TimelineEventSchema, TruthBodyInputSchema, TruthCommitError, TruthCommitResponseSchema, TruthRecordKeySchema, TruthRecordMetadataSchema, TruthWriteRequestSchema, activityCursor, approvalTimelineCursor, authenticateBearer, authenticateDeviceProof, createAuthPlane, createByokCloud, createHmacTokenSigner, createInMemoryBlobs, createInMemoryByokCloud, createInMemoryCloudStores, createWebCrypto, declares, extractBearerToken, fullCapabilityDeclaration, handleInboundEnvelope, isCloudError, isTruthCommitError, parseApprovalObservations, parseTimelineEvents, projectTerminalResult, projectTimelineEvents, routeKey, tenantStoresFor, terminalReceiptKey, truthManifestMetadata, truthRecordMetadata, validateActivityAppend, validateApprovalTimelineAppend, verifyNonceSignature };
3136
+ export { ACCESS_TOKEN_TTL_SECONDS, APPROVAL_SUMMARY_MAX_BYTES, ActivityAppendRequestSchema, AllowAllRateLimiter, ApprovalObservationSchema, ApprovalTimelineEventSchema, BLOB_URL_TTL_MS, BoardFeedClient, BoardFeedRetryableError, BoardFeedStoppedError, ByokCloudError, CLOUD_CAPABILITIES, CLOUD_ERROR_CODES, CLOUD_PORT_INTERFACES, CLOUD_PORT_METHODS, CLOUD_STORE_NAMES, CapabilitiesResponseSchema, CloudRouteRegistry, DEDUP_RING_CAPACITY, DEFAULT_ACTIVITY_BOUNDS, DEFAULT_ACTIVITY_CAPACITY, DEFAULT_ACTIVITY_MAX_BYTES, DEFAULT_ACTIVITY_MAX_EVENTS, DEFAULT_ACTIVITY_TTL_MS, DEFAULT_APPROVAL_TIMELINE_CAPACITY, DEFAULT_APPROVAL_TIMELINE_TTL_MS, DEFAULT_BOARD_CHANNEL_MAX_BYTES, DEFAULT_BOARD_PAGE_LIMIT, DEFAULT_BOARD_STREAM_HEARTBEAT_INTERVAL_MS, DEFAULT_BOARD_STREAM_QUERY_INTERVAL_MS, DEFAULT_BOARD_STREAM_RECONCILIATION_INTERVAL_MS, DEFAULT_BOARD_TITLE_MAX_BYTES, DEFAULT_DEVICE_PROOF_CLOCK_SKEW_MS, DEFAULT_DEVICE_PROOF_MAX_LIFETIME_MS, DEFAULT_EVENTS_PAGE_LIMIT, DEFAULT_LONG_POLL_HOLD_MS, DEFAULT_LONG_POLL_INTERVAL_MS, DEFAULT_MAX_BLOB_SIZE_BYTES, DEFAULT_MAX_TRUTH_REQUEST_BYTES, DEFAULT_PRESENCE_DETAIL_MAX_BYTES, DEFAULT_PRESENCE_MINIMUM_INTERVAL_MS, DEFAULT_PRESENCE_TTL_MS, DEFAULT_SKILL_PACK_PAGE_LIMIT, DEVICE_IDENTITY_PROOF_KEY_EPOCH, DEVICE_IDENTITY_PROOF_KEY_ID, InMemoryActivityStore, InMemoryBlobContentProxy, InMemoryCloudBlobStore, InMemoryDeviceDirectory, InMemoryInboundDedupStore, InMemoryNonceStore, InMemoryPairingCodeStore, InMemoryProofRequestReceiptStore, InMemoryRequestReceiptStore, InMemoryTaskAttemptStore, InMemoryTaskCancellationStore, MAX_DEVICE_PROOF_CLOCK_SKEW_MS, MAX_DEVICE_PROOF_HEADER_BYTES, MAX_DEVICE_PROOF_MAX_LIFETIME_MS, NONCE_TTL_MS, PAIRING_CODE_TTL_MS, ROUTE_CLASSES, ROUTE_METHODS, TASK_ATTEMPT_STATUSES, TRUTH_BATCH_MAX_RECORDS, TRUTH_INLINE_CONTENT_TYPE, TRUTH_LABEL_MAX_LENGTH, TRUTH_MANIFEST_MAX_LIMIT, TRUTH_RECORD_CAPABILITY, TRUTH_RECORD_KEY_MAX_LENGTH, TRUTH_REQUEST_ID_MAX_LENGTH, TimelineEventSchema, TruthBodyInputSchema, TruthCommitError, TruthCommitResponseSchema, TruthRecordKeySchema, TruthRecordMetadataSchema, TruthWriteRequestSchema, activityCursor, approvalTimelineCursor, authenticateBearer, authenticateDeviceProof, authenticateHostedDeviceAssertion, createAuthPlane, createByokCloud, createHmacTokenSigner, createInMemoryBlobs, createInMemoryByokCloud, createInMemoryCloudStores, createWebCrypto, declares, extractBearerToken, fullCapabilityDeclaration, handleInboundEnvelope, isCloudError, isTruthCommitError, parseApprovalObservations, parseTimelineEvents, projectTerminalResult, projectTimelineEvents, routeKey, tenantStoresFor, terminalReceiptKey, truthManifestMetadata, truthRecordMetadata, validateActivityAppend, validateApprovalTimelineAppend, verifyNonceSignature };
2878
3137
  //# sourceMappingURL=index.js.map
2879
3138
  //# sourceMappingURL=index.js.map