@byok-sdk/cloud 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,11 +1,73 @@
1
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, PROTOCOL_VERSION, DAEMON_TO_SERVER_TYPES, AGENT_EGRESS_RELIABLE_ACK_CAPABILITY, encodeEnvelope, AgentHomeProjectionCompletionRequestSchema, decodeEnvelope, BYOK_PAIR_PATH, BYOK_CHALLENGE_PATH, BYOK_TOKEN_PATH, BYOK_CAPABILITIES_PATH, BYOK_AGENT_MEMORY_PROJECTIONS_PATH, BYOK_EVENTS_PATH, BYOK_MESSAGES_PATH, BYOK_AGENT_HOME_PROJECTION_COMPLETION_ROUTE, 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, AgentHomeProjectionPayloadSchema, AgentContentReadPayloadSchema, AGENT_EGRESS_POLICY_CAPABILITY, AGENT_EGRESS_FRESH_SESSION_CAPABILITY, AGENT_MESSAGE_EGRESS_CAPABILITY, TERMINAL_PROJECTION_SELECTION_CAPABILITY, TaskOfferForAgentWithEgressFreshPayloadSchema, AgentMessageServerContextSchema, TaskOfferForAgentWithEgressPayloadSchema, TaskOfferForAgentPayloadSchema, AgentMemoryProjectionMutationSchema, AgentMemoryProjectionReceiptSchema, AGENT_MEMORY_PROJECTION_MAX_ORDERING_VALUE, AgentMemoryProjectionEraseResultSchema, STRICT_AGENT_ONLY_CAPABILITY, AGENT_CONTENT_ARTIFACT_READ_CAPABILITY, AGENT_CONTENT_TRANSCRIPT_READ_CAPABILITY, AGENT_CONTENT_WORKSPACE_READ_CAPABILITY, AGENT_HOME_PROJECTION_CAPABILITY, PairRequestSchema, PairResponseSchema, ChallengeRequestSchema, TokenRequestSchema, AgentMemoryProjectionCommitRequestSchema, agentMemoryProjectionBase64UrlByteLength, MessagesSendRequestSchema, PresencePublishRequestSchema, CreateBlobRequestSchema, byokBlobContentPath, AgentEgressAckPayloadSchema, AgentContentReceiptPayloadSchema } from '@byok-sdk/protocol';
3
+ import { AgentEventOrUnknownSchema, PROTOCOL_VERSION, DAEMON_TO_SERVER_TYPES, AGENT_EGRESS_RELIABLE_ACK_CAPABILITY, encodeEnvelope, decodeEnvelope, AgentHomeProjectionCompletionRequestSchema, BYOK_PAIR_PATH, BYOK_CHALLENGE_PATH, BYOK_TOKEN_PATH, BYOK_CAPABILITIES_PATH, BYOK_AGENT_MEMORY_PROJECTIONS_PATH, BYOK_EVENTS_PATH, BYOK_MESSAGES_PATH, BYOK_AGENT_HOME_PROJECTION_COMPLETION_ROUTE, 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, AgentHomeProjectionPayloadSchema, AgentContentReadPayloadSchema, AGENT_EGRESS_POLICY_CAPABILITY, AGENT_EGRESS_FRESH_SESSION_CAPABILITY, AGENT_MESSAGE_EGRESS_CAPABILITY, TERMINAL_PROJECTION_SELECTION_CAPABILITY, TaskOfferForAgentWithEgressFreshPayloadSchema, AgentMessageServerContextSchema, TaskOfferForAgentWithEgressPayloadSchema, TaskOfferForAgentPayloadSchema, AgentMemoryProjectionMutationSchema, AgentMemoryProjectionReceiptSchema, AGENT_MEMORY_PROJECTION_MAX_ORDERING_VALUE, AgentMemoryProjectionEraseResultSchema, STRICT_AGENT_ONLY_CAPABILITY, AGENT_CONTENT_ARTIFACT_READ_CAPABILITY, AGENT_CONTENT_TRANSCRIPT_READ_CAPABILITY, AGENT_CONTENT_WORKSPACE_READ_CAPABILITY, AGENT_HOME_PROJECTION_CAPABILITY, AgentRefSchema, PairRequestSchema, PairResponseSchema, ChallengeRequestSchema, TokenRequestSchema, AgentMemoryProjectionCommitRequestSchema, agentMemoryProjectionBase64UrlByteLength, MessagesSendRequestSchema, PresencePublishRequestSchema, CreateBlobRequestSchema, byokBlobContentPath, AgentEgressAckPayloadSchema, AgentContentReceiptPayloadSchema } from '@byok-sdk/protocol';
4
4
  import { z } from 'zod';
5
5
  import { Hono } from 'hono';
6
6
 
7
7
  // src/index.ts
8
8
 
9
+ // src/approval-control.ts
10
+ var StaleApprovalError = class extends Error {
11
+ constructor(taskId, requestedApprovalId, currentApprovalId) {
12
+ super(
13
+ `cannot resolve approval ${requestedApprovalId} for task ${taskId}: the currently pending approval is ${currentApprovalId ?? "(none recorded)"}`
14
+ );
15
+ this.taskId = taskId;
16
+ this.requestedApprovalId = requestedApprovalId;
17
+ this.currentApprovalId = currentApprovalId;
18
+ this.name = "StaleApprovalError";
19
+ }
20
+ taskId;
21
+ requestedApprovalId;
22
+ currentApprovalId;
23
+ };
24
+ function pendingApproval(tail) {
25
+ if (tail === void 0) return void 0;
26
+ let pending;
27
+ for (const entry of tail.entries) {
28
+ const event = entry.event;
29
+ if (event.type === "approval_requested") {
30
+ pending = {
31
+ sourceEnvelopeId: entry.sourceEnvelopeId,
32
+ revision: entry.revision,
33
+ ...event.approvalId === void 0 ? {} : { approvalId: event.approvalId }
34
+ };
35
+ continue;
36
+ }
37
+ if (pending === void 0) continue;
38
+ if (pending.approvalId === void 0 || pending.approvalId === event.approvalId) {
39
+ pending = void 0;
40
+ }
41
+ }
42
+ return pending;
43
+ }
44
+
45
+ // src/steer-control.ts
46
+ function steerRejectionMessage(taskId, code, status, runtime) {
47
+ switch (code) {
48
+ case "task_terminal":
49
+ return `cannot steer task ${taskId}: task is already terminal (status ${status})`;
50
+ case "task_not_running":
51
+ return `cannot steer task ${taskId}: not running (status ${status})`;
52
+ case "steer_unsupported_runtime":
53
+ return `cannot steer task ${taskId}: claimed runtime ${runtime ?? "(unknown)"} does not support steering`;
54
+ }
55
+ }
56
+ var SteerRejectedError = class extends Error {
57
+ constructor(taskId, code, status, runtime) {
58
+ super(steerRejectionMessage(taskId, code, status, runtime));
59
+ this.taskId = taskId;
60
+ this.code = code;
61
+ this.status = status;
62
+ this.runtime = runtime;
63
+ this.name = "SteerRejectedError";
64
+ }
65
+ taskId;
66
+ code;
67
+ status;
68
+ runtime;
69
+ };
70
+
9
71
  // src/crypto/web-crypto.ts
10
72
  var PAIRING_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
11
73
  var HEX_DIGITS = "0123456789abcdef";
@@ -321,6 +383,15 @@ var CLOUD_ERROR_CODES = {
321
383
  activity_batch_too_large: "activity_batch_too_large",
322
384
  /** Host control-plane task lookup is tenant-closed and found no task. */
323
385
  task_not_found: "task_not_found",
386
+ /**
387
+ * A host `approveTask`/`rejectTask` names a task with no approval to
388
+ * resolve: the durable timeline holds no unresolved `approval_requested`
389
+ * (including a tail that has expired or was never written), the attempt has
390
+ * already reached a terminal status, or no device ever claimed it so there
391
+ * is nothing to notify. Fail-closed by construction — cloud never invents a
392
+ * pending approval to make the call succeed.
393
+ */
394
+ task_not_awaiting_approval: "task_not_awaiting_approval",
324
395
  /** Durable target-device capability is absent, revoked, or unavailable. */
325
396
  agent_capability_missing: "agent_capability_missing",
326
397
  /** Inbound Agent identity did not exactly match the offered identity. */
@@ -387,6 +458,12 @@ var TimelineEventSchema = z.object({
387
458
  sourceEnvelopeId: SourceEnvelopeIdSchema,
388
459
  batchSeq: z.number().int().nonnegative(),
389
460
  eventIndex: z.number().int().nonnegative(),
461
+ /**
462
+ * The producer-declared loss attached to this source batch. It travels with
463
+ * every retained event so a replay includes it in the canonical batch
464
+ * binding rather than treating the aggregate as an incidental counter.
465
+ */
466
+ sourceDropped: z.number().int().nonnegative().optional(),
390
467
  receivedAt: z.iso.datetime(),
391
468
  event: AgentEventOrUnknownSchema
392
469
  });
@@ -420,11 +497,24 @@ function projectTimelineEvents(input, receivedAt) {
420
497
  sourceEnvelopeId: input.sourceEnvelopeId,
421
498
  batchSeq: input.batchSeq,
422
499
  eventIndex,
500
+ sourceDropped: input.dropped,
423
501
  receivedAt,
424
502
  event
425
503
  })
426
504
  );
427
505
  }
506
+ function activitySourceBatchState(entries, input) {
507
+ const existing = entries.filter((entry) => entry.sourceEnvelopeId === input.sourceEnvelopeId).sort((left, right) => left.eventIndex - right.eventIndex);
508
+ if (existing.length === 0) return "absent";
509
+ if (existing.length !== input.events.length) return "conflict";
510
+ for (const [eventIndex, event] of input.events.entries()) {
511
+ const stored = existing[eventIndex];
512
+ if (stored === void 0 || stored.taskId !== input.taskId || stored.batchSeq !== input.batchSeq || stored.eventIndex !== eventIndex || stored.sourceDropped !== input.dropped || JSON.stringify(stored.event) !== JSON.stringify(event)) {
513
+ return "conflict";
514
+ }
515
+ }
516
+ return "same";
517
+ }
428
518
  function parseTimelineEvents(value) {
429
519
  return z.array(TimelineEventSchema).parse(value);
430
520
  }
@@ -469,6 +559,14 @@ function assertBoardLabels(channel, title, limits) {
469
559
  );
470
560
  }
471
561
  }
562
+ function assertTaskAttemptListLimit(limit) {
563
+ if (!Number.isSafeInteger(limit) || limit <= 0) {
564
+ throw new ByokCloudError(
565
+ "coordination_input_invalid",
566
+ `Task attempt list limit must be a positive integer; received ${String(limit)}.`
567
+ );
568
+ }
569
+ }
472
570
  function validateActivityEvents(events, dropped, bounds) {
473
571
  if (!Number.isSafeInteger(dropped) || dropped < 0) {
474
572
  throw new ByokCloudError(
@@ -520,6 +618,9 @@ async function authenticateBearer(header, deps) {
520
618
  const device = await deps.devices.get(tenantId(claims.tenantId), claims.deviceId);
521
619
  if (device === void 0 || device.revoked) return void 0;
522
620
  if (device.productId !== claims.productId) return void 0;
621
+ if (deps.instanceProductId !== void 0 && claims.productId !== deps.instanceProductId) {
622
+ return void 0;
623
+ }
523
624
  return {
524
625
  kind: "device",
525
626
  tenantId: device.tenantId,
@@ -579,6 +680,7 @@ function tenantStoresFor(principal, root) {
579
680
  finalizeAgentMessage: (input) => cloud.tasks.finalizeAgentMessage(tenant, input),
580
681
  get: (taskId) => cloud.tasks.get(tenant, taskId),
581
682
  getMany: (taskIds) => cloud.tasks.getMany(tenant, taskIds),
683
+ list: (query) => cloud.tasks.list(tenant, query),
582
684
  claim: (input) => cloud.tasks.claim(tenant, input),
583
685
  recordStatus: (input) => cloud.tasks.recordStatus(tenant, input)
584
686
  },
@@ -586,7 +688,8 @@ function tenantStoresFor(principal, root) {
586
688
  request: (input) => cloud.cancellations.request(tenant, input)
587
689
  },
588
690
  dedup: {
589
- checkAndRecord: (deviceId, envelopeId) => cloud.dedup.checkAndRecord(tenant, deviceId, envelopeId)
691
+ checkAndRecord: (deviceId, envelopeId) => cloud.dedup.checkAndRecord(tenant, deviceId, envelopeId),
692
+ checkAndRecordAgent: (deviceId, agentRef, envelopeId) => cloud.dedup.checkAndRecordAgent(tenant, deviceId, agentRef, envelopeId)
590
693
  },
591
694
  receipts: {
592
695
  record: (input) => cloud.receipts.record(tenant, input),
@@ -594,7 +697,7 @@ function tenantStoresFor(principal, root) {
594
697
  },
595
698
  egress: {
596
699
  record: (input) => cloud.egress.record(tenant, input),
597
- get: (deviceId, eventId) => cloud.egress.get(tenant, deviceId, eventId)
700
+ get: (deviceId, agentRef, eventId) => cloud.egress.get(tenant, deviceId, agentRef, eventId)
598
701
  },
599
702
  blobs: {
600
703
  createUpload: (reservation) => cloud.blobs.createUpload(tenant, reservation),
@@ -927,11 +1030,20 @@ function tokenHandler(deps) {
927
1030
  }
928
1031
  var CLOUD_PROTOCOL_CAPABILITIES = [
929
1032
  "result-document",
1033
+ // The kernel's inbound handles `task.approval_resolved` on both the plain
1034
+ // and the approval-timeline paths (`packages/cloud/src/inbound.ts:576,610`).
1035
+ // Without this flag `TaskRunner.sendApprovalResolved` returns early and a
1036
+ // locally-resolved approval is never reported on the wire at all.
1037
+ "approval_resolved",
930
1038
  "agent-home-contract",
931
1039
  "agent-home-projection",
932
1040
  "agent-egress-policy",
933
1041
  "agent-egress-reliable-ack",
934
1042
  "agent-egress-fresh-session",
1043
+ // `handleAgentMessagePublish` accepts `agent.message_egress` gated on exactly
1044
+ // this capability (`packages/cloud/src/inbound.ts:124`) — the same inbound
1045
+ // family as the three egress flags above, omitted by the same drift.
1046
+ "agent-message-egress",
935
1047
  "agent-content-workspace-read",
936
1048
  "agent-content-transcript-read",
937
1049
  "agent-content-artifact-read"
@@ -973,6 +1085,9 @@ function eventsHandler(deps) {
973
1085
  afterSeq: scanCursor,
974
1086
  limit: deps.pageLimit
975
1087
  });
1088
+ if (cursor < page.recoverableFrom - 1) {
1089
+ return c.json({ error: "cursor_too_old", recoverableFrom: page.recoverableFrom }, 409);
1090
+ }
976
1091
  if (page.messages.length === 0) break;
977
1092
  const decoded = page.messages.map((message) => decodeEnvelope(message.body));
978
1093
  const offeredTaskIds = decoded.flatMap(
@@ -1018,19 +1133,45 @@ var DEFAULT_APPROVAL_TIMELINE_TTL_MS = 10 * 60 * 1e3;
1018
1133
  var APPROVAL_SUMMARY_MAX_BYTES = 16 * 1024;
1019
1134
  var NonBlankIdSchema = z.string().max(200).regex(/\S/, "value must not be blank");
1020
1135
  var TaskIdSchema = z.string().min(1).max(200);
1021
- var ApprovalTimelineEventSchema = z.discriminatedUnion("type", [
1022
- z.object({
1023
- type: z.literal("approval_requested"),
1024
- summary: z.string(),
1025
- approvalId: NonBlankIdSchema.optional()
1026
- }),
1027
- z.object({
1028
- type: z.literal("approval_resolved"),
1029
- approvalId: NonBlankIdSchema,
1030
- decision: z.enum(["approve", "reject"]),
1031
- resolvedBy: z.enum(["local"]),
1032
- at: z.iso.datetime({ offset: true })
1033
- })
1136
+ var ApprovalRequestedEventSchema = z.object({
1137
+ type: z.literal("approval_requested"),
1138
+ summary: z.string(),
1139
+ approvalId: NonBlankIdSchema.optional()
1140
+ }).strict();
1141
+ var LocalApprovalResolutionEventSchema = z.object({
1142
+ type: z.literal("approval_resolved"),
1143
+ // Absent only when a daemon resolved the single outstanding request without
1144
+ // native approval identity. No id is synthesized.
1145
+ approvalId: NonBlankIdSchema.optional(),
1146
+ decision: z.enum(["approve", "reject"]),
1147
+ // A daemon's `task.approval_resolved` is pure wire observation; it cannot
1148
+ // declare a host command payload.
1149
+ resolvedBy: z.literal("local"),
1150
+ at: z.iso.datetime({ offset: true })
1151
+ }).strict();
1152
+ var HostApprovalResolutionEventSchema = z.object({
1153
+ type: z.literal("approval_resolved"),
1154
+ approvalId: NonBlankIdSchema.optional(),
1155
+ decision: z.literal("approve"),
1156
+ resolvedBy: z.literal("host"),
1157
+ at: z.iso.datetime({ offset: true })
1158
+ }).strict();
1159
+ var HostRejectResolutionEventSchema = z.object({
1160
+ type: z.literal("approval_resolved"),
1161
+ approvalId: NonBlankIdSchema.optional(),
1162
+ decision: z.literal("reject"),
1163
+ resolvedBy: z.literal("host"),
1164
+ // Required even when the host deliberately omitted a wire reason. `null`
1165
+ // records that complete payload, so a historical host reject with no field
1166
+ // cannot be mistaken for a recoverable executable command.
1167
+ reason: z.string().nullable(),
1168
+ at: z.iso.datetime({ offset: true })
1169
+ }).strict();
1170
+ var ApprovalTimelineEventSchema = z.union([
1171
+ ApprovalRequestedEventSchema,
1172
+ LocalApprovalResolutionEventSchema,
1173
+ HostApprovalResolutionEventSchema,
1174
+ HostRejectResolutionEventSchema
1034
1175
  ]);
1035
1176
  var ApprovalObservationSchema = z.object({
1036
1177
  taskId: TaskIdSchema,
@@ -1073,6 +1214,20 @@ function validateApprovalTimelineAppend(input) {
1073
1214
  }
1074
1215
  return { capacity, ttlMs, event: parsed.data.event };
1075
1216
  }
1217
+ function validateApprovalTimelineResolve(input) {
1218
+ const validated = validateApprovalTimelineAppend(input);
1219
+ const expected = z.object({
1220
+ expectedSourceEnvelopeId: NonBlankIdSchema,
1221
+ expectedRevision: z.number().int().positive()
1222
+ }).safeParse(input);
1223
+ if (!expected.success || validated.event.type !== "approval_resolved" || validated.event.resolvedBy !== "host") {
1224
+ throw new ByokCloudError(
1225
+ "coordination_input_invalid",
1226
+ "Host approval resolution requires an exact pending source/revision and a host resolution event."
1227
+ );
1228
+ }
1229
+ return validated;
1230
+ }
1076
1231
  function approvalTimelineKey(tenant, taskId) {
1077
1232
  return tenantKey(tenant, taskId);
1078
1233
  }
@@ -1096,6 +1251,10 @@ async function projectTerminalToReview(board, taskId) {
1096
1251
  throw caught;
1097
1252
  }
1098
1253
  }
1254
+ function agentReliabilityKey(domain, deviceId, agentRef, id) {
1255
+ const exact = AgentRefSchema.parse(agentRef);
1256
+ return `${domain}:${JSON.stringify([deviceId, exact.agentId, exact.profileRevision, id])}`;
1257
+ }
1099
1258
 
1100
1259
  // src/inbound.ts
1101
1260
  async function handleAgentMessagePublish(stores, deviceId, taskId, payload, consume, alreadyDeduplicated) {
@@ -1112,7 +1271,6 @@ async function handleAgentMessagePublish(stores, deviceId, taskId, payload, cons
1112
1271
  if (!sameAgentRef(frozen.agentRef, payload.agentRef) || frozen.context === void 0 || frozen.requirement?.contract !== payload.contract || frozen.sessionRef !== void 0 && frozen.sessionRef !== payload.sessionRef || frozen.requirement.contentType !== payload.contentType || typeof frozen.requirement.maxBytes !== "number" || payload.byteCount > frozen.requirement.maxBytes) return { outcome: "rejected" };
1113
1272
  const priorDisposition = await readAgentMessageDisposition(stores, deviceId, taskId, payload);
1114
1273
  if (priorDisposition !== void 0) return { outcome: "duplicate", disposition: priorDisposition };
1115
- if (alreadyDeduplicated) return { outcome: "rejected" };
1116
1274
  const payloadBody = JSON.stringify(payload);
1117
1275
  const reservation = await stores.tasks.reserveAgentMessage({
1118
1276
  taskId,
@@ -1120,17 +1278,15 @@ async function handleAgentMessagePublish(stores, deviceId, taskId, payload, cons
1120
1278
  messageId: payload.messageId,
1121
1279
  payloadBody
1122
1280
  });
1123
- if (reservation === "pending") return { outcome: "rejected" };
1124
- if (reservation !== "reserved") return { outcome: "rejected" };
1281
+ if (reservation !== "reserved" && reservation !== "pending") return { outcome: "rejected" };
1125
1282
  let decision;
1126
1283
  if (consume === void 0) {
1284
+ if (reservation === "pending") {
1285
+ throw new Error("Agent message consumer is unavailable while reconciling an exact pending admission.");
1286
+ }
1127
1287
  decision = { outcome: "held", reasonCode: "consumer_unavailable" };
1128
1288
  } else {
1129
- try {
1130
- decision = await consume({ tenant: stores.tenant, deviceId, taskId, context: frozen.context, payload });
1131
- } catch {
1132
- decision = { outcome: "held", reasonCode: "consumer_failed" };
1133
- }
1289
+ decision = await consume({ tenant: stores.tenant, deviceId, taskId, context: frozen.context, payload });
1134
1290
  }
1135
1291
  const disposition = agentMessageDisposition(payload, decision);
1136
1292
  const admission = await stores.tasks.finalizeAgentMessage({
@@ -1195,17 +1351,17 @@ function contentReadCapability(surface) {
1195
1351
  function hasCapabilities(capabilities, required) {
1196
1352
  return capabilities !== void 0 && required.every((capability) => capabilities.includes(capability));
1197
1353
  }
1198
- function contentRequestReceiptKey(deviceId, requestId) {
1199
- return `agent-content-request:${deviceId}:${requestId}`;
1354
+ function contentRequestReceiptKey(deviceId, agentRef, requestId) {
1355
+ return agentReliabilityKey("agent-content-request", deviceId, agentRef, requestId);
1200
1356
  }
1201
- function contentReceiptKey(deviceId, requestId) {
1202
- return `agent-content:${deviceId}:${requestId}`;
1357
+ function contentReceiptKey(deviceId, agentRef, requestId) {
1358
+ return agentReliabilityKey("agent-content-receipt", deviceId, agentRef, requestId);
1203
1359
  }
1204
1360
  function matchesContentReadReceipt(request, receipt) {
1205
1361
  return request.requestId === receipt.requestId && request.surface === receipt.surface && request.actor.kind === receipt.actor.kind && request.actor.id === receipt.actor.id && sameAgentRef(request.agentRef, receipt.agentRef) && request.sessionRef === receipt.sessionRef && request.runtime === receipt.runtime && request.cwd === receipt.cwd && request.policyRevision === receipt.policyRevision && request.target === receipt.target && request.mimeType === receipt.mimeType && request.decodeAs === receipt.decodeAs;
1206
1362
  }
1207
- async function readContentRequest(stores, deviceId, requestId) {
1208
- const stored = await stores.receipts.get(contentRequestReceiptKey(deviceId, requestId));
1363
+ async function readContentRequest(stores, deviceId, agentRef, requestId) {
1364
+ const stored = await stores.receipts.get(contentRequestReceiptKey(deviceId, agentRef, requestId));
1209
1365
  if (stored === void 0) return void 0;
1210
1366
  try {
1211
1367
  return AgentContentReadPayloadSchema.parse(JSON.parse(stored.body));
@@ -1228,7 +1384,23 @@ function inboundAgentRef(envelope) {
1228
1384
  function agentRefEchoRequired(envelope) {
1229
1385
  return envelope.type === "task.claim" || envelope.type === "task.decline" || envelope.type === "task.complete" || envelope.type === "task.fail" || envelope.type === "task.cancelled";
1230
1386
  }
1231
- async function handleInboundEnvelope(stores, deviceId, envelope, activityBounds = DEFAULT_ACTIVITY_BOUNDS, agentMessageConsume) {
1387
+ async function handleInboundEnvelope(stores, deviceId, envelope, activityBounds = DEFAULT_ACTIVITY_BOUNDS, agentMessageConsume, observer) {
1388
+ const outcome = await applyInboundGate(
1389
+ stores,
1390
+ deviceId,
1391
+ envelope,
1392
+ activityBounds,
1393
+ agentMessageConsume
1394
+ );
1395
+ if (outcome === "accepted" && observer !== void 0) {
1396
+ try {
1397
+ observer.onInboundCommitted({ tenantId: stores.tenant, deviceId, envelope, outcome });
1398
+ } catch {
1399
+ }
1400
+ }
1401
+ return outcome;
1402
+ }
1403
+ async function applyInboundGate(stores, deviceId, envelope, activityBounds, agentMessageConsume) {
1232
1404
  if (!await stores.rateLimiter.consume(deviceId)) return "rate_limited";
1233
1405
  if (envelope.type === "conn.hello") {
1234
1406
  if (envelope.payload.deviceId !== deviceId) return "rejected";
@@ -1237,20 +1409,20 @@ async function handleInboundEnvelope(stores, deviceId, envelope, activityBounds
1237
1409
  if (device === void 0 || device.revoked || device.productId !== envelope.payload.productId) {
1238
1410
  return "rejected";
1239
1411
  }
1240
- if (await stores.dedup.checkAndRecord(deviceId, envelope.id)) return "duplicate";
1241
- return await stores.devices.recordCapabilities({ capabilities: envelope.payload.capabilities }) === void 0 ? "rejected" : "accepted";
1412
+ if (await stores.devices.recordCapabilities({ capabilities: envelope.payload.capabilities }) === void 0) {
1413
+ return "rejected";
1414
+ }
1415
+ return completeInboundEnvelope(stores, deviceId, envelope.id, void 0);
1242
1416
  }
1243
1417
  if (!DAEMON_TO_SERVER_TYPES.includes(envelope.type)) return "rejected";
1244
1418
  if (envelope.type === "agent.message.publish") {
1245
- const alreadyDeduplicated = await stores.dedup.checkAndRecord(deviceId, envelope.id);
1246
- return (await handleAgentMessagePublish(
1419
+ const message = await handleAgentMessagePublish(
1247
1420
  stores,
1248
1421
  deviceId,
1249
1422
  envelope.task_id,
1250
1423
  envelope.payload,
1251
- agentMessageConsume,
1252
- alreadyDeduplicated
1253
- )).outcome;
1424
+ agentMessageConsume);
1425
+ return message.outcome === "rejected" ? "rejected" : completeInboundEnvelope(stores, deviceId, envelope.id, envelope.payload.agentRef);
1254
1426
  }
1255
1427
  if (envelope.type === "agent.egress.reliable") {
1256
1428
  const device = await stores.devices.get(deviceId);
@@ -1262,19 +1434,21 @@ async function handleInboundEnvelope(stores, deviceId, envelope, activityBounds
1262
1434
  payload: envelope.payload,
1263
1435
  receiptId: crypto.randomUUID()
1264
1436
  });
1265
- return outcome.created || sameReliableEgress(outcome.record.payload, envelope.payload) ? outcome.created ? "accepted" : "duplicate" : "rejected";
1437
+ if (!outcome.created && !sameReliableEgress(outcome.record.payload, envelope.payload)) return "rejected";
1438
+ return completeInboundEnvelope(stores, deviceId, envelope.id, envelope.payload.agentRef);
1266
1439
  }
1267
1440
  if (envelope.type === "agent.content.receipt") {
1268
1441
  const device = await stores.devices.get(deviceId);
1269
1442
  if (device === void 0 || device.revoked || envelope.payload.eventId !== envelope.payload.requestId || !hasCapabilities(device.capabilities, [contentReadCapability(envelope.payload.surface), AGENT_EGRESS_RELIABLE_ACK_CAPABILITY])) {
1270
1443
  return "rejected";
1271
1444
  }
1272
- const request = await readContentRequest(stores, deviceId, envelope.payload.requestId);
1445
+ const request = await readContentRequest(stores, deviceId, envelope.payload.agentRef, envelope.payload.requestId);
1273
1446
  if (request === void 0 || !matchesContentReadReceipt(request, envelope.payload)) return "rejected";
1274
- const key = contentReceiptKey(deviceId, envelope.payload.requestId);
1447
+ const key = contentReceiptKey(deviceId, envelope.payload.agentRef, envelope.payload.requestId);
1275
1448
  const body = JSON.stringify(envelope.payload);
1276
1449
  const receipt = await stores.receipts.record({ key, body });
1277
- return receipt.created ? "accepted" : receipt.receipt.body === body ? "duplicate" : "rejected";
1450
+ if (!receipt.created && receipt.receipt.body !== body) return "rejected";
1451
+ return completeInboundEnvelope(stores, deviceId, envelope.id, envelope.payload.agentRef);
1278
1452
  }
1279
1453
  const taskId = envelope.task_id;
1280
1454
  if (taskId === void 0) return "rejected";
@@ -1284,6 +1458,7 @@ async function handleInboundEnvelope(stores, deviceId, envelope, activityBounds
1284
1458
  if (attempt?.agentRef !== void 0 && agentRefEchoRequired(envelope) && !sameAgentRef(attempt.agentRef, inboundAgentRef(envelope))) {
1285
1459
  return "rejected";
1286
1460
  }
1461
+ if (terminalStatus(envelope) !== void 0 && attempt === void 0) return "rejected";
1287
1462
  if (envelope.type === "task.progress" && envelope.payload.events.length > 0) {
1288
1463
  try {
1289
1464
  validateActivityBatch(
@@ -1312,14 +1487,22 @@ async function handleInboundEnvelope(stores, deviceId, envelope, activityBounds
1312
1487
  throw caught;
1313
1488
  }
1314
1489
  }
1315
- if (await stores.dedup.checkAndRecord(deviceId, envelope.id)) return "duplicate";
1316
1490
  await applyLifecycle(stores, deviceId, taskId, envelope, activityBounds, attempt?.agentRef);
1317
- return "accepted";
1491
+ return completeInboundEnvelope(stores, deviceId, envelope.id, attempt?.agentRef);
1492
+ }
1493
+ async function completeInboundEnvelope(stores, deviceId, envelopeId, agentRef) {
1494
+ const duplicate = agentRef === void 0 ? await stores.dedup.checkAndRecord(deviceId, envelopeId) : await stores.dedup.checkAndRecordAgent(deviceId, agentRef, envelopeId);
1495
+ return duplicate ? "duplicate" : "accepted";
1318
1496
  }
1319
1497
  async function applyLifecycle(stores, deviceId, taskId, envelope, activityBounds, persistedAgentRef) {
1320
1498
  switch (envelope.type) {
1321
1499
  case "task.claim":
1322
- await stores.tasks.claim({ taskId, deviceId });
1500
+ await stores.tasks.claim({
1501
+ taskId,
1502
+ deviceId,
1503
+ ...envelope.payload.runtime === void 0 ? {} : { runtime: envelope.payload.runtime },
1504
+ ...envelope.payload.capabilities === void 0 ? {} : { capabilities: envelope.payload.capabilities }
1505
+ });
1323
1506
  return;
1324
1507
  case "task.started":
1325
1508
  await stores.tasks.recordStatus({
@@ -1329,11 +1512,7 @@ async function applyLifecycle(stores, deviceId, taskId, envelope, activityBounds
1329
1512
  });
1330
1513
  return;
1331
1514
  case "task.decline":
1332
- await stores.tasks.recordStatus({
1333
- taskId,
1334
- status: "failed",
1335
- ...persistedAgentRef === void 0 ? {} : { agentRef: persistedAgentRef }
1336
- });
1515
+ await recordTerminal(stores, taskId, envelope);
1337
1516
  return;
1338
1517
  case "task.progress":
1339
1518
  if (envelope.payload.events.length > 0) {
@@ -1357,13 +1536,13 @@ async function applyLifecycle(stores, deviceId, taskId, envelope, activityBounds
1357
1536
  await stores.approvals.append(approvalTimelineAppendInput(taskId, envelope));
1358
1537
  return;
1359
1538
  case "task.complete":
1360
- await recordTerminal(stores, taskId, envelope, "complete");
1539
+ await recordTerminal(stores, taskId, envelope);
1361
1540
  return;
1362
1541
  case "task.fail":
1363
- await recordTerminal(stores, taskId, envelope, "failed");
1542
+ await recordTerminal(stores, taskId, envelope);
1364
1543
  return;
1365
1544
  case "task.cancelled":
1366
- await recordTerminal(stores, taskId, envelope, "cancelled");
1545
+ await recordTerminal(stores, taskId, envelope);
1367
1546
  return;
1368
1547
  default:
1369
1548
  return;
@@ -1397,14 +1576,18 @@ function approvalTimelineAppendInput(taskId, envelope) {
1397
1576
  return void 0;
1398
1577
  }
1399
1578
  }
1400
- async function recordTerminal(stores, taskId, envelope, status) {
1401
- const attempt = await stores.tasks.get(taskId);
1402
- const { created } = await stores.receipts.record({
1579
+ async function recordTerminal(stores, taskId, envelope) {
1580
+ const receipt = await stores.receipts.record({
1403
1581
  key: terminalReceiptKey(taskId),
1404
1582
  body: encodeEnvelope(envelope)
1405
1583
  });
1584
+ const winner = terminalReceiptBinding(taskId, receipt.receipt.body);
1585
+ if (winner === void 0) {
1586
+ throw new Error(`terminal receipt for ${taskId} is not a canonical terminal binding`);
1587
+ }
1588
+ const attempt = await stores.tasks.get(taskId);
1406
1589
  if (attempt?.cancellation !== void 0) {
1407
- if (status === "cancelled") {
1590
+ if (winner.status === "cancelled") {
1408
1591
  await stores.tasks.recordStatus({
1409
1592
  taskId,
1410
1593
  status: "cancelled",
@@ -1413,17 +1596,40 @@ async function recordTerminal(stores, taskId, envelope, status) {
1413
1596
  }
1414
1597
  return;
1415
1598
  }
1416
- if (!created) return;
1417
- const cause = envelope.type === "task.fail" || envelope.type === "task.cancelled" ? envelope.payload.reason : void 0;
1418
1599
  const recordedAttempt = await stores.tasks.recordStatus({
1419
1600
  taskId,
1420
- status,
1601
+ status: winner.status,
1421
1602
  ...attempt?.agentRef === void 0 ? {} : { agentRef: attempt.agentRef },
1422
- ...cause === void 0 ? {} : { terminalCause: cause }
1603
+ ...winner.cause === void 0 ? {} : { terminalCause: winner.cause }
1423
1604
  });
1424
- if (recordedAttempt?.status !== status || recordedAttempt.cancellation !== void 0) return;
1605
+ if (recordedAttempt?.status !== winner.status || recordedAttempt.cancellation !== void 0) return;
1425
1606
  await projectTerminalToReview(stores.board, taskId);
1426
1607
  }
1608
+ function terminalStatus(envelope) {
1609
+ switch (envelope.type) {
1610
+ case "task.complete":
1611
+ return "complete";
1612
+ case "task.fail":
1613
+ case "task.decline":
1614
+ return "failed";
1615
+ case "task.cancelled":
1616
+ return "cancelled";
1617
+ default:
1618
+ return void 0;
1619
+ }
1620
+ }
1621
+ function terminalReceiptBinding(taskId, body) {
1622
+ let envelope;
1623
+ try {
1624
+ envelope = decodeEnvelope(body);
1625
+ } catch {
1626
+ return void 0;
1627
+ }
1628
+ const status = terminalStatus(envelope);
1629
+ if (status === void 0 || envelope.task_id !== taskId || encodeEnvelope(envelope) !== body) return void 0;
1630
+ const cause = envelope.type === "task.fail" || envelope.type === "task.cancelled" || envelope.type === "task.decline" ? envelope.payload.reason : void 0;
1631
+ return { status, ...cause === void 0 ? {} : { cause } };
1632
+ }
1427
1633
 
1428
1634
  // src/handlers/messages.ts
1429
1635
  var MESSAGES_JSON_BODY_MAX_BYTES = 2 * 1024 * 1024;
@@ -1444,7 +1650,8 @@ function messagesHandler(deps) {
1444
1650
  device.deviceId,
1445
1651
  envelope,
1446
1652
  deps.activityBounds,
1447
- envelope.type === "agent.message.publish" ? deps.agentMessage?.consume : void 0
1653
+ envelope.type === "agent.message.publish" ? deps.agentMessage?.consume : void 0,
1654
+ deps.observer
1448
1655
  );
1449
1656
  if (outcome === "rate_limited") return c.json({ error: "rate limit exceeded" }, 429);
1450
1657
  if (envelope.type === "agent.message.publish") {
@@ -1454,7 +1661,7 @@ function messagesHandler(deps) {
1454
1661
  }
1455
1662
  }
1456
1663
  if (envelope.type === "agent.egress.reliable" && (outcome === "accepted" || outcome === "duplicate")) {
1457
- const record = await stores.egress.get(device.deviceId, envelope.payload.eventId);
1664
+ const record = await stores.egress.get(device.deviceId, envelope.payload.agentRef, envelope.payload.eventId);
1458
1665
  if (record === void 0) {
1459
1666
  throw new Error(`Accepted reliable egress ${envelope.payload.eventId} has no durable receipt record.`);
1460
1667
  }
@@ -2273,11 +2480,11 @@ var CloudRouteRegistry = class {
2273
2480
  return this.#app.fetch;
2274
2481
  }
2275
2482
  };
2276
- function agentHomeProjectionRequestKey(deviceId, requestId) {
2277
- return `agent-home-projection:v1:${deviceId}:${requestId}:request`;
2483
+ function agentHomeProjectionRequestKey(deviceId, agentRef, requestId) {
2484
+ return agentReliabilityKey("agent-home-projection-request", deviceId, agentRef, requestId);
2278
2485
  }
2279
- function agentHomeProjectionCompletionKey(deviceId, requestId) {
2280
- return `agent-home-projection:v1:${deviceId}:${requestId}:completion`;
2486
+ function agentHomeProjectionCompletionKey(deviceId, agentRef, requestId) {
2487
+ return agentReliabilityKey("agent-home-projection-completion", deviceId, agentRef, requestId);
2281
2488
  }
2282
2489
  function sameAgentHomeProjectionRequest(expected, actual) {
2283
2490
  return expected.requestId === actual.requestId && expected.agentRef.agentId === actual.agentRef.agentId && expected.agentRef.profileRevision === actual.agentRef.profileRevision && expected.projectionHash === actual.projectionHash && JSON.stringify(expected.projection) === JSON.stringify(actual.projection);
@@ -2311,7 +2518,7 @@ function parseCompletionBody(body) {
2311
2518
  }
2312
2519
  }
2313
2520
  async function readAgentHomeProjectionStatus(receipts, tenant, deviceId, input) {
2314
- const storedRequest = await receipts.get(agentHomeProjectionRequestKey(deviceId, input.requestId));
2521
+ const storedRequest = await receipts.get(agentHomeProjectionRequestKey(deviceId, input.agentRef, input.requestId));
2315
2522
  if (storedRequest === void 0) return void 0;
2316
2523
  const request = parseRequestBody(storedRequest.body);
2317
2524
  if (!statusInputMatchesAgentHomeProjection(request, input)) {
@@ -2320,7 +2527,7 @@ async function readAgentHomeProjectionStatus(receipts, tenant, deviceId, input)
2320
2527
  `Agent-home projection request ${input.requestId} does not match its immutable desired fact.`
2321
2528
  );
2322
2529
  }
2323
- const storedCompletion = await receipts.get(agentHomeProjectionCompletionKey(deviceId, input.requestId));
2530
+ const storedCompletion = await receipts.get(agentHomeProjectionCompletionKey(deviceId, request.agentRef, input.requestId));
2324
2531
  if (storedCompletion === void 0) {
2325
2532
  return {
2326
2533
  tenantId: tenant,
@@ -2350,7 +2557,7 @@ async function readAgentHomeProjectionStatus(receipts, tenant, deviceId, input)
2350
2557
  }
2351
2558
  async function recordAgentHomeProjectionCompletion(receipts, tenant, deviceId, receiptInput) {
2352
2559
  const receipt = AgentHomeProjectionCompletionRequestSchema.parse(receiptInput);
2353
- const storedRequest = await receipts.get(agentHomeProjectionRequestKey(deviceId, receipt.requestId));
2560
+ const storedRequest = await receipts.get(agentHomeProjectionRequestKey(deviceId, receipt.agentRef, receipt.requestId));
2354
2561
  if (storedRequest === void 0) {
2355
2562
  throw new ByokCloudError(
2356
2563
  "agent_home_projection_request_not_found",
@@ -2366,7 +2573,7 @@ async function recordAgentHomeProjectionCompletion(receipts, tenant, deviceId, r
2366
2573
  }
2367
2574
  const body = JSON.stringify(receipt);
2368
2575
  const storedCompletion = await receipts.record({
2369
- key: agentHomeProjectionCompletionKey(deviceId, receipt.requestId),
2576
+ key: agentHomeProjectionCompletionKey(deviceId, request.agentRef, receipt.requestId),
2370
2577
  body
2371
2578
  });
2372
2579
  if (!storedCompletion.created && storedCompletion.receipt.body !== body) {
@@ -2420,6 +2627,21 @@ function projectTerminalResult(taskId, receipt) {
2420
2627
  ...envelope.payload.usage !== void 0 ? { usage: envelope.payload.usage } : {},
2421
2628
  recordedAt: receipt.recordedAt
2422
2629
  };
2630
+ // `task.decline` is the pre-claim terminal (§3.2, `Offered -> Failed`).
2631
+ // It projects onto the same `failed` state as `task.fail` — there is no
2632
+ // separate declined state — and carries the device's own `retryable`
2633
+ // verbatim, exactly like `task.fail` does; this projection never decides
2634
+ // that a decline is retryable on the device's behalf.
2635
+ case "task.decline":
2636
+ return {
2637
+ taskId,
2638
+ state: "failed",
2639
+ ...envelope.payload.agentRef === void 0 ? {} : { agentRef: envelope.payload.agentRef },
2640
+ reason: envelope.payload.reason,
2641
+ terminalCause: envelope.payload.reason,
2642
+ ...envelope.payload.retryable !== void 0 ? { retryable: envelope.payload.retryable } : {},
2643
+ recordedAt: receipt.recordedAt
2644
+ };
2423
2645
  case "task.cancelled":
2424
2646
  return {
2425
2647
  taskId,
@@ -2444,9 +2666,25 @@ var DEFAULT_LONG_POLL_HOLD_MS = 5e4;
2444
2666
  var DEFAULT_LONG_POLL_INTERVAL_MS = 250;
2445
2667
  var DEFAULT_EVENTS_PAGE_LIMIT = 50;
2446
2668
  var AGENT_HOME_CONTRACT_CAPABILITY = "agent-home-contract";
2669
+ function isTerminalAttemptStatus(status) {
2670
+ return status === "complete" || status === "failed" || status === "cancelled";
2671
+ }
2447
2672
  function sameAgentRef2(expected, actual) {
2448
2673
  return actual?.agentId === expected.agentId && actual.profileRevision === expected.profileRevision;
2449
2674
  }
2675
+ function uuidFromSha256(value) {
2676
+ const match = /^sha256:([0-9a-f]{64})$/.exec(value);
2677
+ if (match === null) throw new Error("CloudCrypto.sha256 must return canonical sha256 hex");
2678
+ const digest = match[1];
2679
+ if (digest === void 0) throw new Error("CloudCrypto.sha256 must return canonical sha256 hex");
2680
+ const hex = digest.slice(0, 32).split("");
2681
+ hex[12] = "8";
2682
+ hex[16] = ["8", "9", "a", "b"][Number.parseInt(hex[16], 16) & 3];
2683
+ return `${hex.slice(0, 8).join("")}-${hex.slice(8, 12).join("")}-${hex.slice(12, 16).join("")}-${hex.slice(16, 20).join("")}-${hex.slice(20).join("")}`;
2684
+ }
2685
+ function sameOfferEnvelope(expected, actual) {
2686
+ return expected.id === actual.id && expected.type === actual.type && expected.task_id === actual.task_id && JSON.stringify(expected.payload) === JSON.stringify(actual.payload);
2687
+ }
2450
2688
  function createByokCloud(options) {
2451
2689
  const declaration = parseDeclaration(options.capabilities);
2452
2690
  assertNoOverDeclaration(
@@ -2468,7 +2706,11 @@ function createByokCloud(options) {
2468
2706
  });
2469
2707
  const deviceRouteDeps = {
2470
2708
  root,
2471
- bearer: { tokenSigner: options.tokenSigner, devices: options.cloud.devices }
2709
+ bearer: {
2710
+ tokenSigner: options.tokenSigner,
2711
+ devices: options.cloud.devices,
2712
+ ...options.instanceProductId === void 0 ? {} : { instanceProductId: options.instanceProductId }
2713
+ }
2472
2714
  };
2473
2715
  const registry = new CloudRouteRegistry();
2474
2716
  const activityBounds = {
@@ -2513,7 +2755,8 @@ function createByokCloud(options) {
2513
2755
  appendReliableEgressAck: enqueueReliableEgressAck,
2514
2756
  appendContentReceiptAck: enqueueContentReceiptAck,
2515
2757
  agentMessage: options.agentMessage,
2516
- appendAgentMessageDisposition: enqueueAgentMessageDisposition
2758
+ appendAgentMessageDisposition: enqueueAgentMessageDisposition,
2759
+ observer: options.observer
2517
2760
  })
2518
2761
  );
2519
2762
  }
@@ -2645,56 +2888,60 @@ function createByokCloud(options) {
2645
2888
  async function enqueueTaskEnvelope(tenant, deviceId, requestedTaskId, agentRef, buildEnvelope, beforeMailbox) {
2646
2889
  const stores = tenantStoresFor(controlPlane(tenant), root);
2647
2890
  const taskId = requestedTaskId ?? `task_${options.crypto.randomUuid()}`;
2648
- const messageId = options.crypto.randomUuid();
2891
+ const messageId = uuidFromSha256(
2892
+ await options.crypto.sha256(
2893
+ new TextEncoder().encode(
2894
+ JSON.stringify({ domain: "byok:task-offer", tenant, taskId, deviceId, agentRef })
2895
+ )
2896
+ )
2897
+ );
2649
2898
  const reservation = agentRef === void 0 ? void 0 : await stores.tasks.reserveAgentOffer({ taskId, deviceId, agentRef });
2650
- const openedBeforeAppend = reservation?.attempt;
2651
- if (agentRef !== void 0 && (openedBeforeAppend === void 0 || openedBeforeAppend.deviceId !== deviceId || openedBeforeAppend.agentRef === void 0 || !sameAgentRef2(agentRef, openedBeforeAppend.agentRef))) {
2899
+ const openedBeforeAppend = reservation?.attempt ?? (agentRef === void 0 ? await stores.tasks.open({ taskId, deviceId }) : void 0);
2900
+ if (openedBeforeAppend === void 0 || openedBeforeAppend.deviceId !== deviceId || (agentRef === void 0 ? openedBeforeAppend.agentRef !== void 0 : openedBeforeAppend.agentRef === void 0 || !sameAgentRef2(agentRef, openedBeforeAppend.agentRef))) {
2652
2901
  throw new ByokCloudError(
2653
- "agent_ref_mismatch",
2654
- `Task ${taskId} already has a different durable Agent identity or target device.`
2902
+ agentRef === void 0 ? "coordination_input_invalid" : "agent_ref_mismatch",
2903
+ `Task ${taskId} already has a different durable target or Agent identity.`
2655
2904
  );
2656
2905
  }
2657
- if (reservation !== void 0 && !reservation.created) {
2906
+ const offerReceiptKey = `task-offer:${messageId}`;
2907
+ const deliveryReceiptKey = `task-offer-delivered:${messageId}`;
2908
+ if (agentRef !== void 0 && reservation?.created === false && await stores.receipts.get(deliveryReceiptKey) !== void 0) {
2658
2909
  throw new ByokCloudError(
2659
2910
  "agent_task_already_exists",
2660
2911
  `Task ${taskId} already has a durable Agent attempt and cannot be enqueued again.`
2661
2912
  );
2662
2913
  }
2663
- let message;
2664
- try {
2665
- await beforeMailbox?.(stores, taskId);
2666
- message = await stores.mailbox.append({
2667
- deviceId,
2668
- messageId,
2669
- materialize: async (seq) => {
2670
- const envelope2 = buildEnvelope(taskId, seq, messageId);
2671
- const body = encodeEnvelope(envelope2);
2672
- const bytes = new TextEncoder().encode(body);
2673
- return {
2674
- body,
2675
- bodyHash: contentHash(await options.crypto.sha256(bytes)),
2676
- byteSize: BigInt(bytes.length)
2677
- };
2678
- }
2679
- });
2680
- } catch (error) {
2681
- if (openedBeforeAppend !== void 0) {
2682
- try {
2683
- await stores.tasks.recordStatus({
2684
- taskId,
2685
- status: "failed",
2686
- agentRef,
2687
- terminalCause: "mailbox append failed before Agent offer delivery"
2688
- });
2689
- } catch (cleanupError) {
2690
- throw new AggregateError(
2691
- [error, cleanupError],
2692
- `Agent task ${taskId} could not be delivered or durably closed`
2693
- );
2694
- }
2695
- }
2696
- throw error;
2914
+ const proposed = buildEnvelope(taskId, 0, messageId);
2915
+ const proposedBody = JSON.stringify({
2916
+ deviceId,
2917
+ type: proposed.type,
2918
+ payload: proposed.payload
2919
+ });
2920
+ const recorded = await stores.receipts.record({
2921
+ key: offerReceiptKey,
2922
+ body: proposedBody
2923
+ });
2924
+ if (!recorded.created && recorded.receipt.body !== proposedBody) {
2925
+ throw new ByokCloudError(
2926
+ "coordination_input_invalid",
2927
+ `Task ${taskId} already has a different executable offer body.`
2928
+ );
2697
2929
  }
2930
+ await beforeMailbox?.(stores, taskId);
2931
+ const message = await stores.mailbox.append({
2932
+ deviceId,
2933
+ messageId,
2934
+ materialize: async (seq) => {
2935
+ const envelope2 = buildEnvelope(taskId, seq, messageId);
2936
+ const body = encodeEnvelope(envelope2);
2937
+ const bytes = new TextEncoder().encode(body);
2938
+ return {
2939
+ body,
2940
+ bodyHash: contentHash(await options.crypto.sha256(bytes)),
2941
+ byteSize: BigInt(bytes.length)
2942
+ };
2943
+ }
2944
+ });
2698
2945
  const envelope = decodeEnvelope(message.body);
2699
2946
  if (envelope.seq !== message.seq) {
2700
2947
  throw new ByokCloudError(
@@ -2702,11 +2949,22 @@ function createByokCloud(options) {
2702
2949
  `Mailbox stored offer seq ${String(envelope.seq)} at row ${message.seq}; the daemon's redelivery cursor would be wrong.`
2703
2950
  );
2704
2951
  }
2705
- const attempt = openedBeforeAppend ?? await stores.tasks.open({
2706
- taskId,
2707
- deviceId
2708
- });
2709
- return { taskId, seq: message.seq, envelope, attempt };
2952
+ if (!sameOfferEnvelope(buildEnvelope(taskId, message.seq, messageId), envelope)) {
2953
+ throw new ByokCloudError(
2954
+ "coordination_input_invalid",
2955
+ `Task ${taskId} already has a different executable offer body.`
2956
+ );
2957
+ }
2958
+ if (agentRef !== void 0) {
2959
+ const delivered = await stores.receipts.record({ key: deliveryReceiptKey, body: messageId });
2960
+ if (reservation?.created === false && !delivered.created) {
2961
+ throw new ByokCloudError(
2962
+ "agent_task_already_exists",
2963
+ `Task ${taskId} already has a durable Agent attempt and cannot be enqueued again.`
2964
+ );
2965
+ }
2966
+ }
2967
+ return { taskId, seq: message.seq, envelope, attempt: openedBeforeAppend };
2710
2968
  }
2711
2969
  async function assertAgentCapabilities(tenant, deviceId, required) {
2712
2970
  const device = await options.cloud.devices.get(tenant, deviceId);
@@ -2761,6 +3019,236 @@ function createByokCloud(options) {
2761
3019
  }
2762
3020
  return { seq: message.seq, envelope };
2763
3021
  }
3022
+ async function agentControlEnvelopeId(domain, tenant, deviceId, agentRef, logicalRequestId) {
3023
+ return uuidFromSha256(
3024
+ await options.crypto.sha256(
3025
+ new TextEncoder().encode(
3026
+ JSON.stringify({ domain, tenant, deviceId, agentRef, logicalRequestId })
3027
+ )
3028
+ )
3029
+ );
3030
+ }
3031
+ async function approvalDecisionSource(tenant, taskId, attempt, pending, decision) {
3032
+ const agent = attempt.agentRef;
3033
+ return uuidFromSha256(
3034
+ await options.crypto.sha256(
3035
+ new TextEncoder().encode(
3036
+ JSON.stringify({
3037
+ domain: "byok:approval-decision",
3038
+ tenant,
3039
+ taskId,
3040
+ deviceId: attempt.deviceId,
3041
+ ownerDeviceId: attempt.ownerDeviceId,
3042
+ agentRef: agent,
3043
+ requestSourceEnvelopeId: pending.sourceEnvelopeId,
3044
+ requestRevision: pending.revision,
3045
+ decision
3046
+ })
3047
+ )
3048
+ )
3049
+ );
3050
+ }
3051
+ function latestApprovalRequest(tail) {
3052
+ if (tail === void 0) return void 0;
3053
+ for (let index = tail.entries.length - 1; index >= 0; index -= 1) {
3054
+ const entry = tail.entries[index];
3055
+ if (entry.event.type !== "approval_requested") continue;
3056
+ return {
3057
+ sourceEnvelopeId: entry.sourceEnvelopeId,
3058
+ revision: entry.revision,
3059
+ ...entry.event.approvalId === void 0 ? {} : { approvalId: entry.event.approvalId }
3060
+ };
3061
+ }
3062
+ return void 0;
3063
+ }
3064
+ async function recordedHostDecision(tenant, taskId, attempt, tail, request) {
3065
+ for (const decision of ["approve", "reject"]) {
3066
+ const sourceEnvelopeId = await approvalDecisionSource(tenant, taskId, attempt, request, decision);
3067
+ const index = tail?.entries.findIndex((candidate) => candidate.sourceEnvelopeId === sourceEnvelopeId) ?? -1;
3068
+ const entry = index < 0 ? void 0 : tail.entries[index];
3069
+ if (entry?.event.type !== "approval_resolved" || entry.event.resolvedBy !== "host") continue;
3070
+ const laterResolution = tail.entries.slice(index + 1).some(
3071
+ (candidate) => candidate.event.type === "approval_resolved" && (request.approvalId === void 0 || candidate.event.approvalId === request.approvalId)
3072
+ );
3073
+ if (!laterResolution) {
3074
+ if (entry.event.decision === "reject") {
3075
+ return {
3076
+ decision: "reject",
3077
+ ...entry.event.approvalId === void 0 ? {} : { approvalId: entry.event.approvalId },
3078
+ reason: entry.event.reason
3079
+ };
3080
+ }
3081
+ return {
3082
+ decision: "approve",
3083
+ ...entry.event.approvalId === void 0 ? {} : { approvalId: entry.event.approvalId }
3084
+ };
3085
+ }
3086
+ }
3087
+ return void 0;
3088
+ }
3089
+ async function resolveApproval(tenant, taskId, decision, opts) {
3090
+ const stores = tenantStoresFor(controlPlane(tenant), root);
3091
+ const attempt = await stores.tasks.get(taskId);
3092
+ if (attempt === void 0) {
3093
+ throw new ByokCloudError("task_not_found", `Task ${taskId} was not found for this tenant.`);
3094
+ }
3095
+ if (isTerminalAttemptStatus(attempt.status)) {
3096
+ throw new ByokCloudError(
3097
+ "task_not_awaiting_approval",
3098
+ `Task ${taskId} already reached ${attempt.status}; there is no approval left to ${decision}.`
3099
+ );
3100
+ }
3101
+ const tail = await stores.approvals.read(taskId);
3102
+ const pending = pendingApproval(tail);
3103
+ const latestRequest = latestApprovalRequest(tail);
3104
+ if (pending === void 0) {
3105
+ if (latestRequest !== void 0) {
3106
+ const recorded = await recordedHostDecision(tenant, taskId, attempt, tail, latestRequest);
3107
+ if (recorded !== void 0) {
3108
+ if (recorded.decision !== decision) {
3109
+ throw new ByokCloudError(
3110
+ "coordination_input_invalid",
3111
+ `Task ${taskId} already has the opposite host approval decision.`
3112
+ );
3113
+ }
3114
+ if (recorded.decision === "reject" && recorded.reason !== (opts?.reason ?? null)) {
3115
+ throw new ByokCloudError(
3116
+ "coordination_input_invalid",
3117
+ `Task ${taskId} already has a host rejection with a different immutable reason.`
3118
+ );
3119
+ }
3120
+ const ownerDeviceId2 = attempt.ownerDeviceId;
3121
+ if (ownerDeviceId2 === void 0) {
3122
+ throw new ByokCloudError(
3123
+ "task_not_awaiting_approval",
3124
+ `Task ${taskId} has no owning device to receive its recorded ${decision}.`
3125
+ );
3126
+ }
3127
+ const messageId2 = await approvalDecisionSource(tenant, taskId, attempt, latestRequest, decision);
3128
+ const targeting2 = recorded.approvalId === void 0 ? {} : { approvalId: recorded.approvalId };
3129
+ const recordedReason = recorded.decision === "reject" ? recorded.reason : void 0;
3130
+ return enqueueAgentControlEnvelope(
3131
+ tenant,
3132
+ ownerDeviceId2,
3133
+ messageId2,
3134
+ (seq) => decision === "approve" ? createEnvelope("task.approve", targeting2, { id: messageId2, taskId, seq }) : createEnvelope(
3135
+ "task.reject",
3136
+ { ...targeting2, ...recordedReason === null || recordedReason === void 0 ? {} : { reason: recordedReason } },
3137
+ { id: messageId2, taskId, seq }
3138
+ )
3139
+ );
3140
+ }
3141
+ }
3142
+ throw new ByokCloudError(
3143
+ "task_not_awaiting_approval",
3144
+ `Task ${taskId} has no pending approval to ${decision}.`
3145
+ );
3146
+ }
3147
+ if (opts?.approvalId !== void 0 && pending.approvalId !== void 0 && opts.approvalId !== pending.approvalId) {
3148
+ throw new StaleApprovalError(taskId, opts.approvalId, pending.approvalId);
3149
+ }
3150
+ const ownerDeviceId = attempt.ownerDeviceId;
3151
+ if (ownerDeviceId === void 0) {
3152
+ throw new ByokCloudError(
3153
+ "task_not_awaiting_approval",
3154
+ `Task ${taskId} has no owning device; no runtime is paused on an approval to ${decision}.`
3155
+ );
3156
+ }
3157
+ const approvalId = pending.approvalId;
3158
+ const messageId = await approvalDecisionSource(tenant, taskId, attempt, pending, decision);
3159
+ const event = decision === "reject" ? {
3160
+ type: "approval_resolved",
3161
+ ...approvalId === void 0 ? {} : { approvalId },
3162
+ decision: "reject",
3163
+ resolvedBy: "host",
3164
+ reason: opts?.reason ?? null,
3165
+ at: (/* @__PURE__ */ new Date()).toISOString()
3166
+ } : {
3167
+ type: "approval_resolved",
3168
+ ...approvalId === void 0 ? {} : { approvalId },
3169
+ decision: "approve",
3170
+ resolvedBy: "host",
3171
+ at: (/* @__PURE__ */ new Date()).toISOString()
3172
+ };
3173
+ const resolved = await root.cloud.approvals.resolvePending(tenant, {
3174
+ taskId,
3175
+ sourceEnvelopeId: messageId,
3176
+ expectedSourceEnvelopeId: pending.sourceEnvelopeId,
3177
+ expectedRevision: pending.revision,
3178
+ event
3179
+ });
3180
+ let deliveredApprovalId = approvalId;
3181
+ let deliveredReason = decision === "reject" ? opts?.reason ?? null : void 0;
3182
+ if (resolved.status === "conflict" || resolved.status === "superseded" || resolved.status === "absent") {
3183
+ const currentTail = resolved.tail ?? await stores.approvals.read(taskId);
3184
+ const currentPending = pendingApproval(currentTail);
3185
+ const recorded = await recordedHostDecision(tenant, taskId, attempt, currentTail, pending);
3186
+ if (recorded?.decision === decision) {
3187
+ deliveredApprovalId = recorded.approvalId;
3188
+ if (recorded.decision === "reject") {
3189
+ if (recorded.reason !== deliveredReason) {
3190
+ throw new ByokCloudError(
3191
+ "coordination_input_invalid",
3192
+ `Task ${taskId} already has a host rejection with a different immutable reason.`
3193
+ );
3194
+ }
3195
+ deliveredReason = recorded.reason;
3196
+ }
3197
+ } else if (recorded !== void 0) {
3198
+ throw new ByokCloudError(
3199
+ "coordination_input_invalid",
3200
+ `Task ${taskId} already has the opposite host approval decision.`
3201
+ );
3202
+ } else if (currentPending !== void 0 && opts?.approvalId !== void 0 && currentPending.approvalId !== void 0 && currentPending.approvalId !== opts.approvalId) {
3203
+ throw new StaleApprovalError(taskId, opts.approvalId, currentPending.approvalId);
3204
+ } else {
3205
+ throw new ByokCloudError(
3206
+ "task_not_awaiting_approval",
3207
+ `Task ${taskId} no longer has the pending approval to ${decision}.`
3208
+ );
3209
+ }
3210
+ }
3211
+ const targeting = deliveredApprovalId === void 0 ? {} : { approvalId: deliveredApprovalId };
3212
+ return enqueueAgentControlEnvelope(
3213
+ tenant,
3214
+ ownerDeviceId,
3215
+ messageId,
3216
+ (seq) => decision === "approve" ? createEnvelope("task.approve", targeting, { id: messageId, taskId, seq }) : createEnvelope(
3217
+ "task.reject",
3218
+ { ...targeting, ...deliveredReason === null || deliveredReason === void 0 ? {} : { reason: deliveredReason } },
3219
+ { id: messageId, taskId, seq }
3220
+ )
3221
+ );
3222
+ }
3223
+ async function steerTask(tenant, taskId, payload) {
3224
+ const stores = tenantStoresFor(controlPlane(tenant), root);
3225
+ const attempt = await stores.tasks.get(taskId);
3226
+ if (attempt === void 0) {
3227
+ throw new ByokCloudError("task_not_found", `Task ${taskId} was not found for this tenant.`);
3228
+ }
3229
+ if (isTerminalAttemptStatus(attempt.status)) {
3230
+ throw new SteerRejectedError(taskId, "task_terminal", attempt.status, attempt.claimedRuntime);
3231
+ }
3232
+ const ownerDeviceId = attempt.ownerDeviceId;
3233
+ if (attempt.status !== "running" || ownerDeviceId === void 0) {
3234
+ throw new SteerRejectedError(taskId, "task_not_running", attempt.status, attempt.claimedRuntime);
3235
+ }
3236
+ if (attempt.claimedRuntimeCapabilities?.steer !== true) {
3237
+ throw new SteerRejectedError(
3238
+ taskId,
3239
+ "steer_unsupported_runtime",
3240
+ attempt.status,
3241
+ attempt.claimedRuntime
3242
+ );
3243
+ }
3244
+ const messageId = options.crypto.randomUuid();
3245
+ return enqueueAgentControlEnvelope(
3246
+ tenant,
3247
+ ownerDeviceId,
3248
+ messageId,
3249
+ (seq) => createEnvelope("task.steer", { text: payload.text }, { id: messageId, taskId, seq })
3250
+ );
3251
+ }
2764
3252
  async function getAgentHomeProjectionStatus(tenant, deviceId, input) {
2765
3253
  return readAgentHomeProjectionStatus(
2766
3254
  tenantStoresFor(controlPlane(tenant), root).receipts,
@@ -3034,7 +3522,7 @@ function createByokCloud(options) {
3034
3522
  ]);
3035
3523
  const stores = tenantStoresFor(controlPlane(tenant), root);
3036
3524
  const persisted = await stores.receipts.record({
3037
- key: `agent-content-request:${deviceId}:${payload.requestId}`,
3525
+ key: agentReliabilityKey("agent-content-request", deviceId, payload.agentRef, payload.requestId),
3038
3526
  body: JSON.stringify(payload)
3039
3527
  });
3040
3528
  if (!persisted.created && persisted.receipt.body !== JSON.stringify(payload)) {
@@ -3043,11 +3531,18 @@ function createByokCloud(options) {
3043
3531
  `Request ${payload.requestId} already exists with a different immutable content-read body.`
3044
3532
  );
3045
3533
  }
3534
+ const messageId = await agentControlEnvelopeId(
3535
+ "byok:agent-content-read",
3536
+ tenant,
3537
+ deviceId,
3538
+ payload.agentRef,
3539
+ payload.requestId
3540
+ );
3046
3541
  const control = await enqueueAgentControlEnvelope(
3047
3542
  tenant,
3048
3543
  deviceId,
3049
- payload.requestId,
3050
- (seq) => createEnvelope("agent.content.read", payload, { id: payload.requestId, seq })
3544
+ messageId,
3545
+ (seq) => createEnvelope("agent.content.read", payload, { id: messageId, seq })
3051
3546
  );
3052
3547
  if (control.envelope.type !== "agent.content.read" || JSON.stringify(control.envelope.payload) !== JSON.stringify(payload)) {
3053
3548
  throw new ByokCloudError(
@@ -3066,7 +3561,7 @@ function createByokCloud(options) {
3066
3561
  const stores = tenantStoresFor(controlPlane(tenant), root);
3067
3562
  const requestBody = JSON.stringify(payload);
3068
3563
  const persisted = await stores.receipts.record({
3069
- key: agentHomeProjectionRequestKey(deviceId, payload.requestId),
3564
+ key: agentHomeProjectionRequestKey(deviceId, payload.agentRef, payload.requestId),
3070
3565
  body: requestBody
3071
3566
  });
3072
3567
  const persistedPayload = AgentHomeProjectionPayloadSchema.parse(JSON.parse(persisted.receipt.body));
@@ -3076,11 +3571,18 @@ function createByokCloud(options) {
3076
3571
  `Agent-home projection request ${payload.requestId} already exists with a different immutable desired body.`
3077
3572
  );
3078
3573
  }
3574
+ const messageId = await agentControlEnvelopeId(
3575
+ "byok:agent-home-projection",
3576
+ tenant,
3577
+ deviceId,
3578
+ payload.agentRef,
3579
+ payload.requestId
3580
+ );
3079
3581
  const control = await enqueueAgentControlEnvelope(
3080
3582
  tenant,
3081
3583
  deviceId,
3082
- payload.requestId,
3083
- (seq) => createEnvelope("agent.home.projection", payload, { id: payload.requestId, seq })
3584
+ messageId,
3585
+ (seq) => createEnvelope("agent.home.projection", payload, { id: messageId, seq })
3084
3586
  );
3085
3587
  if (control.envelope.type !== "agent.home.projection" || !sameAgentHomeProjectionRequest(payload, control.envelope.payload)) {
3086
3588
  throw new ByokCloudError(
@@ -3104,6 +3606,13 @@ function createByokCloud(options) {
3104
3606
  getAgentHomeProjectionStatus,
3105
3607
  completeAgentHomeProjection,
3106
3608
  eraseAgentMemoryProjection,
3609
+ approveTask(tenant, taskId, opts) {
3610
+ return resolveApproval(tenant, taskId, "approve", opts);
3611
+ },
3612
+ rejectTask(tenant, taskId, opts) {
3613
+ return resolveApproval(tenant, taskId, "reject", opts);
3614
+ },
3615
+ steerTask,
3107
3616
  async cancelTask(tenant, taskId, reason) {
3108
3617
  const stores = tenantStoresFor(controlPlane(tenant), root);
3109
3618
  const proposedMessageId = options.crypto.randomUuid();
@@ -3143,11 +3652,14 @@ function createByokCloud(options) {
3143
3652
  readTaskAttempt(tenant, taskId) {
3144
3653
  return tenantStoresFor(controlPlane(tenant), root).tasks.get(taskId);
3145
3654
  },
3655
+ listTaskAttempts(tenant, query) {
3656
+ return tenantStoresFor(controlPlane(tenant), root).tasks.list(query);
3657
+ },
3146
3658
  readTerminalReceipt(tenant, taskId) {
3147
3659
  return tenantStoresFor(controlPlane(tenant), root).receipts.get(terminalReceiptKey(taskId));
3148
3660
  },
3149
- readAgentEgress(tenant, deviceId, eventId) {
3150
- return tenantStoresFor(controlPlane(tenant), root).egress.get(deviceId, eventId);
3661
+ readAgentEgress(tenant, deviceId, agentRef, eventId) {
3662
+ return tenantStoresFor(controlPlane(tenant), root).egress.get(deviceId, agentRef, eventId);
3151
3663
  },
3152
3664
  async readTaskResult(tenant, taskId) {
3153
3665
  const stores = tenantStoresFor(controlPlane(tenant), root);
@@ -3534,7 +4046,15 @@ var InMemoryInboundDedupStore = class {
3534
4046
  this.#capacity = capacity;
3535
4047
  }
3536
4048
  async checkAndRecord(tenant, deviceId, envelopeId) {
3537
- const key = tenantKey(tenant, deviceId);
4049
+ return this.#checkAndRecord(tenantKey(tenant, deviceId), envelopeId);
4050
+ }
4051
+ async checkAndRecordAgent(tenant, deviceId, agentRef, envelopeId) {
4052
+ return this.#checkAndRecord(
4053
+ tenantKey(tenant, agentReliabilityKey("inbound-dedup", deviceId, agentRef, "")),
4054
+ envelopeId
4055
+ );
4056
+ }
4057
+ #checkAndRecord(key, envelopeId) {
3538
4058
  let ring = this.#rings.get(key);
3539
4059
  if (ring === void 0) {
3540
4060
  ring = /* @__PURE__ */ new Set();
@@ -3777,22 +4297,23 @@ var InMemoryTaskAttemptStore = class {
3777
4297
  async reserveAgentMessage(tenant, input) {
3778
4298
  const key = tenantKey(tenant, input.taskId);
3779
4299
  return this.#state.mutate(key, () => {
3780
- const attempt = this.#state.attempts.get(key);
3781
- if (attempt === void 0 || attempt.deviceId !== input.deviceId || attempt.cancellation !== void 0 || !["offered", "claimed", "running"].includes(attempt.status)) return "rejected";
3782
4300
  const existing = this.#state.messageAdmissions.get(key);
3783
- if (existing === void 0) {
3784
- this.#state.messageAdmissions.set(key, {
3785
- messageId: input.messageId,
3786
- payloadBody: input.payloadBody
3787
- });
3788
- return "reserved";
4301
+ if (existing !== void 0) {
4302
+ return existing.deviceId === input.deviceId && existing.messageId === input.messageId && existing.payloadBody === input.payloadBody ? "pending" : "rejected";
3789
4303
  }
3790
- return existing.messageId === input.messageId && existing.payloadBody === input.payloadBody ? "pending" : "rejected";
4304
+ const attempt = this.#state.attempts.get(key);
4305
+ if (attempt === void 0 || attempt.deviceId !== input.deviceId || attempt.cancellation !== void 0 || !["offered", "claimed", "running"].includes(attempt.status)) return "rejected";
4306
+ this.#state.messageAdmissions.set(key, {
4307
+ deviceId: input.deviceId,
4308
+ messageId: input.messageId,
4309
+ payloadBody: input.payloadBody
4310
+ });
4311
+ return "reserved";
3791
4312
  });
3792
4313
  }
3793
4314
  async readAgentMessage(tenant, input) {
3794
4315
  const admission = this.#state.messageAdmissions.get(tenantKey(tenant, input.taskId));
3795
- return admission !== void 0 && admission.messageId === input.messageId && admission.payloadBody === input.payloadBody ? admission : void 0;
4316
+ return admission !== void 0 && admission.deviceId === input.deviceId && admission.messageId === input.messageId && admission.payloadBody === input.payloadBody ? admission : void 0;
3796
4317
  }
3797
4318
  async finalizeAgentMessage(tenant, input) {
3798
4319
  const key = tenantKey(tenant, input.taskId);
@@ -3800,7 +4321,7 @@ var InMemoryTaskAttemptStore = class {
3800
4321
  }
3801
4322
  #terminalize(key, input) {
3802
4323
  const admission = this.#state.messageAdmissions.get(key);
3803
- if (admission === void 0 || admission.messageId !== input.messageId || admission.payloadBody !== input.payloadBody) {
4324
+ if (admission === void 0 || admission.deviceId !== input.deviceId || admission.messageId !== input.messageId || admission.payloadBody !== input.payloadBody) {
3804
4325
  return void 0;
3805
4326
  }
3806
4327
  if (admission.terminalBody !== void 0) return admission;
@@ -3817,6 +4338,18 @@ var InMemoryTaskAttemptStore = class {
3817
4338
  return attempt === void 0 ? [] : [attempt];
3818
4339
  });
3819
4340
  }
4341
+ async list(tenant, query) {
4342
+ assertTaskAttemptListLimit(query.limit);
4343
+ const cursor = query.cursor;
4344
+ const matches = [...this.#state.attempts.values()].filter((attempt) => attempt.tenantId === tenant).filter((attempt) => cursor === void 0 || attempt.taskId > cursor).sort((left, right) => left.taskId < right.taskId ? -1 : left.taskId > right.taskId ? 1 : 0);
4345
+ const attempts = matches.slice(0, query.limit);
4346
+ const hasMore = matches.length > attempts.length;
4347
+ const last = attempts.at(-1);
4348
+ return {
4349
+ attempts,
4350
+ ...hasMore && last !== void 0 ? { nextCursor: last.taskId } : {}
4351
+ };
4352
+ }
3820
4353
  async claim(tenant, input) {
3821
4354
  const key = tenantKey(tenant, input.taskId);
3822
4355
  return this.#state.mutate(key, () => {
@@ -3826,6 +4359,8 @@ var InMemoryTaskAttemptStore = class {
3826
4359
  const claimed = {
3827
4360
  ...existing,
3828
4361
  ownerDeviceId: input.deviceId,
4362
+ ...input.runtime === void 0 ? {} : { claimedRuntime: input.runtime },
4363
+ ...input.capabilities === void 0 ? {} : { claimedRuntimeCapabilities: { ...input.capabilities } },
3829
4364
  status: "claimed",
3830
4365
  updatedAt: this.#now()
3831
4366
  };
@@ -3948,6 +4483,14 @@ var InMemoryActivityStore = class {
3948
4483
  const key = activityTailKey(tenant, input.taskId);
3949
4484
  const existing = this.#tails.get(key);
3950
4485
  const live = existing !== void 0 && receivedAt < existing.expiresAt ? existing : void 0;
4486
+ const sourceState = activitySourceBatchState(live?.entries ?? [], input);
4487
+ if (sourceState === "same") return live;
4488
+ if (sourceState === "conflict") {
4489
+ throw new ByokCloudError(
4490
+ "coordination_input_invalid",
4491
+ `Activity source envelope ${input.sourceEnvelopeId} already belongs to another canonical batch.`
4492
+ );
4493
+ }
3951
4494
  const incoming = projectTimelineEvents(input, receivedAt);
3952
4495
  for (const next of incoming) {
3953
4496
  const collision = live?.entries.find(
@@ -4039,6 +4582,46 @@ var InMemoryApprovalTimelineStore = class {
4039
4582
  this.#tails.set(key, tail);
4040
4583
  return tail;
4041
4584
  }
4585
+ async resolvePending(tenant, input) {
4586
+ const { capacity, ttlMs, event } = validateApprovalTimelineResolve(input);
4587
+ const now = this.clock.now();
4588
+ const receivedAt = now.toISOString();
4589
+ const key = approvalTimelineKey(tenant, input.taskId);
4590
+ const existing = this.#tails.get(key);
4591
+ const live = existing !== void 0 && receivedAt < existing.expiresAt ? existing : void 0;
4592
+ if (live === void 0) return { status: "absent" };
4593
+ const duplicate = live.entries.find((entry) => entry.sourceEnvelopeId === input.sourceEnvelopeId);
4594
+ if (duplicate !== void 0) {
4595
+ if (JSON.stringify(duplicate.event) !== JSON.stringify(event)) return { status: "conflict", tail: live };
4596
+ return { status: "replayed", tail: live };
4597
+ }
4598
+ const pending = pendingObservation(live.entries);
4599
+ if (pending === void 0) return { status: "absent", tail: live };
4600
+ if (pending.sourceEnvelopeId !== input.expectedSourceEnvelopeId || pending.revision !== input.expectedRevision) {
4601
+ return { status: "superseded", tail: live };
4602
+ }
4603
+ const revision = (live.cursor ?? 0) + 1;
4604
+ const observation = ApprovalObservationSchema.parse({
4605
+ taskId: input.taskId,
4606
+ sourceEnvelopeId: input.sourceEnvelopeId,
4607
+ revision,
4608
+ receivedAt,
4609
+ event
4610
+ });
4611
+ const allEntries = [...live.entries, observation];
4612
+ const evicted = Math.max(allEntries.length - capacity, 0);
4613
+ const tail = {
4614
+ tenantId: tenant,
4615
+ taskId: input.taskId,
4616
+ entries: allEntries.slice(evicted),
4617
+ cursor: revision,
4618
+ dropped: live.dropped + evicted,
4619
+ capacity,
4620
+ expiresAt: new Date(now.getTime() + ttlMs).toISOString()
4621
+ };
4622
+ this.#tails.set(key, tail);
4623
+ return { status: "applied", tail };
4624
+ }
4042
4625
  async read(tenant, taskId) {
4043
4626
  const key = approvalTimelineKey(tenant, taskId);
4044
4627
  const tail = this.#tails.get(key);
@@ -4050,6 +4633,19 @@ var InMemoryApprovalTimelineStore = class {
4050
4633
  return tail;
4051
4634
  }
4052
4635
  };
4636
+ function pendingObservation(entries) {
4637
+ let pending;
4638
+ for (const entry of entries) {
4639
+ if (entry.event.type === "approval_requested") {
4640
+ pending = entry;
4641
+ continue;
4642
+ }
4643
+ if (pending !== void 0 && (pending.event.approvalId === void 0 || pending.event.approvalId === entry.event.approvalId)) {
4644
+ pending = void 0;
4645
+ }
4646
+ }
4647
+ return pending;
4648
+ }
4053
4649
  var InMemoryAgentEgressStore = class {
4054
4650
  #records = /* @__PURE__ */ new Map();
4055
4651
  #clock;
@@ -4057,7 +4653,10 @@ var InMemoryAgentEgressStore = class {
4057
4653
  this.#clock = clock;
4058
4654
  }
4059
4655
  async record(tenant, input) {
4060
- const key = tenantKey(tenant, `${input.deviceId}\0${input.payload.eventId}`);
4656
+ const key = tenantKey(
4657
+ tenant,
4658
+ agentReliabilityKey("agent-egress", input.deviceId, input.payload.agentRef, input.payload.eventId)
4659
+ );
4061
4660
  const existing = this.#records.get(key);
4062
4661
  if (existing !== void 0) return { record: existing, created: false };
4063
4662
  const record = {
@@ -4070,8 +4669,8 @@ var InMemoryAgentEgressStore = class {
4070
4669
  this.#records.set(key, record);
4071
4670
  return { record, created: true };
4072
4671
  }
4073
- async get(tenant, deviceId, eventId) {
4074
- return this.#records.get(tenantKey(tenant, `${deviceId}\0${eventId}`));
4672
+ async get(tenant, deviceId, agentRef, eventId) {
4673
+ return this.#records.get(tenantKey(tenant, agentReliabilityKey("agent-egress", deviceId, agentRef, eventId)));
4075
4674
  }
4076
4675
  };
4077
4676
 
@@ -4138,7 +4737,9 @@ function createInMemoryByokCloud(options = {}) {
4138
4737
  ...options.truthCommitter === void 0 ? {} : { truthCommitter: options.truthCommitter },
4139
4738
  ...options.truthObjectDownloads === void 0 ? {} : { truthObjectDownloads: options.truthObjectDownloads },
4140
4739
  ...options.operatorId !== void 0 ? { operatorId: options.operatorId } : {},
4740
+ ...options.instanceProductId === void 0 ? {} : { instanceProductId: options.instanceProductId },
4141
4741
  ...options.agentMessage === void 0 ? {} : { agentMessage: options.agentMessage },
4742
+ ...options.observer === void 0 ? {} : { observer: options.observer },
4142
4743
  ...options.agentMemoryProjectionAuthorizer === void 0 ? {} : { agentMemoryProjectionAuthorizer: options.agentMemoryProjectionAuthorizer },
4143
4744
  ...options.agentMemoryProjectionStore === void 0 ? {} : { agentMemoryProjectionStore: options.agentMemoryProjectionStore },
4144
4745
  ...options.maxBlobSizeBytes !== void 0 ? { maxBlobSizeBytes: options.maxBlobSizeBytes } : {},
@@ -4505,12 +5106,12 @@ function parseSseFrame(frame) {
4505
5106
  // src/stores/ports-contract.ts
4506
5107
  var CLOUD_PORT_METHODS = {
4507
5108
  activity: ["append", "read"],
4508
- approvals: ["append", "read"],
5109
+ approvals: ["append", "read", "resolvePending"],
4509
5110
  devices: ["register", "get", "revoke", "list", "readiness", "recordCapabilities", "resolveByDeviceId"],
4510
5111
  pairingCodes: ["issue"],
4511
5112
  pairing: ["redeemAndRegister"],
4512
5113
  nonces: ["issue", "consumeIfValid"],
4513
- dedup: ["checkAndRecord"],
5114
+ dedup: ["checkAndRecord", "checkAndRecordAgent"],
4514
5115
  tasks: [
4515
5116
  "open",
4516
5117
  "reserveAgentOffer",
@@ -4519,6 +5120,7 @@ var CLOUD_PORT_METHODS = {
4519
5120
  "finalizeAgentMessage",
4520
5121
  "get",
4521
5122
  "getMany",
5123
+ "list",
4522
5124
  "claim",
4523
5125
  "recordStatus"
4524
5126
  ],
@@ -4549,6 +5151,6 @@ var CLOUD_PORT_INTERFACES = {
4549
5151
  rateLimiter: "InboundRateLimiter"
4550
5152
  };
4551
5153
 
4552
- export { ACCESS_TOKEN_TTL_SECONDS, AGENT_HOME_CONTRACT_CAPABILITY, APPROVAL_SUMMARY_MAX_BYTES, ActivityAppendRequestSchema, AllowAllRateLimiter, ApprovalObservationSchema, ApprovalTimelineEventSchema, BLOB_READ_ERROR_CODES, 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, InMemoryAgentEgressStore, InMemoryAgentMemoryProjectionAuthorizer, InMemoryAgentMemoryProjectionStore, 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, agentHomeProjectionCompletionKey, agentHomeProjectionRequestKey, approvalTimelineCursor, authenticateBearer, authenticateDeviceProof, authenticateHostedDeviceAssertion, createAuthPlane, createByokCloud, createHmacTokenSigner, createInMemoryBlobs, createInMemoryByokCloud, createInMemoryCloudStores, createWebCrypto, declares, extractBearerToken, fullCapabilityDeclaration, handleInboundEnvelope, isCloudError, isTruthCommitError, parseApprovalObservations, parseTimelineEvents, projectTerminalResult, projectTimelineEvents, readAgentHomeProjectionStatus, recordAgentHomeProjectionCompletion, routeKey, tenantStoresFor, terminalReceiptKey, truthManifestMetadata, truthRecordMetadata, validateActivityAppend, validateApprovalTimelineAppend, verifyNonceSignature };
5154
+ export { ACCESS_TOKEN_TTL_SECONDS, AGENT_HOME_CONTRACT_CAPABILITY, APPROVAL_SUMMARY_MAX_BYTES, ActivityAppendRequestSchema, AllowAllRateLimiter, ApprovalObservationSchema, ApprovalTimelineEventSchema, BLOB_READ_ERROR_CODES, 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, InMemoryAgentEgressStore, InMemoryAgentMemoryProjectionAuthorizer, InMemoryAgentMemoryProjectionStore, 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, StaleApprovalError, SteerRejectedError, 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, activitySourceBatchState, agentHomeProjectionCompletionKey, agentHomeProjectionRequestKey, approvalTimelineCursor, assertTaskAttemptListLimit, authenticateBearer, authenticateDeviceProof, authenticateHostedDeviceAssertion, createAuthPlane, createByokCloud, createHmacTokenSigner, createInMemoryBlobs, createInMemoryByokCloud, createInMemoryCloudStores, createWebCrypto, declares, extractBearerToken, fullCapabilityDeclaration, handleInboundEnvelope, isCloudError, isTruthCommitError, parseApprovalObservations, parseTimelineEvents, pendingApproval, projectTerminalResult, projectTimelineEvents, readAgentHomeProjectionStatus, recordAgentHomeProjectionCompletion, routeKey, tenantStoresFor, terminalReceiptKey, truthManifestMetadata, truthRecordMetadata, validateActivityAppend, validateApprovalTimelineAppend, validateApprovalTimelineResolve, verifyNonceSignature };
4553
5155
  //# sourceMappingURL=index.js.map
4554
5156
  //# sourceMappingURL=index.js.map