@byok-sdk/cloud 0.5.0 → 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/cloud.d.ts CHANGED
@@ -14,7 +14,7 @@
14
14
  * quietly undo that — a Running/session map — is asserted absent by
15
15
  * `src/__tests__/constraints.test.ts`.
16
16
  */
17
- import { type BoardItem, type BoardItemInput, type BoardListQuery, type BoardPage, type CapabilityDeclaration, type Clock, type CoreStores, type PresenceHint, type SkillPackStore, type TenantId } from '@byok-sdk/core';
17
+ import { type BoardItem, type BoardItemInput, type BoardListQuery, type BoardPage, type CapabilityDeclaration, type Clock, type CoreStores, type PresenceHint, type SkillPackStore, type TenantId, type TenantReadiness } from '@byok-sdk/core';
18
18
  import type { ActivityTail } from './activity';
19
19
  import type { ApprovalTimelineTail } from './approval-timeline';
20
20
  import { type Envelope, type TaskOfferPayload, type TaskOfferWithToolsetsPayload } from '@byok-sdk/protocol';
@@ -127,17 +127,18 @@ export interface ByokCloud {
127
127
  enqueueOffer(tenant: TenantId, deviceId: string, input: EnqueueOfferInput): Promise<EnqueuedOffer>;
128
128
  /** Host control plane: enqueue the additive fail-closed offer variant that requires local MCP toolsets. */
129
129
  enqueueToolsetOffer(tenant: TenantId, deviceId: string, input: EnqueueToolsetOfferInput): Promise<EnqueuedOffer>;
130
+ /** Host control plane: durably request cancellation by tenant/task id. Idempotent. */
131
+ cancelTask(tenant: TenantId, taskId: string, reason?: string): Promise<TaskAttempt>;
130
132
  readTaskAttempt(tenant: TenantId, taskId: string): Promise<TaskAttempt | undefined>;
131
133
  /** The recorded terminal for a task — the first one, re-encoded canonically under the frozen v1 codec (see `recordTerminal`, `inbound.ts`: the stored body is `encodeEnvelope` of the zod-parsed envelope, not the device's original byte sequence). */
132
134
  readTerminalReceipt(tenant: TenantId, taskId: string): Promise<RequestReceipt | undefined>;
133
135
  /**
134
136
  * Host control plane: the same first terminal, decoded into the typed read
135
137
  * model ({@link TerminalResult}) so a host reads result fields, not envelope
136
- * prose. `undefined` ONLY means no terminal fact is recorded yet:
137
- * first-terminal-wins is inherited from the receipt store
138
- * ({@link ByokCloud.readTerminalReceipt} reads the same row), and a declined
139
- * task records no terminal at all use {@link ByokCloud.readTaskAttempt}
140
- * for that attempt status. An absent `document` covers both a legacy
138
+ * prose. An accepted host cancellation tombstone outranks a later device
139
+ * receipt; the raw receipt remains readable as device evidence through
140
+ * {@link ByokCloud.readTerminalReceipt}. `undefined` means neither fact
141
+ * exists. An absent `document` covers both a legacy
141
142
  * pre-`result-document` daemon build and a daemon with no `resultDocument`
142
143
  * extractor; a receipt whose body is not a terminal envelope throws rather
143
144
  * than returning a best-effort shape.
@@ -151,6 +152,8 @@ export interface ByokCloud {
151
152
  /** Human review authority. Device routes cannot produce `done`. */
152
153
  acceptBoardItem(tenant: TenantId, itemId: string): Promise<BoardItem>;
153
154
  listPresence(tenant: TenantId): Promise<readonly PresenceHint[]>;
155
+ /** SDK-owned observation only; never a scheduler or execution authority. */
156
+ readTenantReadiness(tenant: TenantId): Promise<TenantReadiness>;
154
157
  readActivity(tenant: TenantId, taskId: string): Promise<ActivityTail | undefined>;
155
158
  readApprovalTimeline(tenant: TenantId, taskId: string): Promise<ApprovalTimelineTail | undefined>;
156
159
  }
package/dist/errors.d.ts CHANGED
@@ -41,6 +41,8 @@ export declare const CLOUD_ERROR_CODES: {
41
41
  readonly terminal_receipt_unreadable: 'terminal_receipt_unreadable';
42
42
  /** A progress/activity batch exceeded the configured event or byte ceiling. */
43
43
  readonly activity_batch_too_large: 'activity_batch_too_large';
44
+ /** Host control-plane task lookup is tenant-closed and found no task. */
45
+ readonly task_not_found: 'task_not_found';
44
46
  };
45
47
  export type CloudErrorCode = (typeof CLOUD_ERROR_CODES)[keyof typeof CLOUD_ERROR_CODES];
46
48
  export declare class ByokCloudError extends Error {
package/dist/index.d.ts CHANGED
@@ -61,6 +61,6 @@ export { TruthCommitError, isTruthCommitError } from './truth/errors';
61
61
  export type { TruthCommitErrorCode } from './truth/errors';
62
62
  export { CLOUD_STORE_NAMES, TASK_ATTEMPT_STATUSES } from './stores/ports';
63
63
  export { CLOUD_PORT_INTERFACES, CLOUD_PORT_METHODS } from './stores/ports-contract';
64
- export type { BlobContent, BlobContentProxy, BlobDeclaration, BlobObservation, BlobWriteResult, CloudBlobStore, CloudStoreName, CloudStores, DeviceDirectory, DeviceRecord, DeviceRegistration, InboundDedupStore, InboundRateLimiter, NonceStore, PairingCodeClaims, PairingCodeInfo, PairingCodeIssueInput, PairingCodeStore, ProofRequestReceipt, ProofRequestReceiptInput, ProofRequestReceiptStore, RequestReceipt, RequestReceiptStore, TaskAttempt, TaskAttemptStatus, TaskAttemptStore, } from './stores/ports';
65
- export { AllowAllRateLimiter, BLOB_URL_TTL_MS, DEDUP_RING_CAPACITY, InMemoryBlobContentProxy, InMemoryCloudBlobStore, InMemoryActivityStore, InMemoryDeviceDirectory, InMemoryInboundDedupStore, InMemoryNonceStore, InMemoryPairingCodeStore, InMemoryRequestReceiptStore, InMemoryProofRequestReceiptStore, InMemoryTaskAttemptStore, NONCE_TTL_MS, createInMemoryBlobs, createInMemoryCloudStores, } from './stores/in-memory/index';
64
+ export type { BlobContent, BlobContentProxy, BlobDeclaration, BlobObservation, BlobWriteResult, CloudBlobStore, CloudStoreName, CloudStores, DeviceDirectory, DeviceRecord, DeviceRegistration, InboundDedupStore, InboundRateLimiter, NonceStore, PairingCodeClaims, PairingCodeInfo, PairingCodeIssueInput, PairingCodeStore, ProofRequestReceipt, ProofRequestReceiptInput, ProofRequestReceiptStore, RequestReceipt, RequestReceiptStore, TaskAttempt, TaskAttemptStatus, TaskAttemptStore, TaskCancellationMutation, TaskCancellationRequest, TaskCancellationStore, } from './stores/ports';
65
+ export { AllowAllRateLimiter, BLOB_URL_TTL_MS, DEDUP_RING_CAPACITY, InMemoryBlobContentProxy, InMemoryCloudBlobStore, InMemoryActivityStore, InMemoryDeviceDirectory, InMemoryInboundDedupStore, InMemoryNonceStore, InMemoryPairingCodeStore, InMemoryRequestReceiptStore, InMemoryProofRequestReceiptStore, InMemoryTaskAttemptStore, InMemoryTaskCancellationStore, NONCE_TTL_MS, createInMemoryBlobs, createInMemoryCloudStores, } from './stores/in-memory/index';
66
66
  export type { InMemoryBlobStoreOptions, InMemoryBlobs, InMemoryCloudComposition, } from './stores/in-memory/index';
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, authenticateDeviceAssertion, 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,
@@ -2847,6 +3080,7 @@ var TASK_ATTEMPT_STATUSES = [
2847
3080
  "offered",
2848
3081
  "claimed",
2849
3082
  "running",
3083
+ "cancel_requested",
2850
3084
  "complete",
2851
3085
  "failed",
2852
3086
  "cancelled"
@@ -2859,6 +3093,7 @@ var CLOUD_STORE_NAMES = [
2859
3093
  "nonces",
2860
3094
  "dedup",
2861
3095
  "tasks",
3096
+ "cancellations",
2862
3097
  "receipts",
2863
3098
  "proofReceipts",
2864
3099
  "blobs",
@@ -2869,11 +3104,12 @@ var CLOUD_STORE_NAMES = [
2869
3104
  var CLOUD_PORT_METHODS = {
2870
3105
  activity: ["append", "read"],
2871
3106
  approvals: ["append", "read"],
2872
- devices: ["register", "get", "revoke", "list", "resolveByDeviceId"],
3107
+ devices: ["register", "get", "revoke", "list", "readiness", "resolveByDeviceId"],
2873
3108
  pairingCodes: ["issue", "redeem"],
2874
3109
  nonces: ["issue", "validate", "markUsed"],
2875
3110
  dedup: ["checkAndRecord"],
2876
- tasks: ["open", "get", "claim", "recordStatus"],
3111
+ tasks: ["open", "get", "getMany", "claim", "recordStatus"],
3112
+ cancellations: ["request"],
2877
3113
  receipts: ["record", "get"],
2878
3114
  proofReceipts: ["record", "get"],
2879
3115
  // Three methods, not six: the byte-proxy trio moved to `BlobContentProxy`,
@@ -2890,12 +3126,13 @@ var CLOUD_PORT_INTERFACES = {
2890
3126
  nonces: "NonceStore",
2891
3127
  dedup: "InboundDedupStore",
2892
3128
  tasks: "TaskAttemptStore",
3129
+ cancellations: "TaskCancellationStore",
2893
3130
  receipts: "RequestReceiptStore",
2894
3131
  proofReceipts: "ProofRequestReceiptStore",
2895
3132
  blobs: "CloudBlobStore",
2896
3133
  rateLimiter: "InboundRateLimiter"
2897
3134
  };
2898
3135
 
2899
- 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, 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 };
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 };
2900
3137
  //# sourceMappingURL=index.js.map
2901
3138
  //# sourceMappingURL=index.js.map