@byok-sdk/client 0.11.0 → 0.12.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.
@@ -3,7 +3,7 @@ import { randomUUID, createHash, randomBytes, timingSafeEqual, createHmac, creat
3
3
  import { readFileSync, promises, linkSync, fstatSync, lstatSync, unlinkSync, constants, readSync, openSync, writeFileSync, fchmodSync, fsyncSync, closeSync, opendirSync, existsSync, realpathSync, mkdirSync, renameSync, chmodSync, statSync, readdirSync } from 'fs';
4
4
  import * as path3 from 'path';
5
5
  import path3__default, { isAbsolute, join } from 'path';
6
- import { AGENT_CONTENT_ARTIFACT_READ_CAPABILITY, AGENT_CONTENT_TRANSCRIPT_READ_CAPABILITY, AGENT_CONTENT_WORKSPACE_READ_CAPABILITY, TASK_STATES, AgentEgressPolicySchema, AgentHomeProjectionPayloadSchema, CONFIGURED_TOOLSETS_MAX_ITEMS, ToolsetIdSchema, AgentRefSchema, AgentContentReceiptPayloadSchema, BYOK_PAIR_PATH, PairResponseSchema, BYOK_CHALLENGE_PATH, BYOK_TOKEN_PATH, partitionAgentEvents, TASK_TRANSITIONS, encodeEnvelope, PROTOCOL_VERSION, createEnvelope, AGENT_EGRESS_RELIABLE_ACK_CAPABILITY, byokBlobUrlPath, BYOK_BLOBS_PATH, byokBlobFinalizePath, TERMINAL_PROJECTION_SELECTION_CAPABILITY, STRICT_AGENT_ONLY_CAPABILITY, AGENT_HOME_PROJECTION_CAPABILITY, AGENT_EGRESS_POLICY_CAPABILITY, AGENT_EGRESS_FRESH_SESSION_CAPABILITY, AGENT_MESSAGE_EGRESS_CAPABILITY, AgentHomeProjectionCompletionRequestSchema, byokAgentHomeProjectionCompletionPath, AgentHomeProjectionReadbackSchema, parseMessage, checkResultDocument, MAX_MESSAGES_PER_BATCH, BYOK_CAPABILITIES_PATH, BYOK_PRESENCE_PATH, AgentMessagePublishPayloadSchema, AgentMessageDispositionPayloadSchema, RuntimeIdSchema, AGENT_MEMORY_PROJECTION_CAPABILITY, TERMINAL_INFERENCE_USAGE_MAX_DURATION_MS, TERMINAL_INFERENCE_USAGE_MAX_TOKENS, RESULT_DOCUMENT_MAX_BYTES, decodeEnvelope, BYOK_EVENTS_PATH, BYOK_MESSAGES_PATH, MessagesSendResponseSchema, UnknownMessageTypeError, AGENT_MEMORY_PROJECTION_MAX_ORDERING_VALUE, AgentMemoryProjectionMutationSchema, AGENT_MEMORY_PROJECTION_MAX_REDACTED_BYTES, BYOK_WS_PATH } from '@byok-sdk/protocol';
6
+ import { AGENT_CONTENT_ARTIFACT_READ_CAPABILITY, AGENT_CONTENT_TRANSCRIPT_READ_CAPABILITY, AGENT_CONTENT_WORKSPACE_READ_CAPABILITY, TASK_STATES, AgentEgressPolicySchema, AgentHomeProjectionPayloadSchema, CONFIGURED_TOOLSETS_MAX_ITEMS, ToolsetIdSchema, AgentRefSchema, AgentContentReceiptPayloadSchema, BYOK_PAIR_PATH, PairResponseSchema, BYOK_CHALLENGE_PATH, BYOK_TOKEN_PATH, partitionAgentEvents, TASK_TRANSITIONS, encodeEnvelope, PROTOCOL_VERSION, createEnvelope, AGENT_EGRESS_RELIABLE_ACK_CAPABILITY, byokBlobUrlPath, BYOK_BLOBS_PATH, byokBlobFinalizePath, TERMINAL_PROJECTION_SELECTION_CAPABILITY, STRICT_AGENT_ONLY_CAPABILITY, AGENT_HOME_PROJECTION_CAPABILITY, AGENT_EGRESS_POLICY_CAPABILITY, AGENT_EGRESS_FRESH_SESSION_CAPABILITY, AGENT_MESSAGE_EGRESS_CAPABILITY, AgentHomeProjectionCompletionRequestSchema, byokAgentHomeProjectionCompletionPath, AgentHomeProjectionReadbackSchema, parseMessage, checkResultDocument, MAX_MESSAGES_PER_BATCH, BYOK_CAPABILITIES_PATH, BYOK_PRESENCE_PATH, AgentMessagePublishPayloadSchema, AgentMessageDispositionPayloadSchema, RuntimeIdSchema, AGENT_MEMORY_PROJECTION_CAPABILITY, TERMINAL_INFERENCE_USAGE_MAX_DURATION_MS, TERMINAL_INFERENCE_USAGE_MAX_TOKENS, RESULT_DOCUMENT_MAX_BYTES, decodeEnvelope, BYOK_EVENTS_PATH, BYOK_MESSAGES_PATH, MessagesSendResponseSchema, UnknownMessageTypeError, BYOK_WS_PATH, AGENT_MEMORY_PROJECTION_MAX_ORDERING_VALUE, AgentMemoryProjectionMutationSchema, AGENT_MEMORY_PROJECTION_MAX_REDACTED_BYTES } from '@byok-sdk/protocol';
7
7
  import net, { createServer, createConnection } from 'net';
8
8
  import * as os from 'os';
9
9
  import os__default from 'os';
@@ -768,6 +768,120 @@ var AgentHomeLeaseManager = class _AgentHomeLeaseManager {
768
768
  }
769
769
  }
770
770
  };
771
+ function executionKey(input) {
772
+ const value = input.sessionRef === void 0 ? input.taskId : input.sessionRef;
773
+ const label = input.sessionRef === void 0 ? "taskId" : "sessionRef";
774
+ if (typeof value !== "string" || value.length === 0 || /[\u0000\r\n]/u.test(value)) {
775
+ throw new AgentHomeResolutionError(`Agent execution ${label} must be a non-empty single-line string`);
776
+ }
777
+ return `${input.sessionRef === void 0 ? "task" : "session"}\0${value}`;
778
+ }
779
+ var AgentHomeExecutionLeaseManager = class _AgentHomeExecutionLeaseManager {
780
+ constructor(manager) {
781
+ this.manager = manager;
782
+ }
783
+ manager;
784
+ static groups = /* @__PURE__ */ new Map();
785
+ static queues = /* @__PURE__ */ new Map();
786
+ async acquire(resolution, input) {
787
+ const initialKey = executionKey(input);
788
+ return this.exclusive(resolution.canonicalHome, async () => {
789
+ let group = _AgentHomeExecutionLeaseManager.groups.get(resolution.canonicalHome);
790
+ if (group === void 0) {
791
+ const baseLease = await this.manager.acquire(resolution);
792
+ group = {
793
+ manager: this.manager,
794
+ baseLease,
795
+ agentId: resolution.agentRef.agentId,
796
+ leasesByKey: /* @__PURE__ */ new Map()
797
+ };
798
+ _AgentHomeExecutionLeaseManager.groups.set(resolution.canonicalHome, group);
799
+ } else if (group.manager !== this.manager || group.agentId !== resolution.agentRef.agentId) {
800
+ throw new AgentHomeBusyError(`Agent home ${resolution.canonicalHome} is active under another execution owner`);
801
+ }
802
+ if (group.leasesByKey.has(initialKey)) {
803
+ throw new AgentHomeBusyError(`Agent session already has an active execution lease in ${resolution.canonicalHome}`);
804
+ }
805
+ const leaseId = randomUUID();
806
+ group.leasesByKey.set(initialKey, leaseId);
807
+ let currentKey = initialKey;
808
+ let sessionBound = input.sessionRef !== void 0;
809
+ let released = false;
810
+ return Object.freeze({
811
+ leaseId,
812
+ agentRef: resolution.agentRef,
813
+ canonicalHome: resolution.canonicalHome,
814
+ cwd: resolution.canonicalHome,
815
+ homeIdentity: group.baseLease.homeIdentity,
816
+ bindSession: async (sessionRef) => {
817
+ const nextKey = executionKey({ taskId: input.taskId, sessionRef });
818
+ await this.exclusive(resolution.canonicalHome, async () => {
819
+ if (released) throw new AgentHomeBusyError(`Agent execution lease ${leaseId} is already released`);
820
+ const currentGroup = _AgentHomeExecutionLeaseManager.groups.get(resolution.canonicalHome);
821
+ if (currentGroup !== group || currentGroup.leasesByKey.get(currentKey) !== leaseId) {
822
+ throw new AgentHomeBusyError(`Agent execution lease ${leaseId} is no longer owned by this process`);
823
+ }
824
+ if (nextKey === currentKey) return;
825
+ if (sessionBound) {
826
+ throw new AgentHomeBusyError(
827
+ `Agent session execution lease ${leaseId} cannot rebind to a different runtime session`
828
+ );
829
+ }
830
+ if (currentGroup.leasesByKey.has(nextKey)) {
831
+ throw new AgentHomeBusyError(`Agent session already has an active execution lease in ${resolution.canonicalHome}`);
832
+ }
833
+ currentGroup.leasesByKey.set(nextKey, leaseId);
834
+ currentGroup.leasesByKey.delete(currentKey);
835
+ currentKey = nextKey;
836
+ sessionBound = true;
837
+ });
838
+ },
839
+ release: async () => {
840
+ await this.exclusive(resolution.canonicalHome, async () => {
841
+ if (released) return;
842
+ const currentGroup = _AgentHomeExecutionLeaseManager.groups.get(resolution.canonicalHome);
843
+ if (currentGroup !== group || currentGroup.leasesByKey.get(currentKey) !== leaseId) {
844
+ released = true;
845
+ throw new AgentHomeBusyError(`Agent execution lease ${leaseId} is no longer owned by this process`);
846
+ }
847
+ currentGroup.leasesByKey.delete(currentKey);
848
+ released = true;
849
+ if (currentGroup.leasesByKey.size === 0) {
850
+ _AgentHomeExecutionLeaseManager.groups.delete(resolution.canonicalHome);
851
+ await currentGroup.baseLease.release();
852
+ }
853
+ });
854
+ }
855
+ });
856
+ });
857
+ }
858
+ async mutate(binding, operation) {
859
+ return this.exclusive(binding.resolution.canonicalHome, async () => {
860
+ const group = _AgentHomeExecutionLeaseManager.groups.get(binding.resolution.canonicalHome);
861
+ if (group === void 0 || group.manager !== this.manager || ![...group.leasesByKey.values()].includes(binding.lease.leaseId)) {
862
+ throw new AgentHomeBusyError("Agent execution lease does not own this home mutation");
863
+ }
864
+ return operation();
865
+ });
866
+ }
867
+ async exclusive(canonicalHome, operation) {
868
+ const prior = _AgentHomeExecutionLeaseManager.queues.get(canonicalHome) ?? Promise.resolve();
869
+ let release;
870
+ const tail = new Promise((resolve) => {
871
+ release = resolve;
872
+ });
873
+ _AgentHomeExecutionLeaseManager.queues.set(canonicalHome, tail);
874
+ await prior;
875
+ try {
876
+ return await operation();
877
+ } finally {
878
+ release();
879
+ if (_AgentHomeExecutionLeaseManager.queues.get(canonicalHome) === tail) {
880
+ _AgentHomeExecutionLeaseManager.queues.delete(canonicalHome);
881
+ }
882
+ }
883
+ }
884
+ };
771
885
  async function initializeAgentHome(resolution) {
772
886
  await ensureDirectoryNoSymlink(
773
887
  resolution.canonicalHome,
@@ -843,10 +957,12 @@ var AgentHomeManager = class {
843
957
  layout;
844
958
  projection;
845
959
  leaseManager;
960
+ executionLeaseManager;
846
961
  constructor(options) {
847
962
  this.layout = new AgentHomeLayout(options.hostStorageRoot);
848
963
  this.projection = options.projection;
849
964
  this.leaseManager = options.leaseManager ?? new AgentHomeLeaseManager();
965
+ this.executionLeaseManager = new AgentHomeExecutionLeaseManager(this.leaseManager);
850
966
  }
851
967
  async prepare(agentRef) {
852
968
  const binding = await this.acquire(agentRef);
@@ -873,12 +989,25 @@ var AgentHomeManager = class {
873
989
  const lease = await this.leaseManager.acquire(resolution);
874
990
  return Object.freeze({ resolution, lease });
875
991
  }
992
+ async acquireExecution(agentRef, input) {
993
+ const resolution = await this.layout.resolve(agentRef);
994
+ const lease = await this.executionLeaseManager.acquire(resolution, input);
995
+ return Object.freeze({ resolution, lease });
996
+ }
876
997
  /** Initialize only after any requested session exact-match has succeeded. */
877
998
  async initialize(binding) {
878
- const { resolution, lease } = binding;
999
+ await this.initializeResolved(binding.resolution, binding.lease.cwd);
1000
+ }
1001
+ async initializeExecution(binding) {
1002
+ await this.mutateExecution(binding, () => this.initializeResolved(binding.resolution, binding.lease.cwd));
1003
+ }
1004
+ async mutateExecution(binding, operation) {
1005
+ return this.executionLeaseManager.mutate(binding, operation);
1006
+ }
1007
+ async initializeResolved(resolution, cwd) {
879
1008
  await initializeAgentHome(resolution);
880
1009
  const prepare = this.projection?.prepare;
881
- if (prepare !== void 0) await prepare({ ...resolution, cwd: lease.cwd });
1010
+ if (prepare !== void 0) await prepare({ ...resolution, cwd });
882
1011
  if (await promises.realpath(resolution.homeDir) !== resolution.canonicalHome) {
883
1012
  throw new AgentHomeResolutionError("Agent projection changed the canonical home path");
884
1013
  }
@@ -6371,9 +6500,18 @@ function assertRecord(value) {
6371
6500
  throw new DeviceCredentialStoreError("OS credential entry has an incomplete device credential");
6372
6501
  }
6373
6502
  }
6374
- function encode(record) {
6375
- assertRecord(record);
6376
- const encoded = Buffer.from(JSON.stringify(record), "utf8").toString("base64");
6503
+ function isFirstPairingAttempt(value) {
6504
+ return "kind" in value && value.kind === "first-pairing-attempt-v1";
6505
+ }
6506
+ function assertFirstPairingAttempt(value) {
6507
+ if (typeof value !== "object" || value === null || Array.isArray(value) || value.kind !== "first-pairing-attempt-v1" || typeof value.deviceName !== "string" || typeof value.devicePublicKey !== "string" || typeof value.devicePrivateKeyPem !== "string" || value.machineId !== void 0 && typeof value.machineId !== "string" || value.deviceName.length === 0 || value.devicePublicKey.length === 0 || value.devicePrivateKeyPem.length === 0) {
6508
+ throw new DeviceCredentialStoreError("OS credential entry has an invalid first-pairing attempt shape");
6509
+ }
6510
+ }
6511
+ function encode(authority) {
6512
+ if (isFirstPairingAttempt(authority)) assertFirstPairingAttempt(authority);
6513
+ else assertRecord(authority);
6514
+ const encoded = Buffer.from(JSON.stringify(authority), "utf8").toString("base64");
6377
6515
  const value = `${ENCODED_PREFIX}${encoded}`;
6378
6516
  if (Buffer.byteLength(value, "utf8") > 2400) {
6379
6517
  throw new DeviceCredentialStoreError("device credential exceeds the OS credential entry bound");
@@ -6402,6 +6540,16 @@ function decode(value) {
6402
6540
  } catch {
6403
6541
  throw new DeviceCredentialStoreError("OS credential entry is not valid JSON");
6404
6542
  }
6543
+ if (typeof parsed === "object" && parsed !== null && parsed.kind === "first-pairing-attempt-v1") {
6544
+ assertFirstPairingAttempt(parsed);
6545
+ return Object.freeze({
6546
+ kind: parsed.kind,
6547
+ deviceName: parsed.deviceName,
6548
+ devicePublicKey: parsed.devicePublicKey,
6549
+ devicePrivateKeyPem: parsed.devicePrivateKeyPem,
6550
+ ...parsed.machineId === void 0 ? {} : { machineId: parsed.machineId }
6551
+ });
6552
+ }
6405
6553
  assertRecord(parsed);
6406
6554
  return Object.freeze({
6407
6555
  deviceId: parsed.deviceId,
@@ -6432,6 +6580,18 @@ var DeviceCredentialStore = class {
6432
6580
  this.#run = options.commandRunner ?? runDeviceCommand;
6433
6581
  }
6434
6582
  async read() {
6583
+ const authority = await this.#readAuthority();
6584
+ return authority === void 0 || isFirstPairingAttempt(authority) ? void 0 : authority;
6585
+ }
6586
+ async readFirstPairingAttempt() {
6587
+ const authority = await this.#readAuthority();
6588
+ return authority !== void 0 && isFirstPairingAttempt(authority) ? authority : void 0;
6589
+ }
6590
+ async saveFirstPairingAttempt(attempt) {
6591
+ assertFirstPairingAttempt(attempt);
6592
+ await this.#replaceAuthority(attempt);
6593
+ }
6594
+ async #readAuthority() {
6435
6595
  const result = await this.#invoke("read");
6436
6596
  if (result.exitCode === NOT_FOUND || this.#platform === "linux" && result.exitCode === 1 && result.stderr.trim().length === 0) return void 0;
6437
6597
  if (result.exitCode === 127) throw new DeviceCredentialStoreUnavailableError();
@@ -6444,6 +6604,12 @@ var DeviceCredentialStore = class {
6444
6604
  }
6445
6605
  async replace(record) {
6446
6606
  const encoded = encode(record);
6607
+ await this.#replaceEncoded(encoded);
6608
+ }
6609
+ async #replaceAuthority(authority) {
6610
+ await this.#replaceEncoded(encode(authority));
6611
+ }
6612
+ async #replaceEncoded(encoded) {
6447
6613
  const result = await this.#invoke("replace", encoded);
6448
6614
  if (result.exitCode === 127) throw new DeviceCredentialStoreUnavailableError();
6449
6615
  if (result.exitCode !== 0) {
@@ -6454,7 +6620,7 @@ var DeviceCredentialStore = class {
6454
6620
  }
6455
6621
  /** Returns true only after the sole secret authority is confirmed absent. */
6456
6622
  async clear() {
6457
- const before = await this.read();
6623
+ const before = await this.#readAuthority();
6458
6624
  if (before === void 0) return false;
6459
6625
  const result = await this.#invoke("clear");
6460
6626
  if (result.exitCode === 127) throw new DeviceCredentialStoreUnavailableError();
@@ -6463,7 +6629,7 @@ var DeviceCredentialStore = class {
6463
6629
  `operating-system credential provider could not clear device credentials${providerDiagnostic(result.stderr)}`
6464
6630
  );
6465
6631
  }
6466
- if (await this.read() !== void 0) {
6632
+ if (await this.#readAuthority() !== void 0) {
6467
6633
  throw new DeviceCredentialStoreError("operating-system credential provider reported deletion but device credentials remain");
6468
6634
  }
6469
6635
  return true;
@@ -6538,17 +6704,24 @@ var DeviceCredentialStore = class {
6538
6704
  }
6539
6705
  };
6540
6706
  var InMemoryDeviceCredentialStore = class {
6541
- #record;
6707
+ #authority;
6542
6708
  async read() {
6543
- return this.#record === void 0 ? void 0 : Object.freeze({ ...this.#record });
6709
+ return this.#authority === void 0 || isFirstPairingAttempt(this.#authority) ? void 0 : Object.freeze({ ...this.#authority });
6710
+ }
6711
+ async readFirstPairingAttempt() {
6712
+ return this.#authority !== void 0 && isFirstPairingAttempt(this.#authority) ? Object.freeze({ ...this.#authority }) : void 0;
6713
+ }
6714
+ async saveFirstPairingAttempt(attempt) {
6715
+ assertFirstPairingAttempt(attempt);
6716
+ this.#authority = Object.freeze({ ...attempt });
6544
6717
  }
6545
6718
  async replace(record) {
6546
6719
  assertRecord(record);
6547
- this.#record = Object.freeze({ ...record });
6720
+ this.#authority = Object.freeze({ ...record });
6548
6721
  }
6549
6722
  async clear() {
6550
- const had = this.#record !== void 0;
6551
- this.#record = void 0;
6723
+ const had = this.#authority !== void 0;
6724
+ this.#authority = void 0;
6552
6725
  return had;
6553
6726
  }
6554
6727
  };
@@ -6820,6 +6993,10 @@ function describeEndpoint(transport, url) {
6820
6993
  const parsed = typeof url === "string" ? new URL(url) : url;
6821
6994
  return { transport, host: parsed.host, path: parsed.pathname };
6822
6995
  }
6996
+ function formatServerUrl(value) {
6997
+ const url = typeof value === "string" ? new URL(value) : value;
6998
+ return `${url.protocol}//${url.host}${url.pathname}`;
6999
+ }
6823
7000
  var InsecureServerUrlError = class extends Error {
6824
7001
  constructor(message) {
6825
7002
  super(message);
@@ -6838,9 +7015,10 @@ function assertServerUrlAllowed(rawUrl, opts = {}) {
6838
7015
  let url;
6839
7016
  try {
6840
7017
  url = new URL(rawUrl);
6841
- } catch (err) {
6842
- throw new InsecureServerUrlError(`invalid server URL "${rawUrl}": ${err instanceof Error ? err.message : String(err)}`);
7018
+ } catch {
7019
+ throw new InsecureServerUrlError("invalid server URL");
6843
7020
  }
7021
+ const endpoint = formatServerUrl(url);
6844
7022
  switch (url.protocol) {
6845
7023
  case "https:":
6846
7024
  case "wss:":
@@ -6849,11 +7027,11 @@ function assertServerUrlAllowed(rawUrl, opts = {}) {
6849
7027
  case "ws:":
6850
7028
  if (opts.dangerouslyAllowInsecureRemote || isLoopbackHostname(url.hostname)) return;
6851
7029
  throw new InsecureServerUrlError(
6852
- `refusing to connect to "${rawUrl}" over plaintext ${url.protocol.replace(":", "")} \u2014 "${url.hostname}" is not a loopback host. Use wss:/https: for any non-loopback server, or pass { dangerouslyAllowInsecureRemote: true } (DaemonConfig) if you understand and accept the risk of sending device credentials over an unencrypted connection.`
7030
+ `refusing to connect to "${endpoint}" over plaintext ${url.protocol.replace(":", "")} \u2014 "${url.hostname}" is not a loopback host. Use wss:/https: for any non-loopback server, or pass { dangerouslyAllowInsecureRemote: true } (DaemonConfig) if you understand and accept the risk of sending device credentials over an unencrypted connection.`
6853
7031
  );
6854
7032
  default:
6855
7033
  throw new InsecureServerUrlError(
6856
- `refusing to connect to "${rawUrl}" \u2014 unsupported scheme "${url.protocol}" (expected http:, https:, ws:, or wss:).`
7034
+ `refusing to connect to "${endpoint}" \u2014 unsupported scheme "${url.protocol}" (expected http:, https:, ws:, or wss:).`
6857
7035
  );
6858
7036
  }
6859
7037
  }
@@ -6865,12 +7043,22 @@ var DeviceRevokedError = class extends Error {
6865
7043
  this.name = "DeviceRevokedError";
6866
7044
  }
6867
7045
  };
7046
+ var AuthRequestAbortedError = class extends Error {
7047
+ constructor(reason) {
7048
+ super(reason === "deadline" ? "authentication request exceeded its deadline" : "authentication request was cancelled during shutdown");
7049
+ this.reason = reason;
7050
+ this.name = "AuthRequestAbortedError";
7051
+ }
7052
+ reason;
7053
+ };
6868
7054
  var ASSUMED_PAIR_TOKEN_TTL_MS = 45 * 60 * 1e3;
6869
7055
  var RENEW_MARGIN_MS = 60 * 1e3;
7056
+ var DEFAULT_AUTH_REQUEST_DEADLINE_MS = 15e3;
6870
7057
  var AuthManager = class {
6871
7058
  constructor(opts) {
6872
7059
  this.opts = opts;
6873
7060
  this.credentials = opts.credentials ?? opts.store.credentials;
7061
+ this.requestDeadlineMs = resolveRequestDeadlineMs(opts.authRequestDeadlineMs);
6874
7062
  }
6875
7063
  opts;
6876
7064
  record;
@@ -6880,7 +7068,10 @@ var AuthManager = class {
6880
7068
  stopped = false;
6881
7069
  pairing = false;
6882
7070
  credentialMutationTail = Promise.resolve();
7071
+ /** The sole cancellation authority for the request currently inside the serialized credential mutation. */
7072
+ activeRequest;
6883
7073
  credentials;
7074
+ requestDeadlineMs;
6884
7075
  get deviceId() {
6885
7076
  return this.record?.deviceId;
6886
7077
  }
@@ -6916,23 +7107,51 @@ var AuthManager = class {
6916
7107
  if (!(error instanceof DeviceRecordRePairRequiredError)) throw error;
6917
7108
  }
6918
7109
  }
6919
- const keyPair = existing ? { privateKey: importPrivateKeyPem(existing.devicePrivateKeyPem), publicKeyBase64Url: existing.devicePublicKey } : generateDeviceKeyPair();
7110
+ const observedDeviceName = this.opts.deviceName ?? os__default.hostname();
7111
+ const observedMachineId = await this.opts.machineId?.();
7112
+ let keyPair;
7113
+ let pairingDeviceName = observedDeviceName;
7114
+ let pairingMachineId = observedMachineId;
7115
+ if (existing) {
7116
+ keyPair = { privateKey: importPrivateKeyPem(existing.devicePrivateKeyPem), publicKeyBase64Url: existing.devicePublicKey };
7117
+ } else {
7118
+ const firstAttempt = await this.credentials.readFirstPairingAttempt();
7119
+ if (firstAttempt) {
7120
+ keyPair = {
7121
+ privateKey: importPrivateKeyPem(firstAttempt.devicePrivateKeyPem),
7122
+ publicKeyBase64Url: firstAttempt.devicePublicKey
7123
+ };
7124
+ pairingDeviceName = firstAttempt.deviceName;
7125
+ pairingMachineId = firstAttempt.machineId;
7126
+ } else {
7127
+ keyPair = generateDeviceKeyPair();
7128
+ await this.credentials.saveFirstPairingAttempt({
7129
+ kind: "first-pairing-attempt-v1",
7130
+ deviceName: pairingDeviceName,
7131
+ devicePublicKey: keyPair.publicKeyBase64Url,
7132
+ devicePrivateKeyPem: exportPrivateKeyPem(keyPair.privateKey),
7133
+ ...pairingMachineId === void 0 ? {} : { machineId: pairingMachineId }
7134
+ });
7135
+ }
7136
+ }
6920
7137
  const url = new URL(BYOK_PAIR_PATH, toHttpBase(this.opts.serverUrl));
6921
- const machineId = await this.opts.machineId?.();
6922
- const res = await fetch(url, {
6923
- method: "POST",
6924
- headers: { "content-type": "application/json" },
6925
- body: JSON.stringify({
6926
- pairingCode,
6927
- deviceName: this.opts.deviceName ?? os__default.hostname(),
6928
- devicePublicKey: keyPair.publicKeyBase64Url,
6929
- ...machineId === void 0 ? {} : { machineId }
6930
- })
7138
+ const body = await this.runRequest(async (signal) => {
7139
+ const res = await fetch(url, {
7140
+ method: "POST",
7141
+ headers: { "content-type": "application/json" },
7142
+ body: JSON.stringify({
7143
+ pairingCode,
7144
+ deviceName: pairingDeviceName,
7145
+ devicePublicKey: keyPair.publicKeyBase64Url,
7146
+ ...pairingMachineId === void 0 ? {} : { machineId: pairingMachineId }
7147
+ }),
7148
+ signal
7149
+ });
7150
+ if (!res.ok) {
7151
+ throw new Error(`pairing failed: HTTP ${res.status} ${await safeErrorText(res)}`.trimEnd());
7152
+ }
7153
+ return PairResponseSchema.parse(await res.json());
6931
7154
  });
6932
- if (!res.ok) {
6933
- throw new Error(`pairing failed: HTTP ${res.status} ${await safeErrorText(res)}`.trimEnd());
6934
- }
6935
- const body = PairResponseSchema.parse(await res.json());
6936
7155
  const metadata = {
6937
7156
  deviceId: body.deviceId,
6938
7157
  tenantId: body.tenantId,
@@ -6971,6 +7190,7 @@ var AuthManager = class {
6971
7190
  this.stopped = true;
6972
7191
  if (this.proactiveTimer) clearTimeout(this.proactiveTimer);
6973
7192
  this.proactiveTimer = void 0;
7193
+ this.activeRequest?.abort();
6974
7194
  await this.credentialMutationTail;
6975
7195
  }
6976
7196
  async renew() {
@@ -6987,29 +7207,35 @@ var AuthManager = class {
6987
7207
  const record = this.record;
6988
7208
  const base = toHttpBase(this.opts.serverUrl);
6989
7209
  const privateKey = importPrivateKeyPem(record.devicePrivateKeyPem);
6990
- const challengeRes = await fetch(new URL(BYOK_CHALLENGE_PATH, base), {
6991
- method: "POST",
6992
- headers: { "content-type": "application/json" },
6993
- body: JSON.stringify({ deviceId: record.deviceId })
7210
+ const { nonce } = await this.runRequest(async (signal) => {
7211
+ const challengeRes = await fetch(new URL(BYOK_CHALLENGE_PATH, base), {
7212
+ method: "POST",
7213
+ headers: { "content-type": "application/json" },
7214
+ body: JSON.stringify({ deviceId: record.deviceId }),
7215
+ signal
7216
+ });
7217
+ if (challengeRes.status === 401) this.markRevoked();
7218
+ if (!challengeRes.ok) {
7219
+ throw new Error(
7220
+ `token renewal (challenge) failed: HTTP ${challengeRes.status} ${await safeErrorText(challengeRes)}`.trimEnd()
7221
+ );
7222
+ }
7223
+ return await challengeRes.json();
6994
7224
  });
6995
- if (challengeRes.status === 401) this.markRevoked();
6996
- if (!challengeRes.ok) {
6997
- throw new Error(
6998
- `token renewal (challenge) failed: HTTP ${challengeRes.status} ${await safeErrorText(challengeRes)}`.trimEnd()
6999
- );
7000
- }
7001
- const { nonce } = await challengeRes.json();
7002
7225
  const signature = signNonce(privateKey, nonce);
7003
- const tokenRes = await fetch(new URL(BYOK_TOKEN_PATH, base), {
7004
- method: "POST",
7005
- headers: { "content-type": "application/json" },
7006
- body: JSON.stringify({ deviceId: record.deviceId, nonce, signature })
7226
+ const body = await this.runRequest(async (signal) => {
7227
+ const tokenRes = await fetch(new URL(BYOK_TOKEN_PATH, base), {
7228
+ method: "POST",
7229
+ headers: { "content-type": "application/json" },
7230
+ body: JSON.stringify({ deviceId: record.deviceId, nonce, signature }),
7231
+ signal
7232
+ });
7233
+ if (tokenRes.status === 401) this.markRevoked();
7234
+ if (!tokenRes.ok) {
7235
+ throw new Error(`token renewal (token) failed: HTTP ${tokenRes.status} ${await safeErrorText(tokenRes)}`.trimEnd());
7236
+ }
7237
+ return await tokenRes.json();
7007
7238
  });
7008
- if (tokenRes.status === 401) this.markRevoked();
7009
- if (!tokenRes.ok) {
7010
- throw new Error(`token renewal (token) failed: HTTP ${tokenRes.status} ${await safeErrorText(tokenRes)}`.trimEnd());
7011
- }
7012
- const body = await tokenRes.json();
7013
7239
  const updated = {
7014
7240
  ...record,
7015
7241
  accessToken: body.accessToken,
@@ -7039,6 +7265,37 @@ var AuthManager = class {
7039
7265
  timer.unref?.();
7040
7266
  this.proactiveTimer = timer;
7041
7267
  }
7268
+ /**
7269
+ * Bounds one complete auth exchange rather than fetch alone. Keeping the
7270
+ * controller active through `json()`/`text()` makes a non-cooperative or
7271
+ * partial response body cancellable by the same authority that owns fetch.
7272
+ */
7273
+ async runRequest(operation) {
7274
+ if (this.stopped) throw new AuthRequestAbortedError("stopped");
7275
+ const controller = new AbortController();
7276
+ this.activeRequest = controller;
7277
+ let deadlineElapsed = false;
7278
+ const deadline = setTimeout(() => {
7279
+ deadlineElapsed = true;
7280
+ controller.abort();
7281
+ }, this.requestDeadlineMs);
7282
+ let rejectOnAbort;
7283
+ const aborted = new Promise((_, reject) => {
7284
+ rejectOnAbort = reject;
7285
+ });
7286
+ const onAbort = () => rejectOnAbort(new AuthRequestAbortedError(deadlineElapsed ? "deadline" : "stopped"));
7287
+ controller.signal.addEventListener("abort", onAbort, { once: true });
7288
+ try {
7289
+ return await Promise.race([operation(controller.signal), aborted]);
7290
+ } catch (error) {
7291
+ if (controller.signal.aborted) throw new AuthRequestAbortedError(deadlineElapsed ? "deadline" : "stopped");
7292
+ throw error;
7293
+ } finally {
7294
+ clearTimeout(deadline);
7295
+ controller.signal.removeEventListener("abort", onAbort);
7296
+ if (this.activeRequest === controller) this.activeRequest = void 0;
7297
+ }
7298
+ }
7042
7299
  async runCredentialMutation(operation) {
7043
7300
  const predecessor = this.credentialMutationTail;
7044
7301
  let release;
@@ -7081,6 +7338,13 @@ function resolvePairExpiry(refreshHint) {
7081
7338
  }
7082
7339
  return new Date(Date.now() + ASSUMED_PAIR_TOKEN_TTL_MS).toISOString();
7083
7340
  }
7341
+ function resolveRequestDeadlineMs(value) {
7342
+ const deadline = value ?? DEFAULT_AUTH_REQUEST_DEADLINE_MS;
7343
+ if (!Number.isSafeInteger(deadline) || deadline <= 0) {
7344
+ throw new Error("authRequestDeadlineMs must be a positive safe integer");
7345
+ }
7346
+ return deadline;
7347
+ }
7084
7348
  async function safeErrorText(res) {
7085
7349
  try {
7086
7350
  return await res.text();
@@ -7102,37 +7366,48 @@ function withAuth(init, token) {
7102
7366
  }
7103
7367
 
7104
7368
  // src/daemon/blob-client.ts
7105
- async function safeErrorText2(res) {
7106
- try {
7107
- return await res.text();
7108
- } catch {
7109
- return "";
7369
+ var BlobRequestAbortedError = class extends Error {
7370
+ constructor(reason) {
7371
+ super(reason === "deadline" ? "blob request deadline elapsed" : "blob request cancelled");
7372
+ this.reason = reason;
7373
+ this.name = "BlobRequestAbortedError";
7110
7374
  }
7111
- }
7375
+ reason;
7376
+ };
7112
7377
  var BlobClient = class {
7113
- constructor(serverUrl, auth) {
7378
+ constructor(serverUrl, auth, options = {}) {
7114
7379
  this.serverUrl = serverUrl;
7115
7380
  this.auth = auth;
7381
+ this.options = options;
7382
+ const requestDeadlineMs = options.requestDeadlineMs ?? 15e3;
7383
+ if (!Number.isSafeInteger(requestDeadlineMs) || requestDeadlineMs <= 0) {
7384
+ throw new Error("BlobClient requestDeadlineMs must be a positive safe integer");
7385
+ }
7386
+ this.requestDeadlineMs = requestDeadlineMs;
7116
7387
  }
7117
7388
  serverUrl;
7118
7389
  auth;
7390
+ options;
7391
+ requestDeadlineMs;
7119
7392
  /** `blobRef` -> `GET /byok/blobs/:id/url` -> fetch the presigned download URL -> text content. Always resolves fresh rather than trusting any inlined `BlobRef.url`, per docs/protocol.md §7. */
7120
- async resolveInstruction(blobRef) {
7393
+ async resolveInstruction(blobRef, options = {}) {
7121
7394
  const base = toHttpBase(this.serverUrl);
7122
- const urlRes = await authedFetch(
7123
- new URL(byokBlobUrlPath(blobRef.blobId), base),
7124
- { method: "GET" },
7125
- this.auth
7395
+ const urlRes = await this.#request(
7396
+ (signal) => authedFetch(new URL(byokBlobUrlPath(blobRef.blobId), base), { method: "GET", signal }, this.auth),
7397
+ options.signal
7126
7398
  );
7127
7399
  if (!urlRes.ok) {
7128
- throw new Error(`failed to resolve blob download url: HTTP ${urlRes.status} ${await safeErrorText2(urlRes)}`.trimEnd());
7400
+ throw new Error(`failed to resolve blob download url: HTTP ${urlRes.status} ${await this.#safeErrorText(urlRes, options.signal)}`.trimEnd());
7401
+ }
7402
+ const { downloadUrl } = await this.#readJson(urlRes, options.signal);
7403
+ if (typeof downloadUrl !== "string" || downloadUrl.length === 0) {
7404
+ throw new Error("failed to resolve blob download url: response omitted downloadUrl");
7129
7405
  }
7130
- const { downloadUrl } = await urlRes.json();
7131
- const contentRes = await fetch(new URL(downloadUrl, base));
7406
+ const contentRes = await this.#request((signal) => fetch(new URL(downloadUrl, base), { signal }), options.signal);
7132
7407
  if (!contentRes.ok) {
7133
7408
  throw new Error(`failed to download blob content: HTTP ${contentRes.status}`);
7134
7409
  }
7135
- const bytes = new Uint8Array(await contentRes.arrayBuffer());
7410
+ const bytes = await this.#readBody(contentRes, options.signal);
7136
7411
  const observedHash = `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
7137
7412
  if (observedHash !== blobRef.contentHash || bytes.length !== blobRef.size) {
7138
7413
  throw new Error(
@@ -7141,63 +7416,71 @@ var BlobClient = class {
7141
7416
  }
7142
7417
  return new TextDecoder().decode(bytes);
7143
7418
  }
7144
- /** `POST /byok/blobs` (declares size/contentType/contentHash) -> PUT the bytes to the presigned upload URL -> a `BlobRef` for `task.artifact.blobRef`. */
7419
+ /** `POST /byok/blobs` -> PUT the bytes to the presigned URL -> finalize into a `BlobRef`. */
7145
7420
  async uploadArtifact(content, contentType, options = {}) {
7146
7421
  const bytes = typeof content === "string" ? new TextEncoder().encode(content) : content;
7147
7422
  const contentHash2 = `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
7148
7423
  const base = toHttpBase(this.serverUrl);
7149
7424
  const reservationId = options.idempotencyKey ?? `blob_${randomUUID()}`;
7150
- const createRes = await authedFetch(
7151
- new URL(BYOK_BLOBS_PATH, base),
7152
- {
7153
- method: "POST",
7154
- headers: {
7155
- "content-type": "application/json",
7156
- "idempotency-key": reservationId
7425
+ const createRes = await this.#request(
7426
+ (signal) => authedFetch(
7427
+ new URL(BYOK_BLOBS_PATH, base),
7428
+ {
7429
+ method: "POST",
7430
+ headers: { "content-type": "application/json", "idempotency-key": reservationId },
7431
+ body: JSON.stringify({ size: bytes.length, contentType, contentHash: contentHash2 }),
7432
+ signal
7157
7433
  },
7158
- body: JSON.stringify({ size: bytes.length, contentType, contentHash: contentHash2 })
7159
- },
7160
- this.auth
7434
+ this.auth
7435
+ ),
7436
+ options.signal
7161
7437
  );
7162
7438
  if (!createRes.ok) {
7163
- throw new Error(`failed to create blob: HTTP ${createRes.status} ${await safeErrorText2(createRes)}`.trimEnd());
7439
+ throw new Error(`failed to create blob: HTTP ${createRes.status} ${await this.#safeErrorText(createRes, options.signal)}`.trimEnd());
7440
+ }
7441
+ const { blobId, uploadUrl } = await this.#readJson(createRes, options.signal);
7442
+ if (typeof blobId !== "string" || blobId.length === 0 || typeof uploadUrl !== "string" || uploadUrl.length === 0) {
7443
+ throw new Error("failed to create blob: response omitted blobId or uploadUrl");
7164
7444
  }
7165
- const { blobId, uploadUrl } = await createRes.json();
7166
7445
  const blobRef = { blobId, contentHash: contentHash2, size: bytes.length, contentType };
7167
- if (options.idempotencyKey !== void 0 && await this.#hasExactCommittedBlob(base, blobRef)) {
7446
+ if (options.idempotencyKey !== void 0 && await this.#hasExactCommittedBlob(base, blobRef, options.signal)) {
7168
7447
  return blobRef;
7169
7448
  }
7170
- const putRes = await fetch(new URL(uploadUrl, base), {
7171
- method: "PUT",
7172
- headers: { "content-type": contentType },
7173
- body: bytes
7174
- });
7449
+ const putRes = await this.#request(
7450
+ (signal) => fetch(new URL(uploadUrl, base), {
7451
+ method: "PUT",
7452
+ headers: { "content-type": contentType },
7453
+ body: bytes,
7454
+ signal
7455
+ }),
7456
+ options.signal
7457
+ );
7175
7458
  if (!putRes.ok) {
7176
7459
  throw new Error(`failed to upload blob content: HTTP ${putRes.status}`);
7177
7460
  }
7178
- await this.#finalize(base, blobId, reservationId);
7461
+ this.#throwIfAborted(options.signal);
7462
+ await this.#finalize(base, blobId, reservationId, options.signal);
7179
7463
  return blobRef;
7180
7464
  }
7181
- async #hasExactCommittedBlob(base, blobRef) {
7182
- const urlRes = await authedFetch(
7183
- new URL(byokBlobUrlPath(blobRef.blobId), base),
7184
- { method: "GET" },
7185
- this.auth
7465
+ async #hasExactCommittedBlob(base, blobRef, signal) {
7466
+ const urlRes = await this.#request(
7467
+ (requestSignal) => authedFetch(new URL(byokBlobUrlPath(blobRef.blobId), base), { method: "GET", signal: requestSignal }, this.auth),
7468
+ signal
7186
7469
  );
7187
7470
  if (urlRes.status === 404) return false;
7188
7471
  if (!urlRes.ok) {
7189
- throw new Error(`failed to read back idempotent blob: HTTP ${urlRes.status} ${await safeErrorText2(urlRes)}`.trimEnd());
7472
+ throw new Error(`failed to read back idempotent blob: HTTP ${urlRes.status} ${await this.#safeErrorText(urlRes, signal)}`.trimEnd());
7190
7473
  }
7191
- const { downloadUrl } = await urlRes.json();
7474
+ const { downloadUrl } = await this.#readJson(urlRes, signal);
7192
7475
  if (typeof downloadUrl !== "string" || downloadUrl.length === 0) {
7193
7476
  throw new Error("failed to read back idempotent blob: response omitted downloadUrl");
7194
7477
  }
7195
- const contentRes = await fetch(new URL(downloadUrl, base));
7478
+ const contentRes = await this.#request((requestSignal) => fetch(new URL(downloadUrl, base), { signal: requestSignal }), signal);
7196
7479
  if (contentRes.status === 404) return false;
7197
7480
  if (!contentRes.ok) {
7198
7481
  throw new Error(`failed to read back idempotent blob content: HTTP ${contentRes.status}`);
7199
7482
  }
7200
- const bytes = new Uint8Array(await contentRes.arrayBuffer());
7483
+ const bytes = await this.#readBody(contentRes, signal);
7201
7484
  const observedHash = `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
7202
7485
  if (observedHash !== blobRef.contentHash || bytes.length !== blobRef.size) {
7203
7486
  throw new Error(
@@ -7206,18 +7489,19 @@ var BlobClient = class {
7206
7489
  }
7207
7490
  return true;
7208
7491
  }
7209
- async #finalize(base, blobId, reservationId) {
7492
+ async #finalize(base, blobId, reservationId, signal) {
7210
7493
  let lastFailure;
7211
7494
  for (let attempt = 1; attempt <= 2; attempt += 1) {
7495
+ this.#throwIfAborted(signal);
7212
7496
  let response;
7213
7497
  try {
7214
- response = await authedFetch(
7215
- new URL(byokBlobFinalizePath(blobId), base),
7216
- {
7217
- method: "POST",
7218
- headers: { "idempotency-key": reservationId }
7219
- },
7220
- this.auth
7498
+ response = await this.#request(
7499
+ (requestSignal) => authedFetch(
7500
+ new URL(byokBlobFinalizePath(blobId), base),
7501
+ { method: "POST", headers: { "idempotency-key": reservationId }, signal: requestSignal },
7502
+ this.auth
7503
+ ),
7504
+ signal
7221
7505
  );
7222
7506
  } catch (error) {
7223
7507
  lastFailure = error;
@@ -7227,13 +7511,105 @@ var BlobClient = class {
7227
7511
  if (response.ok) return;
7228
7512
  if (response.status < 500 || attempt === 2) {
7229
7513
  throw new Error(
7230
- `failed to finalize blob: HTTP ${response.status} ${await safeErrorText2(response)}`.trimEnd()
7514
+ `failed to finalize blob: HTTP ${response.status} ${await this.#safeErrorText(response, signal)}`.trimEnd()
7231
7515
  );
7232
7516
  }
7233
7517
  lastFailure = new Error(`failed to finalize blob: HTTP ${response.status}`);
7234
7518
  }
7235
7519
  throw lastFailure;
7236
7520
  }
7521
+ async #readJson(res, signal) {
7522
+ return JSON.parse(await this.#readText(res, signal));
7523
+ }
7524
+ async #safeErrorText(res, signal) {
7525
+ try {
7526
+ return await this.#readText(res, signal);
7527
+ } catch (error) {
7528
+ if (error instanceof BlobRequestAbortedError) throw error;
7529
+ return "";
7530
+ }
7531
+ }
7532
+ async #readText(res, signal) {
7533
+ return new TextDecoder().decode(await this.#readBody(res, signal));
7534
+ }
7535
+ /**
7536
+ * `Response.arrayBuffer()` only leaves cancellation observable through the
7537
+ * Fetch implementation. Read the body directly so our lifecycle/deadline
7538
+ * authority cancels the actual stream even when a fetch implementation has
7539
+ * already resolved at headers.
7540
+ */
7541
+ async #readBody(res, signal) {
7542
+ return this.#request(async (requestSignal) => {
7543
+ if (res.body === null) return new Uint8Array();
7544
+ const reader = res.body.getReader();
7545
+ const cancelBody = () => {
7546
+ void reader.cancel().catch(() => void 0);
7547
+ };
7548
+ requestSignal.addEventListener("abort", cancelBody, { once: true });
7549
+ try {
7550
+ const chunks = [];
7551
+ let length = 0;
7552
+ for (; ; ) {
7553
+ const { done, value } = await reader.read();
7554
+ if (done) break;
7555
+ chunks.push(value);
7556
+ length += value.byteLength;
7557
+ }
7558
+ const bytes = new Uint8Array(length);
7559
+ let offset = 0;
7560
+ for (const chunk of chunks) {
7561
+ bytes.set(chunk, offset);
7562
+ offset += chunk.byteLength;
7563
+ }
7564
+ return bytes;
7565
+ } finally {
7566
+ requestSignal.removeEventListener("abort", cancelBody);
7567
+ reader.releaseLock();
7568
+ }
7569
+ }, signal);
7570
+ }
7571
+ #throwIfAborted(signal) {
7572
+ if (this.options.signal?.aborted || signal?.aborted) {
7573
+ throw new BlobRequestAbortedError("cancelled");
7574
+ }
7575
+ }
7576
+ async #request(request, signal) {
7577
+ this.#throwIfAborted(signal);
7578
+ const controller = new AbortController();
7579
+ let abortReason = "cancelled";
7580
+ const abort = (reason) => {
7581
+ if (controller.signal.aborted) return;
7582
+ abortReason = reason;
7583
+ controller.abort();
7584
+ };
7585
+ const inheritedSignals = [this.options.signal, signal].filter((value) => value !== void 0);
7586
+ const abortForCancellation = () => abort("cancelled");
7587
+ for (const inheritedSignal of inheritedSignals) {
7588
+ inheritedSignal.addEventListener("abort", abortForCancellation, { once: true });
7589
+ }
7590
+ const deadline = setTimeout(() => abort("deadline"), this.requestDeadlineMs);
7591
+ deadline.unref?.();
7592
+ let rejectAbort = () => {
7593
+ };
7594
+ const aborted = new Promise((_resolve, reject) => {
7595
+ rejectAbort = reject;
7596
+ });
7597
+ const rejectOnAbort = () => rejectAbort(new BlobRequestAbortedError(abortReason));
7598
+ controller.signal.addEventListener("abort", rejectOnAbort, { once: true });
7599
+ try {
7600
+ return await Promise.race([request(controller.signal), aborted]);
7601
+ } catch (error) {
7602
+ if (error instanceof BlobRequestAbortedError) throw error;
7603
+ if (controller.signal.aborted) throw new BlobRequestAbortedError(abortReason);
7604
+ throw error;
7605
+ } finally {
7606
+ clearTimeout(deadline);
7607
+ controller.signal.removeEventListener("abort", rejectOnAbort);
7608
+ for (const inheritedSignal of inheritedSignals) {
7609
+ inheritedSignal.removeEventListener("abort", abortForCancellation);
7610
+ }
7611
+ }
7612
+ }
7237
7613
  };
7238
7614
  var DEFAULT_TIMEOUT_MS2 = 2e3;
7239
7615
  var DARWIN_UUID_RE = /"IOPlatformUUID"\s*=\s*"([^"]+)"/u;
@@ -7480,6 +7856,7 @@ var AnotherControlServerRunningError = class extends Error {
7480
7856
  }
7481
7857
  };
7482
7858
  var MAX_HALF_OPEN_CONNECTIONS = 8;
7859
+ var MAX_OUTBOUND_QUEUE_BYTES = 1024 * 1024;
7483
7860
  function errorMessage4(err) {
7484
7861
  return err instanceof Error ? err.message : String(err);
7485
7862
  }
@@ -7556,11 +7933,12 @@ async function bindControlEndpoint(server, endpoint) {
7556
7933
  }
7557
7934
  function handleConnection(socket, token, methods, handshakeTimeoutMs, onHandshakeSettled) {
7558
7935
  const reader = new NdjsonLineReader();
7559
- const activeStreams = /* @__PURE__ */ new Map();
7936
+ const activeRequests = /* @__PURE__ */ new Map();
7560
7937
  let phase = "client-hello";
7561
7938
  let serverNonce = "";
7562
7939
  let destroyed = false;
7563
7940
  let handshakeSettled = false;
7941
+ let disposeOutboundWriter;
7564
7942
  function settleHandshake() {
7565
7943
  if (handshakeSettled) return;
7566
7944
  handshakeSettled = true;
@@ -7570,8 +7948,83 @@ function handleConnection(socket, token, methods, handshakeTimeoutMs, onHandshak
7570
7948
  if (phase !== "ready") socket.destroy();
7571
7949
  }, handshakeTimeoutMs);
7572
7950
  handshakeTimer.unref?.();
7951
+ function terminateConnection() {
7952
+ if (destroyed) return;
7953
+ destroyed = true;
7954
+ clearTimeout(handshakeTimer);
7955
+ settleHandshake();
7956
+ disposeOutboundWriter?.();
7957
+ const requests = [...activeRequests.values()];
7958
+ activeRequests.clear();
7959
+ for (const request of requests) {
7960
+ if (request.kind === "stream" && !request.controller.signal.aborted) request.controller.abort();
7961
+ }
7962
+ if (!socket.destroyed) socket.destroy();
7963
+ }
7964
+ const queuedFrames = [];
7965
+ let queuedBytes = 0;
7966
+ let blocked = false;
7967
+ let terminal = false;
7968
+ let drainListening = false;
7969
+ function disposeQueuedFrames() {
7970
+ terminal = true;
7971
+ blocked = false;
7972
+ queuedFrames.length = 0;
7973
+ queuedBytes = 0;
7974
+ if (drainListening) {
7975
+ socket.removeListener("drain", onDrain);
7976
+ drainListening = false;
7977
+ }
7978
+ }
7979
+ function waitForDrain() {
7980
+ if (drainListening || terminal) return;
7981
+ drainListening = true;
7982
+ socket.once("drain", onDrain);
7983
+ }
7984
+ function writeEncodedFrame(encoded) {
7985
+ if (terminal) return;
7986
+ if (!socket.writable) {
7987
+ terminateConnection();
7988
+ return;
7989
+ }
7990
+ try {
7991
+ if (!socket.write(encoded)) {
7992
+ blocked = true;
7993
+ waitForDrain();
7994
+ }
7995
+ } catch {
7996
+ terminateConnection();
7997
+ }
7998
+ }
7999
+ function onDrain() {
8000
+ drainListening = false;
8001
+ if (terminal) return;
8002
+ blocked = false;
8003
+ while (!blocked && queuedFrames.length > 0) {
8004
+ const frame = queuedFrames.shift();
8005
+ queuedBytes -= frame.bytes;
8006
+ writeEncodedFrame(frame.encoded);
8007
+ }
8008
+ }
8009
+ disposeOutboundWriter = disposeQueuedFrames;
7573
8010
  function sendFrame(frame) {
7574
- if (!destroyed && socket.writable) socket.write(encodeFrame(frame));
8011
+ if (destroyed || terminal) return;
8012
+ const encoded = encodeFrame(frame);
8013
+ const bytes = Buffer.byteLength(encoded);
8014
+ if (bytes > MAX_OUTBOUND_QUEUE_BYTES) {
8015
+ terminateConnection();
8016
+ return;
8017
+ }
8018
+ if (blocked) {
8019
+ if (queuedBytes + bytes > MAX_OUTBOUND_QUEUE_BYTES) {
8020
+ terminateConnection();
8021
+ return;
8022
+ }
8023
+ queuedFrames.push({ encoded, bytes });
8024
+ queuedBytes += bytes;
8025
+ return;
8026
+ }
8027
+ writeEncodedFrame(encoded);
7575
8028
  }
7576
8029
  function handleClientHello(parsed) {
7577
8030
  const hello = parseClientHello(parsed);
@@ -7594,21 +8047,51 @@ function handleConnection(socket, token, methods, handshakeTimeoutMs, onHandshak
7594
8047
  settleHandshake();
7595
8048
  sendFrame({ v: 1, ready: true });
7596
8049
  }
8050
+ function rejectDuplicateRequest(id) {
8051
+ sendFrame({
8052
+ v: 1,
8053
+ id,
8054
+ ok: false,
8055
+ error: { code: "duplicate_request_id", message: `request id "${id}" is already active` }
8056
+ });
8057
+ }
8058
+ function registerRequest(id, record) {
8059
+ if (activeRequests.has(id)) {
8060
+ rejectDuplicateRequest(id);
8061
+ return false;
8062
+ }
8063
+ activeRequests.set(id, record);
8064
+ return true;
8065
+ }
8066
+ function releaseRequest(id, record) {
8067
+ if (activeRequests.get(id) !== record) return false;
8068
+ activeRequests.delete(id);
8069
+ return true;
8070
+ }
7597
8071
  function dispatch(id, method, params) {
7598
8072
  const unary = methods.unary[method];
7599
8073
  if (unary) {
7600
- Promise.resolve().then(() => unary(params)).then((result) => sendFrame({ v: 1, id, ok: true, result })).catch((err) => sendFrame({ v: 1, id, ok: false, error: toControlErrorShape(err) }));
8074
+ const record = { kind: "unary" };
8075
+ if (!registerRequest(id, record)) return;
8076
+ Promise.resolve().then(() => unary(params)).then((result) => {
8077
+ if (!releaseRequest(id, record)) return;
8078
+ sendFrame({ v: 1, id, ok: true, result });
8079
+ }).catch((err) => {
8080
+ if (!releaseRequest(id, record)) return;
8081
+ sendFrame({ v: 1, id, ok: false, error: toControlErrorShape(err) });
8082
+ });
7601
8083
  return;
7602
8084
  }
7603
8085
  const stream = methods.stream[method];
7604
8086
  if (stream) {
7605
8087
  const controller = new AbortController();
7606
- activeStreams.set(id, controller);
7607
- stream(params, { emit: (event) => sendFrame({ v: 1, id, event }), signal: controller.signal }).then(() => {
7608
- activeStreams.delete(id);
8088
+ const record = { kind: "stream", controller };
8089
+ if (!registerRequest(id, record)) return;
8090
+ Promise.resolve().then(() => stream(params, { emit: (event) => sendFrame({ v: 1, id, event }), signal: controller.signal })).then(() => {
8091
+ if (!releaseRequest(id, record)) return;
7609
8092
  if (!controller.signal.aborted) sendFrame({ v: 1, id, ok: true, done: true });
7610
8093
  }).catch((err) => {
7611
- activeStreams.delete(id);
8094
+ if (!releaseRequest(id, record)) return;
7612
8095
  sendFrame({ v: 1, id, ok: false, error: toControlErrorShape(err) });
7613
8096
  });
7614
8097
  return;
@@ -7650,13 +8133,10 @@ function handleConnection(socket, token, methods, handshakeTimeoutMs, onHandshak
7650
8133
  for (const line of lines) handleLine(line);
7651
8134
  });
7652
8135
  socket.on("error", () => {
8136
+ terminateConnection();
7653
8137
  });
7654
8138
  socket.on("close", () => {
7655
- destroyed = true;
7656
- clearTimeout(handshakeTimer);
7657
- settleHandshake();
7658
- for (const controller of activeStreams.values()) controller.abort();
7659
- activeStreams.clear();
8139
+ terminateConnection();
7660
8140
  });
7661
8141
  }
7662
8142
  async function startControlServer(opts) {
@@ -8574,6 +9054,20 @@ function createFleetJitter(productId, deviceId) {
8574
9054
  delay: (domain, sequence, baseMs) => deterministicJitterMs({ seed, domain, sequence, baseMs })
8575
9055
  };
8576
9056
  }
9057
+
9058
+ // src/daemon/replay-cursor.ts
9059
+ var ReplayCursorTooOldError = class extends Error {
9060
+ constructor(recoverableFrom) {
9061
+ super(
9062
+ recoverableFrom === void 0 ? "server cannot replay the acknowledged cursor; re-pair or operator recovery is required" : `server cannot replay the acknowledged cursor; retained history starts at ${recoverableFrom}`
9063
+ );
9064
+ this.recoverableFrom = recoverableFrom;
9065
+ this.name = "ReplayCursorTooOldError";
9066
+ }
9067
+ recoverableFrom;
9068
+ };
9069
+
9070
+ // src/daemon/long-poll-transport.ts
8577
9071
  var LongPollRouteError = class extends Error {
8578
9072
  constructor(endpoint, status, cause) {
8579
9073
  super(
@@ -8753,6 +9247,12 @@ var LongPollClient = class {
8753
9247
  throw err;
8754
9248
  }
8755
9249
  if (!res.ok) {
9250
+ const replayCursorTooOld = await parseReplayCursorTooOld(res);
9251
+ if (replayCursorTooOld) {
9252
+ this.running = false;
9253
+ this.opts.onReplayCursorTooOld?.(replayCursorTooOld);
9254
+ return;
9255
+ }
8756
9256
  this.warnRouteFailure(this.eventsEndpoint, res.status, void 0);
8757
9257
  this.opts.onServerCapabilities?.([]);
8758
9258
  this.opts.onOperationalOutcome?.("failure");
@@ -8821,6 +9321,21 @@ var LongPollClient = class {
8821
9321
  }
8822
9322
  }
8823
9323
  };
9324
+ async function parseReplayCursorTooOld(res) {
9325
+ if (res.status !== 409) return void 0;
9326
+ let body;
9327
+ try {
9328
+ body = await res.json();
9329
+ } catch {
9330
+ return void 0;
9331
+ }
9332
+ if (typeof body !== "object" || body === null) return void 0;
9333
+ const { error, recoverableFrom } = body;
9334
+ if (error !== "cursor_too_old" || typeof recoverableFrom !== "number" || !Number.isSafeInteger(recoverableFrom) || recoverableFrom < 0) {
9335
+ return void 0;
9336
+ }
9337
+ return new ReplayCursorTooOldError(recoverableFrom);
9338
+ }
8824
9339
  function sleep(ms) {
8825
9340
  return new Promise((resolve) => setTimeout(resolve, ms));
8826
9341
  }
@@ -8965,16 +9480,17 @@ var WsTransport = class {
8965
9480
  }
8966
9481
  this.opts.onEnvelope(envelope);
8967
9482
  });
8968
- socket.on("close", () => {
9483
+ socket.on("close", (code, reason) => {
8969
9484
  this.socket = void 0;
8970
9485
  this.stopLivenessCheck();
8971
9486
  this.opts.onStateChange?.("closed");
8972
9487
  const acked = this.everAckedThisAttempt;
8973
9488
  const status = this.lastUnexpectedStatus;
8974
9489
  this.lastUnexpectedStatus = void 0;
9490
+ const replayCursorTooOld = code === 1008 && reason.toString("utf8") === "cursor_too_old" ? new ReplayCursorTooOldError() : void 0;
8975
9491
  this.opts.onConnectOutcome?.(
8976
9492
  acked,
8977
- status !== void 0 ? new WsUnexpectedStatusError(status, endpoint) : void 0,
9493
+ replayCursorTooOld ?? (status !== void 0 ? new WsUnexpectedStatusError(status, endpoint) : void 0),
8978
9494
  endpoint
8979
9495
  );
8980
9496
  if (!this.closedByUser && this.autoReconnect) this.scheduleReconnect();
@@ -9060,6 +9576,7 @@ var ConnectionManager = class {
9060
9576
  if (this.mode === "long-poll") this.serverCapabilities = capabilities;
9061
9577
  },
9062
9578
  onRevoked: () => this.enterRevoked(),
9579
+ onReplayCursorTooOld: (error) => this.enterReplayCursorTooOld(error),
9063
9580
  // M4 Phase 4 (version-negotiation drill fix): a batch entry
9064
9581
  // LongPollClient couldn't parse into a known Envelope at all (an
9065
9582
  // unrecognized message type) still needs its cursor/watermark
@@ -9148,6 +9665,7 @@ var ConnectionManager = class {
9148
9665
  draining = false;
9149
9666
  stopped = false;
9150
9667
  revoked = false;
9668
+ terminalError;
9151
9669
  settledWaiters = [];
9152
9670
  pendingCursorSave = Promise.resolve();
9153
9671
  /**
@@ -9220,6 +9738,7 @@ var ConnectionManager = class {
9220
9738
  */
9221
9739
  serverCapabilities = [];
9222
9740
  async start() {
9741
+ if (this.terminalError) throw this.terminalError;
9223
9742
  this.cursor = await this.opts.cursorStore.load(this.opts.serverUrl, this.opts.deviceId);
9224
9743
  this.ws.connect({ auto: true });
9225
9744
  }
@@ -9335,6 +9854,12 @@ var ConnectionManager = class {
9335
9854
  getServerCapabilities() {
9336
9855
  return this.serverCapabilities;
9337
9856
  }
9857
+ getTerminalError() {
9858
+ return this.terminalError;
9859
+ }
9860
+ getMode() {
9861
+ return this.mode;
9862
+ }
9338
9863
  isConnected() {
9339
9864
  return this.mode === "ws" && this.ws.isOpen;
9340
9865
  }
@@ -9355,6 +9880,7 @@ var ConnectionManager = class {
9355
9880
  */
9356
9881
  waitForAck(timeoutMs = 1e4) {
9357
9882
  if (this.ws.isOpen || this.mode === "long-poll") return Promise.resolve();
9883
+ if (this.terminalError) return Promise.reject(this.terminalError);
9358
9884
  if (this.revoked) return Promise.reject(new DeviceRevokedError());
9359
9885
  return new Promise((resolve, reject) => {
9360
9886
  let settle = () => {
@@ -9683,6 +10209,7 @@ var ConnectionManager = class {
9683
10209
  * (which is close-only) at all.
9684
10210
  */
9685
10211
  onAcked(capabilities) {
10212
+ if (this.terminalError) return;
9686
10213
  this.serverCapabilities = capabilities;
9687
10214
  this.consecutiveFailures = 0;
9688
10215
  this.notifySettled();
@@ -9691,6 +10218,10 @@ var ConnectionManager = class {
9691
10218
  void this.drainOutbox();
9692
10219
  }
9693
10220
  onWsOutcome(acked, err) {
10221
+ if (err instanceof ReplayCursorTooOldError) {
10222
+ this.enterReplayCursorTooOld(err);
10223
+ return;
10224
+ }
9694
10225
  if (this.stopped || this.revoked) return;
9695
10226
  if (acked) this.serverCapabilities = [];
9696
10227
  if (err instanceof WsUnexpectedStatusError && err.status === 401) {
@@ -9727,6 +10258,20 @@ var ConnectionManager = class {
9727
10258
  void this.drainOutbox();
9728
10259
  this.scheduleWsProbe();
9729
10260
  }
10261
+ enterReplayCursorTooOld(error) {
10262
+ if (this.terminalError) return;
10263
+ this.terminalError = error;
10264
+ this.stopped = true;
10265
+ this.serverCapabilities = [];
10266
+ if (this.wsRetryTimer) clearInterval(this.wsRetryTimer);
10267
+ this.longPoll.stop();
10268
+ this.ws.stopAutoReconnect();
10269
+ this.ws.close();
10270
+ this.cancelPendingDrainRetry?.();
10271
+ this.notifySettled(error);
10272
+ this.opts.onStateChange?.("closed");
10273
+ this.opts.onTerminalError?.(error);
10274
+ }
9730
10275
  exitLongPoll() {
9731
10276
  if (this.wsRetryTimer) {
9732
10277
  clearInterval(this.wsRetryTimer);
@@ -12490,21 +13035,28 @@ async function recordAuditWarning(context, kind, values) {
12490
13035
  }
12491
13036
  }
12492
13037
  var agentMemoryHomeQueues = /* @__PURE__ */ new Map();
12493
- async function exclusiveAgentMemoryHome(home, fn) {
12494
- const previous = agentMemoryHomeQueues.get(home) ?? Promise.resolve();
13038
+ var agentMemoryProjectionTransactionQueues = /* @__PURE__ */ new Map();
13039
+ async function exclusiveAgentMemoryHomeQueue(queues2, home, fn) {
13040
+ const previous = queues2.get(home) ?? Promise.resolve();
12495
13041
  let release;
12496
13042
  const next = new Promise((resolve) => {
12497
13043
  release = resolve;
12498
13044
  });
12499
- agentMemoryHomeQueues.set(home, next);
13045
+ queues2.set(home, next);
12500
13046
  await previous;
12501
13047
  try {
12502
13048
  return await fn();
12503
13049
  } finally {
12504
13050
  release();
12505
- if (agentMemoryHomeQueues.get(home) === next) agentMemoryHomeQueues.delete(home);
13051
+ if (queues2.get(home) === next) queues2.delete(home);
12506
13052
  }
12507
13053
  }
13054
+ async function exclusiveAgentMemoryHome(home, fn) {
13055
+ return exclusiveAgentMemoryHomeQueue(agentMemoryHomeQueues, home, fn);
13056
+ }
13057
+ async function exclusiveAgentMemoryProjectionTransaction(home, fn) {
13058
+ return exclusiveAgentMemoryHomeQueue(agentMemoryProjectionTransactionQueues, home, fn);
13059
+ }
12508
13060
  var AgentMemoryService = class {
12509
13061
  constructor(input) {
12510
13062
  this.input = input;
@@ -12808,13 +13360,16 @@ var AgentMemoryRedactedOutbox = class _AgentMemoryRedactedOutbox {
12808
13360
  async function snapshotAndProjectAgentMemory(input, projection) {
12809
13361
  if (projection?.capability !== AGENT_MEMORY_PROJECTION_CAPABILITY || !projection.grant || !projection.redactor || !projection.port) return;
12810
13362
  const context = taskContext(input);
12811
- const outbox = await AgentMemoryRedactedOutbox.open(context, projection.grant);
12812
- const initialReplay = await outbox.replay(projection.port);
12813
- if (initialReplay.status === "pending") throw new AgentMemoryProjectionReplayPendingError(initialReplay);
12814
- const snapshot = await captureAgentMemorySnapshot(context);
12815
- await outbox.append(redactedBytes(snapshot, await projection.redactor.redact(snapshot)));
12816
- const trailingReplay = await outbox.replay(projection.port);
12817
- if (trailingReplay.status === "pending") throw new AgentMemoryProjectionReplayPendingError(trailingReplay);
13363
+ const { grant, redactor, port } = projection;
13364
+ await exclusiveAgentMemoryProjectionTransaction(context.canonicalHome, async () => {
13365
+ const outbox = await AgentMemoryRedactedOutbox.open(context, grant);
13366
+ const initialReplay = await outbox.replay(port);
13367
+ if (initialReplay.status === "pending") throw new AgentMemoryProjectionReplayPendingError(initialReplay);
13368
+ const snapshot = await captureAgentMemorySnapshot(context);
13369
+ await outbox.append(redactedBytes(snapshot, await redactor.redact(snapshot)));
13370
+ const trailingReplay = await outbox.replay(port);
13371
+ if (trailingReplay.status === "pending") throw new AgentMemoryProjectionReplayPendingError(trailingReplay);
13372
+ });
12818
13373
  }
12819
13374
  var AGENT_MEMORY_FILESYSTEM_HELPER_PROTOCOL = 2;
12820
13375
  var AGENT_MEMORY_FILESYSTEM_HELPER_VERSION = "2";
@@ -13222,6 +13777,7 @@ var TaskRunner = class {
13222
13777
  deps;
13223
13778
  tasks = /* @__PURE__ */ new Map();
13224
13779
  pendingMessageTasks = /* @__PURE__ */ new Map();
13780
+ messageOutboxesByHome = /* @__PURE__ */ new Map();
13225
13781
  messageContextByToken = /* @__PURE__ */ new Map();
13226
13782
  messageContextByTask = /* @__PURE__ */ new Map();
13227
13783
  memoryContextByToken = /* @__PURE__ */ new Map();
@@ -13282,6 +13838,8 @@ var TaskRunner = class {
13282
13838
  * costs nothing.
13283
13839
  */
13284
13840
  inFlightOffers = /* @__PURE__ */ new Set();
13841
+ /** Blob I/O before an offer becomes an active task still belongs to that offer's cancellation authority. */
13842
+ inFlightBlobAborts = /* @__PURE__ */ new Map();
13285
13843
  /**
13286
13844
  * Finding P2 (Fix 2c): taskIds that have reached a terminal outcome
13287
13845
  * (Complete/Failed/Cancelled) this session — populated in `finish()`.
@@ -13408,6 +13966,7 @@ var TaskRunner = class {
13408
13966
  async recoverAgentMessageOutboxes(agentsRoot) {
13409
13967
  if (this.deps.tenantId === void 0) throw new Error("Agent message recovery requires authenticated tenant enrollment");
13410
13968
  for (const outbox of await AgentMessageOutbox.recover(agentsRoot, this.deps.tenantId)) {
13969
+ this.messageOutboxesByHome.set(outbox.homeDir, Promise.resolve(outbox));
13411
13970
  for (const record of outbox.retryableRecords()) {
13412
13971
  if (record.sessionRef === void 0) continue;
13413
13972
  const existing = this.recoveredMessageOutboxes.get(record.taskId);
@@ -13416,6 +13975,16 @@ var TaskRunner = class {
13416
13975
  }
13417
13976
  }
13418
13977
  }
13978
+ agentMessageOutbox(homeDir) {
13979
+ const existing = this.messageOutboxesByHome.get(homeDir);
13980
+ if (existing !== void 0) return existing;
13981
+ const opened = AgentMessageOutbox.open(homeDir);
13982
+ this.messageOutboxesByHome.set(homeDir, opened);
13983
+ void opened.catch(() => {
13984
+ if (this.messageOutboxesByHome.get(homeDir) === opened) this.messageOutboxesByHome.delete(homeDir);
13985
+ });
13986
+ return opened;
13987
+ }
13419
13988
  /** Retry stable recovered records after a transport handshake/re-handshake. */
13420
13989
  retryRecoveredAgentMessages() {
13421
13990
  for (const [taskId, outbox] of this.recoveredMessageOutboxes) {
@@ -13571,6 +14140,7 @@ var TaskRunner = class {
13571
14140
  if (active.finalizationStarted) return this.finish(active.taskId);
13572
14141
  if (!this.reserveSemanticTerminal(active)) return active.semanticTerminalSettled ?? false;
13573
14142
  active.beingTornDown = true;
14143
+ active.blobAbort.abort();
13574
14144
  await this.observeGit(active, "salvage");
13575
14145
  const timeoutMs = this.deps.shutdownInterruptTimeoutMs ?? DEFAULT_SHUTDOWN_INTERRUPT_TIMEOUT_MS;
13576
14146
  await raceSettleFirst(() => active.session.interrupt(), timeoutMs);
@@ -13717,6 +14287,8 @@ var TaskRunner = class {
13717
14287
  return;
13718
14288
  }
13719
14289
  this.inFlightOffers.add(taskId);
14290
+ const blobAbort = new AbortController();
14291
+ this.inFlightBlobAborts.set(taskId, blobAbort);
13720
14292
  let agentBinding;
13721
14293
  let agentLeaseTransferred = false;
13722
14294
  try {
@@ -13875,7 +14447,7 @@ var TaskRunner = class {
13875
14447
  }
13876
14448
  if (agentRef !== void 0) {
13877
14449
  try {
13878
- agentBinding = await this.deps.agentHome.acquire(agentRef);
14450
+ agentBinding = await this.deps.agentHome.acquireExecution(agentRef, { taskId, sessionRef });
13879
14451
  } catch (error) {
13880
14452
  decline(
13881
14453
  `Agent home admission failed: ${errorMessage5(error)}`,
@@ -13910,7 +14482,7 @@ var TaskRunner = class {
13910
14482
  }
13911
14483
  }
13912
14484
  try {
13913
- await this.deps.agentHome.initialize(agentBinding);
14485
+ await this.deps.agentHome.initializeExecution(agentBinding);
13914
14486
  } catch (error) {
13915
14487
  await agentBinding.lease.release().catch(() => {
13916
14488
  });
@@ -13920,7 +14492,7 @@ var TaskRunner = class {
13920
14492
  }
13921
14493
  if (messageRequirement !== void 0) {
13922
14494
  try {
13923
- const outbox = await AgentMessageOutbox.open(agentBinding.resolution.canonicalHome);
14495
+ const outbox = await this.agentMessageOutbox(agentBinding.resolution.canonicalHome);
13924
14496
  this.pendingMessageTasks.set(taskId, {
13925
14497
  taskId,
13926
14498
  agentRef: agentBinding.resolution.agentRef,
@@ -14039,7 +14611,7 @@ var TaskRunner = class {
14039
14611
  );
14040
14612
  let resolvedInstruction;
14041
14613
  try {
14042
- resolvedInstruction = await this.resolveInstruction(payload.instruction);
14614
+ resolvedInstruction = await this.resolveInstruction(payload.instruction, blobAbort.signal);
14043
14615
  if (plainWorkspaceNeedsResolve) workspaceDir = await this.resolveWorkspaceDir(taskId, known?.workspaceDir);
14044
14616
  } catch (err) {
14045
14617
  gitLease?.release();
@@ -14152,9 +14724,29 @@ var TaskRunner = class {
14152
14724
  }
14153
14725
  return;
14154
14726
  }
14727
+ if (agentBinding !== void 0) {
14728
+ try {
14729
+ await agentBinding.lease.bindSession(session.sessionRef);
14730
+ } catch (error) {
14731
+ await session.close().catch(() => {
14732
+ });
14733
+ await this.failClaimedAgent(
14734
+ taskId,
14735
+ `Agent session execution lease could not bind the runtime session: ${errorMessage5(error)}`,
14736
+ false,
14737
+ {
14738
+ binding: agentBinding,
14739
+ runtimeId: pick.descriptor.id,
14740
+ sessionRef: session.sessionRef
14741
+ }
14742
+ );
14743
+ return;
14744
+ }
14745
+ }
14155
14746
  let active;
14156
14747
  active = {
14157
14748
  taskId,
14749
+ blobAbort,
14158
14750
  egressEnabled: "egressPolicy" in payload,
14159
14751
  adapter: pick.adapter,
14160
14752
  session,
@@ -14195,15 +14787,16 @@ var TaskRunner = class {
14195
14787
  ...terminalProjection === void 0 ? {} : { terminalProjection }
14196
14788
  };
14197
14789
  if (agentBinding !== void 0) {
14790
+ const binding = agentBinding;
14198
14791
  try {
14199
- await this.deps.agentSessionHandoffs.record({
14200
- agentRef: agentBinding.resolution.agentRef,
14792
+ await this.deps.agentHome.mutateExecution(binding, () => this.deps.agentSessionHandoffs.record({
14793
+ agentRef: binding.resolution.agentRef,
14201
14794
  taskId,
14202
14795
  sessionRef: session.sessionRef,
14203
14796
  runtimeId: pick.descriptor.id,
14204
14797
  cwd: workspaceDir,
14205
- leaseId: agentBinding.lease.leaseId
14206
- });
14798
+ leaseId: binding.lease.leaseId
14799
+ }));
14207
14800
  } catch (error) {
14208
14801
  await session.close().catch(() => {
14209
14802
  });
@@ -14245,6 +14838,7 @@ var TaskRunner = class {
14245
14838
  }
14246
14839
  this.deps.send(createEnvelope("task.started", {}, { taskId }));
14247
14840
  agentLeaseTransferred = agentBinding !== void 0;
14841
+ this.inFlightBlobAborts.delete(taskId);
14248
14842
  this.tasks.set(taskId, active);
14249
14843
  this.pendingMessageTasks.delete(taskId);
14250
14844
  if (active.messageOutbox !== void 0 && activatedMessageRecord !== void 0) {
@@ -14276,6 +14870,7 @@ var TaskRunner = class {
14276
14870
  await agentBinding.lease.release().catch(() => {
14277
14871
  });
14278
14872
  }
14873
+ this.inFlightBlobAborts.delete(taskId);
14279
14874
  this.inFlightOffers.delete(taskId);
14280
14875
  }
14281
14876
  }
@@ -14427,9 +15022,9 @@ var TaskRunner = class {
14427
15022
  void this.closeAgentMemoryFilesystem(taskId);
14428
15023
  }
14429
15024
  /** Protocol §7: an instruction too large to inline arrives as a `blobRef` — resolve it via the blob client rather than failing closed. */
14430
- async resolveInstruction(instruction) {
15025
+ async resolveInstruction(instruction, signal) {
14431
15026
  if (typeof instruction === "string") return instruction;
14432
- return this.deps.blobClient.resolveInstruction(instruction.blobRef);
15027
+ return this.deps.blobClient.resolveInstruction(instruction.blobRef, { signal });
14433
15028
  }
14434
15029
  /** Resolve every requested logical id locally and reject missing/colliding server authority before claim. */
14435
15030
  resolveMcpServers(requiredToolsets) {
@@ -14661,7 +15256,9 @@ var TaskRunner = class {
14661
15256
  }
14662
15257
  }
14663
15258
  try {
14664
- const blobRef = await this.deps.blobClient.uploadArtifact(bytes, contentType);
15259
+ const blobRef = await this.deps.blobClient.uploadArtifact(bytes, contentType, {
15260
+ signal: active.blobAbort.signal
15261
+ });
14665
15262
  this.deps.send(createEnvelope("task.artifact", { name, contentType, blobRef }, { taskId: active.taskId }));
14666
15263
  } catch (err) {
14667
15264
  this.reportArtifactError(active, name, `failed to upload artifact "${name}": ${errorMessage5(err)}`);
@@ -14676,6 +15273,7 @@ var TaskRunner = class {
14676
15273
  const active = this.tasks.get(taskId);
14677
15274
  if (!active) {
14678
15275
  this.setPendingCancelled(taskId, reason);
15276
+ this.inFlightBlobAborts.get(taskId)?.abort();
14679
15277
  return;
14680
15278
  }
14681
15279
  if (active.finalizationStarted) {
@@ -14686,6 +15284,7 @@ var TaskRunner = class {
14686
15284
  await active.semanticTerminalSettled;
14687
15285
  return;
14688
15286
  }
15287
+ active.blobAbort.abort();
14689
15288
  try {
14690
15289
  await active.session.interrupt();
14691
15290
  } catch {
@@ -15185,7 +15784,7 @@ var TaskRunner = class {
15185
15784
  async failClaimedAgent(taskId, reason, retryable, context) {
15186
15785
  const agentRef = context.binding.resolution.agentRef;
15187
15786
  const result = await this.retryAgentTerminalEvidence(
15188
- () => this.deps.agentSessionHandoffs.recordTaskTerminal({
15787
+ () => this.deps.agentHome.mutateExecution(context.binding, () => this.deps.agentSessionHandoffs.recordTaskTerminal({
15189
15788
  agentRef,
15190
15789
  taskId,
15191
15790
  runtimeId: context.runtimeId,
@@ -15193,7 +15792,7 @@ var TaskRunner = class {
15193
15792
  leaseId: context.binding.lease.leaseId,
15194
15793
  ...context.sessionRef === void 0 ? {} : { sessionRef: context.sessionRef },
15195
15794
  terminalReason: reason
15196
- })
15795
+ }))
15197
15796
  );
15198
15797
  if (!result.ok) {
15199
15798
  this.reportAgentTerminalEvidenceFailure({
@@ -16182,6 +16781,8 @@ var AgentEgressController = class {
16182
16781
  options;
16183
16782
  latest = new AgentLatestValueState();
16184
16783
  spools = /* @__PURE__ */ new Map();
16784
+ spoolOpens = /* @__PURE__ */ new Map();
16785
+ reliableAppendTail = Promise.resolve();
16185
16786
  latestStatus = emptyLane();
16186
16787
  reliableStatus = emptyLane();
16187
16788
  drops = [];
@@ -16236,7 +16837,8 @@ var AgentEgressController = class {
16236
16837
  return latest === void 0 ? [] : Object.freeze([latest]);
16237
16838
  }
16238
16839
  async appendReliable(input) {
16239
- if (!this.active || this.options.tenantId === void 0) {
16840
+ const tenantId = this.options.tenantId;
16841
+ if (!this.active || tenantId === void 0) {
16240
16842
  this.noteDrop("reliable", "policy_denied", input.agentRef);
16241
16843
  return { ok: false, reason: "policy_denied" };
16242
16844
  }
@@ -16251,17 +16853,21 @@ var AgentEgressController = class {
16251
16853
  return { ok: false, reason: "sanitizer_rejected" };
16252
16854
  }
16253
16855
  try {
16254
- const spool = await this.spoolFor(input.homeDir, input.agentRef);
16255
- const record = await spool.append({
16256
- agentRef: input.agentRef,
16257
- tenantId: this.options.tenantId,
16258
- policyRevision: this.options.policy.policyRevision,
16259
- payload,
16260
- sessionRef: input.sessionRef,
16261
- ...input.taskId === void 0 ? {} : { taskId: input.taskId },
16262
- ...input.eventId === void 0 ? {} : { eventId: input.eventId }
16263
- }, this.options.policy, this.tenantPendingBytes());
16264
- return { ok: true, record };
16856
+ const spoolOpen = this.spoolFor(input.homeDir, input.agentRef);
16857
+ return await this.withAppendTail(async () => {
16858
+ const spool = await spoolOpen;
16859
+ this.bindSpool(input.agentRef, spool, spoolOpen);
16860
+ const record = await spool.append({
16861
+ agentRef: input.agentRef,
16862
+ tenantId,
16863
+ policyRevision: this.options.policy.policyRevision,
16864
+ payload,
16865
+ sessionRef: input.sessionRef,
16866
+ ...input.taskId === void 0 ? {} : { taskId: input.taskId },
16867
+ ...input.eventId === void 0 ? {} : { eventId: input.eventId }
16868
+ }, this.options.policy, this.tenantPendingBytes());
16869
+ return { ok: true, record };
16870
+ });
16265
16871
  } catch (error) {
16266
16872
  const reason = error instanceof AgentReliableQuotaError ? error.reason : "backpressure";
16267
16873
  this.noteDrop("reliable", reason, input.agentRef);
@@ -16274,21 +16880,26 @@ var AgentEgressController = class {
16274
16880
  * with `wireType: agent.content.receipt` before any transport attempt.
16275
16881
  */
16276
16882
  async appendContentReceipt(input) {
16277
- if (!this.active || this.options.tenantId === void 0) {
16883
+ const tenantId = this.options.tenantId;
16884
+ if (!this.active || tenantId === void 0) {
16278
16885
  this.noteDrop("reliable", "policy_denied", input.agentRef);
16279
16886
  return { ok: false, reason: "policy_denied" };
16280
16887
  }
16281
16888
  try {
16282
- const spool = await this.spoolFor(input.homeDir, input.agentRef);
16283
- const record = await spool.appendContentReceipt({
16284
- agentRef: input.agentRef,
16285
- tenantId: this.options.tenantId,
16286
- policyRevision: this.options.policy.policyRevision,
16287
- sessionRef: input.payload.sessionRef,
16288
- payload: input.payload,
16289
- ...input.taskId === void 0 ? {} : { taskId: input.taskId }
16290
- }, this.options.policy, this.tenantPendingBytes());
16291
- return { ok: true, record };
16889
+ const spoolOpen = this.spoolFor(input.homeDir, input.agentRef);
16890
+ return await this.withAppendTail(async () => {
16891
+ const spool = await spoolOpen;
16892
+ this.bindSpool(input.agentRef, spool, spoolOpen);
16893
+ const record = await spool.appendContentReceipt({
16894
+ agentRef: input.agentRef,
16895
+ tenantId,
16896
+ policyRevision: this.options.policy.policyRevision,
16897
+ sessionRef: input.payload.sessionRef,
16898
+ payload: input.payload,
16899
+ ...input.taskId === void 0 ? {} : { taskId: input.taskId }
16900
+ }, this.options.policy, this.tenantPendingBytes());
16901
+ return { ok: true, record };
16902
+ });
16292
16903
  } catch (error) {
16293
16904
  const reason = error instanceof AgentReliableQuotaError ? error.reason : "backpressure";
16294
16905
  this.noteDrop("reliable", reason, input.agentRef);
@@ -16363,20 +16974,59 @@ var AgentEgressController = class {
16363
16974
  }
16364
16975
  return this.reliableRecords();
16365
16976
  }
16366
- async spoolFor(homeDir, agentRef) {
16977
+ spoolFor(homeDir, agentRef) {
16367
16978
  const key = agentKey(agentRef);
16368
16979
  const existing = this.spools.get(key);
16369
- if (existing) return existing;
16370
- const spool = await AgentReliableSpool.open(homeDir);
16371
- if (spool.records().some((record) => !sameAgent(record.agentRef, agentRef))) {
16372
- throw new Error("Agent-local reliable spool has a different AgentRef");
16980
+ if (existing) {
16981
+ if (existing.homeDir !== homeDir) throw new Error("Agent reliable spool is already bound to a different home");
16982
+ return Promise.resolve(existing);
16983
+ }
16984
+ const inFlight = this.spoolOpens.get(key);
16985
+ if (inFlight !== void 0) {
16986
+ if (inFlight.homeDir !== homeDir) throw new Error("Agent reliable spool opening is already bound to a different home");
16987
+ return inFlight.promise;
16988
+ }
16989
+ const opened = (async () => {
16990
+ const spool = await AgentReliableSpool.open(homeDir);
16991
+ if (spool.records().some((record) => !sameAgent(record.agentRef, agentRef))) {
16992
+ throw new Error("Agent-local reliable spool has a different AgentRef");
16993
+ }
16994
+ return spool;
16995
+ })();
16996
+ const slot = { homeDir, promise: opened };
16997
+ this.spoolOpens.set(key, slot);
16998
+ void opened.catch(() => {
16999
+ if (this.spoolOpens.get(key) === slot) this.spoolOpens.delete(key);
17000
+ });
17001
+ return opened;
17002
+ }
17003
+ bindSpool(agentRef, spool, spoolOpen) {
17004
+ const key = agentKey(agentRef);
17005
+ const existing = this.spools.get(key);
17006
+ try {
17007
+ if (existing && existing !== spool) throw new Error("multiple Agent egress spools claim the same AgentRef");
17008
+ this.spools.set(key, spool);
17009
+ } finally {
17010
+ const slot = this.spoolOpens.get(key);
17011
+ if (slot?.promise === spoolOpen) this.spoolOpens.delete(key);
16373
17012
  }
16374
- this.spools.set(key, spool);
16375
- return spool;
16376
17013
  }
16377
17014
  tenantPendingBytes() {
16378
17015
  return this.reliableRecords().reduce((total, record) => total + record.byteCount, 0);
16379
17016
  }
17017
+ async withAppendTail(operation) {
17018
+ const previous = this.reliableAppendTail;
17019
+ let release;
17020
+ this.reliableAppendTail = new Promise((resolve) => {
17021
+ release = resolve;
17022
+ });
17023
+ try {
17024
+ await previous;
17025
+ return await operation();
17026
+ } finally {
17027
+ release();
17028
+ }
17029
+ }
16380
17030
  noteDrop(lane, reason, agentRef, countDrop = true) {
16381
17031
  const laneStatus = lane === "latest-value" ? this.latestStatus : this.reliableStatus;
16382
17032
  if (countDrop) laneStatus.dropped += 1;
@@ -17648,6 +18298,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
17648
18298
  const observer = new DaemonObserver();
17649
18299
  const approvalRegistry = new ApprovalRegistry();
17650
18300
  let connection;
18301
+ let blobLifecycleAbort;
17651
18302
  let connectionState = "closed";
17652
18303
  let daemonStarted = false;
17653
18304
  let tenantRebinding = false;
@@ -17666,6 +18317,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
17666
18317
  function buildAuthManager() {
17667
18318
  return new AuthManager({
17668
18319
  serverUrl: config.serverUrl,
18320
+ authRequestDeadlineMs: config.authRequestDeadlineMs,
17669
18321
  store,
17670
18322
  deviceName: config.deviceName,
17671
18323
  machineId: config.machineId ?? (() => resolveMachineId({ productId: config.productId })),
@@ -17683,7 +18335,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
17683
18335
  assertServerUrlAllowed(config.serverUrl, { dangerouslyAllowInsecureRemote: true });
17684
18336
  const reason = err instanceof Error ? err.message : String(err);
17685
18337
  console.warn(
17686
- `[byok/client] WARNING: dangerouslyAllowInsecureRemote is set \u2014 proceeding with an insecure server URL (${config.serverUrl}). Device credentials and task data will be sent in the clear to a non-loopback host. This should never be used in a real deployment. (${reason})`
18338
+ `[byok/client] WARNING: dangerouslyAllowInsecureRemote is set \u2014 proceeding with an insecure server URL (${formatServerUrl(config.serverUrl)}). Device credentials and task data will be sent in the clear to a non-loopback host. This should never be used in a real deployment. (${reason})`
17687
18339
  );
17688
18340
  }
17689
18341
  }
@@ -17843,9 +18495,10 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
17843
18495
  await gitWorkspaceStore?.initialize();
17844
18496
  await gitWorkspaceStore?.reconcile();
17845
18497
  }
18498
+ blobLifecycleAbort = new AbortController();
17846
18499
  const [runtimes, blobClient] = await Promise.all([
17847
18500
  detectRuntimes(adapters),
17848
- Promise.resolve(new BlobClient(config.serverUrl, auth))
18501
+ Promise.resolve(new BlobClient(config.serverUrl, auth, { signal: blobLifecycleAbort.signal }))
17849
18502
  ]);
17850
18503
  observer.noteRuntimesDetected(runtimes);
17851
18504
  detectedRuntimeFacts = runtimes;
@@ -18274,6 +18927,8 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
18274
18927
  }
18275
18928
  async function runShutdownSequence(reason, opts = {}) {
18276
18929
  shuttingDown = true;
18930
+ blobLifecycleAbort?.abort();
18931
+ blobLifecycleAbort = void 0;
18277
18932
  const errors = [];
18278
18933
  let mutationBarrierComplete = hostedStorageInitializationBarrierComplete;
18279
18934
  if (!hostedStorageInitializationBarrierComplete) {
@@ -19253,7 +19908,7 @@ function createServiceLifecycle(def, opts = {}) {
19253
19908
 
19254
19909
  // src/bin/official-release.ts
19255
19910
  var OFFICIAL_LOCAL_AGENT_RELEASE = resolveLocalAgentReleaseIdentity({
19256
- version: "0.11.0"
19911
+ version: "0.12.0"
19257
19912
  });
19258
19913
 
19259
19914
  // src/bin/config.ts