@byok-sdk/client 0.11.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.
Files changed (34) hide show
  1. package/README.md +28 -3
  2. package/dist/adapters/index.js +74 -5
  3. package/dist/adapters/index.js.map +1 -1
  4. package/dist/adapters/pi/pi-adapter.d.ts +3 -0
  5. package/dist/adapters/pi/subagents-policy-extension.js +1 -1
  6. package/dist/adapters/pi/subagents-policy-extension.js.map +1 -1
  7. package/dist/agent-home.d.ts +107 -0
  8. package/dist/agent-memory/index.d.ts +1 -1
  9. package/dist/agent-memory/index.js +8 -5
  10. package/dist/agent-memory/index.js.map +1 -1
  11. package/dist/bin/byok-agent-memory-mcp.js.map +1 -1
  12. package/dist/bin/byok-agent-message-mcp.js.map +1 -1
  13. package/dist/bin/byok-agent-team-mcp.js.map +1 -1
  14. package/dist/bin/byok-agent.js +1279 -642
  15. package/dist/bin/byok-agent.js.map +1 -1
  16. package/dist/bin/byok-approval-mcp.js.map +1 -1
  17. package/dist/daemon/agent-egress-controller.d.ts +4 -0
  18. package/dist/daemon/auth-manager.d.ts +20 -0
  19. package/dist/daemon/blob-client.d.ts +24 -6
  20. package/dist/daemon/connection-manager.d.ts +69 -126
  21. package/dist/daemon/control-protocol.d.ts +8 -1
  22. package/dist/daemon/create-daemon.d.ts +28 -13
  23. package/dist/daemon/device-credential-store.d.ts +16 -0
  24. package/dist/daemon/long-poll-transport.d.ts +32 -23
  25. package/dist/daemon/observer.d.ts +1 -1
  26. package/dist/daemon/replay-cursor.d.ts +9 -0
  27. package/dist/daemon/task-runner.d.ts +22 -0
  28. package/dist/daemon/url.d.ts +17 -13
  29. package/dist/diagnostics/support-bundle.d.ts +1 -1
  30. package/dist/index.d.ts +5 -4
  31. package/dist/index.js +1278 -641
  32. package/dist/index.js.map +1 -1
  33. package/package.json +5 -7
  34. package/dist/daemon/ws-transport.d.ts +0 -139
@@ -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, PROVIDER_PROFILE_BINDING_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, BYOK_EVENTS_PATH, BYOK_MESSAGES_PATH, MessagesSendResponseSchema, UnknownMessageTypeError, MESSAGE_TYPES, 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';
@@ -13,7 +13,6 @@ import 'readline';
13
13
  import { isTenantId, DEVICE_ASSERTION_AUDIENCE_MAX_BYTES, DEVICE_ASSERTION_DEFAULT_TTL_MS, DEVICE_ASSERTION_MAX_TTL_MS, nonceSigningBytes, CapabilityDeclarationSchema, hasCapability, DeviceAssertionClaimsSchema, deviceAssertionSigningInput, DEVICE_ASSERTION_SCHEMA_ID } from '@byok-sdk/core';
14
14
  import { promisify } from 'util';
15
15
  import * as fs13 from 'fs/promises';
16
- import { WebSocket } from 'ws';
17
16
  import { createRequire } from 'module';
18
17
  import { createInterface } from 'readline/promises';
19
18
 
@@ -445,6 +444,23 @@ async function resolveExistingAncestor(inputPath) {
445
444
  }
446
445
  }
447
446
  }
447
+ async function canonicalPath(inputPath) {
448
+ const { canonical: canonical2, tail } = await resolveExistingAncestor(inputPath);
449
+ return path3__default.resolve(canonical2, ...tail);
450
+ }
451
+ async function assertRealDirectoryIfPresent(target) {
452
+ let stat;
453
+ try {
454
+ stat = await promises.lstat(target);
455
+ } catch (error) {
456
+ const code = error.code;
457
+ if (code === "ENOENT" || code === "ENOTDIR") return;
458
+ throw error;
459
+ }
460
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
461
+ throw new AgentHomeResolutionError(`Agent home path component is not a real directory: ${target}`);
462
+ }
463
+ }
448
464
  async function materializeDirectory(inputPath) {
449
465
  const { canonical: canonical2, tail } = await resolveExistingAncestor(inputPath);
450
466
  let cursor = canonical2;
@@ -539,6 +555,39 @@ var AgentHomeLayout = class {
539
555
  await gate.release();
540
556
  }
541
557
  }
558
+ /**
559
+ * Pure canonical-home derivation for read-only callers, such as the
560
+ * pre-admission single-writer count. It validates the AgentRef and joins
561
+ * exactly the same `<hostStorageRoot>/agents/<agentId>` segments
562
+ * {@link AgentHomeLayout.resolve} would, canonicalizing only the components
563
+ * that already exist.
564
+ *
565
+ * It deliberately creates no directory, takes no cross-process mutation
566
+ * gate and records no Agent binding, so an offer the host vetoes after the
567
+ * count leaves nothing behind on disk. `resolve()` stays the only path that
568
+ * may materialize a home or bind it to an Agent identity.
569
+ *
570
+ * An `agents` root or `agents/<agentId>` leaf that already exists but is a
571
+ * symlink (or any non-directory) is rejected here with the same error class
572
+ * and message `resolve()` raises for it, so an in-root `two -> one` link
573
+ * fails closed instead of silently keying the count of `one`. A leaf that
574
+ * does not exist yet is not an error: this derivation runs before the home
575
+ * is materialized. A home canonicalizing outside the `agents` root stays
576
+ * rejected as before.
577
+ */
578
+ async canonicalHomePath(agentRefInput) {
579
+ const agentRef = validateAgentRef(agentRefInput);
580
+ const hostStorageRoot = this.canonicalRoot ?? await canonicalPath(this.hostStorageRootInput);
581
+ const agentsRoot = path3__default.join(hostStorageRoot, AGENT_HOME_DIRECTORY);
582
+ await assertRealDirectoryIfPresent(agentsRoot);
583
+ const lexicalHome = path3__default.join(agentsRoot, agentRef.agentId);
584
+ await assertRealDirectoryIfPresent(lexicalHome);
585
+ const canonicalHome = await canonicalPath(lexicalHome);
586
+ if (canonicalHome === agentsRoot || !isWithin(agentsRoot, canonicalHome)) {
587
+ throw new AgentHomeResolutionError(`Agent home resolves outside the Agent home root: ${canonicalHome}`);
588
+ }
589
+ return canonicalHome;
590
+ }
542
591
  /**
543
592
  * Prove the canonical root is materializable and writable before the daemon
544
593
  * advertises Agent-home capability. No Agent identity or persistent Agent
@@ -768,6 +817,158 @@ var AgentHomeLeaseManager = class _AgentHomeLeaseManager {
768
817
  }
769
818
  }
770
819
  };
820
+ function executionKey(input) {
821
+ const value = input.sessionRef === void 0 ? input.taskId : input.sessionRef;
822
+ const label = input.sessionRef === void 0 ? "taskId" : "sessionRef";
823
+ if (typeof value !== "string" || value.length === 0 || /[\u0000\r\n]/u.test(value)) {
824
+ throw new AgentHomeResolutionError(`Agent execution ${label} must be a non-empty single-line string`);
825
+ }
826
+ return `${input.sessionRef === void 0 ? "task" : "session"}\0${value}`;
827
+ }
828
+ var AgentHomeExecutionLeaseManager = class _AgentHomeExecutionLeaseManager {
829
+ constructor(manager) {
830
+ this.manager = manager;
831
+ }
832
+ manager;
833
+ static groups = /* @__PURE__ */ new Map();
834
+ static queues = /* @__PURE__ */ new Map();
835
+ async acquire(resolution, input) {
836
+ const initialKey = executionKey(input);
837
+ return this.exclusive(resolution.canonicalHome, async () => {
838
+ let group = _AgentHomeExecutionLeaseManager.groups.get(resolution.canonicalHome);
839
+ if (group === void 0) {
840
+ const baseLease = await this.manager.acquire(resolution);
841
+ group = {
842
+ manager: this.manager,
843
+ baseLease,
844
+ agentId: resolution.agentRef.agentId,
845
+ leasesByKey: /* @__PURE__ */ new Map()
846
+ };
847
+ _AgentHomeExecutionLeaseManager.groups.set(resolution.canonicalHome, group);
848
+ } else if (group.manager !== this.manager || group.agentId !== resolution.agentRef.agentId) {
849
+ throw new AgentHomeBusyError(`Agent home ${resolution.canonicalHome} is active under another execution owner`);
850
+ }
851
+ if (group.leasesByKey.has(initialKey)) {
852
+ throw new AgentHomeBusyError(`Agent session already has an active execution lease in ${resolution.canonicalHome}`);
853
+ }
854
+ const leaseId = randomUUID();
855
+ group.leasesByKey.set(initialKey, leaseId);
856
+ let currentKey = initialKey;
857
+ let sessionBound = input.sessionRef !== void 0;
858
+ let released = false;
859
+ return Object.freeze({
860
+ leaseId,
861
+ agentRef: resolution.agentRef,
862
+ canonicalHome: resolution.canonicalHome,
863
+ cwd: resolution.canonicalHome,
864
+ homeIdentity: group.baseLease.homeIdentity,
865
+ bindSession: async (sessionRef) => {
866
+ const nextKey = executionKey({ taskId: input.taskId, sessionRef });
867
+ await this.exclusive(resolution.canonicalHome, async () => {
868
+ if (released) throw new AgentHomeBusyError(`Agent execution lease ${leaseId} is already released`);
869
+ const currentGroup = _AgentHomeExecutionLeaseManager.groups.get(resolution.canonicalHome);
870
+ if (currentGroup !== group || currentGroup.leasesByKey.get(currentKey) !== leaseId) {
871
+ throw new AgentHomeBusyError(`Agent execution lease ${leaseId} is no longer owned by this process`);
872
+ }
873
+ if (nextKey === currentKey) return;
874
+ if (sessionBound) {
875
+ throw new AgentHomeBusyError(
876
+ `Agent session execution lease ${leaseId} cannot rebind to a different runtime session`
877
+ );
878
+ }
879
+ if (currentGroup.leasesByKey.has(nextKey)) {
880
+ throw new AgentHomeBusyError(`Agent session already has an active execution lease in ${resolution.canonicalHome}`);
881
+ }
882
+ currentGroup.leasesByKey.set(nextKey, leaseId);
883
+ currentGroup.leasesByKey.delete(currentKey);
884
+ currentKey = nextKey;
885
+ sessionBound = true;
886
+ });
887
+ },
888
+ release: async () => {
889
+ await this.exclusive(resolution.canonicalHome, async () => {
890
+ if (released) return;
891
+ const currentGroup = _AgentHomeExecutionLeaseManager.groups.get(resolution.canonicalHome);
892
+ if (currentGroup !== group || currentGroup.leasesByKey.get(currentKey) !== leaseId) {
893
+ released = true;
894
+ throw new AgentHomeBusyError(`Agent execution lease ${leaseId} is no longer owned by this process`);
895
+ }
896
+ currentGroup.leasesByKey.delete(currentKey);
897
+ released = true;
898
+ if (currentGroup.leasesByKey.size === 0) {
899
+ _AgentHomeExecutionLeaseManager.groups.delete(resolution.canonicalHome);
900
+ await currentGroup.baseLease.release();
901
+ }
902
+ });
903
+ }
904
+ });
905
+ });
906
+ }
907
+ /**
908
+ * WP0: Attempts currently holding an execution lease on this exact
909
+ * canonical home, across every lane and every session. This is the number
910
+ * the daemon's admission gate reads before any side effect — see
911
+ * `TaskRunner.handleOffer`'s per-home busy gate.
912
+ *
913
+ * Derived from the one lease registry above rather than a second tally, so
914
+ * it inherits the lease lifecycle exactly: an entry appears at `acquire()`,
915
+ * survives `bindSession()` (which rekeys in place), and disappears only at
916
+ * `release()`, which the task runner calls after the attempt is terminal
917
+ * AND `Session.close()` resolved. A failed disposal never reaches
918
+ * `release()`, so the slot stays held — fail closed, the same posture as
919
+ * `runtime-disposal-failed`. Crash residue needs nothing extra here: a
920
+ * restarted daemon starts with an empty registry and reclaims the on-disk
921
+ * marker only under the same stable owner identity (`openLeaseMarker`).
922
+ *
923
+ * Counted regardless of which lease manager owns the group: the invariant
924
+ * being protected is the filesystem path (`MEMORY.md`, `notes/`, `.git`),
925
+ * not the owner identity.
926
+ */
927
+ activeAttemptCount(canonicalHome) {
928
+ return _AgentHomeExecutionLeaseManager.groups.get(canonicalHome)?.leasesByKey.size ?? 0;
929
+ }
930
+ /**
931
+ * Counts-only readback for daemon/control status. Scoped to this manager's
932
+ * own leases, so the number describes this daemon rather than every home
933
+ * any manager in the process happens to hold. Never exposes a home path.
934
+ */
935
+ activeAttemptSummary() {
936
+ let homes = 0;
937
+ let attempts = 0;
938
+ for (const group of _AgentHomeExecutionLeaseManager.groups.values()) {
939
+ if (group.manager !== this.manager) continue;
940
+ homes += 1;
941
+ attempts += group.leasesByKey.size;
942
+ }
943
+ return { homes, attempts };
944
+ }
945
+ async mutate(binding, operation) {
946
+ return this.exclusive(binding.resolution.canonicalHome, async () => {
947
+ const group = _AgentHomeExecutionLeaseManager.groups.get(binding.resolution.canonicalHome);
948
+ if (group === void 0 || group.manager !== this.manager || ![...group.leasesByKey.values()].includes(binding.lease.leaseId)) {
949
+ throw new AgentHomeBusyError("Agent execution lease does not own this home mutation");
950
+ }
951
+ return operation();
952
+ });
953
+ }
954
+ async exclusive(canonicalHome, operation) {
955
+ const prior = _AgentHomeExecutionLeaseManager.queues.get(canonicalHome) ?? Promise.resolve();
956
+ let release;
957
+ const tail = new Promise((resolve) => {
958
+ release = resolve;
959
+ });
960
+ _AgentHomeExecutionLeaseManager.queues.set(canonicalHome, tail);
961
+ await prior;
962
+ try {
963
+ return await operation();
964
+ } finally {
965
+ release();
966
+ if (_AgentHomeExecutionLeaseManager.queues.get(canonicalHome) === tail) {
967
+ _AgentHomeExecutionLeaseManager.queues.delete(canonicalHome);
968
+ }
969
+ }
970
+ }
971
+ };
771
972
  async function initializeAgentHome(resolution) {
772
973
  await ensureDirectoryNoSymlink(
773
974
  resolution.canonicalHome,
@@ -843,10 +1044,12 @@ var AgentHomeManager = class {
843
1044
  layout;
844
1045
  projection;
845
1046
  leaseManager;
1047
+ executionLeaseManager;
846
1048
  constructor(options) {
847
1049
  this.layout = new AgentHomeLayout(options.hostStorageRoot);
848
1050
  this.projection = options.projection;
849
1051
  this.leaseManager = options.leaseManager ?? new AgentHomeLeaseManager();
1052
+ this.executionLeaseManager = new AgentHomeExecutionLeaseManager(this.leaseManager);
850
1053
  }
851
1054
  async prepare(agentRef) {
852
1055
  const binding = await this.acquire(agentRef);
@@ -873,12 +1076,25 @@ var AgentHomeManager = class {
873
1076
  const lease = await this.leaseManager.acquire(resolution);
874
1077
  return Object.freeze({ resolution, lease });
875
1078
  }
1079
+ async acquireExecution(agentRef, input) {
1080
+ const resolution = await this.layout.resolve(agentRef);
1081
+ const lease = await this.executionLeaseManager.acquire(resolution, input);
1082
+ return Object.freeze({ resolution, lease });
1083
+ }
876
1084
  /** Initialize only after any requested session exact-match has succeeded. */
877
1085
  async initialize(binding) {
878
- const { resolution, lease } = binding;
1086
+ await this.initializeResolved(binding.resolution, binding.lease.cwd);
1087
+ }
1088
+ async initializeExecution(binding) {
1089
+ await this.mutateExecution(binding, () => this.initializeResolved(binding.resolution, binding.lease.cwd));
1090
+ }
1091
+ async mutateExecution(binding, operation) {
1092
+ return this.executionLeaseManager.mutate(binding, operation);
1093
+ }
1094
+ async initializeResolved(resolution, cwd) {
879
1095
  await initializeAgentHome(resolution);
880
1096
  const prepare = this.projection?.prepare;
881
- if (prepare !== void 0) await prepare({ ...resolution, cwd: lease.cwd });
1097
+ if (prepare !== void 0) await prepare({ ...resolution, cwd });
882
1098
  if (await promises.realpath(resolution.homeDir) !== resolution.canonicalHome) {
883
1099
  throw new AgentHomeResolutionError("Agent projection changed the canonical home path");
884
1100
  }
@@ -1689,13 +1905,22 @@ function freezeRuntimeAdapterDescriptor(descriptor) {
1689
1905
  });
1690
1906
  }
1691
1907
  function sealRuntimeOperationManifest(manifest) {
1908
+ const dispatchSelection = manifest.dispatchSelection === void 0 ? void 0 : manifest.dispatchSelection.lane === "byok-profile" ? Object.freeze({
1909
+ ...manifest.dispatchSelection,
1910
+ providerProfile: Object.freeze({
1911
+ ...manifest.dispatchSelection.providerProfile,
1912
+ requiredCapabilities: Object.freeze([
1913
+ ...manifest.dispatchSelection.providerProfile.requiredCapabilities
1914
+ ])
1915
+ })
1916
+ }) : Object.freeze({ ...manifest.dispatchSelection });
1692
1917
  return Object.freeze({
1693
1918
  taskId: manifest.taskId,
1694
1919
  runtimeId: manifest.runtimeId,
1695
1920
  descriptor: freezeRuntimeAdapterDescriptor(manifest.descriptor),
1696
1921
  policy: frozenPolicy(manifest.policy),
1697
1922
  requiredToolsetIds: Object.freeze([...manifest.requiredToolsetIds]),
1698
- ...manifest.dispatchSelection === void 0 ? {} : { dispatchSelection: Object.freeze({ ...manifest.dispatchSelection }) },
1923
+ ...dispatchSelection === void 0 ? {} : { dispatchSelection },
1699
1924
  ...manifest.sessionRef === void 0 ? {} : { sessionRef: manifest.sessionRef },
1700
1925
  ...manifest.agentRef === void 0 ? {} : { agentRef: Object.freeze({ agentId: manifest.agentRef.agentId, profileRevision: manifest.agentRef.profileRevision }) },
1701
1926
  cwd: manifest.cwd ?? manifest.workspace.workspaceDir,
@@ -3723,7 +3948,11 @@ function validatePiByokLauncherConfig(launcher) {
3723
3948
  "--macos-keychain-path",
3724
3949
  "--secret-service-prefix",
3725
3950
  "--provider",
3726
- "--model"
3951
+ "--model",
3952
+ "--profile-revision",
3953
+ "--profile-hash",
3954
+ "--required-capabilities",
3955
+ "--validate-only"
3727
3956
  ]);
3728
3957
  const conflicting = launcher.args?.find((arg) => reserved.has(arg));
3729
3958
  if (conflicting !== void 0) {
@@ -3785,11 +4014,17 @@ var PiAdapter = class {
3785
4014
  const bin = this.resolveBin();
3786
4015
  const extensions = (this.options.resolveExtensions ?? resolvePiExtensions)();
3787
4016
  const selection = input.offer.dispatchSelection;
3788
- const pinnedSelection = selection === void 0 ? void 0 : Object.freeze({ ...selection });
4017
+ const pinnedSelection = selection === void 0 ? void 0 : selection.lane === "byok-profile" ? Object.freeze({
4018
+ ...selection,
4019
+ providerProfile: Object.freeze({
4020
+ ...selection.providerProfile,
4021
+ requiredCapabilities: Object.freeze([...selection.providerProfile.requiredCapabilities])
4022
+ })
4023
+ }) : Object.freeze({ ...selection });
3789
4024
  let command = bin.command;
3790
4025
  let launcherArgs;
3791
4026
  if (pinnedSelection !== void 0) {
3792
- if (pinnedSelection.lane !== "byok" || pinnedSelection.runtimeId !== "pi") {
4027
+ if (pinnedSelection.lane !== "byok" && pinnedSelection.lane !== "byok-profile" || pinnedSelection.runtimeId !== "pi") {
3793
4028
  return { kind: "reject", reason: `pi adapter cannot execute ${pinnedSelection.lane} selection for runtime ${pinnedSelection.runtimeId}`, retryable: false };
3794
4029
  }
3795
4030
  const launcher = this.options.byokLauncher;
@@ -3797,6 +4032,21 @@ var PiAdapter = class {
3797
4032
  return { kind: "reject", reason: "pi BYOK selection requires a configured credential-custody launcher", retryable: false };
3798
4033
  }
3799
4034
  command = launcher.command;
4035
+ const providerProfile = pinnedSelection.lane === "byok-profile" ? pinnedSelection.providerProfile : void 0;
4036
+ if (providerProfile !== void 0) {
4037
+ try {
4038
+ await (this.options.validateProviderProfileBinding ?? validateProviderProfileBindingWithLauncher)(
4039
+ providerProfile,
4040
+ launcher
4041
+ );
4042
+ } catch (error) {
4043
+ return {
4044
+ kind: "reject",
4045
+ reason: `provider profile admission failed: ${errorMessage2(error)}`,
4046
+ retryable: false
4047
+ };
4048
+ }
4049
+ }
3800
4050
  launcherArgs = [
3801
4051
  ...launcher.args ?? [],
3802
4052
  "--pi-bin",
@@ -3808,9 +4058,19 @@ var PiAdapter = class {
3808
4058
  ...launcher.macosKeychainPath !== void 0 ? ["--macos-keychain-path", launcher.macosKeychainPath] : [],
3809
4059
  ...launcher.secretServicePrefix ? ["--secret-service-prefix", launcher.secretServicePrefix] : [],
3810
4060
  "--provider",
3811
- pinnedSelection.providerId,
4061
+ providerProfile?.profileRef ?? (pinnedSelection.lane === "byok" ? pinnedSelection.providerId : ""),
3812
4062
  "--model",
3813
- pinnedSelection.modelId
4063
+ providerProfile?.modelId ?? (pinnedSelection.lane === "byok" ? pinnedSelection.modelId : ""),
4064
+ ...providerProfile === void 0 ? [] : [
4065
+ "--profile-revision",
4066
+ providerProfile.profileRevision,
4067
+ "--profile-hash",
4068
+ providerProfile.profileHash,
4069
+ "--required-capabilities",
4070
+ JSON.stringify(providerProfile.requiredCapabilities),
4071
+ "--validate-only",
4072
+ "false"
4073
+ ]
3814
4074
  ];
3815
4075
  }
3816
4076
  return {
@@ -3953,8 +4213,42 @@ var PiAdapter = class {
3953
4213
  return (this.options.resolveBin ?? resolvePiBin)();
3954
4214
  }
3955
4215
  };
4216
+ async function validateProviderProfileBindingWithLauncher(binding, launcher) {
4217
+ await execFileAsync(launcher.command, [
4218
+ ...launcher.args ?? [],
4219
+ "--pi-bin",
4220
+ process.execPath,
4221
+ "--profile-db",
4222
+ launcher.profileDbPath,
4223
+ "--session-dir",
4224
+ launcher.sessionDir,
4225
+ "--provider",
4226
+ binding.profileRef,
4227
+ "--model",
4228
+ binding.modelId,
4229
+ "--profile-revision",
4230
+ binding.profileRevision,
4231
+ "--profile-hash",
4232
+ binding.profileHash,
4233
+ "--required-capabilities",
4234
+ JSON.stringify(binding.requiredCapabilities),
4235
+ "--validate-only",
4236
+ "true",
4237
+ ...launcher.macosKeychainPath !== void 0 ? ["--macos-keychain-path", launcher.macosKeychainPath] : [],
4238
+ ...launcher.secretServicePrefix ? ["--secret-service-prefix", launcher.secretServicePrefix] : []
4239
+ ], { timeout: DETECT_TIMEOUT_MS });
4240
+ }
3956
4241
  function sameDispatchSelection(left, right) {
3957
4242
  if (left === void 0 || right === void 0) return left === right;
4243
+ if (left.lane === "byok-profile" || right.lane === "byok-profile") {
4244
+ if (left.lane !== "byok-profile" || right.lane !== "byok-profile") return false;
4245
+ if (left.runtimeId !== right.runtimeId) return false;
4246
+ const leftProfile = left.providerProfile;
4247
+ const rightProfile = right.providerProfile;
4248
+ return leftProfile.profileRef === rightProfile.profileRef && leftProfile.profileRevision === rightProfile.profileRevision && leftProfile.profileHash === rightProfile.profileHash && leftProfile.modelId === rightProfile.modelId && leftProfile.requiredCapabilities.length === rightProfile.requiredCapabilities.length && leftProfile.requiredCapabilities.every(
4249
+ (capability, index) => capability === rightProfile.requiredCapabilities[index]
4250
+ );
4251
+ }
3958
4252
  return left.lane === right.lane && left.runtimeId === right.runtimeId && left.providerId === right.providerId && left.modelId === right.modelId;
3959
4253
  }
3960
4254
  async function resolveAuthoritativeSessionId(rpc) {
@@ -6371,9 +6665,18 @@ function assertRecord(value) {
6371
6665
  throw new DeviceCredentialStoreError("OS credential entry has an incomplete device credential");
6372
6666
  }
6373
6667
  }
6374
- function encode(record) {
6375
- assertRecord(record);
6376
- const encoded = Buffer.from(JSON.stringify(record), "utf8").toString("base64");
6668
+ function isFirstPairingAttempt(value) {
6669
+ return "kind" in value && value.kind === "first-pairing-attempt-v1";
6670
+ }
6671
+ function assertFirstPairingAttempt(value) {
6672
+ 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) {
6673
+ throw new DeviceCredentialStoreError("OS credential entry has an invalid first-pairing attempt shape");
6674
+ }
6675
+ }
6676
+ function encode(authority) {
6677
+ if (isFirstPairingAttempt(authority)) assertFirstPairingAttempt(authority);
6678
+ else assertRecord(authority);
6679
+ const encoded = Buffer.from(JSON.stringify(authority), "utf8").toString("base64");
6377
6680
  const value = `${ENCODED_PREFIX}${encoded}`;
6378
6681
  if (Buffer.byteLength(value, "utf8") > 2400) {
6379
6682
  throw new DeviceCredentialStoreError("device credential exceeds the OS credential entry bound");
@@ -6402,6 +6705,16 @@ function decode(value) {
6402
6705
  } catch {
6403
6706
  throw new DeviceCredentialStoreError("OS credential entry is not valid JSON");
6404
6707
  }
6708
+ if (typeof parsed === "object" && parsed !== null && parsed.kind === "first-pairing-attempt-v1") {
6709
+ assertFirstPairingAttempt(parsed);
6710
+ return Object.freeze({
6711
+ kind: parsed.kind,
6712
+ deviceName: parsed.deviceName,
6713
+ devicePublicKey: parsed.devicePublicKey,
6714
+ devicePrivateKeyPem: parsed.devicePrivateKeyPem,
6715
+ ...parsed.machineId === void 0 ? {} : { machineId: parsed.machineId }
6716
+ });
6717
+ }
6405
6718
  assertRecord(parsed);
6406
6719
  return Object.freeze({
6407
6720
  deviceId: parsed.deviceId,
@@ -6432,6 +6745,18 @@ var DeviceCredentialStore = class {
6432
6745
  this.#run = options.commandRunner ?? runDeviceCommand;
6433
6746
  }
6434
6747
  async read() {
6748
+ const authority = await this.#readAuthority();
6749
+ return authority === void 0 || isFirstPairingAttempt(authority) ? void 0 : authority;
6750
+ }
6751
+ async readFirstPairingAttempt() {
6752
+ const authority = await this.#readAuthority();
6753
+ return authority !== void 0 && isFirstPairingAttempt(authority) ? authority : void 0;
6754
+ }
6755
+ async saveFirstPairingAttempt(attempt) {
6756
+ assertFirstPairingAttempt(attempt);
6757
+ await this.#replaceAuthority(attempt);
6758
+ }
6759
+ async #readAuthority() {
6435
6760
  const result = await this.#invoke("read");
6436
6761
  if (result.exitCode === NOT_FOUND || this.#platform === "linux" && result.exitCode === 1 && result.stderr.trim().length === 0) return void 0;
6437
6762
  if (result.exitCode === 127) throw new DeviceCredentialStoreUnavailableError();
@@ -6444,6 +6769,12 @@ var DeviceCredentialStore = class {
6444
6769
  }
6445
6770
  async replace(record) {
6446
6771
  const encoded = encode(record);
6772
+ await this.#replaceEncoded(encoded);
6773
+ }
6774
+ async #replaceAuthority(authority) {
6775
+ await this.#replaceEncoded(encode(authority));
6776
+ }
6777
+ async #replaceEncoded(encoded) {
6447
6778
  const result = await this.#invoke("replace", encoded);
6448
6779
  if (result.exitCode === 127) throw new DeviceCredentialStoreUnavailableError();
6449
6780
  if (result.exitCode !== 0) {
@@ -6454,7 +6785,7 @@ var DeviceCredentialStore = class {
6454
6785
  }
6455
6786
  /** Returns true only after the sole secret authority is confirmed absent. */
6456
6787
  async clear() {
6457
- const before = await this.read();
6788
+ const before = await this.#readAuthority();
6458
6789
  if (before === void 0) return false;
6459
6790
  const result = await this.#invoke("clear");
6460
6791
  if (result.exitCode === 127) throw new DeviceCredentialStoreUnavailableError();
@@ -6463,7 +6794,7 @@ var DeviceCredentialStore = class {
6463
6794
  `operating-system credential provider could not clear device credentials${providerDiagnostic(result.stderr)}`
6464
6795
  );
6465
6796
  }
6466
- if (await this.read() !== void 0) {
6797
+ if (await this.#readAuthority() !== void 0) {
6467
6798
  throw new DeviceCredentialStoreError("operating-system credential provider reported deletion but device credentials remain");
6468
6799
  }
6469
6800
  return true;
@@ -6538,17 +6869,24 @@ var DeviceCredentialStore = class {
6538
6869
  }
6539
6870
  };
6540
6871
  var InMemoryDeviceCredentialStore = class {
6541
- #record;
6872
+ #authority;
6542
6873
  async read() {
6543
- return this.#record === void 0 ? void 0 : Object.freeze({ ...this.#record });
6874
+ return this.#authority === void 0 || isFirstPairingAttempt(this.#authority) ? void 0 : Object.freeze({ ...this.#authority });
6875
+ }
6876
+ async readFirstPairingAttempt() {
6877
+ return this.#authority !== void 0 && isFirstPairingAttempt(this.#authority) ? Object.freeze({ ...this.#authority }) : void 0;
6878
+ }
6879
+ async saveFirstPairingAttempt(attempt) {
6880
+ assertFirstPairingAttempt(attempt);
6881
+ this.#authority = Object.freeze({ ...attempt });
6544
6882
  }
6545
6883
  async replace(record) {
6546
6884
  assertRecord(record);
6547
- this.#record = Object.freeze({ ...record });
6885
+ this.#authority = Object.freeze({ ...record });
6548
6886
  }
6549
6887
  async clear() {
6550
- const had = this.#record !== void 0;
6551
- this.#record = void 0;
6888
+ const had = this.#authority !== void 0;
6889
+ this.#authority = void 0;
6552
6890
  return had;
6553
6891
  }
6554
6892
  };
@@ -6803,23 +7141,22 @@ function signNonce(privateKey, nonce) {
6803
7141
  const signature = sign(null, nonceSigningBytes(nonce), privateKey);
6804
7142
  return signature.toString("base64url");
6805
7143
  }
7144
+
7145
+ // src/daemon/url.ts
6806
7146
  function toHttpBase(serverUrl) {
6807
7147
  const url = new URL(serverUrl);
6808
- if (url.protocol === "ws:") url.protocol = "http:";
6809
- else if (url.protocol === "wss:") url.protocol = "https:";
6810
7148
  url.pathname = "/";
6811
7149
  url.search = "";
6812
7150
  return url.toString();
6813
7151
  }
6814
- function toWsUrl(serverUrl) {
6815
- const url = new URL(BYOK_WS_PATH, toHttpBase(serverUrl));
6816
- url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
6817
- return url.toString();
6818
- }
6819
7152
  function describeEndpoint(transport, url) {
6820
7153
  const parsed = typeof url === "string" ? new URL(url) : url;
6821
7154
  return { transport, host: parsed.host, path: parsed.pathname };
6822
7155
  }
7156
+ function formatServerUrl(value) {
7157
+ const url = typeof value === "string" ? new URL(value) : value;
7158
+ return `${url.protocol}//${url.host}${url.pathname}`;
7159
+ }
6823
7160
  var InsecureServerUrlError = class extends Error {
6824
7161
  constructor(message) {
6825
7162
  super(message);
@@ -6838,22 +7175,21 @@ function assertServerUrlAllowed(rawUrl, opts = {}) {
6838
7175
  let url;
6839
7176
  try {
6840
7177
  url = new URL(rawUrl);
6841
- } catch (err) {
6842
- throw new InsecureServerUrlError(`invalid server URL "${rawUrl}": ${err instanceof Error ? err.message : String(err)}`);
7178
+ } catch {
7179
+ throw new InsecureServerUrlError("invalid server URL");
6843
7180
  }
7181
+ const endpoint = formatServerUrl(url);
6844
7182
  switch (url.protocol) {
6845
7183
  case "https:":
6846
- case "wss:":
6847
7184
  return;
6848
7185
  case "http:":
6849
- case "ws:":
6850
7186
  if (opts.dangerouslyAllowInsecureRemote || isLoopbackHostname(url.hostname)) return;
6851
7187
  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.`
7188
+ `refusing to connect to "${endpoint}" over plaintext ${url.protocol.replace(":", "")} \u2014 "${url.hostname}" is not a loopback host. Use 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
7189
  );
6854
7190
  default:
6855
7191
  throw new InsecureServerUrlError(
6856
- `refusing to connect to "${rawUrl}" \u2014 unsupported scheme "${url.protocol}" (expected http:, https:, ws:, or wss:).`
7192
+ `refusing to connect to "${endpoint}" \u2014 unsupported scheme "${url.protocol}" (expected http: or https:).`
6857
7193
  );
6858
7194
  }
6859
7195
  }
@@ -6865,12 +7201,22 @@ var DeviceRevokedError = class extends Error {
6865
7201
  this.name = "DeviceRevokedError";
6866
7202
  }
6867
7203
  };
7204
+ var AuthRequestAbortedError = class extends Error {
7205
+ constructor(reason) {
7206
+ super(reason === "deadline" ? "authentication request exceeded its deadline" : "authentication request was cancelled during shutdown");
7207
+ this.reason = reason;
7208
+ this.name = "AuthRequestAbortedError";
7209
+ }
7210
+ reason;
7211
+ };
6868
7212
  var ASSUMED_PAIR_TOKEN_TTL_MS = 45 * 60 * 1e3;
6869
7213
  var RENEW_MARGIN_MS = 60 * 1e3;
7214
+ var DEFAULT_AUTH_REQUEST_DEADLINE_MS = 15e3;
6870
7215
  var AuthManager = class {
6871
7216
  constructor(opts) {
6872
7217
  this.opts = opts;
6873
7218
  this.credentials = opts.credentials ?? opts.store.credentials;
7219
+ this.requestDeadlineMs = resolveRequestDeadlineMs(opts.authRequestDeadlineMs);
6874
7220
  }
6875
7221
  opts;
6876
7222
  record;
@@ -6880,7 +7226,10 @@ var AuthManager = class {
6880
7226
  stopped = false;
6881
7227
  pairing = false;
6882
7228
  credentialMutationTail = Promise.resolve();
7229
+ /** The sole cancellation authority for the request currently inside the serialized credential mutation. */
7230
+ activeRequest;
6883
7231
  credentials;
7232
+ requestDeadlineMs;
6884
7233
  get deviceId() {
6885
7234
  return this.record?.deviceId;
6886
7235
  }
@@ -6916,23 +7265,51 @@ var AuthManager = class {
6916
7265
  if (!(error instanceof DeviceRecordRePairRequiredError)) throw error;
6917
7266
  }
6918
7267
  }
6919
- const keyPair = existing ? { privateKey: importPrivateKeyPem(existing.devicePrivateKeyPem), publicKeyBase64Url: existing.devicePublicKey } : generateDeviceKeyPair();
7268
+ const observedDeviceName = this.opts.deviceName ?? os__default.hostname();
7269
+ const observedMachineId = await this.opts.machineId?.();
7270
+ let keyPair;
7271
+ let pairingDeviceName = observedDeviceName;
7272
+ let pairingMachineId = observedMachineId;
7273
+ if (existing) {
7274
+ keyPair = { privateKey: importPrivateKeyPem(existing.devicePrivateKeyPem), publicKeyBase64Url: existing.devicePublicKey };
7275
+ } else {
7276
+ const firstAttempt = await this.credentials.readFirstPairingAttempt();
7277
+ if (firstAttempt) {
7278
+ keyPair = {
7279
+ privateKey: importPrivateKeyPem(firstAttempt.devicePrivateKeyPem),
7280
+ publicKeyBase64Url: firstAttempt.devicePublicKey
7281
+ };
7282
+ pairingDeviceName = firstAttempt.deviceName;
7283
+ pairingMachineId = firstAttempt.machineId;
7284
+ } else {
7285
+ keyPair = generateDeviceKeyPair();
7286
+ await this.credentials.saveFirstPairingAttempt({
7287
+ kind: "first-pairing-attempt-v1",
7288
+ deviceName: pairingDeviceName,
7289
+ devicePublicKey: keyPair.publicKeyBase64Url,
7290
+ devicePrivateKeyPem: exportPrivateKeyPem(keyPair.privateKey),
7291
+ ...pairingMachineId === void 0 ? {} : { machineId: pairingMachineId }
7292
+ });
7293
+ }
7294
+ }
6920
7295
  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
- })
7296
+ const body = await this.runRequest(async (signal) => {
7297
+ const res = await fetch(url, {
7298
+ method: "POST",
7299
+ headers: { "content-type": "application/json" },
7300
+ body: JSON.stringify({
7301
+ pairingCode,
7302
+ deviceName: pairingDeviceName,
7303
+ devicePublicKey: keyPair.publicKeyBase64Url,
7304
+ ...pairingMachineId === void 0 ? {} : { machineId: pairingMachineId }
7305
+ }),
7306
+ signal
7307
+ });
7308
+ if (!res.ok) {
7309
+ throw new Error(`pairing failed: HTTP ${res.status} ${await safeErrorText(res)}`.trimEnd());
7310
+ }
7311
+ return PairResponseSchema.parse(await res.json());
6931
7312
  });
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
7313
  const metadata = {
6937
7314
  deviceId: body.deviceId,
6938
7315
  tenantId: body.tenantId,
@@ -6971,6 +7348,7 @@ var AuthManager = class {
6971
7348
  this.stopped = true;
6972
7349
  if (this.proactiveTimer) clearTimeout(this.proactiveTimer);
6973
7350
  this.proactiveTimer = void 0;
7351
+ this.activeRequest?.abort();
6974
7352
  await this.credentialMutationTail;
6975
7353
  }
6976
7354
  async renew() {
@@ -6987,29 +7365,35 @@ var AuthManager = class {
6987
7365
  const record = this.record;
6988
7366
  const base = toHttpBase(this.opts.serverUrl);
6989
7367
  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 })
7368
+ const { nonce } = await this.runRequest(async (signal) => {
7369
+ const challengeRes = await fetch(new URL(BYOK_CHALLENGE_PATH, base), {
7370
+ method: "POST",
7371
+ headers: { "content-type": "application/json" },
7372
+ body: JSON.stringify({ deviceId: record.deviceId }),
7373
+ signal
7374
+ });
7375
+ if (challengeRes.status === 401) this.markRevoked();
7376
+ if (!challengeRes.ok) {
7377
+ throw new Error(
7378
+ `token renewal (challenge) failed: HTTP ${challengeRes.status} ${await safeErrorText(challengeRes)}`.trimEnd()
7379
+ );
7380
+ }
7381
+ return await challengeRes.json();
6994
7382
  });
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
7383
  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 })
7384
+ const body = await this.runRequest(async (signal) => {
7385
+ const tokenRes = await fetch(new URL(BYOK_TOKEN_PATH, base), {
7386
+ method: "POST",
7387
+ headers: { "content-type": "application/json" },
7388
+ body: JSON.stringify({ deviceId: record.deviceId, nonce, signature }),
7389
+ signal
7390
+ });
7391
+ if (tokenRes.status === 401) this.markRevoked();
7392
+ if (!tokenRes.ok) {
7393
+ throw new Error(`token renewal (token) failed: HTTP ${tokenRes.status} ${await safeErrorText(tokenRes)}`.trimEnd());
7394
+ }
7395
+ return await tokenRes.json();
7007
7396
  });
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
7397
  const updated = {
7014
7398
  ...record,
7015
7399
  accessToken: body.accessToken,
@@ -7039,6 +7423,37 @@ var AuthManager = class {
7039
7423
  timer.unref?.();
7040
7424
  this.proactiveTimer = timer;
7041
7425
  }
7426
+ /**
7427
+ * Bounds one complete auth exchange rather than fetch alone. Keeping the
7428
+ * controller active through `json()`/`text()` makes a non-cooperative or
7429
+ * partial response body cancellable by the same authority that owns fetch.
7430
+ */
7431
+ async runRequest(operation) {
7432
+ if (this.stopped) throw new AuthRequestAbortedError("stopped");
7433
+ const controller = new AbortController();
7434
+ this.activeRequest = controller;
7435
+ let deadlineElapsed = false;
7436
+ const deadline = setTimeout(() => {
7437
+ deadlineElapsed = true;
7438
+ controller.abort();
7439
+ }, this.requestDeadlineMs);
7440
+ let rejectOnAbort;
7441
+ const aborted = new Promise((_, reject) => {
7442
+ rejectOnAbort = reject;
7443
+ });
7444
+ const onAbort = () => rejectOnAbort(new AuthRequestAbortedError(deadlineElapsed ? "deadline" : "stopped"));
7445
+ controller.signal.addEventListener("abort", onAbort, { once: true });
7446
+ try {
7447
+ return await Promise.race([operation(controller.signal), aborted]);
7448
+ } catch (error) {
7449
+ if (controller.signal.aborted) throw new AuthRequestAbortedError(deadlineElapsed ? "deadline" : "stopped");
7450
+ throw error;
7451
+ } finally {
7452
+ clearTimeout(deadline);
7453
+ controller.signal.removeEventListener("abort", onAbort);
7454
+ if (this.activeRequest === controller) this.activeRequest = void 0;
7455
+ }
7456
+ }
7042
7457
  async runCredentialMutation(operation) {
7043
7458
  const predecessor = this.credentialMutationTail;
7044
7459
  let release;
@@ -7081,6 +7496,13 @@ function resolvePairExpiry(refreshHint) {
7081
7496
  }
7082
7497
  return new Date(Date.now() + ASSUMED_PAIR_TOKEN_TTL_MS).toISOString();
7083
7498
  }
7499
+ function resolveRequestDeadlineMs(value) {
7500
+ const deadline = value ?? DEFAULT_AUTH_REQUEST_DEADLINE_MS;
7501
+ if (!Number.isSafeInteger(deadline) || deadline <= 0) {
7502
+ throw new Error("authRequestDeadlineMs must be a positive safe integer");
7503
+ }
7504
+ return deadline;
7505
+ }
7084
7506
  async function safeErrorText(res) {
7085
7507
  try {
7086
7508
  return await res.text();
@@ -7102,37 +7524,48 @@ function withAuth(init, token) {
7102
7524
  }
7103
7525
 
7104
7526
  // src/daemon/blob-client.ts
7105
- async function safeErrorText2(res) {
7106
- try {
7107
- return await res.text();
7108
- } catch {
7109
- return "";
7527
+ var BlobRequestAbortedError = class extends Error {
7528
+ constructor(reason) {
7529
+ super(reason === "deadline" ? "blob request deadline elapsed" : "blob request cancelled");
7530
+ this.reason = reason;
7531
+ this.name = "BlobRequestAbortedError";
7110
7532
  }
7111
- }
7533
+ reason;
7534
+ };
7112
7535
  var BlobClient = class {
7113
- constructor(serverUrl, auth) {
7536
+ constructor(serverUrl, auth, options = {}) {
7114
7537
  this.serverUrl = serverUrl;
7115
7538
  this.auth = auth;
7539
+ this.options = options;
7540
+ const requestDeadlineMs = options.requestDeadlineMs ?? 15e3;
7541
+ if (!Number.isSafeInteger(requestDeadlineMs) || requestDeadlineMs <= 0) {
7542
+ throw new Error("BlobClient requestDeadlineMs must be a positive safe integer");
7543
+ }
7544
+ this.requestDeadlineMs = requestDeadlineMs;
7116
7545
  }
7117
7546
  serverUrl;
7118
7547
  auth;
7548
+ options;
7549
+ requestDeadlineMs;
7119
7550
  /** `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) {
7551
+ async resolveInstruction(blobRef, options = {}) {
7121
7552
  const base = toHttpBase(this.serverUrl);
7122
- const urlRes = await authedFetch(
7123
- new URL(byokBlobUrlPath(blobRef.blobId), base),
7124
- { method: "GET" },
7125
- this.auth
7553
+ const urlRes = await this.#request(
7554
+ (signal) => authedFetch(new URL(byokBlobUrlPath(blobRef.blobId), base), { method: "GET", signal }, this.auth),
7555
+ options.signal
7126
7556
  );
7127
7557
  if (!urlRes.ok) {
7128
- throw new Error(`failed to resolve blob download url: HTTP ${urlRes.status} ${await safeErrorText2(urlRes)}`.trimEnd());
7558
+ throw new Error(`failed to resolve blob download url: HTTP ${urlRes.status} ${await this.#safeErrorText(urlRes, options.signal)}`.trimEnd());
7559
+ }
7560
+ const { downloadUrl } = await this.#readJson(urlRes, options.signal);
7561
+ if (typeof downloadUrl !== "string" || downloadUrl.length === 0) {
7562
+ throw new Error("failed to resolve blob download url: response omitted downloadUrl");
7129
7563
  }
7130
- const { downloadUrl } = await urlRes.json();
7131
- const contentRes = await fetch(new URL(downloadUrl, base));
7564
+ const contentRes = await this.#request((signal) => fetch(new URL(downloadUrl, base), { signal }), options.signal);
7132
7565
  if (!contentRes.ok) {
7133
7566
  throw new Error(`failed to download blob content: HTTP ${contentRes.status}`);
7134
7567
  }
7135
- const bytes = new Uint8Array(await contentRes.arrayBuffer());
7568
+ const bytes = await this.#readBody(contentRes, options.signal);
7136
7569
  const observedHash = `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
7137
7570
  if (observedHash !== blobRef.contentHash || bytes.length !== blobRef.size) {
7138
7571
  throw new Error(
@@ -7141,63 +7574,71 @@ var BlobClient = class {
7141
7574
  }
7142
7575
  return new TextDecoder().decode(bytes);
7143
7576
  }
7144
- /** `POST /byok/blobs` (declares size/contentType/contentHash) -> PUT the bytes to the presigned upload URL -> a `BlobRef` for `task.artifact.blobRef`. */
7577
+ /** `POST /byok/blobs` -> PUT the bytes to the presigned URL -> finalize into a `BlobRef`. */
7145
7578
  async uploadArtifact(content, contentType, options = {}) {
7146
7579
  const bytes = typeof content === "string" ? new TextEncoder().encode(content) : content;
7147
7580
  const contentHash2 = `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
7148
7581
  const base = toHttpBase(this.serverUrl);
7149
7582
  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
7583
+ const createRes = await this.#request(
7584
+ (signal) => authedFetch(
7585
+ new URL(BYOK_BLOBS_PATH, base),
7586
+ {
7587
+ method: "POST",
7588
+ headers: { "content-type": "application/json", "idempotency-key": reservationId },
7589
+ body: JSON.stringify({ size: bytes.length, contentType, contentHash: contentHash2 }),
7590
+ signal
7157
7591
  },
7158
- body: JSON.stringify({ size: bytes.length, contentType, contentHash: contentHash2 })
7159
- },
7160
- this.auth
7592
+ this.auth
7593
+ ),
7594
+ options.signal
7161
7595
  );
7162
7596
  if (!createRes.ok) {
7163
- throw new Error(`failed to create blob: HTTP ${createRes.status} ${await safeErrorText2(createRes)}`.trimEnd());
7597
+ throw new Error(`failed to create blob: HTTP ${createRes.status} ${await this.#safeErrorText(createRes, options.signal)}`.trimEnd());
7598
+ }
7599
+ const { blobId, uploadUrl } = await this.#readJson(createRes, options.signal);
7600
+ if (typeof blobId !== "string" || blobId.length === 0 || typeof uploadUrl !== "string" || uploadUrl.length === 0) {
7601
+ throw new Error("failed to create blob: response omitted blobId or uploadUrl");
7164
7602
  }
7165
- const { blobId, uploadUrl } = await createRes.json();
7166
7603
  const blobRef = { blobId, contentHash: contentHash2, size: bytes.length, contentType };
7167
- if (options.idempotencyKey !== void 0 && await this.#hasExactCommittedBlob(base, blobRef)) {
7604
+ if (options.idempotencyKey !== void 0 && await this.#hasExactCommittedBlob(base, blobRef, options.signal)) {
7168
7605
  return blobRef;
7169
7606
  }
7170
- const putRes = await fetch(new URL(uploadUrl, base), {
7171
- method: "PUT",
7172
- headers: { "content-type": contentType },
7173
- body: bytes
7174
- });
7607
+ const putRes = await this.#request(
7608
+ (signal) => fetch(new URL(uploadUrl, base), {
7609
+ method: "PUT",
7610
+ headers: { "content-type": contentType },
7611
+ body: bytes,
7612
+ signal
7613
+ }),
7614
+ options.signal
7615
+ );
7175
7616
  if (!putRes.ok) {
7176
7617
  throw new Error(`failed to upload blob content: HTTP ${putRes.status}`);
7177
7618
  }
7178
- await this.#finalize(base, blobId, reservationId);
7619
+ this.#throwIfAborted(options.signal);
7620
+ await this.#finalize(base, blobId, reservationId, options.signal);
7179
7621
  return blobRef;
7180
7622
  }
7181
- async #hasExactCommittedBlob(base, blobRef) {
7182
- const urlRes = await authedFetch(
7183
- new URL(byokBlobUrlPath(blobRef.blobId), base),
7184
- { method: "GET" },
7185
- this.auth
7623
+ async #hasExactCommittedBlob(base, blobRef, signal) {
7624
+ const urlRes = await this.#request(
7625
+ (requestSignal) => authedFetch(new URL(byokBlobUrlPath(blobRef.blobId), base), { method: "GET", signal: requestSignal }, this.auth),
7626
+ signal
7186
7627
  );
7187
7628
  if (urlRes.status === 404) return false;
7188
7629
  if (!urlRes.ok) {
7189
- throw new Error(`failed to read back idempotent blob: HTTP ${urlRes.status} ${await safeErrorText2(urlRes)}`.trimEnd());
7630
+ throw new Error(`failed to read back idempotent blob: HTTP ${urlRes.status} ${await this.#safeErrorText(urlRes, signal)}`.trimEnd());
7190
7631
  }
7191
- const { downloadUrl } = await urlRes.json();
7632
+ const { downloadUrl } = await this.#readJson(urlRes, signal);
7192
7633
  if (typeof downloadUrl !== "string" || downloadUrl.length === 0) {
7193
7634
  throw new Error("failed to read back idempotent blob: response omitted downloadUrl");
7194
7635
  }
7195
- const contentRes = await fetch(new URL(downloadUrl, base));
7636
+ const contentRes = await this.#request((requestSignal) => fetch(new URL(downloadUrl, base), { signal: requestSignal }), signal);
7196
7637
  if (contentRes.status === 404) return false;
7197
7638
  if (!contentRes.ok) {
7198
7639
  throw new Error(`failed to read back idempotent blob content: HTTP ${contentRes.status}`);
7199
7640
  }
7200
- const bytes = new Uint8Array(await contentRes.arrayBuffer());
7641
+ const bytes = await this.#readBody(contentRes, signal);
7201
7642
  const observedHash = `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
7202
7643
  if (observedHash !== blobRef.contentHash || bytes.length !== blobRef.size) {
7203
7644
  throw new Error(
@@ -7206,18 +7647,19 @@ var BlobClient = class {
7206
7647
  }
7207
7648
  return true;
7208
7649
  }
7209
- async #finalize(base, blobId, reservationId) {
7650
+ async #finalize(base, blobId, reservationId, signal) {
7210
7651
  let lastFailure;
7211
7652
  for (let attempt = 1; attempt <= 2; attempt += 1) {
7653
+ this.#throwIfAborted(signal);
7212
7654
  let response;
7213
7655
  try {
7214
- response = await authedFetch(
7215
- new URL(byokBlobFinalizePath(blobId), base),
7216
- {
7217
- method: "POST",
7218
- headers: { "idempotency-key": reservationId }
7219
- },
7220
- this.auth
7656
+ response = await this.#request(
7657
+ (requestSignal) => authedFetch(
7658
+ new URL(byokBlobFinalizePath(blobId), base),
7659
+ { method: "POST", headers: { "idempotency-key": reservationId }, signal: requestSignal },
7660
+ this.auth
7661
+ ),
7662
+ signal
7221
7663
  );
7222
7664
  } catch (error) {
7223
7665
  lastFailure = error;
@@ -7227,13 +7669,105 @@ var BlobClient = class {
7227
7669
  if (response.ok) return;
7228
7670
  if (response.status < 500 || attempt === 2) {
7229
7671
  throw new Error(
7230
- `failed to finalize blob: HTTP ${response.status} ${await safeErrorText2(response)}`.trimEnd()
7672
+ `failed to finalize blob: HTTP ${response.status} ${await this.#safeErrorText(response, signal)}`.trimEnd()
7231
7673
  );
7232
7674
  }
7233
7675
  lastFailure = new Error(`failed to finalize blob: HTTP ${response.status}`);
7234
7676
  }
7235
7677
  throw lastFailure;
7236
7678
  }
7679
+ async #readJson(res, signal) {
7680
+ return JSON.parse(await this.#readText(res, signal));
7681
+ }
7682
+ async #safeErrorText(res, signal) {
7683
+ try {
7684
+ return await this.#readText(res, signal);
7685
+ } catch (error) {
7686
+ if (error instanceof BlobRequestAbortedError) throw error;
7687
+ return "";
7688
+ }
7689
+ }
7690
+ async #readText(res, signal) {
7691
+ return new TextDecoder().decode(await this.#readBody(res, signal));
7692
+ }
7693
+ /**
7694
+ * `Response.arrayBuffer()` only leaves cancellation observable through the
7695
+ * Fetch implementation. Read the body directly so our lifecycle/deadline
7696
+ * authority cancels the actual stream even when a fetch implementation has
7697
+ * already resolved at headers.
7698
+ */
7699
+ async #readBody(res, signal) {
7700
+ return this.#request(async (requestSignal) => {
7701
+ if (res.body === null) return new Uint8Array();
7702
+ const reader = res.body.getReader();
7703
+ const cancelBody = () => {
7704
+ void reader.cancel().catch(() => void 0);
7705
+ };
7706
+ requestSignal.addEventListener("abort", cancelBody, { once: true });
7707
+ try {
7708
+ const chunks = [];
7709
+ let length = 0;
7710
+ for (; ; ) {
7711
+ const { done, value } = await reader.read();
7712
+ if (done) break;
7713
+ chunks.push(value);
7714
+ length += value.byteLength;
7715
+ }
7716
+ const bytes = new Uint8Array(length);
7717
+ let offset = 0;
7718
+ for (const chunk of chunks) {
7719
+ bytes.set(chunk, offset);
7720
+ offset += chunk.byteLength;
7721
+ }
7722
+ return bytes;
7723
+ } finally {
7724
+ requestSignal.removeEventListener("abort", cancelBody);
7725
+ reader.releaseLock();
7726
+ }
7727
+ }, signal);
7728
+ }
7729
+ #throwIfAborted(signal) {
7730
+ if (this.options.signal?.aborted || signal?.aborted) {
7731
+ throw new BlobRequestAbortedError("cancelled");
7732
+ }
7733
+ }
7734
+ async #request(request, signal) {
7735
+ this.#throwIfAborted(signal);
7736
+ const controller = new AbortController();
7737
+ let abortReason = "cancelled";
7738
+ const abort = (reason) => {
7739
+ if (controller.signal.aborted) return;
7740
+ abortReason = reason;
7741
+ controller.abort();
7742
+ };
7743
+ const inheritedSignals = [this.options.signal, signal].filter((value) => value !== void 0);
7744
+ const abortForCancellation = () => abort("cancelled");
7745
+ for (const inheritedSignal of inheritedSignals) {
7746
+ inheritedSignal.addEventListener("abort", abortForCancellation, { once: true });
7747
+ }
7748
+ const deadline = setTimeout(() => abort("deadline"), this.requestDeadlineMs);
7749
+ deadline.unref?.();
7750
+ let rejectAbort = () => {
7751
+ };
7752
+ const aborted = new Promise((_resolve, reject) => {
7753
+ rejectAbort = reject;
7754
+ });
7755
+ const rejectOnAbort = () => rejectAbort(new BlobRequestAbortedError(abortReason));
7756
+ controller.signal.addEventListener("abort", rejectOnAbort, { once: true });
7757
+ try {
7758
+ return await Promise.race([request(controller.signal), aborted]);
7759
+ } catch (error) {
7760
+ if (error instanceof BlobRequestAbortedError) throw error;
7761
+ if (controller.signal.aborted) throw new BlobRequestAbortedError(abortReason);
7762
+ throw error;
7763
+ } finally {
7764
+ clearTimeout(deadline);
7765
+ controller.signal.removeEventListener("abort", rejectOnAbort);
7766
+ for (const inheritedSignal of inheritedSignals) {
7767
+ inheritedSignal.removeEventListener("abort", abortForCancellation);
7768
+ }
7769
+ }
7770
+ }
7237
7771
  };
7238
7772
  var DEFAULT_TIMEOUT_MS2 = 2e3;
7239
7773
  var DARWIN_UUID_RE = /"IOPlatformUUID"\s*=\s*"([^"]+)"/u;
@@ -7480,6 +8014,7 @@ var AnotherControlServerRunningError = class extends Error {
7480
8014
  }
7481
8015
  };
7482
8016
  var MAX_HALF_OPEN_CONNECTIONS = 8;
8017
+ var MAX_OUTBOUND_QUEUE_BYTES = 1024 * 1024;
7483
8018
  function errorMessage4(err) {
7484
8019
  return err instanceof Error ? err.message : String(err);
7485
8020
  }
@@ -7556,11 +8091,12 @@ async function bindControlEndpoint(server, endpoint) {
7556
8091
  }
7557
8092
  function handleConnection(socket, token, methods, handshakeTimeoutMs, onHandshakeSettled) {
7558
8093
  const reader = new NdjsonLineReader();
7559
- const activeStreams = /* @__PURE__ */ new Map();
8094
+ const activeRequests = /* @__PURE__ */ new Map();
7560
8095
  let phase = "client-hello";
7561
8096
  let serverNonce = "";
7562
8097
  let destroyed = false;
7563
8098
  let handshakeSettled = false;
8099
+ let disposeOutboundWriter;
7564
8100
  function settleHandshake() {
7565
8101
  if (handshakeSettled) return;
7566
8102
  handshakeSettled = true;
@@ -7570,8 +8106,83 @@ function handleConnection(socket, token, methods, handshakeTimeoutMs, onHandshak
7570
8106
  if (phase !== "ready") socket.destroy();
7571
8107
  }, handshakeTimeoutMs);
7572
8108
  handshakeTimer.unref?.();
8109
+ function terminateConnection() {
8110
+ if (destroyed) return;
8111
+ destroyed = true;
8112
+ clearTimeout(handshakeTimer);
8113
+ settleHandshake();
8114
+ disposeOutboundWriter?.();
8115
+ const requests = [...activeRequests.values()];
8116
+ activeRequests.clear();
8117
+ for (const request of requests) {
8118
+ if (request.kind === "stream" && !request.controller.signal.aborted) request.controller.abort();
8119
+ }
8120
+ if (!socket.destroyed) socket.destroy();
8121
+ }
8122
+ const queuedFrames = [];
8123
+ let queuedBytes = 0;
8124
+ let blocked = false;
8125
+ let terminal = false;
8126
+ let drainListening = false;
8127
+ function disposeQueuedFrames() {
8128
+ terminal = true;
8129
+ blocked = false;
8130
+ queuedFrames.length = 0;
8131
+ queuedBytes = 0;
8132
+ if (drainListening) {
8133
+ socket.removeListener("drain", onDrain);
8134
+ drainListening = false;
8135
+ }
8136
+ }
8137
+ function waitForDrain() {
8138
+ if (drainListening || terminal) return;
8139
+ drainListening = true;
8140
+ socket.once("drain", onDrain);
8141
+ }
8142
+ function writeEncodedFrame(encoded) {
8143
+ if (terminal) return;
8144
+ if (!socket.writable) {
8145
+ terminateConnection();
8146
+ return;
8147
+ }
8148
+ try {
8149
+ if (!socket.write(encoded)) {
8150
+ blocked = true;
8151
+ waitForDrain();
8152
+ }
8153
+ } catch {
8154
+ terminateConnection();
8155
+ }
8156
+ }
8157
+ function onDrain() {
8158
+ drainListening = false;
8159
+ if (terminal) return;
8160
+ blocked = false;
8161
+ while (!blocked && queuedFrames.length > 0) {
8162
+ const frame = queuedFrames.shift();
8163
+ queuedBytes -= frame.bytes;
8164
+ writeEncodedFrame(frame.encoded);
8165
+ }
8166
+ }
8167
+ disposeOutboundWriter = disposeQueuedFrames;
7573
8168
  function sendFrame(frame) {
7574
- if (!destroyed && socket.writable) socket.write(encodeFrame(frame));
8169
+ if (destroyed || terminal) return;
8170
+ const encoded = encodeFrame(frame);
8171
+ const bytes = Buffer.byteLength(encoded);
8172
+ if (bytes > MAX_OUTBOUND_QUEUE_BYTES) {
8173
+ terminateConnection();
8174
+ return;
8175
+ }
8176
+ if (blocked) {
8177
+ if (queuedBytes + bytes > MAX_OUTBOUND_QUEUE_BYTES) {
8178
+ terminateConnection();
8179
+ return;
8180
+ }
8181
+ queuedFrames.push({ encoded, bytes });
8182
+ queuedBytes += bytes;
8183
+ return;
8184
+ }
8185
+ writeEncodedFrame(encoded);
7575
8186
  }
7576
8187
  function handleClientHello(parsed) {
7577
8188
  const hello = parseClientHello(parsed);
@@ -7594,21 +8205,51 @@ function handleConnection(socket, token, methods, handshakeTimeoutMs, onHandshak
7594
8205
  settleHandshake();
7595
8206
  sendFrame({ v: 1, ready: true });
7596
8207
  }
8208
+ function rejectDuplicateRequest(id) {
8209
+ sendFrame({
8210
+ v: 1,
8211
+ id,
8212
+ ok: false,
8213
+ error: { code: "duplicate_request_id", message: `request id "${id}" is already active` }
8214
+ });
8215
+ }
8216
+ function registerRequest(id, record) {
8217
+ if (activeRequests.has(id)) {
8218
+ rejectDuplicateRequest(id);
8219
+ return false;
8220
+ }
8221
+ activeRequests.set(id, record);
8222
+ return true;
8223
+ }
8224
+ function releaseRequest(id, record) {
8225
+ if (activeRequests.get(id) !== record) return false;
8226
+ activeRequests.delete(id);
8227
+ return true;
8228
+ }
7597
8229
  function dispatch(id, method, params) {
7598
8230
  const unary = methods.unary[method];
7599
8231
  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) }));
8232
+ const record = { kind: "unary" };
8233
+ if (!registerRequest(id, record)) return;
8234
+ Promise.resolve().then(() => unary(params)).then((result) => {
8235
+ if (!releaseRequest(id, record)) return;
8236
+ sendFrame({ v: 1, id, ok: true, result });
8237
+ }).catch((err) => {
8238
+ if (!releaseRequest(id, record)) return;
8239
+ sendFrame({ v: 1, id, ok: false, error: toControlErrorShape(err) });
8240
+ });
7601
8241
  return;
7602
8242
  }
7603
8243
  const stream = methods.stream[method];
7604
8244
  if (stream) {
7605
8245
  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);
8246
+ const record = { kind: "stream", controller };
8247
+ if (!registerRequest(id, record)) return;
8248
+ Promise.resolve().then(() => stream(params, { emit: (event) => sendFrame({ v: 1, id, event }), signal: controller.signal })).then(() => {
8249
+ if (!releaseRequest(id, record)) return;
7609
8250
  if (!controller.signal.aborted) sendFrame({ v: 1, id, ok: true, done: true });
7610
8251
  }).catch((err) => {
7611
- activeStreams.delete(id);
8252
+ if (!releaseRequest(id, record)) return;
7612
8253
  sendFrame({ v: 1, id, ok: false, error: toControlErrorShape(err) });
7613
8254
  });
7614
8255
  return;
@@ -7650,13 +8291,10 @@ function handleConnection(socket, token, methods, handshakeTimeoutMs, onHandshak
7650
8291
  for (const line of lines) handleLine(line);
7651
8292
  });
7652
8293
  socket.on("error", () => {
8294
+ terminateConnection();
7653
8295
  });
7654
8296
  socket.on("close", () => {
7655
- destroyed = true;
7656
- clearTimeout(handshakeTimer);
7657
- settleHandshake();
7658
- for (const controller of activeStreams.values()) controller.abort();
7659
- activeStreams.clear();
8297
+ terminateConnection();
7660
8298
  });
7661
8299
  }
7662
8300
  async function startControlServer(opts) {
@@ -8574,6 +9212,20 @@ function createFleetJitter(productId, deviceId) {
8574
9212
  delay: (domain, sequence, baseMs) => deterministicJitterMs({ seed, domain, sequence, baseMs })
8575
9213
  };
8576
9214
  }
9215
+
9216
+ // src/daemon/replay-cursor.ts
9217
+ var ReplayCursorTooOldError = class extends Error {
9218
+ constructor(recoverableFrom) {
9219
+ super(
9220
+ 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}`
9221
+ );
9222
+ this.recoverableFrom = recoverableFrom;
9223
+ this.name = "ReplayCursorTooOldError";
9224
+ }
9225
+ recoverableFrom;
9226
+ };
9227
+
9228
+ // src/daemon/long-poll-transport.ts
8577
9229
  var LongPollRouteError = class extends Error {
8578
9230
  constructor(endpoint, status, cause) {
8579
9231
  super(
@@ -8589,7 +9241,7 @@ var LongPollRouteError = class extends Error {
8589
9241
  };
8590
9242
  var MAX_TRACKED_VALIDATION_FAILURE_WARNINGS = 1e3;
8591
9243
  var MAX_TRACKED_ROUTE_FAILURE_WARNINGS = 1e3;
8592
- function parseLooseEventsPollResponse(raw) {
9244
+ function parseLooseEventsPollResponse(raw, requestedCursor) {
8593
9245
  if (typeof raw !== "object" || raw === null) {
8594
9246
  throw new Error("events poll response is not an object");
8595
9247
  }
@@ -8597,19 +9249,48 @@ function parseLooseEventsPollResponse(raw) {
8597
9249
  if (!Array.isArray(events)) {
8598
9250
  throw new Error("events poll response.events is not an array");
8599
9251
  }
8600
- if (typeof cursor !== "number" || !Number.isInteger(cursor)) {
8601
- throw new Error("events poll response.cursor is not an integer");
9252
+ if (!isSafeNonnegativeInteger(cursor)) {
9253
+ throw new Error("events poll response.cursor is not a safe nonnegative integer");
9254
+ }
9255
+ if (cursor < requestedCursor) {
9256
+ throw new Error("events poll response.cursor regressed below the requested cursor");
8602
9257
  }
8603
9258
  if (capabilities !== void 0 && (!Array.isArray(capabilities) || capabilities.some((flag) => typeof flag !== "string"))) {
8604
9259
  throw new Error("events poll response.capabilities is not an array of strings");
8605
9260
  }
8606
9261
  return { events, cursor, capabilities: capabilities ?? [] };
8607
9262
  }
9263
+ function isSafeNonnegativeInteger(value) {
9264
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
9265
+ }
9266
+ function validateTrustedEventsPage(events, requestedCursor, pageCursor) {
9267
+ let previousTaskSeq;
9268
+ for (const raw of events) {
9269
+ if (typeof raw !== "object" || raw === null) continue;
9270
+ const { type, seq } = raw;
9271
+ if (typeof type !== "string" || !type.startsWith("task.")) continue;
9272
+ if (!isSafeNonnegativeInteger(seq)) {
9273
+ throw new Error("events poll task seq is not a safe nonnegative integer");
9274
+ }
9275
+ if (seq <= requestedCursor || seq > pageCursor) {
9276
+ throw new Error("events poll task seq lies outside the trusted page cursor range");
9277
+ }
9278
+ if (previousTaskSeq !== void 0 && seq <= previousTaskSeq) {
9279
+ throw new Error("events poll task seq is not strictly page ordered");
9280
+ }
9281
+ const knownTaskType = MESSAGE_TYPES.includes(type);
9282
+ const predecessor = previousTaskSeq ?? requestedCursor;
9283
+ if (!knownTaskType && seq !== predecessor + 1) {
9284
+ throw new Error("events poll unknown task seq would cross an untrusted gap");
9285
+ }
9286
+ previousTaskSeq = seq;
9287
+ }
9288
+ }
8608
9289
  function extractSkippableSeq(raw) {
8609
9290
  if (typeof raw !== "object" || raw === null) return void 0;
8610
9291
  const { type, seq } = raw;
8611
9292
  if (typeof type !== "string" || !type.startsWith("task.")) return void 0;
8612
- return typeof seq === "number" && Number.isInteger(seq) ? seq : void 0;
9293
+ return isSafeNonnegativeInteger(seq) ? seq : void 0;
8613
9294
  }
8614
9295
  var LongPollClient = class {
8615
9296
  constructor(opts) {
@@ -8620,6 +9301,8 @@ var LongPollClient = class {
8620
9301
  }
8621
9302
  opts;
8622
9303
  running = false;
9304
+ /** Owns exactly one active loop generation, including its held GET and retry delays. */
9305
+ loopAbortController;
8623
9306
  /**
8624
9307
  * Finding R1: seqs this loop has already `console.warn`'d about for a
8625
9308
  * validation-failed (recognized-type, invalid-payload) entry — a poison
@@ -8679,16 +9362,22 @@ var LongPollClient = class {
8679
9362
  noteRevoked(err) {
8680
9363
  if (!(err instanceof DeviceRevokedError)) return false;
8681
9364
  this.running = false;
9365
+ this.loopAbortController?.abort();
8682
9366
  this.opts.onRevoked?.();
8683
9367
  return true;
8684
9368
  }
8685
9369
  start() {
8686
9370
  if (this.running) return;
8687
9371
  this.running = true;
8688
- void this.loop();
9372
+ const controller = new AbortController();
9373
+ this.loopAbortController = controller;
9374
+ void this.loop(controller.signal).finally(() => {
9375
+ if (this.loopAbortController === controller) this.loopAbortController = void 0;
9376
+ });
8689
9377
  }
8690
9378
  stop() {
8691
9379
  this.running = false;
9380
+ this.loopAbortController?.abort();
8692
9381
  }
8693
9382
  /**
8694
9383
  * POST one batch of envelopes to `/byok/messages` (finding F6/protocol
@@ -8697,14 +9386,14 @@ var LongPollClient = class {
8697
9386
  * (`ConnectionHub.handleInbound`), so a resend of the SAME batch (same
8698
9387
  * envelope `id`s — the caller must never rebuild them) is deduped
8699
9388
  * server-side into a safe no-op rather than reprocessed (§9). Returns
8700
- * `true` once the server has accepted the batch.
9389
+ * validated frozen-v1 counts only after a readable response body.
8701
9390
  */
8702
9391
  async postBatch(envelopes) {
8703
9392
  try {
8704
9393
  await this.opts.auth.getValidAccessToken();
8705
9394
  } catch (err) {
8706
9395
  this.noteRevoked(err);
8707
- return false;
9396
+ return void 0;
8708
9397
  }
8709
9398
  let res;
8710
9399
  try {
@@ -8720,23 +9409,26 @@ var LongPollClient = class {
8720
9409
  );
8721
9410
  } catch (err) {
8722
9411
  if (!this.noteRevoked(err)) this.warnRouteFailure(this.messagesEndpoint, void 0, err);
8723
- return false;
9412
+ return void 0;
8724
9413
  }
8725
9414
  if (!res.ok) {
8726
9415
  this.warnRouteFailure(this.messagesEndpoint, res.status, void 0);
8727
- return false;
9416
+ return void 0;
8728
9417
  }
8729
9418
  try {
8730
- MessagesSendResponseSchema.parse(await res.json());
9419
+ const response = MessagesSendResponseSchema.parse(await res.json());
9420
+ if (response.accepted + (response.rejected ?? 0) !== envelopes.length) {
9421
+ throw new Error("messages response counts do not match the posted batch length");
9422
+ }
9423
+ return response;
8731
9424
  } catch (err) {
8732
9425
  this.warnRouteFailure(this.messagesEndpoint, res.status, err);
8733
- return false;
9426
+ return void 0;
8734
9427
  }
8735
- return true;
8736
9428
  }
8737
- async loop() {
9429
+ async loop(signal) {
8738
9430
  let retryAttempt = 0;
8739
- while (this.running) {
9431
+ while (this.running && !signal.aborted) {
8740
9432
  try {
8741
9433
  await this.opts.auth.getValidAccessToken();
8742
9434
  const base = toHttpBase(this.opts.serverUrl);
@@ -8745,30 +9437,40 @@ var LongPollClient = class {
8745
9437
  if (cursor !== void 0) url.searchParams.set("cursor", String(cursor));
8746
9438
  let res;
8747
9439
  try {
8748
- res = await authedFetch(url, { method: "GET" }, this.opts.auth);
9440
+ res = await authedFetch(url, { method: "GET", signal }, this.opts.auth);
8749
9441
  } catch (err) {
9442
+ if (signal.aborted) return;
8750
9443
  if (!(err instanceof DeviceRevokedError)) {
8751
9444
  this.warnRouteFailure(this.eventsEndpoint, void 0, err);
8752
9445
  }
8753
9446
  throw err;
8754
9447
  }
8755
9448
  if (!res.ok) {
9449
+ const replayCursorTooOld = await parseReplayCursorTooOld(res);
9450
+ if (replayCursorTooOld) {
9451
+ this.running = false;
9452
+ this.opts.onReplayCursorTooOld?.(replayCursorTooOld);
9453
+ return;
9454
+ }
8756
9455
  this.warnRouteFailure(this.eventsEndpoint, res.status, void 0);
8757
- this.opts.onServerCapabilities?.([]);
9456
+ this.opts.onServerCapabilitiesInvalidated?.();
9457
+ this.opts.onPollFailure?.();
8758
9458
  this.opts.onOperationalOutcome?.("failure");
8759
9459
  const baseMs = this.opts.retryDelayMs ?? 2e3;
8760
- await sleep(this.opts.retryDelayForAttempt?.(retryAttempt++, baseMs) ?? baseMs);
9460
+ await sleep(this.opts.retryDelayForAttempt?.(retryAttempt++, baseMs) ?? baseMs, signal);
8761
9461
  continue;
8762
9462
  }
8763
9463
  let parsed;
8764
9464
  try {
8765
- parsed = parseLooseEventsPollResponse(await res.json());
9465
+ parsed = parseLooseEventsPollResponse(await res.json(), cursor ?? 0);
9466
+ validateTrustedEventsPage(parsed.events, cursor ?? 0, parsed.cursor);
8766
9467
  } catch (err) {
8767
9468
  this.warnRouteFailure(this.eventsEndpoint, res.status, err);
8768
9469
  throw err;
8769
9470
  }
8770
9471
  this.opts.onServerCapabilities?.(parsed.capabilities);
8771
9472
  let hadValidationFailureThisBatch = false;
9473
+ let acceptedAnyEntry = false;
8772
9474
  for (const raw of parsed.events) {
8773
9475
  let envelope;
8774
9476
  try {
@@ -8776,7 +9478,10 @@ var LongPollClient = class {
8776
9478
  } catch (err) {
8777
9479
  if (err instanceof UnknownMessageTypeError) {
8778
9480
  const skippableSeq = extractSkippableSeq(raw);
8779
- if (skippableSeq !== void 0) this.opts.onSkippedSeq?.(skippableSeq);
9481
+ if (skippableSeq !== void 0) {
9482
+ this.opts.onSkippedSeq?.(skippableSeq);
9483
+ acceptedAnyEntry = true;
9484
+ }
8780
9485
  } else {
8781
9486
  const failedSeq = extractSkippableSeq(raw);
8782
9487
  if (failedSeq !== void 0) {
@@ -8796,45 +9501,65 @@ var LongPollClient = class {
8796
9501
  }
8797
9502
  continue;
8798
9503
  }
8799
- this.opts.onEnvelope(envelope);
9504
+ if (this.opts.onEnvelope(envelope) !== false) acceptedAnyEntry = true;
8800
9505
  }
8801
9506
  if (parsed.events.length === 0) {
8802
9507
  retryAttempt = 0;
8803
9508
  this.opts.onOperationalOutcome?.("success");
8804
- await sleep(this.opts.idleDelayMs ?? 250);
8805
- } else if (this.opts.isStalled?.() || hadValidationFailureThisBatch) {
9509
+ await sleep(this.opts.idleDelayMs ?? 250, signal);
9510
+ } else if (this.opts.isStalled?.() || hadValidationFailureThisBatch || !acceptedAnyEntry && this.opts.getCursor() === cursor) {
8806
9511
  this.opts.onOperationalOutcome?.("failure");
8807
9512
  const baseMs = this.opts.retryDelayMs ?? 2e3;
8808
- await sleep(this.opts.retryDelayForAttempt?.(retryAttempt++, baseMs) ?? baseMs);
9513
+ await sleep(this.opts.retryDelayForAttempt?.(retryAttempt++, baseMs) ?? baseMs, signal);
8809
9514
  } else {
8810
9515
  retryAttempt = 0;
8811
9516
  this.opts.onOperationalOutcome?.("success");
8812
9517
  }
8813
9518
  } catch (err) {
8814
- this.opts.onServerCapabilities?.([]);
9519
+ this.opts.onServerCapabilitiesInvalidated?.();
8815
9520
  if (this.noteRevoked(err)) return;
8816
- if (!this.running) return;
9521
+ if (!this.running || signal.aborted) return;
9522
+ this.opts.onPollFailure?.();
8817
9523
  this.opts.onOperationalOutcome?.("failure");
8818
9524
  const baseMs = this.opts.retryDelayMs ?? 2e3;
8819
- await sleep(this.opts.retryDelayForAttempt?.(retryAttempt++, baseMs) ?? baseMs);
9525
+ await sleep(this.opts.retryDelayForAttempt?.(retryAttempt++, baseMs) ?? baseMs, signal);
8820
9526
  }
8821
9527
  }
8822
9528
  }
8823
9529
  };
8824
- function sleep(ms) {
8825
- return new Promise((resolve) => setTimeout(resolve, ms));
8826
- }
8827
- var WsUnexpectedStatusError = class extends Error {
8828
- constructor(status, endpoint) {
8829
- super(`WS upgrade rejected with HTTP ${status} (ws ${endpoint.host}${endpoint.path})`);
8830
- this.status = status;
8831
- this.endpoint = endpoint;
8832
- this.name = "WsUnexpectedStatusError";
9530
+ async function parseReplayCursorTooOld(res) {
9531
+ if (res.status !== 409) return void 0;
9532
+ let body;
9533
+ try {
9534
+ body = await res.json();
9535
+ } catch {
9536
+ return void 0;
8833
9537
  }
8834
- status;
8835
- endpoint;
8836
- };
8837
- function createConnectionHelloEnvelope(opts) {
9538
+ if (typeof body !== "object" || body === null) return void 0;
9539
+ const { error, recoverableFrom } = body;
9540
+ if (error !== "cursor_too_old" || typeof recoverableFrom !== "number" || !Number.isSafeInteger(recoverableFrom) || recoverableFrom < 0) {
9541
+ return void 0;
9542
+ }
9543
+ return new ReplayCursorTooOldError(recoverableFrom);
9544
+ }
9545
+ function sleep(ms, signal) {
9546
+ if (signal.aborted) return Promise.resolve();
9547
+ return new Promise((resolve) => {
9548
+ const finish = () => {
9549
+ clearTimeout(timer);
9550
+ signal.removeEventListener("abort", finish);
9551
+ resolve();
9552
+ };
9553
+ const timer = setTimeout(finish, ms);
9554
+ signal.addEventListener("abort", finish, { once: true });
9555
+ });
9556
+ }
9557
+
9558
+ // src/daemon/connection-manager.ts
9559
+ function isCursorEnvelopeType(type) {
9560
+ return type.startsWith("task.") || type === "agent.egress.ack" || type === "agent.content.read" || type === "agent.home.projection";
9561
+ }
9562
+ function createConnectionHelloEnvelope(opts, getCursor) {
8838
9563
  const configuredToolsets = opts.getConfiguredToolsets?.();
8839
9564
  return createEnvelope("conn.hello", {
8840
9565
  protocolVersions: [PROTOCOL_VERSION],
@@ -8844,222 +9569,34 @@ function createConnectionHelloEnvelope(opts) {
8844
9569
  clientVersion: opts.clientVersion,
8845
9570
  runtimes: opts.runtimes,
8846
9571
  configuredToolsets: configuredToolsets === void 0 ? void 0 : [...configuredToolsets],
8847
- cursor: opts.getCursor?.()
9572
+ cursor: getCursor()
8848
9573
  });
8849
9574
  }
8850
- var WsTransport = class {
8851
- constructor(opts) {
8852
- this.opts = opts;
8853
- }
8854
- opts;
8855
- socket;
8856
- closedByUser = false;
8857
- autoReconnect = true;
8858
- acked = false;
8859
- everAckedThisAttempt = false;
8860
- reconnectAttempt = 0;
8861
- reconnectTimer;
8862
- livenessTimer;
8863
- lastActivity = 0;
8864
- lastUnexpectedStatus;
8865
- ackWaiters = [];
8866
- connect(opts = {}) {
8867
- this.autoReconnect = opts.auto ?? true;
8868
- this.closedByUser = false;
8869
- void this.openSocket();
8870
- }
8871
- /** Stop scheduling further reconnect attempts (any pending one is cancelled too); the current socket, if any, is left alone. Used when handing retry ownership to a slower external cadence (e.g. the long-poll fallback's periodic WS probe). */
8872
- stopAutoReconnect() {
8873
- this.autoReconnect = false;
8874
- if (this.reconnectTimer) {
8875
- clearTimeout(this.reconnectTimer);
8876
- this.reconnectTimer = void 0;
8877
- }
8878
- }
8879
- /** Resume normal auto-reconnect-on-close behavior (does not itself trigger a connect — only affects future closes). */
8880
- resumeAutoReconnect() {
8881
- this.autoReconnect = true;
8882
- }
8883
- get isOpen() {
8884
- return this.acked;
8885
- }
8886
- /**
8887
- * Attempt to send ONE envelope right now; returns whether it actually went
8888
- * out. `false` means the socket isn't currently open+acked — the caller
8889
- * (`ConnectionManager.drainOutbox`, Design B/finding N4) owns re-queueing
8890
- * and retrying later (e.g. on the next `onAcked`), since this transport no
8891
- * longer buffers anything itself.
8892
- */
8893
- sendNow(envelope) {
8894
- if (this.socket && this.socket.readyState === WebSocket.OPEN && this.acked) {
8895
- this.socket.send(encodeEnvelope(envelope));
8896
- return true;
8897
- }
8898
- return false;
8899
- }
8900
- waitForAck(timeoutMs = 1e4) {
8901
- if (this.acked) return Promise.resolve();
8902
- return new Promise((resolve, reject) => {
8903
- const timer = setTimeout(() => reject(new Error("Timed out waiting for conn.ack")), timeoutMs);
8904
- this.ackWaiters.push({
8905
- resolve: () => {
8906
- clearTimeout(timer);
8907
- resolve();
8908
- },
8909
- reject: (err) => {
8910
- clearTimeout(timer);
8911
- reject(err);
8912
- }
8913
- });
8914
- });
8915
- }
8916
- close() {
8917
- this.closedByUser = true;
8918
- if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
8919
- this.socket?.close();
8920
- }
8921
- async openSocket() {
8922
- const url = toWsUrl(this.opts.serverUrl);
8923
- const endpoint = describeEndpoint("ws", url);
8924
- this.acked = false;
8925
- this.everAckedThisAttempt = false;
8926
- this.opts.onStateChange?.("connecting");
8927
- let token;
8928
- try {
8929
- token = await this.opts.getToken();
8930
- } catch (err) {
8931
- this.opts.onStateChange?.("closed");
8932
- this.opts.onConnectOutcome?.(false, err, endpoint);
8933
- if (!this.closedByUser && this.autoReconnect) this.scheduleReconnect();
8934
- return;
8935
- }
8936
- const socket = new WebSocket(url, {
8937
- headers: { Authorization: `Bearer ${token}` }
8938
- });
8939
- this.socket = socket;
8940
- this.lastActivity = Date.now();
8941
- this.startLivenessCheck(socket);
8942
- socket.on("open", () => {
8943
- const hello = createConnectionHelloEnvelope(this.opts);
8944
- socket.send(encodeEnvelope(hello));
8945
- });
8946
- socket.on("ping", () => {
8947
- this.lastActivity = Date.now();
8948
- });
8949
- socket.on("message", (data, isBinary) => {
8950
- this.lastActivity = Date.now();
8951
- const bytes = toBytes(data);
8952
- let envelope;
8953
- try {
8954
- envelope = decodeEnvelope(bytes);
8955
- } catch {
8956
- return;
8957
- }
8958
- if (envelope.type === "conn.ack") {
8959
- this.reconnectAttempt = 0;
8960
- this.acked = true;
8961
- this.everAckedThisAttempt = true;
8962
- this.opts.onStateChange?.("open");
8963
- for (const waiter of this.ackWaiters.splice(0)) waiter.resolve();
8964
- this.opts.onAcked?.(envelope.payload.capabilities);
8965
- }
8966
- this.opts.onEnvelope(envelope);
8967
- });
8968
- socket.on("close", () => {
8969
- this.socket = void 0;
8970
- this.stopLivenessCheck();
8971
- this.opts.onStateChange?.("closed");
8972
- const acked = this.everAckedThisAttempt;
8973
- const status = this.lastUnexpectedStatus;
8974
- this.lastUnexpectedStatus = void 0;
8975
- this.opts.onConnectOutcome?.(
8976
- acked,
8977
- status !== void 0 ? new WsUnexpectedStatusError(status, endpoint) : void 0,
8978
- endpoint
8979
- );
8980
- if (!this.closedByUser && this.autoReconnect) this.scheduleReconnect();
8981
- });
8982
- socket.on("error", () => {
8983
- });
8984
- socket.on("unexpected-response", (_req, res) => {
8985
- this.lastUnexpectedStatus = res.statusCode;
8986
- res.resume();
8987
- socket.terminate();
8988
- });
8989
- }
8990
- scheduleReconnect() {
8991
- const { baseMs = 1e3, maxMs = 3e4, factor = 2 } = this.opts.backoff ?? {};
8992
- const delay5 = Math.min(maxMs, baseMs * factor ** this.reconnectAttempt);
8993
- const jitter = this.opts.reconnectDelayMs?.(this.reconnectAttempt, delay5) ?? delay5;
8994
- this.reconnectAttempt += 1;
8995
- this.reconnectTimer = setTimeout(() => void this.openSocket(), jitter);
8996
- }
8997
- startLivenessCheck(socket) {
8998
- const { timeoutMs = 75e3, checkIntervalMs = Math.max(1e3, Math.floor(timeoutMs / 3)) } = this.opts.liveness ?? {};
8999
- this.livenessTimer = setInterval(() => {
9000
- if (Date.now() - this.lastActivity > timeoutMs) {
9001
- socket.terminate();
9002
- }
9003
- }, checkIntervalMs);
9004
- this.livenessTimer.unref?.();
9005
- }
9006
- stopLivenessCheck() {
9007
- if (this.livenessTimer) {
9008
- clearInterval(this.livenessTimer);
9009
- this.livenessTimer = void 0;
9010
- }
9011
- }
9012
- };
9013
- function toBytes(data, _isBinary) {
9014
- if (Buffer.isBuffer(data)) return data;
9015
- if (Array.isArray(data)) return Buffer.concat(data);
9016
- if (data instanceof ArrayBuffer) return new Uint8Array(data);
9017
- return Buffer.from(String(data), "utf8");
9018
- }
9019
-
9020
- // src/daemon/connection-manager.ts
9021
- function isCursorEnvelopeType(type) {
9022
- return type.startsWith("task.") || type === "agent.egress.ack" || type === "agent.content.read" || type === "agent.home.projection";
9023
- }
9575
+ var MAX_REJECTED_OUTBOX_ENTRIES = 1e3;
9024
9576
  var ConnectionManager = class {
9025
9577
  constructor(opts) {
9026
9578
  this.opts = opts;
9027
9579
  this.fleetJitter = opts.fleetJitter ?? createFleetJitter(opts.productId, opts.deviceId);
9028
- this.ws = new WsTransport({
9029
- serverUrl: opts.serverUrl,
9030
- getToken: () => opts.auth.getValidAccessToken(),
9031
- deviceId: opts.deviceId,
9032
- productId: opts.productId,
9033
- capabilities: opts.capabilities,
9034
- clientVersion: opts.clientVersion,
9035
- runtimes: opts.runtimes,
9036
- getConfiguredToolsets: opts.getConfiguredToolsets,
9037
- getCursor: () => this.cursor,
9038
- onEnvelope: (envelope) => this.deliver(envelope),
9039
- onStateChange: (state) => {
9040
- if (!this.stopped && !this.revoked) this.opts.onStateChange?.(state);
9041
- },
9042
- onAcked: (capabilities) => this.onAcked(capabilities),
9043
- onConnectOutcome: (acked, err) => this.onWsOutcome(acked, err),
9044
- backoff: opts.backoff,
9045
- liveness: opts.liveness,
9046
- reconnectDelayMs: (attempt, baseMs) => this.fleetJitter.delay("reconnect", attempt, baseMs)
9047
- });
9048
9580
  this.longPoll = new LongPollClient({
9049
9581
  serverUrl: opts.serverUrl,
9050
9582
  auth: opts.auth,
9051
- // Design A: the query cursor for the NEXT `GET /byok/events` is the
9052
- // same watermark `deliver()` dedupes against (see `dedupWatermark`) —
9053
- // normally the eager `deliveredSeq` (so an in-flight envelope isn't
9054
- // re-pulled), but the durable `cursor` while `stalledAtSeq` is set, so
9055
- // the failed envelope (and everything after it) IS re-pulled and
9056
- // re-attempted.
9057
- getCursor: () => this.dedupWatermark(),
9583
+ // The long-poll query cursor is also the kernel's irreversible ack.
9584
+ // Only report the successfully processed cursor here. `deliveredSeq`
9585
+ // remains a local dedup watermark; using it on the wire would ack an
9586
+ // in-flight envelope before its handler settles and make a later
9587
+ // failure impossible to redeliver.
9588
+ getCursor: () => this.cursor,
9058
9589
  onEnvelope: (envelope) => this.deliver(envelope),
9059
9590
  onServerCapabilities: (capabilities) => {
9060
- if (this.mode === "long-poll") this.serverCapabilities = capabilities;
9591
+ this.serverCapabilities = capabilities;
9592
+ this.noteConnected();
9061
9593
  },
9594
+ onServerCapabilitiesInvalidated: () => {
9595
+ this.serverCapabilities = [];
9596
+ },
9597
+ onPollFailure: () => this.noteDisconnected(),
9062
9598
  onRevoked: () => this.enterRevoked(),
9599
+ onReplayCursorTooOld: (error) => this.enterReplayCursorTooOld(error),
9063
9600
  // M4 Phase 4 (version-negotiation drill fix): a batch entry
9064
9601
  // LongPollClient couldn't parse into a known Envelope at all (an
9065
9602
  // unrecognized message type) still needs its cursor/watermark
@@ -9085,13 +9622,10 @@ var ConnectionManager = class {
9085
9622
  }
9086
9623
  opts;
9087
9624
  fleetJitter;
9088
- ws;
9089
9625
  longPoll;
9090
- mode = "ws";
9091
- consecutiveFailures = 0;
9092
- wsRetryTimer;
9093
- wsProbeSequence = 0;
9094
9626
  uploadRetryAttempt = 0;
9627
+ started = false;
9628
+ connected = false;
9095
9629
  cursor;
9096
9630
  /**
9097
9631
  * Finding F3 (at-most-once redelivery): the lowest `task.*` envelope `seq`
@@ -9113,10 +9647,9 @@ var ConnectionManager = class {
9113
9647
  * (see `advanceCursor`) — that semantics is unchanged. `deliveredSeq`
9114
9648
  * advances eagerly, the instant a `task.*` envelope is admitted past
9115
9649
  * dedup (see `deliver`/`noteDelivered`), independent of whether its
9116
- * handler has even started, let alone succeeded. It exists so a
9117
- * long-poll re-query (`LongPollClient`'s `getCursor`) doesn't re-pull an
9118
- * envelope that's already been delivered once and is still in flight
9119
- * `handleOffer` is NOT idempotent and must never be re-pulled while a
9650
+ * handler has even started, let alone succeeded. It exists so a repeated
9651
+ * read at the durable cursor does not re-dispatch an envelope already in
9652
+ * flight `handleOffer` must not start a second adapter session while a
9120
9653
  * first attempt is still running. On WS this same field is written the
9121
9654
  * same way, but since a live WS connection only ever pushes a given `seq`
9122
9655
  * once, it never has an observable effect there beyond mirroring
@@ -9127,18 +9660,18 @@ var ConnectionManager = class {
9127
9660
  /** Finding F3: serializes `onEnvelope` calls into a per-connection FIFO — one envelope's handler always fully settles before the next one starts. */
9128
9661
  processingChain = Promise.resolve();
9129
9662
  /**
9130
- * Design B (finding N4): the ONE outbound queue both transports drain
9131
- * from — holds `Envelope` OBJECTS, never re-encoded/rebuilt strings, so a
9663
+ * Design B (finding N4): the ONE outbound queue holds `Envelope` OBJECTS,
9664
+ * never re-encoded/rebuilt strings, so a
9132
9665
  * resend after a failed send attempt is byte-identical to the original
9133
9666
  * (same `id`), which is what lets the server's per-(deviceId,id) dedup
9134
9667
  * (Wave 1) recognize it as a safe no-op retry rather than a second
9135
- * application (protocol §9). A transport switch (long-poll <-> WS) never
9136
- * touches this queue — see `drainOutbox` — so nothing queued while one
9137
- * transport was active is ever stranded when the other takes over.
9668
+ * application (protocol §9).
9138
9669
  */
9139
9670
  outbox = [];
9671
+ /** Terminally rejected outbound envelopes, retained as a bounded observable quarantine. */
9672
+ rejectedOutboundEnvelopes = [];
9140
9673
  /**
9141
- * Finding F5(b): how many envelopes `drainOutbox`'s long-poll branch has
9674
+ * Finding F5(b): how many envelopes `drainOutbox` has
9142
9675
  * currently spliced OUT of `this.outbox` for an in-flight (not yet
9143
9676
  * confirmed delivered) `postBatch` call — 0 the rest of the time. See
9144
9677
  * `outboxLength`'s own doc comment for why this needs to be tracked
@@ -9148,6 +9681,7 @@ var ConnectionManager = class {
9148
9681
  draining = false;
9149
9682
  stopped = false;
9150
9683
  revoked = false;
9684
+ terminalError;
9151
9685
  settledWaiters = [];
9152
9686
  pendingCursorSave = Promise.resolve();
9153
9687
  /**
@@ -9190,38 +9724,22 @@ var ConnectionManager = class {
9190
9724
  */
9191
9725
  cancelPendingDrainRetry;
9192
9726
  /**
9193
- * The capabilities the CURRENT transport's server advertised — untyped
9194
- * `string[]` for forward compatibility. WS populates it from `conn.ack`;
9195
- * long-poll populates it from each successful events response. Empty until
9196
- * the active transport supplies an advertisement.
9197
- *
9198
- * Finding R2 (cross-model re-review — was P1): strictly PER-CONNECTION,
9199
- * not per-daemon-lifetime. Cleared to `[]` the instant the acked WS
9200
- * connection ends for ANY reason — an ordinary disconnect (`onWsOutcome`'s
9201
- * `acked` branch), `stop()`, or a transport switch to long-poll
9202
- * (`enterLongPoll`) — and only repopulated by a fresh advertisement from
9203
- * the transport that is still current.
9204
- * The previous version of this doc comment claimed long-poll mode simply
9205
- * "stays at whatever the last real WS `conn.ack` said" — that was the bug:
9206
- * a daemon that once learned e.g. `approval_resolved` from an earlier WS
9207
- * session kept believing it applied to whatever it's connected to NOW,
9208
- * even after a disconnect/degrade where nothing has actually confirmed
9209
- * that's still true (a reconnect could land on a DIFFERENT server behind a
9210
- * load balancer). Concretely, `TaskRunner.sendApprovalResolved` gates
9211
- * `task.approval_resolved` on this list — sending it to a server that
9212
- * doesn't actually understand it over the long-poll path would get a
9213
- * batch-level 400 from `MessagesSendRequestSchema` (protocol §8.2), which
9214
- * `drainOutbox`'s retry-the-same-batch-forever loop then head-of-line
9215
- * blocks EVERY envelope queued behind it on, permanently. Clearing this
9216
- * eagerly means that gate reliably fails closed (falls back to the
9217
- * pre-existing implicit-resume inference, unconditionally — see
9218
- * `sendApprovalResolved`'s own doc comment) the moment the connection that
9219
- * advertised the capability is no longer the one actually in use.
9727
+ * The capabilities the current server response advertised — untyped
9728
+ * `string[]` for forward compatibility. An advertisement is scoped to the
9729
+ * current long-poll response stream and is cleared after an HTTP failure,
9730
+ * terminal shutdown, or revocation. This keeps capability-gated outbound
9731
+ * messages fail-closed until the current server has explicitly advertised
9732
+ * support.
9220
9733
  */
9221
9734
  serverCapabilities = [];
9222
9735
  async start() {
9736
+ if (this.terminalError) throw this.terminalError;
9223
9737
  this.cursor = await this.opts.cursorStore.load(this.opts.serverUrl, this.opts.deviceId);
9224
- this.ws.connect({ auto: true });
9738
+ this.started = true;
9739
+ this.opts.onStateChange?.("connecting");
9740
+ this.longPoll.start();
9741
+ this.outbox.unshift(createConnectionHelloEnvelope(this.opts, () => this.cursor));
9742
+ void this.drainOutbox();
9225
9743
  }
9226
9744
  /**
9227
9745
  * Design B (finding N4): push onto the single shared outbox and try to
@@ -9232,17 +9750,13 @@ var ConnectionManager = class {
9232
9750
  this.outbox.push(envelope);
9233
9751
  void this.drainOutbox();
9234
9752
  }
9753
+ /** Publish a fresh local configuration snapshot while this daemon is running. */
9754
+ refreshHello() {
9755
+ if (!this.started || this.stopped || this.revoked || this.terminalError) return;
9756
+ this.send(createConnectionHelloEnvelope(this.opts, () => this.cursor));
9757
+ }
9235
9758
  /**
9236
- * Design B (finding N4): drain the shared outbox through whichever
9237
- * transport is currently active, re-checking `this.mode` fresh on every
9238
- * iteration so a transport switch mid-drain is picked up immediately
9239
- * rather than fighting a stale decision made before the switch.
9240
- *
9241
- * WS: a synchronous, one-at-a-time `sendNow` per envelope while open+
9242
- * acked; stops (without dropping anything — the remainder stays queued)
9243
- * the moment it isn't, and is re-invoked once `onAcked` fires.
9244
- *
9245
- * Long-poll: POSTs the outbox in chunks of at most
9759
+ * POSTs the outbox through long-poll in chunks of at most
9246
9760
  * `MAX_MESSAGES_PER_BATCH` (finding P1) — the server hard-caps a single
9247
9761
  * `/byok/messages` batch there (`MessagesSendRequestSchema`, protocol
9248
9762
  * §8.2) and 400s the WHOLE request if it's exceeded, which — before this
@@ -9254,43 +9768,48 @@ var ConnectionManager = class {
9254
9768
  * failure that SAME chunk is unshifted back (order-preserving, same
9255
9769
  * Envelope objects/ids — never rebuilt, so a retry is exactly the resend
9256
9770
  * Wave 1's server-side dedup expects) and retried after a short backoff,
9257
- * re-reading `this.mode` each time so a WS recovery that happens
9258
- * mid-retry is honored on the very next loop iteration instead of only
9259
- * after this attempt's backoff chain gives up.
9260
- *
9261
9771
  * Re-entrancy is guarded by `draining`: a call arriving while a drain is
9262
9772
  * already in progress just returns — the in-progress loop's own
9263
- * `while (this.outbox.length > 0)` check will pick up anything newly
9264
- * pushed (or left over after a mode switch) on its very next iteration.
9773
+ * `while (this.outbox.length > 0)` check picks up anything newly pushed.
9265
9774
  */
9266
9775
  async drainOutbox() {
9267
- if (this.draining) return;
9776
+ if (!this.started || this.draining) return;
9268
9777
  this.draining = true;
9778
+ const isolateRejectedBatch = async (batch) => {
9779
+ const pending = [[...batch]];
9780
+ while (pending.length > 0) {
9781
+ if (this.stopped || this.revoked) return pending.flat();
9782
+ const segment = pending.shift();
9783
+ const result = await this.longPoll.postBatch(segment);
9784
+ if (result === void 0) return [...segment, ...pending.flat()];
9785
+ if ((result.rejected ?? 0) === 0) continue;
9786
+ if (segment.length === 1) {
9787
+ this.quarantineRejectedOutbound(segment[0], "inbound_rejected");
9788
+ continue;
9789
+ }
9790
+ const midpoint = Math.floor(segment.length / 2);
9791
+ pending.unshift(segment.slice(0, midpoint), segment.slice(midpoint));
9792
+ }
9793
+ return void 0;
9794
+ };
9269
9795
  try {
9270
9796
  while (this.outbox.length > 0) {
9271
9797
  if (this.stopped || this.revoked) return;
9272
- if (this.mode === "ws") {
9273
- if (!this.ws.isOpen) return;
9274
- const envelope = this.outbox[0];
9275
- if (!this.ws.sendNow(envelope)) return;
9276
- this.outbox.shift();
9277
- continue;
9278
- }
9279
9798
  const batch = this.outbox.splice(0, MAX_MESSAGES_PER_BATCH);
9280
9799
  this.inFlightBatchSize = batch.length;
9281
- let ok;
9800
+ let retrySegments;
9282
9801
  try {
9283
- ok = await this.longPoll.postBatch(batch);
9802
+ retrySegments = await isolateRejectedBatch(batch);
9284
9803
  } finally {
9285
9804
  this.inFlightBatchSize = 0;
9286
9805
  }
9287
- if (ok) {
9806
+ if (retrySegments === void 0) {
9288
9807
  this.uploadRetryAttempt = 0;
9289
9808
  this.opts.onOperationalOutcome?.("success", "upload");
9290
9809
  continue;
9291
9810
  }
9292
9811
  this.opts.onOperationalOutcome?.("failure", "upload");
9293
- this.outbox.unshift(...batch);
9812
+ this.outbox.unshift(...retrySegments);
9294
9813
  if (this.stopped || this.revoked) return;
9295
9814
  const baseMs = this.opts.longPollRetryDelayMs ?? 2e3;
9296
9815
  await this.drainRetryDelay(this.fleetJitter.delay("upload", this.uploadRetryAttempt++, baseMs));
@@ -9306,8 +9825,7 @@ var ConnectionManager = class {
9306
9825
  * in-flight wait immediately instead of leaving `drainOutbox` parked here
9307
9826
  * for up to the rest of the delay before it next checks `this.revoked` —
9308
9827
  * and (b) unref'd, so the timer never keeps the Node process alive by
9309
- * itself while nothing else (a live long-poll GET, an open WS connection)
9310
- * legitimately is.
9828
+ * itself while nothing else (such as the live long-poll GET) legitimately is.
9311
9829
  */
9312
9830
  drainRetryDelay(ms) {
9313
9831
  return new Promise((resolve) => {
@@ -9323,45 +9841,41 @@ var ConnectionManager = class {
9323
9841
  };
9324
9842
  });
9325
9843
  }
9326
- isTransportDegraded() {
9327
- return this.mode === "long-poll";
9328
- }
9329
9844
  /**
9330
- * The capabilities the CURRENT transport's server advertised: from
9331
- * `conn.ack` on WS, or the latest successful `GET /byok/events` response
9332
- * on long-poll. Empty before either transport has supplied its current
9333
- * advertisement, and cleared across disconnect/switch boundaries.
9845
+ * The capabilities the latest successful `GET /byok/events` response
9846
+ * advertised. Empty before a successful response and after a failed one.
9334
9847
  */
9335
9848
  getServerCapabilities() {
9336
9849
  return this.serverCapabilities;
9337
9850
  }
9851
+ getTerminalError() {
9852
+ return this.terminalError;
9853
+ }
9338
9854
  isConnected() {
9339
- return this.mode === "ws" && this.ws.isOpen;
9855
+ return this.connected;
9340
9856
  }
9341
9857
  isRevoked() {
9342
9858
  return this.revoked;
9343
9859
  }
9344
9860
  /**
9345
- * Resolves once the connection has settled either a working, acked WS
9346
- * connection, or the long-poll fallback taking over (protocol §8). This
9347
- * lets `daemon.start()` return promptly even when WS is unavailable from
9348
- * the very first attempt, rather than hanging until a WS `conn.ack` that
9349
- * may never come.
9861
+ * Resolves after the first successful long-poll response establishes the
9862
+ * authenticated connection.
9350
9863
  *
9351
9864
  * Rejects with {@link DeviceRevokedError} — instead of hanging until
9352
9865
  * `timeoutMs` — if the device turns out to be revoked while settling (or
9353
9866
  * already was): a cold `daemon.start()` against an already-revoked device
9354
9867
  * must fail fast, not surface a generic timeout (protocol §6.3).
9355
9868
  */
9356
- waitForAck(timeoutMs = 1e4) {
9357
- if (this.ws.isOpen || this.mode === "long-poll") return Promise.resolve();
9869
+ waitForConnection(timeoutMs = 1e4) {
9870
+ if (this.connected) return Promise.resolve();
9871
+ if (this.terminalError) return Promise.reject(this.terminalError);
9358
9872
  if (this.revoked) return Promise.reject(new DeviceRevokedError());
9359
9873
  return new Promise((resolve, reject) => {
9360
9874
  let settle = () => {
9361
9875
  };
9362
9876
  const timer = setTimeout(() => {
9363
9877
  this.settledWaiters = this.settledWaiters.filter((w) => w !== settle);
9364
- reject(new Error("Timed out waiting for the connection to settle (WS ack or long-poll fallback)"));
9878
+ reject(new Error("Timed out waiting for the long-poll connection to settle"));
9365
9879
  }, timeoutMs);
9366
9880
  settle = (err) => {
9367
9881
  clearTimeout(timer);
@@ -9372,7 +9886,7 @@ var ConnectionManager = class {
9372
9886
  });
9373
9887
  }
9374
9888
  /**
9375
- * Stops both transports and waits for every in-flight envelope handler
9889
+ * Stops the long-poll transport and waits for every in-flight envelope handler
9376
9890
  * (the F3 FIFO chain) and the most recent cursor write to actually land on
9377
9891
  * disk — otherwise a `stop()` racing a just-processed envelope's
9378
9892
  * persistence could lose that cursor advance, or leave a handler running
@@ -9381,14 +9895,14 @@ var ConnectionManager = class {
9381
9895
  * Finding F5(b) (cross-model adversarial review): `drainTimeoutMs`, when
9382
9896
  * passed, bounds how long this waits for the shared outbox (`this.outbox`
9383
9897
  * — Design B) to actually finish draining BEFORE flipping `this.stopped`
9384
- * and closing the transports. Before this fix, `stop()` set `stopped`
9898
+ * and stopping the transport. Before this fix, `stop()` set `stopped`
9385
9899
  * synchronously and never waited for `drainOutbox` at all: an envelope
9386
9900
  * `send()` had just pushed moments earlier (e.g. `TaskRunner.shutdownTask`'s
9387
9901
  * own `task.fail`, sent right before `create-daemon.ts`'s
9388
9902
  * `performControlShutdown` calls this) could still be sitting UNSENT in
9389
9903
  * `this.outbox` — mid long-poll retry backoff, or simply not yet picked up
9390
9904
  * by the fire-and-forget `drainOutbox()` `send()` kicked off — and this
9391
- * method would happily proceed to `stopped = true` / `ws.close()` regardless,
9905
+ * method would happily proceed to `stopped = true` regardless,
9392
9906
  * after which NOTHING ever drains it again: silently lost, even though
9393
9907
  * `TaskRunner` believed it had been sent. `drainTimeoutMs` omitted (the
9394
9908
  * default) preserves the EXACT prior behavior for every other existing
@@ -9405,9 +9919,9 @@ var ConnectionManager = class {
9405
9919
  }
9406
9920
  this.stopped = true;
9407
9921
  this.serverCapabilities = [];
9408
- if (this.wsRetryTimer) clearInterval(this.wsRetryTimer);
9409
9922
  this.longPoll.stop();
9410
- this.ws.close();
9923
+ this.connected = false;
9924
+ this.opts.onStateChange?.("closed");
9411
9925
  await this.processingChain;
9412
9926
  await this.pendingCursorSave;
9413
9927
  }
@@ -9432,6 +9946,10 @@ var ConnectionManager = class {
9432
9946
  outboxLength() {
9433
9947
  return this.outbox.length + this.inFlightBatchSize;
9434
9948
  }
9949
+ /** A bounded terminal quarantine for operator inspection; these entries are never retried. */
9950
+ rejectedOutbox() {
9951
+ return this.rejectedOutboundEnvelopes;
9952
+ }
9435
9953
  /**
9436
9954
  * Finding F5(b): polls {@link outboxLength} (not `this.outbox.length`
9437
9955
  * alone — see that method's own doc comment for why a spliced-out,
@@ -9439,15 +9957,14 @@ var ConnectionManager = class {
9439
9957
  * a single `drainOutbox()` promise directly — a drain in progress can
9440
9958
  * itself loop through multiple retry/backoff cycles (`drainRetryDelay`)
9441
9959
  * while the server is unreachable, and a fresh, INDEPENDENT
9442
- * `drainOutbox()` call can also be triggered concurrently (`send()`, a
9443
- * mode switch's own `void this.drainOutbox()`) — polling the one thing
9960
+ * `drainOutbox()` call can also be triggered concurrently (`send()`)
9961
+ * polling the one thing
9444
9962
  * that actually matters (is anything still undelivered) can never go
9445
9963
  * stale the way capturing one specific in-flight promise reference
9446
9964
  * could. Kicks off one more `drainOutbox()` attempt itself first
9447
9965
  * (harmless no-op if one is already running — see its own re-entrancy
9448
- * guard) in case nothing is currently actively retrying (e.g. WS just
9449
- * dropped and long-poll hasn't taken over yet), so this bounded wait
9450
- * isn't just passively hoping something else happens to be making
9966
+ * guard) in case nothing is currently actively retrying, so this bounded
9967
+ * wait isn't just passively hoping something else happens to be making
9451
9968
  * progress.
9452
9969
  */
9453
9970
  waitForOutboxDrained(timeoutMs) {
@@ -9490,19 +10007,21 @@ var ConnectionManager = class {
9490
10007
  deliver(envelope) {
9491
10008
  const tracked = isCursorEnvelopeType(envelope.type) && typeof envelope.seq === "number";
9492
10009
  const watermark = this.dedupWatermark();
9493
- if (tracked && watermark !== void 0 && envelope.seq <= watermark) return;
10010
+ if (tracked && watermark !== void 0 && envelope.seq <= watermark) return false;
9494
10011
  if (tracked) {
9495
10012
  const seq = envelope.seq;
9496
- if (this.inFlightSeqs.has(seq) || this.processedSeqs.has(seq)) return;
10013
+ if (this.inFlightSeqs.has(seq) || this.processedSeqs.has(seq)) return false;
9497
10014
  this.inFlightSeqs.add(seq);
9498
10015
  this.noteDelivered(seq);
9499
10016
  }
9500
10017
  this.processingChain = this.processingChain.then(() => this.process(envelope, tracked));
10018
+ return true;
9501
10019
  }
9502
10020
  /**
9503
- * Design A: the watermark `deliver()` dedupes inbound `task.*` envelopes
9504
- * against, and the same value `LongPollClient` queries the next
9505
- * `GET /byok/events` cursor with (see the constructor). Normally this is
10021
+ * The local watermark `deliver()` dedupes inbound `task.*` envelopes
10022
+ * against. It is deliberately NOT the long-poll query cursor: that query
10023
+ * is the kernel acknowledgement and uses only the successfully processed
10024
+ * `cursor` (see the constructor). Normally this local watermark is
9506
10025
  * `deliveredSeq` — which is always >= `cursor` (every envelope that
9507
10026
  * reaches `advanceCursor` already passed through `noteDelivered` first,
9508
10027
  * see `deliver`) — so this is the literal `max(cursor, deliveredSeq)` the
@@ -9518,11 +10037,9 @@ var ConnectionManager = class {
9518
10037
  * whose outcome wasn't known yet. No separate "reset deliveredSeq on
9519
10038
  * reconnect" step is needed for this to be correct — collapsing to
9520
10039
  * `cursor` exactly while stalled already produces the right answer on
9521
- * every redelivery path (long-poll re-query AND a WS reconnect's
9522
- * backlog replay alike), and NOT resetting it unconditionally on every
9523
- * reconnect is what lets `deliveredSeq` keep doing its job of not
9524
- * re-pulling/re-dispatching something already in flight across a
9525
- * reconnect that happens to land while a handler is still running.
10040
+ * every long-poll retry path. NOT resetting it unconditionally on every
10041
+ * retry lets `deliveredSeq` keep doing its job of not re-dispatching
10042
+ * something already in flight while a handler is still running.
9526
10043
  */
9527
10044
  dedupWatermark() {
9528
10045
  return this.stalledAtSeq !== void 0 ? this.cursor : this.deliveredSeq ?? this.cursor;
@@ -9540,11 +10057,13 @@ var ConnectionManager = class {
9540
10057
  }
9541
10058
  await this.opts.onEnvelope(envelope);
9542
10059
  if (!tracked) return;
9543
- this.processedSeqs.add(seq);
9544
- if (this.stalledAtSeq !== void 0 && envelope.seq !== this.stalledAtSeq) return;
10060
+ if (this.stalledAtSeq !== void 0 && envelope.seq !== this.stalledAtSeq) {
10061
+ this.processedSeqs.add(seq);
10062
+ return;
10063
+ }
9545
10064
  this.stalledAtSeq = void 0;
10065
+ await this.advanceCursor(envelope.seq);
9546
10066
  this.processedSeqs.clear();
9547
- this.advanceCursor(envelope.seq);
9548
10067
  } catch (err) {
9549
10068
  if (tracked && this.stalledAtSeq === void 0) this.stalledAtSeq = envelope.seq;
9550
10069
  console.error(
@@ -9558,9 +10077,8 @@ var ConnectionManager = class {
9558
10077
  /**
9559
10078
  * M4 Phase 4 (version-negotiation drill fix): `LongPollClient` calls this
9560
10079
  * for a batch entry it could not parse into a known `Envelope` at all (an
9561
- * unrecognized message type mirrors `ws-transport.ts`'s identical
9562
- * per-frame tolerance, see `long-poll-transport.ts`'s own doc comment on
9563
- * `parseLooseEventsPollResponse`) but which still carried a numeric,
10080
+ * unrecognized message type (see `long-poll-transport.ts`'s own doc
10081
+ * comment on `parseLooseEventsPollResponse`) but which still carried a numeric,
9564
10082
  * task-class envelope-level `seq` (the caller only invokes this for a
9565
10083
  * `task.`-prefixed type — see `long-poll-transport.ts`'s own
9566
10084
  * `extractSkippableSeq`; `conn.*`-shaped or type-less entries never reach
@@ -9600,7 +10118,7 @@ var ConnectionManager = class {
9600
10118
  * `noteDelivered` (the eager, in-memory watermark) stays UNCHAINED —
9601
10119
  * called immediately, unconditionally, regardless of `stalledAtSeq` —
9602
10120
  * matching `deliver()`'s own eager, unconditional call for a real
9603
- * envelope: its only job is "don't re-pull something already handed off,"
10121
+ * envelope: its only job is "don't re-dispatch something already handed off,"
9604
10122
  * independent of outcome, and that property does not depend on FIFO
9605
10123
  * ordering the way the DURABLE cursor does.
9606
10124
  *
@@ -9620,9 +10138,18 @@ var ConnectionManager = class {
9620
10138
  */
9621
10139
  noteSkippedSeq(seq) {
9622
10140
  this.noteDelivered(seq);
9623
- this.processingChain = this.processingChain.then(() => {
10141
+ this.processingChain = this.processingChain.then(async () => {
9624
10142
  if (this.stalledAtSeq === void 0 || seq === this.stalledAtSeq) {
9625
- this.advanceCursor(seq);
10143
+ try {
10144
+ await this.advanceCursor(seq);
10145
+ if (this.stalledAtSeq === seq) this.stalledAtSeq = void 0;
10146
+ } catch (err) {
10147
+ if (this.stalledAtSeq === void 0) this.stalledAtSeq = seq;
10148
+ console.error(
10149
+ `[byok/client] unknown task seq=${seq} could not durably persist its cursor; cursor left unadvanced for redelivery:`,
10150
+ err
10151
+ );
10152
+ }
9626
10153
  }
9627
10154
  });
9628
10155
  }
@@ -9668,94 +10195,52 @@ var ConnectionManager = class {
9668
10195
  if (this.stalledAtSeq === void 0) this.stalledAtSeq = seq;
9669
10196
  });
9670
10197
  }
9671
- advanceCursor(seq) {
10198
+ async advanceCursor(seq) {
9672
10199
  if (this.cursor !== void 0 && seq <= this.cursor) return;
10200
+ const save = this.pendingCursorSave.catch(() => void 0).then(() => this.opts.cursorStore.save(this.opts.serverUrl, this.opts.deviceId, seq));
10201
+ this.pendingCursorSave = save;
10202
+ await save;
9673
10203
  this.cursor = seq;
9674
- this.pendingCursorSave = this.pendingCursorSave.catch(() => {
9675
- }).then(() => this.opts.cursorStore.save(this.opts.serverUrl, this.opts.deviceId, seq)).catch(() => {
9676
- });
9677
- }
9678
- /**
9679
- * Fires the moment a connection attempt reaches `conn.ack` — independent
9680
- * of whether/when it later closes. This is the ONLY place that can
9681
- * reliably detect "WS is back up" while long-polling: a healthy
9682
- * connection stays open indefinitely, so it never reaches `onWsOutcome`
9683
- * (which is close-only) at all.
9684
- */
9685
- onAcked(capabilities) {
9686
- this.serverCapabilities = capabilities;
9687
- this.consecutiveFailures = 0;
9688
- this.notifySettled();
9689
- this.opts.onOperationalOutcome?.("success", "reconnect");
9690
- if (this.mode === "long-poll") this.exitLongPoll();
9691
- void this.drainOutbox();
9692
10204
  }
9693
- onWsOutcome(acked, err) {
9694
- if (this.stopped || this.revoked) return;
9695
- if (acked) this.serverCapabilities = [];
9696
- if (err instanceof WsUnexpectedStatusError && err.status === 401) {
9697
- this.opts.auth.handleUnauthorized().catch((renewErr) => {
9698
- if (renewErr instanceof DeviceRevokedError) this.enterRevoked();
9699
- });
9700
- }
9701
- if (acked) return;
9702
- this.opts.onOperationalOutcome?.("failure", "reconnect");
9703
- this.consecutiveFailures += 1;
9704
- if (this.mode === "ws" && this.consecutiveFailures >= (this.opts.wsFailureThreshold ?? 3)) {
9705
- this.enterLongPoll();
9706
- }
10205
+ quarantineRejectedOutbound(envelope, reason) {
10206
+ if (this.rejectedOutboundEnvelopes.length === MAX_REJECTED_OUTBOX_ENTRIES) this.rejectedOutboundEnvelopes.shift();
10207
+ this.rejectedOutboundEnvelopes.push({ envelope, reason });
10208
+ console.error(
10209
+ `[byok/client] outbound envelope ${envelope.id} was permanently rejected (${reason}) and moved to the terminal quarantine.`
10210
+ );
9707
10211
  }
9708
10212
  notifySettled(err) {
9709
10213
  for (const waiter of this.settledWaiters.splice(0)) waiter(err);
9710
10214
  }
9711
- enterLongPoll() {
9712
- this.mode = "long-poll";
9713
- this.serverCapabilities = [];
9714
- this.ws.stopAutoReconnect();
9715
- this.opts.onStateChange?.("degraded");
9716
- this.longPoll.start();
9717
- this.outbox.unshift(createConnectionHelloEnvelope({
9718
- deviceId: this.opts.deviceId,
9719
- productId: this.opts.productId,
9720
- capabilities: this.opts.capabilities,
9721
- clientVersion: this.opts.clientVersion,
9722
- runtimes: this.opts.runtimes,
9723
- getConfiguredToolsets: this.opts.getConfiguredToolsets,
9724
- getCursor: () => this.cursor
9725
- }));
10215
+ noteConnected() {
10216
+ if (this.connected || this.stopped || this.revoked || this.terminalError) return;
10217
+ this.connected = true;
10218
+ this.opts.onStateChange?.("open");
9726
10219
  this.notifySettled();
9727
- void this.drainOutbox();
9728
- this.scheduleWsProbe();
9729
10220
  }
9730
- exitLongPoll() {
9731
- if (this.wsRetryTimer) {
9732
- clearInterval(this.wsRetryTimer);
9733
- this.wsRetryTimer = void 0;
9734
- }
9735
- this.longPoll.stop();
9736
- this.mode = "ws";
9737
- this.consecutiveFailures = 0;
9738
- this.ws.resumeAutoReconnect();
9739
- void this.drainOutbox();
10221
+ noteDisconnected() {
10222
+ if (!this.connected || this.stopped || this.revoked || this.terminalError) return;
10223
+ this.connected = false;
10224
+ this.opts.onStateChange?.("connecting");
9740
10225
  }
9741
- scheduleWsProbe() {
9742
- const baseMs = this.opts.wsRetryIntervalMs ?? 5 * 60 * 1e3;
9743
- const delayMs = this.fleetJitter.delay("reconnect", this.wsProbeSequence++, baseMs);
9744
- const timer = setTimeout(() => {
9745
- this.wsRetryTimer = void 0;
9746
- if (this.stopped || this.revoked || this.mode !== "long-poll") return;
9747
- this.ws.connect({ auto: false });
9748
- this.scheduleWsProbe();
9749
- }, delayMs);
9750
- timer.unref?.();
9751
- this.wsRetryTimer = timer;
10226
+ enterReplayCursorTooOld(error) {
10227
+ if (this.terminalError) return;
10228
+ this.terminalError = error;
10229
+ this.stopped = true;
10230
+ this.serverCapabilities = [];
10231
+ this.longPoll.stop();
10232
+ this.connected = false;
10233
+ this.cancelPendingDrainRetry?.();
10234
+ this.notifySettled(error);
10235
+ this.opts.onStateChange?.("closed");
10236
+ this.opts.onTerminalError?.(error);
9752
10237
  }
9753
10238
  enterRevoked() {
9754
10239
  if (this.revoked) return;
9755
10240
  this.revoked = true;
9756
- if (this.wsRetryTimer) clearInterval(this.wsRetryTimer);
10241
+ this.serverCapabilities = [];
9757
10242
  this.longPoll.stop();
9758
- this.ws.close();
10243
+ this.connected = false;
9759
10244
  this.cancelPendingDrainRetry?.();
9760
10245
  this.opts.onStateChange?.("revoked");
9761
10246
  this.notifySettled(new DeviceRevokedError());
@@ -12490,21 +12975,28 @@ async function recordAuditWarning(context, kind, values) {
12490
12975
  }
12491
12976
  }
12492
12977
  var agentMemoryHomeQueues = /* @__PURE__ */ new Map();
12493
- async function exclusiveAgentMemoryHome(home, fn) {
12494
- const previous = agentMemoryHomeQueues.get(home) ?? Promise.resolve();
12978
+ var agentMemoryProjectionTransactionQueues = /* @__PURE__ */ new Map();
12979
+ async function exclusiveAgentMemoryHomeQueue(queues2, home, fn) {
12980
+ const previous = queues2.get(home) ?? Promise.resolve();
12495
12981
  let release;
12496
12982
  const next = new Promise((resolve) => {
12497
12983
  release = resolve;
12498
12984
  });
12499
- agentMemoryHomeQueues.set(home, next);
12985
+ queues2.set(home, next);
12500
12986
  await previous;
12501
12987
  try {
12502
12988
  return await fn();
12503
12989
  } finally {
12504
12990
  release();
12505
- if (agentMemoryHomeQueues.get(home) === next) agentMemoryHomeQueues.delete(home);
12991
+ if (queues2.get(home) === next) queues2.delete(home);
12506
12992
  }
12507
12993
  }
12994
+ async function exclusiveAgentMemoryHome(home, fn) {
12995
+ return exclusiveAgentMemoryHomeQueue(agentMemoryHomeQueues, home, fn);
12996
+ }
12997
+ async function exclusiveAgentMemoryProjectionTransaction(home, fn) {
12998
+ return exclusiveAgentMemoryHomeQueue(agentMemoryProjectionTransactionQueues, home, fn);
12999
+ }
12508
13000
  var AgentMemoryService = class {
12509
13001
  constructor(input) {
12510
13002
  this.input = input;
@@ -12808,13 +13300,16 @@ var AgentMemoryRedactedOutbox = class _AgentMemoryRedactedOutbox {
12808
13300
  async function snapshotAndProjectAgentMemory(input, projection) {
12809
13301
  if (projection?.capability !== AGENT_MEMORY_PROJECTION_CAPABILITY || !projection.grant || !projection.redactor || !projection.port) return;
12810
13302
  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);
13303
+ const { grant, redactor, port } = projection;
13304
+ await exclusiveAgentMemoryProjectionTransaction(context.canonicalHome, async () => {
13305
+ const outbox = await AgentMemoryRedactedOutbox.open(context, grant);
13306
+ const initialReplay = await outbox.replay(port);
13307
+ if (initialReplay.status === "pending") throw new AgentMemoryProjectionReplayPendingError(initialReplay);
13308
+ const snapshot = await captureAgentMemorySnapshot(context);
13309
+ await outbox.append(redactedBytes(snapshot, await redactor.redact(snapshot)));
13310
+ const trailingReplay = await outbox.replay(port);
13311
+ if (trailingReplay.status === "pending") throw new AgentMemoryProjectionReplayPendingError(trailingReplay);
13312
+ });
12818
13313
  }
12819
13314
  var AGENT_MEMORY_FILESYSTEM_HELPER_PROTOCOL = 2;
12820
13315
  var AGENT_MEMORY_FILESYSTEM_HELPER_VERSION = "2";
@@ -13113,6 +13608,7 @@ function resultDocumentRejectionDetail(check) {
13113
13608
  }
13114
13609
  }
13115
13610
  var DEFAULT_MAX_TASK_OUTPUT_BYTES = 64 * 1024 * 1024;
13611
+ var DEFAULT_MAX_CONCURRENT_MUTABLE_SESSIONS_PER_AGENT_HOME = 1;
13116
13612
  function isKnownRuntimeId(id) {
13117
13613
  return RuntimeIdSchema.safeParse(id).success;
13118
13614
  }
@@ -13222,6 +13718,7 @@ var TaskRunner = class {
13222
13718
  deps;
13223
13719
  tasks = /* @__PURE__ */ new Map();
13224
13720
  pendingMessageTasks = /* @__PURE__ */ new Map();
13721
+ messageOutboxesByHome = /* @__PURE__ */ new Map();
13225
13722
  messageContextByToken = /* @__PURE__ */ new Map();
13226
13723
  messageContextByTask = /* @__PURE__ */ new Map();
13227
13724
  memoryContextByToken = /* @__PURE__ */ new Map();
@@ -13282,6 +13779,8 @@ var TaskRunner = class {
13282
13779
  * costs nothing.
13283
13780
  */
13284
13781
  inFlightOffers = /* @__PURE__ */ new Set();
13782
+ /** Blob I/O before an offer becomes an active task still belongs to that offer's cancellation authority. */
13783
+ inFlightBlobAborts = /* @__PURE__ */ new Map();
13285
13784
  /**
13286
13785
  * Finding P2 (Fix 2c): taskIds that have reached a terminal outcome
13287
13786
  * (Complete/Failed/Cancelled) this session — populated in `finish()`.
@@ -13356,6 +13855,10 @@ var TaskRunner = class {
13356
13855
  get maxTaskOutputBytes() {
13357
13856
  return this.deps.maxTaskOutputBytes ?? DEFAULT_MAX_TASK_OUTPUT_BYTES;
13358
13857
  }
13858
+ /** WP0: effective per-canonical-Agent-home Attempt cap — see {@link DEFAULT_MAX_CONCURRENT_MUTABLE_SESSIONS_PER_AGENT_HOME}. */
13859
+ get maxConcurrentMutableSessionsPerAgentHome() {
13860
+ return this.deps.maxConcurrentMutableSessionsPerAgentHome ?? DEFAULT_MAX_CONCURRENT_MUTABLE_SESSIONS_PER_AGENT_HOME;
13861
+ }
13359
13862
  /**
13360
13863
  * M4 Phase 4 (part B.3, observability): per-active-task queue watermarks
13361
13864
  * for the control socket's `status` result — see
@@ -13408,6 +13911,7 @@ var TaskRunner = class {
13408
13911
  async recoverAgentMessageOutboxes(agentsRoot) {
13409
13912
  if (this.deps.tenantId === void 0) throw new Error("Agent message recovery requires authenticated tenant enrollment");
13410
13913
  for (const outbox of await AgentMessageOutbox.recover(agentsRoot, this.deps.tenantId)) {
13914
+ this.messageOutboxesByHome.set(outbox.homeDir, Promise.resolve(outbox));
13411
13915
  for (const record of outbox.retryableRecords()) {
13412
13916
  if (record.sessionRef === void 0) continue;
13413
13917
  const existing = this.recoveredMessageOutboxes.get(record.taskId);
@@ -13416,6 +13920,16 @@ var TaskRunner = class {
13416
13920
  }
13417
13921
  }
13418
13922
  }
13923
+ agentMessageOutbox(homeDir) {
13924
+ const existing = this.messageOutboxesByHome.get(homeDir);
13925
+ if (existing !== void 0) return existing;
13926
+ const opened = AgentMessageOutbox.open(homeDir);
13927
+ this.messageOutboxesByHome.set(homeDir, opened);
13928
+ void opened.catch(() => {
13929
+ if (this.messageOutboxesByHome.get(homeDir) === opened) this.messageOutboxesByHome.delete(homeDir);
13930
+ });
13931
+ return opened;
13932
+ }
13419
13933
  /** Retry stable recovered records after a transport handshake/re-handshake. */
13420
13934
  retryRecoveredAgentMessages() {
13421
13935
  for (const [taskId, outbox] of this.recoveredMessageOutboxes) {
@@ -13571,6 +14085,7 @@ var TaskRunner = class {
13571
14085
  if (active.finalizationStarted) return this.finish(active.taskId);
13572
14086
  if (!this.reserveSemanticTerminal(active)) return active.semanticTerminalSettled ?? false;
13573
14087
  active.beingTornDown = true;
14088
+ active.blobAbort.abort();
13574
14089
  await this.observeGit(active, "salvage");
13575
14090
  const timeoutMs = this.deps.shutdownInterruptTimeoutMs ?? DEFAULT_SHUTDOWN_INTERRUPT_TIMEOUT_MS;
13576
14091
  await raceSettleFirst(() => active.session.interrupt(), timeoutMs);
@@ -13717,6 +14232,8 @@ var TaskRunner = class {
13717
14232
  return;
13718
14233
  }
13719
14234
  this.inFlightOffers.add(taskId);
14235
+ const blobAbort = new AbortController();
14236
+ this.inFlightBlobAborts.set(taskId, blobAbort);
13720
14237
  let agentBinding;
13721
14238
  let agentLeaseTransferred = false;
13722
14239
  try {
@@ -13731,6 +14248,24 @@ var TaskRunner = class {
13731
14248
  decline("strict Agent-only daemon refuses legacy task offers", false);
13732
14249
  return;
13733
14250
  }
14251
+ if (agentRef !== void 0) {
14252
+ const limit = this.maxConcurrentMutableSessionsPerAgentHome;
14253
+ let canonicalHome;
14254
+ try {
14255
+ canonicalHome = await this.deps.agentHome.layout.canonicalHomePath(agentRef);
14256
+ } catch (error) {
14257
+ decline(
14258
+ `Agent home admission failed: ${errorMessage5(error)}`,
14259
+ !(error instanceof AgentHomeResolutionError)
14260
+ );
14261
+ return;
14262
+ }
14263
+ const active2 = this.deps.agentHome.executionLeaseManager.activeAttemptCount(canonicalHome);
14264
+ if (active2 >= limit) {
14265
+ decline(`agent home busy: ${active2} active attempt(s)`, true);
14266
+ return;
14267
+ }
14268
+ }
13734
14269
  const guarded = this.deps.admissionGuard?.({ taskId, payload });
13735
14270
  if (guarded !== void 0 && !guarded.admit) {
13736
14271
  decline(guarded.reason, guarded.retryable);
@@ -13875,7 +14410,7 @@ var TaskRunner = class {
13875
14410
  }
13876
14411
  if (agentRef !== void 0) {
13877
14412
  try {
13878
- agentBinding = await this.deps.agentHome.acquire(agentRef);
14413
+ agentBinding = await this.deps.agentHome.acquireExecution(agentRef, { taskId, sessionRef });
13879
14414
  } catch (error) {
13880
14415
  decline(
13881
14416
  `Agent home admission failed: ${errorMessage5(error)}`,
@@ -13910,7 +14445,7 @@ var TaskRunner = class {
13910
14445
  }
13911
14446
  }
13912
14447
  try {
13913
- await this.deps.agentHome.initialize(agentBinding);
14448
+ await this.deps.agentHome.initializeExecution(agentBinding);
13914
14449
  } catch (error) {
13915
14450
  await agentBinding.lease.release().catch(() => {
13916
14451
  });
@@ -13920,7 +14455,7 @@ var TaskRunner = class {
13920
14455
  }
13921
14456
  if (messageRequirement !== void 0) {
13922
14457
  try {
13923
- const outbox = await AgentMessageOutbox.open(agentBinding.resolution.canonicalHome);
14458
+ const outbox = await this.agentMessageOutbox(agentBinding.resolution.canonicalHome);
13924
14459
  this.pendingMessageTasks.set(taskId, {
13925
14460
  taskId,
13926
14461
  agentRef: agentBinding.resolution.agentRef,
@@ -14039,7 +14574,7 @@ var TaskRunner = class {
14039
14574
  );
14040
14575
  let resolvedInstruction;
14041
14576
  try {
14042
- resolvedInstruction = await this.resolveInstruction(payload.instruction);
14577
+ resolvedInstruction = await this.resolveInstruction(payload.instruction, blobAbort.signal);
14043
14578
  if (plainWorkspaceNeedsResolve) workspaceDir = await this.resolveWorkspaceDir(taskId, known?.workspaceDir);
14044
14579
  } catch (err) {
14045
14580
  gitLease?.release();
@@ -14152,9 +14687,29 @@ var TaskRunner = class {
14152
14687
  }
14153
14688
  return;
14154
14689
  }
14690
+ if (agentBinding !== void 0) {
14691
+ try {
14692
+ await agentBinding.lease.bindSession(session.sessionRef);
14693
+ } catch (error) {
14694
+ await session.close().catch(() => {
14695
+ });
14696
+ await this.failClaimedAgent(
14697
+ taskId,
14698
+ `Agent session execution lease could not bind the runtime session: ${errorMessage5(error)}`,
14699
+ false,
14700
+ {
14701
+ binding: agentBinding,
14702
+ runtimeId: pick.descriptor.id,
14703
+ sessionRef: session.sessionRef
14704
+ }
14705
+ );
14706
+ return;
14707
+ }
14708
+ }
14155
14709
  let active;
14156
14710
  active = {
14157
14711
  taskId,
14712
+ blobAbort,
14158
14713
  egressEnabled: "egressPolicy" in payload,
14159
14714
  adapter: pick.adapter,
14160
14715
  session,
@@ -14195,15 +14750,16 @@ var TaskRunner = class {
14195
14750
  ...terminalProjection === void 0 ? {} : { terminalProjection }
14196
14751
  };
14197
14752
  if (agentBinding !== void 0) {
14753
+ const binding = agentBinding;
14198
14754
  try {
14199
- await this.deps.agentSessionHandoffs.record({
14200
- agentRef: agentBinding.resolution.agentRef,
14755
+ await this.deps.agentHome.mutateExecution(binding, () => this.deps.agentSessionHandoffs.record({
14756
+ agentRef: binding.resolution.agentRef,
14201
14757
  taskId,
14202
14758
  sessionRef: session.sessionRef,
14203
14759
  runtimeId: pick.descriptor.id,
14204
14760
  cwd: workspaceDir,
14205
- leaseId: agentBinding.lease.leaseId
14206
- });
14761
+ leaseId: binding.lease.leaseId
14762
+ }));
14207
14763
  } catch (error) {
14208
14764
  await session.close().catch(() => {
14209
14765
  });
@@ -14245,6 +14801,7 @@ var TaskRunner = class {
14245
14801
  }
14246
14802
  this.deps.send(createEnvelope("task.started", {}, { taskId }));
14247
14803
  agentLeaseTransferred = agentBinding !== void 0;
14804
+ this.inFlightBlobAborts.delete(taskId);
14248
14805
  this.tasks.set(taskId, active);
14249
14806
  this.pendingMessageTasks.delete(taskId);
14250
14807
  if (active.messageOutbox !== void 0 && activatedMessageRecord !== void 0) {
@@ -14276,6 +14833,7 @@ var TaskRunner = class {
14276
14833
  await agentBinding.lease.release().catch(() => {
14277
14834
  });
14278
14835
  }
14836
+ this.inFlightBlobAborts.delete(taskId);
14279
14837
  this.inFlightOffers.delete(taskId);
14280
14838
  }
14281
14839
  }
@@ -14427,9 +14985,9 @@ var TaskRunner = class {
14427
14985
  void this.closeAgentMemoryFilesystem(taskId);
14428
14986
  }
14429
14987
  /** 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) {
14988
+ async resolveInstruction(instruction, signal) {
14431
14989
  if (typeof instruction === "string") return instruction;
14432
- return this.deps.blobClient.resolveInstruction(instruction.blobRef);
14990
+ return this.deps.blobClient.resolveInstruction(instruction.blobRef, { signal });
14433
14991
  }
14434
14992
  /** Resolve every requested logical id locally and reject missing/colliding server authority before claim. */
14435
14993
  resolveMcpServers(requiredToolsets) {
@@ -14661,7 +15219,9 @@ var TaskRunner = class {
14661
15219
  }
14662
15220
  }
14663
15221
  try {
14664
- const blobRef = await this.deps.blobClient.uploadArtifact(bytes, contentType);
15222
+ const blobRef = await this.deps.blobClient.uploadArtifact(bytes, contentType, {
15223
+ signal: active.blobAbort.signal
15224
+ });
14665
15225
  this.deps.send(createEnvelope("task.artifact", { name, contentType, blobRef }, { taskId: active.taskId }));
14666
15226
  } catch (err) {
14667
15227
  this.reportArtifactError(active, name, `failed to upload artifact "${name}": ${errorMessage5(err)}`);
@@ -14676,6 +15236,7 @@ var TaskRunner = class {
14676
15236
  const active = this.tasks.get(taskId);
14677
15237
  if (!active) {
14678
15238
  this.setPendingCancelled(taskId, reason);
15239
+ this.inFlightBlobAborts.get(taskId)?.abort();
14679
15240
  return;
14680
15241
  }
14681
15242
  if (active.finalizationStarted) {
@@ -14686,6 +15247,7 @@ var TaskRunner = class {
14686
15247
  await active.semanticTerminalSettled;
14687
15248
  return;
14688
15249
  }
15250
+ active.blobAbort.abort();
14689
15251
  try {
14690
15252
  await active.session.interrupt();
14691
15253
  } catch {
@@ -15185,7 +15747,7 @@ var TaskRunner = class {
15185
15747
  async failClaimedAgent(taskId, reason, retryable, context) {
15186
15748
  const agentRef = context.binding.resolution.agentRef;
15187
15749
  const result = await this.retryAgentTerminalEvidence(
15188
- () => this.deps.agentSessionHandoffs.recordTaskTerminal({
15750
+ () => this.deps.agentHome.mutateExecution(context.binding, () => this.deps.agentSessionHandoffs.recordTaskTerminal({
15189
15751
  agentRef,
15190
15752
  taskId,
15191
15753
  runtimeId: context.runtimeId,
@@ -15193,7 +15755,7 @@ var TaskRunner = class {
15193
15755
  leaseId: context.binding.lease.leaseId,
15194
15756
  ...context.sessionRef === void 0 ? {} : { sessionRef: context.sessionRef },
15195
15757
  terminalReason: reason
15196
- })
15758
+ }))
15197
15759
  );
15198
15760
  if (!result.ok) {
15199
15761
  this.reportAgentTerminalEvidenceFailure({
@@ -16182,6 +16744,8 @@ var AgentEgressController = class {
16182
16744
  options;
16183
16745
  latest = new AgentLatestValueState();
16184
16746
  spools = /* @__PURE__ */ new Map();
16747
+ spoolOpens = /* @__PURE__ */ new Map();
16748
+ reliableAppendTail = Promise.resolve();
16185
16749
  latestStatus = emptyLane();
16186
16750
  reliableStatus = emptyLane();
16187
16751
  drops = [];
@@ -16236,7 +16800,8 @@ var AgentEgressController = class {
16236
16800
  return latest === void 0 ? [] : Object.freeze([latest]);
16237
16801
  }
16238
16802
  async appendReliable(input) {
16239
- if (!this.active || this.options.tenantId === void 0) {
16803
+ const tenantId = this.options.tenantId;
16804
+ if (!this.active || tenantId === void 0) {
16240
16805
  this.noteDrop("reliable", "policy_denied", input.agentRef);
16241
16806
  return { ok: false, reason: "policy_denied" };
16242
16807
  }
@@ -16251,17 +16816,21 @@ var AgentEgressController = class {
16251
16816
  return { ok: false, reason: "sanitizer_rejected" };
16252
16817
  }
16253
16818
  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 };
16819
+ const spoolOpen = this.spoolFor(input.homeDir, input.agentRef);
16820
+ return await this.withAppendTail(async () => {
16821
+ const spool = await spoolOpen;
16822
+ this.bindSpool(input.agentRef, spool, spoolOpen);
16823
+ const record = await spool.append({
16824
+ agentRef: input.agentRef,
16825
+ tenantId,
16826
+ policyRevision: this.options.policy.policyRevision,
16827
+ payload,
16828
+ sessionRef: input.sessionRef,
16829
+ ...input.taskId === void 0 ? {} : { taskId: input.taskId },
16830
+ ...input.eventId === void 0 ? {} : { eventId: input.eventId }
16831
+ }, this.options.policy, this.tenantPendingBytes());
16832
+ return { ok: true, record };
16833
+ });
16265
16834
  } catch (error) {
16266
16835
  const reason = error instanceof AgentReliableQuotaError ? error.reason : "backpressure";
16267
16836
  this.noteDrop("reliable", reason, input.agentRef);
@@ -16274,21 +16843,26 @@ var AgentEgressController = class {
16274
16843
  * with `wireType: agent.content.receipt` before any transport attempt.
16275
16844
  */
16276
16845
  async appendContentReceipt(input) {
16277
- if (!this.active || this.options.tenantId === void 0) {
16846
+ const tenantId = this.options.tenantId;
16847
+ if (!this.active || tenantId === void 0) {
16278
16848
  this.noteDrop("reliable", "policy_denied", input.agentRef);
16279
16849
  return { ok: false, reason: "policy_denied" };
16280
16850
  }
16281
16851
  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 };
16852
+ const spoolOpen = this.spoolFor(input.homeDir, input.agentRef);
16853
+ return await this.withAppendTail(async () => {
16854
+ const spool = await spoolOpen;
16855
+ this.bindSpool(input.agentRef, spool, spoolOpen);
16856
+ const record = await spool.appendContentReceipt({
16857
+ agentRef: input.agentRef,
16858
+ tenantId,
16859
+ policyRevision: this.options.policy.policyRevision,
16860
+ sessionRef: input.payload.sessionRef,
16861
+ payload: input.payload,
16862
+ ...input.taskId === void 0 ? {} : { taskId: input.taskId }
16863
+ }, this.options.policy, this.tenantPendingBytes());
16864
+ return { ok: true, record };
16865
+ });
16292
16866
  } catch (error) {
16293
16867
  const reason = error instanceof AgentReliableQuotaError ? error.reason : "backpressure";
16294
16868
  this.noteDrop("reliable", reason, input.agentRef);
@@ -16363,20 +16937,59 @@ var AgentEgressController = class {
16363
16937
  }
16364
16938
  return this.reliableRecords();
16365
16939
  }
16366
- async spoolFor(homeDir, agentRef) {
16940
+ spoolFor(homeDir, agentRef) {
16367
16941
  const key = agentKey(agentRef);
16368
16942
  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");
16943
+ if (existing) {
16944
+ if (existing.homeDir !== homeDir) throw new Error("Agent reliable spool is already bound to a different home");
16945
+ return Promise.resolve(existing);
16946
+ }
16947
+ const inFlight = this.spoolOpens.get(key);
16948
+ if (inFlight !== void 0) {
16949
+ if (inFlight.homeDir !== homeDir) throw new Error("Agent reliable spool opening is already bound to a different home");
16950
+ return inFlight.promise;
16951
+ }
16952
+ const opened = (async () => {
16953
+ const spool = await AgentReliableSpool.open(homeDir);
16954
+ if (spool.records().some((record) => !sameAgent(record.agentRef, agentRef))) {
16955
+ throw new Error("Agent-local reliable spool has a different AgentRef");
16956
+ }
16957
+ return spool;
16958
+ })();
16959
+ const slot = { homeDir, promise: opened };
16960
+ this.spoolOpens.set(key, slot);
16961
+ void opened.catch(() => {
16962
+ if (this.spoolOpens.get(key) === slot) this.spoolOpens.delete(key);
16963
+ });
16964
+ return opened;
16965
+ }
16966
+ bindSpool(agentRef, spool, spoolOpen) {
16967
+ const key = agentKey(agentRef);
16968
+ const existing = this.spools.get(key);
16969
+ try {
16970
+ if (existing && existing !== spool) throw new Error("multiple Agent egress spools claim the same AgentRef");
16971
+ this.spools.set(key, spool);
16972
+ } finally {
16973
+ const slot = this.spoolOpens.get(key);
16974
+ if (slot?.promise === spoolOpen) this.spoolOpens.delete(key);
16373
16975
  }
16374
- this.spools.set(key, spool);
16375
- return spool;
16376
16976
  }
16377
16977
  tenantPendingBytes() {
16378
16978
  return this.reliableRecords().reduce((total, record) => total + record.byteCount, 0);
16379
16979
  }
16980
+ async withAppendTail(operation) {
16981
+ const previous = this.reliableAppendTail;
16982
+ let release;
16983
+ this.reliableAppendTail = new Promise((resolve) => {
16984
+ release = resolve;
16985
+ });
16986
+ try {
16987
+ await previous;
16988
+ return await operation();
16989
+ } finally {
16990
+ release();
16991
+ }
16992
+ }
16380
16993
  noteDrop(lane, reason, agentRef, countDrop = true) {
16381
16994
  const laneStatus = lane === "latest-value" ? this.latestStatus : this.reliableStatus;
16382
16995
  if (countDrop) laneStatus.dropped += 1;
@@ -17364,7 +17977,7 @@ async function detectRuntimes(adapters) {
17364
17977
  }
17365
17978
  return runtimes;
17366
17979
  }
17367
- function computeCapabilities(adapters, agentHomeConfigured = false, strictAgentOnly = false, agentHomeProjectionConfigured = false, agentEgressConfigured = false, contentReadPolicies) {
17980
+ function computeCapabilities(adapters, agentHomeConfigured = false, strictAgentOnly = false, agentHomeProjectionConfigured = false, agentEgressConfigured = false, contentReadPolicies, providerProfileBindingConfigured = false) {
17368
17981
  const flags = [];
17369
17982
  if (adapters.some((adapter) => adapter.descriptor.capabilities.steer)) flags.push("steer");
17370
17983
  flags.push("blob-upload");
@@ -17376,6 +17989,9 @@ function computeCapabilities(adapters, agentHomeConfigured = false, strictAgentO
17376
17989
  if (selectionAdapters.length > 0 && selectionAdapters.every((adapter) => adapter.descriptor.supportsDispatchSelection === true)) {
17377
17990
  flags.push("dispatch-selection");
17378
17991
  }
17992
+ if (providerProfileBindingConfigured && adapters.some((adapter) => adapter.descriptor.id === "pi" && adapter.descriptor.supportsDispatchSelection)) {
17993
+ flags.push(PROVIDER_PROFILE_BINDING_CAPABILITY);
17994
+ }
17379
17995
  if (adapters.some((adapter) => adapter.descriptor.capabilities.mcpToolsets === true)) {
17380
17996
  flags.push("toolset-selection");
17381
17997
  }
@@ -17538,6 +18154,12 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
17538
18154
  if (config.strictAgentOnly === true && config.agentHome === void 0) {
17539
18155
  throw new Error("DaemonConfig.strictAgentOnly requires DaemonConfig.agentHome");
17540
18156
  }
18157
+ if (config.maxConcurrentMutableSessionsPerAgentHome !== void 0 && !(Number.isSafeInteger(config.maxConcurrentMutableSessionsPerAgentHome) && config.maxConcurrentMutableSessionsPerAgentHome > 0)) {
18158
+ throw new Error(
18159
+ `DaemonConfig.maxConcurrentMutableSessionsPerAgentHome must be a positive safe integer (or omitted to use the default of ${DEFAULT_MAX_CONCURRENT_MUTABLE_SESSIONS_PER_AGENT_HOME}) \u2014 got ${config.maxConcurrentMutableSessionsPerAgentHome}. Raising it above 1 lets that many Attempts co-write one canonical Agent home; 0, a negative number, NaN, or a non-integer is rejected rather than silently treated as "uncapped".`
18160
+ );
18161
+ }
18162
+ const agentHomeAttemptLimit = config.maxConcurrentMutableSessionsPerAgentHome ?? DEFAULT_MAX_CONCURRENT_MUTABLE_SESSIONS_PER_AGENT_HOME;
17541
18163
  const egressPolicy = resolveAgentEgressPolicy(config.agentEgress?.policy);
17542
18164
  const egressBatcherOptions = egressPolicy.activity.mode === "contentful-trajectory" ? {
17543
18165
  ...config.progressBatch,
@@ -17648,6 +18270,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
17648
18270
  const observer = new DaemonObserver();
17649
18271
  const approvalRegistry = new ApprovalRegistry();
17650
18272
  let connection;
18273
+ let blobLifecycleAbort;
17651
18274
  let connectionState = "closed";
17652
18275
  let daemonStarted = false;
17653
18276
  let tenantRebinding = false;
@@ -17666,6 +18289,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
17666
18289
  function buildAuthManager() {
17667
18290
  return new AuthManager({
17668
18291
  serverUrl: config.serverUrl,
18292
+ authRequestDeadlineMs: config.authRequestDeadlineMs,
17669
18293
  store,
17670
18294
  deviceName: config.deviceName,
17671
18295
  machineId: config.machineId ?? (() => resolveMachineId({ productId: config.productId })),
@@ -17683,7 +18307,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
17683
18307
  assertServerUrlAllowed(config.serverUrl, { dangerouslyAllowInsecureRemote: true });
17684
18308
  const reason = err instanceof Error ? err.message : String(err);
17685
18309
  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})`
18310
+ `[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
18311
  );
17688
18312
  }
17689
18313
  }
@@ -17843,9 +18467,10 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
17843
18467
  await gitWorkspaceStore?.initialize();
17844
18468
  await gitWorkspaceStore?.reconcile();
17845
18469
  }
18470
+ blobLifecycleAbort = new AbortController();
17846
18471
  const [runtimes, blobClient] = await Promise.all([
17847
18472
  detectRuntimes(adapters),
17848
- Promise.resolve(new BlobClient(config.serverUrl, auth))
18473
+ Promise.resolve(new BlobClient(config.serverUrl, auth, { signal: blobLifecycleAbort.signal }))
17849
18474
  ]);
17850
18475
  observer.noteRuntimesDetected(runtimes);
17851
18476
  detectedRuntimeFacts = runtimes;
@@ -17855,7 +18480,8 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
17855
18480
  config.strictAgentOnly === true,
17856
18481
  agentHomeManager?.supportsTaskFreeProjection() === true,
17857
18482
  config.agentEgress !== void 0,
17858
- agentContentReadPolicies
18483
+ agentContentReadPolicies,
18484
+ config.piByokLauncher !== void 0
17859
18485
  );
17860
18486
  const agentHomeProjectionCompletion = agentHomeManager?.supportsTaskFreeProjection() === true ? new AgentHomeProjectionCompletionClient({
17861
18487
  serverUrl: config.serverUrl,
@@ -17914,6 +18540,8 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
17914
18540
  workspaceRoot: config.workspaceRoot,
17915
18541
  ...agentHomeManager === void 0 ? {} : { agentHome: agentHomeManager },
17916
18542
  ...config.strictAgentOnly === true ? { strictAgentOnly: true } : {},
18543
+ // WP0: already validated up front — see `DaemonConfig.maxConcurrentMutableSessionsPerAgentHome`.
18544
+ maxConcurrentMutableSessionsPerAgentHome: agentHomeAttemptLimit,
17917
18545
  ...agentSessionHandoffs === void 0 ? {} : { agentSessionHandoffs },
17918
18546
  deviceId: record.deviceId,
17919
18547
  // M5: see `DaemonConfig.runtimeEnvironment`'s own doc comment above.
@@ -18198,16 +18826,12 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
18198
18826
  return runner?.handleEnvelope(envelope) ?? Promise.resolve();
18199
18827
  },
18200
18828
  onStateChange: (state) => {
18201
- const wasSettled = connectionState === "open" || connectionState === "degraded";
18829
+ const wasSettled = connectionState === "open";
18202
18830
  connectionState = state;
18203
18831
  observer.noteConnectionState(state);
18204
- if (!wasSettled && (state === "open" || state === "degraded")) runner?.retryRecoveredAgentMessages();
18205
- if (!wasSettled && (state === "open" || state === "degraded")) runPresenceDiscovery();
18832
+ if (!wasSettled && state === "open") runner?.retryRecoveredAgentMessages();
18833
+ if (!wasSettled && state === "open") runPresenceDiscovery();
18206
18834
  },
18207
- backoff: overrides.backoff,
18208
- liveness: overrides.liveness,
18209
- wsFailureThreshold: overrides.longPoll?.wsFailureThreshold,
18210
- wsRetryIntervalMs: overrides.longPoll?.wsRetryIntervalMs,
18211
18835
  longPollRetryDelayMs: overrides.longPoll?.retryDelayMs,
18212
18836
  longPollIdleDelayMs: overrides.longPoll?.idleDelayMs,
18213
18837
  fleetJitter,
@@ -18219,7 +18843,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
18219
18843
  }
18220
18844
  });
18221
18845
  await connection.start();
18222
- await connection.waitForAck();
18846
+ await connection.waitForConnection();
18223
18847
  runner.retryRecoveredAgentMessages();
18224
18848
  for (const record2 of agentEgress.retryableReliableRecords(connection.getServerCapabilities())) {
18225
18849
  dispatchReliableRecord(record2);
@@ -18274,6 +18898,8 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
18274
18898
  }
18275
18899
  async function runShutdownSequence(reason, opts = {}) {
18276
18900
  shuttingDown = true;
18901
+ blobLifecycleAbort?.abort();
18902
+ blobLifecycleAbort = void 0;
18277
18903
  const errors = [];
18278
18904
  let mutationBarrierComplete = hostedStorageInitializationBarrierComplete;
18279
18905
  if (!hostedStorageInitializationBarrierComplete) {
@@ -18476,7 +19102,9 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
18476
19102
  // fine.
18477
19103
  ...storageStatus === void 0 ? {} : { storage: storageStatus },
18478
19104
  operationalHealth: operationalHealth.snapshot(),
18479
- toolsets: toolsetRegistry.status()
19105
+ toolsets: toolsetRegistry.status(),
19106
+ // WP0: same counts `Daemon.status()` reports, from the same reader.
19107
+ agentHomeExecution: agentHomeExecutionStatus()
18480
19108
  };
18481
19109
  }
18482
19110
  async function performControlShutdown(reason) {
@@ -18711,9 +19339,8 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
18711
19339
  const minted = mintDeviceAssertion({
18712
19340
  record,
18713
19341
  // `toHttpBase` is the one place a configured serverUrl is normalized
18714
- // (ws:->http:, wss:->https:, path stripped), so an operator who
18715
- // configured the websocket spelling and one who configured the HTTP
18716
- // spelling of the same deployment produce the same issuer.
19342
+ // (path stripped), so the issuer ignores an operator-provided path
19343
+ // or query.
18717
19344
  issuer: new URL(toHttpBase(config.serverUrl)).origin,
18718
19345
  productId: config.productId,
18719
19346
  audience: parsed.audience,
@@ -18828,23 +19455,33 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
18828
19455
  await binding.lease.release();
18829
19456
  }
18830
19457
  }
19458
+ function agentHomeExecutionStatus() {
19459
+ const summary = agentHomeManager?.executionLeaseManager.activeAttemptSummary();
19460
+ return {
19461
+ maxConcurrentMutableSessionsPerAgentHome: agentHomeAttemptLimit,
19462
+ activeHomes: summary?.homes ?? 0,
19463
+ activeAttempts: summary?.attempts ?? 0
19464
+ };
19465
+ }
18831
19466
  function status() {
18832
19467
  return {
18833
19468
  localAgentRelease,
18834
19469
  paired: auth.deviceId !== void 0,
18835
19470
  connected: connectionState === "open",
18836
- degraded: connection?.isTransportDegraded() ?? false,
18837
19471
  revoked: connection?.isRevoked() ?? auth.isRevoked(),
18838
19472
  deviceId: auth.deviceId,
18839
19473
  activeTaskCount: runner?.activeTaskCount ?? 0,
18840
19474
  branding: config.branding,
18841
19475
  operationalHealth: operationalHealth.snapshot(),
18842
19476
  toolsets: toolsetRegistry.status(),
18843
- egress: agentEgress.status()
19477
+ egress: agentEgress.status(),
19478
+ agentHomeExecution: agentHomeExecutionStatus()
18844
19479
  };
18845
19480
  }
18846
19481
  function reloadMcpToolsets(mcpToolsets, expectedRevision) {
18847
- return toolsetRegistry.reload(mcpToolsets, expectedRevision);
19482
+ const receipt = toolsetRegistry.reload(mcpToolsets, expectedRevision);
19483
+ if (daemonStarted) connection?.refreshHello();
19484
+ return receipt;
18848
19485
  }
18849
19486
  function reportMcpToolsetObservation(toolsetId, expectedDefinitionRevision, observation) {
18850
19487
  toolsetRegistry.report(toolsetId, expectedDefinitionRevision, observation);
@@ -19253,7 +19890,7 @@ function createServiceLifecycle(def, opts = {}) {
19253
19890
 
19254
19891
  // src/bin/official-release.ts
19255
19892
  var OFFICIAL_LOCAL_AGENT_RELEASE = resolveLocalAgentReleaseIdentity({
19256
- version: "0.11.0"
19893
+ version: "0.13.0"
19257
19894
  });
19258
19895
 
19259
19896
  // src/bin/config.ts
@@ -21333,7 +21970,7 @@ async function recentAuditFacts(storeDir) {
21333
21970
  }
21334
21971
  function projectControl(control) {
21335
21972
  if (control.status === "offline") return { status: "offline" };
21336
- const transport = ["connecting", "open", "closed", "degraded", "revoked"].includes(control.transport) ? control.transport : "unavailable";
21973
+ const transport = ["connecting", "open", "closed", "revoked"].includes(control.transport) ? control.transport : "unavailable";
21337
21974
  const operationalHealth = control.operationalHealth.availability === "available" ? {
21338
21975
  availability: "available",
21339
21976
  state: control.operationalHealth.state,