@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
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import { randomUUID, createHash, randomBytes, createPrivateKey, generateKeyPairS
2
2
  import { promises, mkdirSync, constants, existsSync, linkSync, fstatSync, lstatSync, unlinkSync, renameSync, writeFileSync, chmodSync, statSync, readdirSync, readFileSync, realpathSync } from 'fs';
3
3
  import * as path3 from 'path';
4
4
  import path3__default, { join, isAbsolute } from 'path';
5
- import { AGENT_CONTENT_ARTIFACT_READ_CAPABILITY, AGENT_CONTENT_TRANSCRIPT_READ_CAPABILITY, AGENT_CONTENT_WORKSPACE_READ_CAPABILITY, AgentRefSchema, AgentHomeProjectionPayloadSchema, byokBlobUrlPath, BYOK_BLOBS_PATH, byokBlobFinalizePath, partitionAgentEvents, BYOK_SKILL_PACKS_PATH, byokSkillPackFilePath, BYOK_RECORDS_PATH, byokRecordPath, TASK_TRANSITIONS, AgentEgressPolicySchema, BYOK_PAIR_PATH, PairResponseSchema, BYOK_CHALLENGE_PATH, BYOK_TOKEN_PATH, CONFIGURED_TOOLSETS_MAX_ITEMS, ToolsetIdSchema, AgentContentReceiptPayloadSchema, encodeEnvelope, PROTOCOL_VERSION, createEnvelope, AGENT_EGRESS_RELIABLE_ACK_CAPABILITY, 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';
5
+ import { AGENT_CONTENT_ARTIFACT_READ_CAPABILITY, AGENT_CONTENT_TRANSCRIPT_READ_CAPABILITY, AGENT_CONTENT_WORKSPACE_READ_CAPABILITY, AgentRefSchema, AgentHomeProjectionPayloadSchema, byokBlobUrlPath, BYOK_BLOBS_PATH, byokBlobFinalizePath, partitionAgentEvents, BYOK_SKILL_PACKS_PATH, byokSkillPackFilePath, BYOK_RECORDS_PATH, byokRecordPath, TASK_TRANSITIONS, AgentEgressPolicySchema, BYOK_PAIR_PATH, PairResponseSchema, BYOK_CHALLENGE_PATH, BYOK_TOKEN_PATH, CONFIGURED_TOOLSETS_MAX_ITEMS, ToolsetIdSchema, AgentContentReceiptPayloadSchema, encodeEnvelope, PROTOCOL_VERSION, createEnvelope, AGENT_EGRESS_RELIABLE_ACK_CAPABILITY, 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';
6
6
  import net2, { createServer, createConnection } from 'net';
7
7
  import * as os4 from 'os';
8
8
  import os4__default from 'os';
@@ -12,7 +12,6 @@ import { createInterface } from 'readline';
12
12
  import { parseDeviceAssertionEnvelope, SKILL_PACK_MAX_BYTES, hasCapability, parseSkillPackManifest, checkSkillPackManifest, skillPackContentHashInput, checkSkillPackFileContent, SKILL_PACK_ENTRY_PATH, checkSkillPackEntry, isSkillPackPathSafe, DEVICE_PROOF_HEADER, contentHash as contentHash$1, TRUTH_RECORD_KINDS, DEVICE_ASSERTION_AUDIENCE_MAX_BYTES, DEVICE_ASSERTION_DEFAULT_TTL_MS, DEVICE_ASSERTION_MAX_TTL_MS, CONTENT_HASH_PATTERN, isTenantId, nonceSigningBytes, CapabilityDeclarationSchema, DeviceAssertionClaimsSchema, deviceAssertionSigningInput, DEVICE_ASSERTION_SCHEMA_ID } from '@byok-sdk/core';
13
13
  import { promisify } from 'util';
14
14
  import * as fs14 from 'fs/promises';
15
- import { WebSocket } from 'ws';
16
15
  import { createRequire } from 'module';
17
16
 
18
17
  // src/agent-home.ts
@@ -423,6 +422,23 @@ async function resolveExistingAncestor(inputPath) {
423
422
  }
424
423
  }
425
424
  }
425
+ async function canonicalPath(inputPath) {
426
+ const { canonical: canonical2, tail } = await resolveExistingAncestor(inputPath);
427
+ return path3__default.resolve(canonical2, ...tail);
428
+ }
429
+ async function assertRealDirectoryIfPresent(target) {
430
+ let stat;
431
+ try {
432
+ stat = await promises.lstat(target);
433
+ } catch (error) {
434
+ const code = error.code;
435
+ if (code === "ENOENT" || code === "ENOTDIR") return;
436
+ throw error;
437
+ }
438
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
439
+ throw new AgentHomeResolutionError(`Agent home path component is not a real directory: ${target}`);
440
+ }
441
+ }
426
442
  async function materializeDirectory(inputPath) {
427
443
  const { canonical: canonical2, tail } = await resolveExistingAncestor(inputPath);
428
444
  let cursor = canonical2;
@@ -517,6 +533,39 @@ var AgentHomeLayout = class {
517
533
  await gate.release();
518
534
  }
519
535
  }
536
+ /**
537
+ * Pure canonical-home derivation for read-only callers, such as the
538
+ * pre-admission single-writer count. It validates the AgentRef and joins
539
+ * exactly the same `<hostStorageRoot>/agents/<agentId>` segments
540
+ * {@link AgentHomeLayout.resolve} would, canonicalizing only the components
541
+ * that already exist.
542
+ *
543
+ * It deliberately creates no directory, takes no cross-process mutation
544
+ * gate and records no Agent binding, so an offer the host vetoes after the
545
+ * count leaves nothing behind on disk. `resolve()` stays the only path that
546
+ * may materialize a home or bind it to an Agent identity.
547
+ *
548
+ * An `agents` root or `agents/<agentId>` leaf that already exists but is a
549
+ * symlink (or any non-directory) is rejected here with the same error class
550
+ * and message `resolve()` raises for it, so an in-root `two -> one` link
551
+ * fails closed instead of silently keying the count of `one`. A leaf that
552
+ * does not exist yet is not an error: this derivation runs before the home
553
+ * is materialized. A home canonicalizing outside the `agents` root stays
554
+ * rejected as before.
555
+ */
556
+ async canonicalHomePath(agentRefInput) {
557
+ const agentRef = validateAgentRef(agentRefInput);
558
+ const hostStorageRoot = this.canonicalRoot ?? await canonicalPath(this.hostStorageRootInput);
559
+ const agentsRoot = path3__default.join(hostStorageRoot, AGENT_HOME_DIRECTORY);
560
+ await assertRealDirectoryIfPresent(agentsRoot);
561
+ const lexicalHome = path3__default.join(agentsRoot, agentRef.agentId);
562
+ await assertRealDirectoryIfPresent(lexicalHome);
563
+ const canonicalHome = await canonicalPath(lexicalHome);
564
+ if (canonicalHome === agentsRoot || !isWithin(agentsRoot, canonicalHome)) {
565
+ throw new AgentHomeResolutionError(`Agent home resolves outside the Agent home root: ${canonicalHome}`);
566
+ }
567
+ return canonicalHome;
568
+ }
520
569
  /**
521
570
  * Prove the canonical root is materializable and writable before the daemon
522
571
  * advertises Agent-home capability. No Agent identity or persistent Agent
@@ -746,6 +795,158 @@ var AgentHomeLeaseManager = class _AgentHomeLeaseManager {
746
795
  }
747
796
  }
748
797
  };
798
+ function executionKey(input) {
799
+ const value = input.sessionRef === void 0 ? input.taskId : input.sessionRef;
800
+ const label = input.sessionRef === void 0 ? "taskId" : "sessionRef";
801
+ if (typeof value !== "string" || value.length === 0 || /[\u0000\r\n]/u.test(value)) {
802
+ throw new AgentHomeResolutionError(`Agent execution ${label} must be a non-empty single-line string`);
803
+ }
804
+ return `${input.sessionRef === void 0 ? "task" : "session"}\0${value}`;
805
+ }
806
+ var AgentHomeExecutionLeaseManager = class _AgentHomeExecutionLeaseManager {
807
+ constructor(manager) {
808
+ this.manager = manager;
809
+ }
810
+ manager;
811
+ static groups = /* @__PURE__ */ new Map();
812
+ static queues = /* @__PURE__ */ new Map();
813
+ async acquire(resolution, input) {
814
+ const initialKey = executionKey(input);
815
+ return this.exclusive(resolution.canonicalHome, async () => {
816
+ let group = _AgentHomeExecutionLeaseManager.groups.get(resolution.canonicalHome);
817
+ if (group === void 0) {
818
+ const baseLease = await this.manager.acquire(resolution);
819
+ group = {
820
+ manager: this.manager,
821
+ baseLease,
822
+ agentId: resolution.agentRef.agentId,
823
+ leasesByKey: /* @__PURE__ */ new Map()
824
+ };
825
+ _AgentHomeExecutionLeaseManager.groups.set(resolution.canonicalHome, group);
826
+ } else if (group.manager !== this.manager || group.agentId !== resolution.agentRef.agentId) {
827
+ throw new AgentHomeBusyError(`Agent home ${resolution.canonicalHome} is active under another execution owner`);
828
+ }
829
+ if (group.leasesByKey.has(initialKey)) {
830
+ throw new AgentHomeBusyError(`Agent session already has an active execution lease in ${resolution.canonicalHome}`);
831
+ }
832
+ const leaseId = randomUUID();
833
+ group.leasesByKey.set(initialKey, leaseId);
834
+ let currentKey = initialKey;
835
+ let sessionBound = input.sessionRef !== void 0;
836
+ let released = false;
837
+ return Object.freeze({
838
+ leaseId,
839
+ agentRef: resolution.agentRef,
840
+ canonicalHome: resolution.canonicalHome,
841
+ cwd: resolution.canonicalHome,
842
+ homeIdentity: group.baseLease.homeIdentity,
843
+ bindSession: async (sessionRef) => {
844
+ const nextKey = executionKey({ taskId: input.taskId, sessionRef });
845
+ await this.exclusive(resolution.canonicalHome, async () => {
846
+ if (released) throw new AgentHomeBusyError(`Agent execution lease ${leaseId} is already released`);
847
+ const currentGroup = _AgentHomeExecutionLeaseManager.groups.get(resolution.canonicalHome);
848
+ if (currentGroup !== group || currentGroup.leasesByKey.get(currentKey) !== leaseId) {
849
+ throw new AgentHomeBusyError(`Agent execution lease ${leaseId} is no longer owned by this process`);
850
+ }
851
+ if (nextKey === currentKey) return;
852
+ if (sessionBound) {
853
+ throw new AgentHomeBusyError(
854
+ `Agent session execution lease ${leaseId} cannot rebind to a different runtime session`
855
+ );
856
+ }
857
+ if (currentGroup.leasesByKey.has(nextKey)) {
858
+ throw new AgentHomeBusyError(`Agent session already has an active execution lease in ${resolution.canonicalHome}`);
859
+ }
860
+ currentGroup.leasesByKey.set(nextKey, leaseId);
861
+ currentGroup.leasesByKey.delete(currentKey);
862
+ currentKey = nextKey;
863
+ sessionBound = true;
864
+ });
865
+ },
866
+ release: async () => {
867
+ await this.exclusive(resolution.canonicalHome, async () => {
868
+ if (released) return;
869
+ const currentGroup = _AgentHomeExecutionLeaseManager.groups.get(resolution.canonicalHome);
870
+ if (currentGroup !== group || currentGroup.leasesByKey.get(currentKey) !== leaseId) {
871
+ released = true;
872
+ throw new AgentHomeBusyError(`Agent execution lease ${leaseId} is no longer owned by this process`);
873
+ }
874
+ currentGroup.leasesByKey.delete(currentKey);
875
+ released = true;
876
+ if (currentGroup.leasesByKey.size === 0) {
877
+ _AgentHomeExecutionLeaseManager.groups.delete(resolution.canonicalHome);
878
+ await currentGroup.baseLease.release();
879
+ }
880
+ });
881
+ }
882
+ });
883
+ });
884
+ }
885
+ /**
886
+ * WP0: Attempts currently holding an execution lease on this exact
887
+ * canonical home, across every lane and every session. This is the number
888
+ * the daemon's admission gate reads before any side effect — see
889
+ * `TaskRunner.handleOffer`'s per-home busy gate.
890
+ *
891
+ * Derived from the one lease registry above rather than a second tally, so
892
+ * it inherits the lease lifecycle exactly: an entry appears at `acquire()`,
893
+ * survives `bindSession()` (which rekeys in place), and disappears only at
894
+ * `release()`, which the task runner calls after the attempt is terminal
895
+ * AND `Session.close()` resolved. A failed disposal never reaches
896
+ * `release()`, so the slot stays held — fail closed, the same posture as
897
+ * `runtime-disposal-failed`. Crash residue needs nothing extra here: a
898
+ * restarted daemon starts with an empty registry and reclaims the on-disk
899
+ * marker only under the same stable owner identity (`openLeaseMarker`).
900
+ *
901
+ * Counted regardless of which lease manager owns the group: the invariant
902
+ * being protected is the filesystem path (`MEMORY.md`, `notes/`, `.git`),
903
+ * not the owner identity.
904
+ */
905
+ activeAttemptCount(canonicalHome) {
906
+ return _AgentHomeExecutionLeaseManager.groups.get(canonicalHome)?.leasesByKey.size ?? 0;
907
+ }
908
+ /**
909
+ * Counts-only readback for daemon/control status. Scoped to this manager's
910
+ * own leases, so the number describes this daemon rather than every home
911
+ * any manager in the process happens to hold. Never exposes a home path.
912
+ */
913
+ activeAttemptSummary() {
914
+ let homes = 0;
915
+ let attempts = 0;
916
+ for (const group of _AgentHomeExecutionLeaseManager.groups.values()) {
917
+ if (group.manager !== this.manager) continue;
918
+ homes += 1;
919
+ attempts += group.leasesByKey.size;
920
+ }
921
+ return { homes, attempts };
922
+ }
923
+ async mutate(binding, operation) {
924
+ return this.exclusive(binding.resolution.canonicalHome, async () => {
925
+ const group = _AgentHomeExecutionLeaseManager.groups.get(binding.resolution.canonicalHome);
926
+ if (group === void 0 || group.manager !== this.manager || ![...group.leasesByKey.values()].includes(binding.lease.leaseId)) {
927
+ throw new AgentHomeBusyError("Agent execution lease does not own this home mutation");
928
+ }
929
+ return operation();
930
+ });
931
+ }
932
+ async exclusive(canonicalHome, operation) {
933
+ const prior = _AgentHomeExecutionLeaseManager.queues.get(canonicalHome) ?? Promise.resolve();
934
+ let release;
935
+ const tail = new Promise((resolve) => {
936
+ release = resolve;
937
+ });
938
+ _AgentHomeExecutionLeaseManager.queues.set(canonicalHome, tail);
939
+ await prior;
940
+ try {
941
+ return await operation();
942
+ } finally {
943
+ release();
944
+ if (_AgentHomeExecutionLeaseManager.queues.get(canonicalHome) === tail) {
945
+ _AgentHomeExecutionLeaseManager.queues.delete(canonicalHome);
946
+ }
947
+ }
948
+ }
949
+ };
749
950
  async function initializeAgentHome(resolution) {
750
951
  await ensureDirectoryNoSymlink(
751
952
  resolution.canonicalHome,
@@ -821,10 +1022,12 @@ var AgentHomeManager = class {
821
1022
  layout;
822
1023
  projection;
823
1024
  leaseManager;
1025
+ executionLeaseManager;
824
1026
  constructor(options) {
825
1027
  this.layout = new AgentHomeLayout(options.hostStorageRoot);
826
1028
  this.projection = options.projection;
827
1029
  this.leaseManager = options.leaseManager ?? new AgentHomeLeaseManager();
1030
+ this.executionLeaseManager = new AgentHomeExecutionLeaseManager(this.leaseManager);
828
1031
  }
829
1032
  async prepare(agentRef) {
830
1033
  const binding = await this.acquire(agentRef);
@@ -851,12 +1054,25 @@ var AgentHomeManager = class {
851
1054
  const lease = await this.leaseManager.acquire(resolution);
852
1055
  return Object.freeze({ resolution, lease });
853
1056
  }
1057
+ async acquireExecution(agentRef, input) {
1058
+ const resolution = await this.layout.resolve(agentRef);
1059
+ const lease = await this.executionLeaseManager.acquire(resolution, input);
1060
+ return Object.freeze({ resolution, lease });
1061
+ }
854
1062
  /** Initialize only after any requested session exact-match has succeeded. */
855
1063
  async initialize(binding) {
856
- const { resolution, lease } = binding;
1064
+ await this.initializeResolved(binding.resolution, binding.lease.cwd);
1065
+ }
1066
+ async initializeExecution(binding) {
1067
+ await this.mutateExecution(binding, () => this.initializeResolved(binding.resolution, binding.lease.cwd));
1068
+ }
1069
+ async mutateExecution(binding, operation) {
1070
+ return this.executionLeaseManager.mutate(binding, operation);
1071
+ }
1072
+ async initializeResolved(resolution, cwd) {
857
1073
  await initializeAgentHome(resolution);
858
1074
  const prepare = this.projection?.prepare;
859
- if (prepare !== void 0) await prepare({ ...resolution, cwd: lease.cwd });
1075
+ if (prepare !== void 0) await prepare({ ...resolution, cwd });
860
1076
  if (await promises.realpath(resolution.homeDir) !== resolution.canonicalHome) {
861
1077
  throw new AgentHomeResolutionError("Agent projection changed the canonical home path");
862
1078
  }
@@ -1907,13 +2123,22 @@ function freezeRuntimeAdapterDescriptor(descriptor) {
1907
2123
  });
1908
2124
  }
1909
2125
  function sealRuntimeOperationManifest(manifest) {
2126
+ const dispatchSelection = manifest.dispatchSelection === void 0 ? void 0 : manifest.dispatchSelection.lane === "byok-profile" ? Object.freeze({
2127
+ ...manifest.dispatchSelection,
2128
+ providerProfile: Object.freeze({
2129
+ ...manifest.dispatchSelection.providerProfile,
2130
+ requiredCapabilities: Object.freeze([
2131
+ ...manifest.dispatchSelection.providerProfile.requiredCapabilities
2132
+ ])
2133
+ })
2134
+ }) : Object.freeze({ ...manifest.dispatchSelection });
1910
2135
  return Object.freeze({
1911
2136
  taskId: manifest.taskId,
1912
2137
  runtimeId: manifest.runtimeId,
1913
2138
  descriptor: freezeRuntimeAdapterDescriptor(manifest.descriptor),
1914
2139
  policy: frozenPolicy(manifest.policy),
1915
2140
  requiredToolsetIds: Object.freeze([...manifest.requiredToolsetIds]),
1916
- ...manifest.dispatchSelection === void 0 ? {} : { dispatchSelection: Object.freeze({ ...manifest.dispatchSelection }) },
2141
+ ...dispatchSelection === void 0 ? {} : { dispatchSelection },
1917
2142
  ...manifest.sessionRef === void 0 ? {} : { sessionRef: manifest.sessionRef },
1918
2143
  ...manifest.agentRef === void 0 ? {} : { agentRef: Object.freeze({ agentId: manifest.agentRef.agentId, profileRevision: manifest.agentRef.profileRevision }) },
1919
2144
  cwd: manifest.cwd ?? manifest.workspace.workspaceDir,
@@ -4335,7 +4560,11 @@ function validatePiByokLauncherConfig(launcher) {
4335
4560
  "--macos-keychain-path",
4336
4561
  "--secret-service-prefix",
4337
4562
  "--provider",
4338
- "--model"
4563
+ "--model",
4564
+ "--profile-revision",
4565
+ "--profile-hash",
4566
+ "--required-capabilities",
4567
+ "--validate-only"
4339
4568
  ]);
4340
4569
  const conflicting = launcher.args?.find((arg) => reserved.has(arg));
4341
4570
  if (conflicting !== void 0) {
@@ -4397,11 +4626,17 @@ var PiAdapter = class {
4397
4626
  const bin = this.resolveBin();
4398
4627
  const extensions = (this.options.resolveExtensions ?? resolvePiExtensions)();
4399
4628
  const selection = input.offer.dispatchSelection;
4400
- const pinnedSelection = selection === void 0 ? void 0 : Object.freeze({ ...selection });
4629
+ const pinnedSelection = selection === void 0 ? void 0 : selection.lane === "byok-profile" ? Object.freeze({
4630
+ ...selection,
4631
+ providerProfile: Object.freeze({
4632
+ ...selection.providerProfile,
4633
+ requiredCapabilities: Object.freeze([...selection.providerProfile.requiredCapabilities])
4634
+ })
4635
+ }) : Object.freeze({ ...selection });
4401
4636
  let command = bin.command;
4402
4637
  let launcherArgs;
4403
4638
  if (pinnedSelection !== void 0) {
4404
- if (pinnedSelection.lane !== "byok" || pinnedSelection.runtimeId !== "pi") {
4639
+ if (pinnedSelection.lane !== "byok" && pinnedSelection.lane !== "byok-profile" || pinnedSelection.runtimeId !== "pi") {
4405
4640
  return { kind: "reject", reason: `pi adapter cannot execute ${pinnedSelection.lane} selection for runtime ${pinnedSelection.runtimeId}`, retryable: false };
4406
4641
  }
4407
4642
  const launcher = this.options.byokLauncher;
@@ -4409,6 +4644,21 @@ var PiAdapter = class {
4409
4644
  return { kind: "reject", reason: "pi BYOK selection requires a configured credential-custody launcher", retryable: false };
4410
4645
  }
4411
4646
  command = launcher.command;
4647
+ const providerProfile = pinnedSelection.lane === "byok-profile" ? pinnedSelection.providerProfile : void 0;
4648
+ if (providerProfile !== void 0) {
4649
+ try {
4650
+ await (this.options.validateProviderProfileBinding ?? validateProviderProfileBindingWithLauncher)(
4651
+ providerProfile,
4652
+ launcher
4653
+ );
4654
+ } catch (error) {
4655
+ return {
4656
+ kind: "reject",
4657
+ reason: `provider profile admission failed: ${errorMessage3(error)}`,
4658
+ retryable: false
4659
+ };
4660
+ }
4661
+ }
4412
4662
  launcherArgs = [
4413
4663
  ...launcher.args ?? [],
4414
4664
  "--pi-bin",
@@ -4420,9 +4670,19 @@ var PiAdapter = class {
4420
4670
  ...launcher.macosKeychainPath !== void 0 ? ["--macos-keychain-path", launcher.macosKeychainPath] : [],
4421
4671
  ...launcher.secretServicePrefix ? ["--secret-service-prefix", launcher.secretServicePrefix] : [],
4422
4672
  "--provider",
4423
- pinnedSelection.providerId,
4673
+ providerProfile?.profileRef ?? (pinnedSelection.lane === "byok" ? pinnedSelection.providerId : ""),
4424
4674
  "--model",
4425
- pinnedSelection.modelId
4675
+ providerProfile?.modelId ?? (pinnedSelection.lane === "byok" ? pinnedSelection.modelId : ""),
4676
+ ...providerProfile === void 0 ? [] : [
4677
+ "--profile-revision",
4678
+ providerProfile.profileRevision,
4679
+ "--profile-hash",
4680
+ providerProfile.profileHash,
4681
+ "--required-capabilities",
4682
+ JSON.stringify(providerProfile.requiredCapabilities),
4683
+ "--validate-only",
4684
+ "false"
4685
+ ]
4426
4686
  ];
4427
4687
  }
4428
4688
  return {
@@ -4565,8 +4825,42 @@ var PiAdapter = class {
4565
4825
  return (this.options.resolveBin ?? resolvePiBin)();
4566
4826
  }
4567
4827
  };
4828
+ async function validateProviderProfileBindingWithLauncher(binding, launcher) {
4829
+ await execFileAsync(launcher.command, [
4830
+ ...launcher.args ?? [],
4831
+ "--pi-bin",
4832
+ process.execPath,
4833
+ "--profile-db",
4834
+ launcher.profileDbPath,
4835
+ "--session-dir",
4836
+ launcher.sessionDir,
4837
+ "--provider",
4838
+ binding.profileRef,
4839
+ "--model",
4840
+ binding.modelId,
4841
+ "--profile-revision",
4842
+ binding.profileRevision,
4843
+ "--profile-hash",
4844
+ binding.profileHash,
4845
+ "--required-capabilities",
4846
+ JSON.stringify(binding.requiredCapabilities),
4847
+ "--validate-only",
4848
+ "true",
4849
+ ...launcher.macosKeychainPath !== void 0 ? ["--macos-keychain-path", launcher.macosKeychainPath] : [],
4850
+ ...launcher.secretServicePrefix ? ["--secret-service-prefix", launcher.secretServicePrefix] : []
4851
+ ], { timeout: DETECT_TIMEOUT_MS });
4852
+ }
4568
4853
  function sameDispatchSelection(left, right) {
4569
4854
  if (left === void 0 || right === void 0) return left === right;
4855
+ if (left.lane === "byok-profile" || right.lane === "byok-profile") {
4856
+ if (left.lane !== "byok-profile" || right.lane !== "byok-profile") return false;
4857
+ if (left.runtimeId !== right.runtimeId) return false;
4858
+ const leftProfile = left.providerProfile;
4859
+ const rightProfile = right.providerProfile;
4860
+ 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(
4861
+ (capability, index) => capability === rightProfile.requiredCapabilities[index]
4862
+ );
4863
+ }
4570
4864
  return left.lane === right.lane && left.runtimeId === right.runtimeId && left.providerId === right.providerId && left.modelId === right.modelId;
4571
4865
  }
4572
4866
  async function resolveAuthoritativeSessionId(rpc) {
@@ -6983,9 +7277,18 @@ function assertRecord(value) {
6983
7277
  throw new DeviceCredentialStoreError("OS credential entry has an incomplete device credential");
6984
7278
  }
6985
7279
  }
6986
- function encode(record3) {
6987
- assertRecord(record3);
6988
- const encoded = Buffer.from(JSON.stringify(record3), "utf8").toString("base64");
7280
+ function isFirstPairingAttempt(value) {
7281
+ return "kind" in value && value.kind === "first-pairing-attempt-v1";
7282
+ }
7283
+ function assertFirstPairingAttempt(value) {
7284
+ 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) {
7285
+ throw new DeviceCredentialStoreError("OS credential entry has an invalid first-pairing attempt shape");
7286
+ }
7287
+ }
7288
+ function encode(authority) {
7289
+ if (isFirstPairingAttempt(authority)) assertFirstPairingAttempt(authority);
7290
+ else assertRecord(authority);
7291
+ const encoded = Buffer.from(JSON.stringify(authority), "utf8").toString("base64");
6989
7292
  const value = `${ENCODED_PREFIX}${encoded}`;
6990
7293
  if (Buffer.byteLength(value, "utf8") > 2400) {
6991
7294
  throw new DeviceCredentialStoreError("device credential exceeds the OS credential entry bound");
@@ -7014,6 +7317,16 @@ function decode(value) {
7014
7317
  } catch {
7015
7318
  throw new DeviceCredentialStoreError("OS credential entry is not valid JSON");
7016
7319
  }
7320
+ if (typeof parsed === "object" && parsed !== null && parsed.kind === "first-pairing-attempt-v1") {
7321
+ assertFirstPairingAttempt(parsed);
7322
+ return Object.freeze({
7323
+ kind: parsed.kind,
7324
+ deviceName: parsed.deviceName,
7325
+ devicePublicKey: parsed.devicePublicKey,
7326
+ devicePrivateKeyPem: parsed.devicePrivateKeyPem,
7327
+ ...parsed.machineId === void 0 ? {} : { machineId: parsed.machineId }
7328
+ });
7329
+ }
7017
7330
  assertRecord(parsed);
7018
7331
  return Object.freeze({
7019
7332
  deviceId: parsed.deviceId,
@@ -7044,6 +7357,18 @@ var DeviceCredentialStore = class {
7044
7357
  this.#run = options.commandRunner ?? runDeviceCommand;
7045
7358
  }
7046
7359
  async read() {
7360
+ const authority = await this.#readAuthority();
7361
+ return authority === void 0 || isFirstPairingAttempt(authority) ? void 0 : authority;
7362
+ }
7363
+ async readFirstPairingAttempt() {
7364
+ const authority = await this.#readAuthority();
7365
+ return authority !== void 0 && isFirstPairingAttempt(authority) ? authority : void 0;
7366
+ }
7367
+ async saveFirstPairingAttempt(attempt) {
7368
+ assertFirstPairingAttempt(attempt);
7369
+ await this.#replaceAuthority(attempt);
7370
+ }
7371
+ async #readAuthority() {
7047
7372
  const result = await this.#invoke("read");
7048
7373
  if (result.exitCode === NOT_FOUND || this.#platform === "linux" && result.exitCode === 1 && result.stderr.trim().length === 0) return void 0;
7049
7374
  if (result.exitCode === 127) throw new DeviceCredentialStoreUnavailableError();
@@ -7056,6 +7381,12 @@ var DeviceCredentialStore = class {
7056
7381
  }
7057
7382
  async replace(record3) {
7058
7383
  const encoded = encode(record3);
7384
+ await this.#replaceEncoded(encoded);
7385
+ }
7386
+ async #replaceAuthority(authority) {
7387
+ await this.#replaceEncoded(encode(authority));
7388
+ }
7389
+ async #replaceEncoded(encoded) {
7059
7390
  const result = await this.#invoke("replace", encoded);
7060
7391
  if (result.exitCode === 127) throw new DeviceCredentialStoreUnavailableError();
7061
7392
  if (result.exitCode !== 0) {
@@ -7066,7 +7397,7 @@ var DeviceCredentialStore = class {
7066
7397
  }
7067
7398
  /** Returns true only after the sole secret authority is confirmed absent. */
7068
7399
  async clear() {
7069
- const before = await this.read();
7400
+ const before = await this.#readAuthority();
7070
7401
  if (before === void 0) return false;
7071
7402
  const result = await this.#invoke("clear");
7072
7403
  if (result.exitCode === 127) throw new DeviceCredentialStoreUnavailableError();
@@ -7075,7 +7406,7 @@ var DeviceCredentialStore = class {
7075
7406
  `operating-system credential provider could not clear device credentials${providerDiagnostic(result.stderr)}`
7076
7407
  );
7077
7408
  }
7078
- if (await this.read() !== void 0) {
7409
+ if (await this.#readAuthority() !== void 0) {
7079
7410
  throw new DeviceCredentialStoreError("operating-system credential provider reported deletion but device credentials remain");
7080
7411
  }
7081
7412
  return true;
@@ -7150,17 +7481,24 @@ var DeviceCredentialStore = class {
7150
7481
  }
7151
7482
  };
7152
7483
  var InMemoryDeviceCredentialStore = class {
7153
- #record;
7484
+ #authority;
7154
7485
  async read() {
7155
- return this.#record === void 0 ? void 0 : Object.freeze({ ...this.#record });
7486
+ return this.#authority === void 0 || isFirstPairingAttempt(this.#authority) ? void 0 : Object.freeze({ ...this.#authority });
7487
+ }
7488
+ async readFirstPairingAttempt() {
7489
+ return this.#authority !== void 0 && isFirstPairingAttempt(this.#authority) ? Object.freeze({ ...this.#authority }) : void 0;
7490
+ }
7491
+ async saveFirstPairingAttempt(attempt) {
7492
+ assertFirstPairingAttempt(attempt);
7493
+ this.#authority = Object.freeze({ ...attempt });
7156
7494
  }
7157
7495
  async replace(record3) {
7158
7496
  assertRecord(record3);
7159
- this.#record = Object.freeze({ ...record3 });
7497
+ this.#authority = Object.freeze({ ...record3 });
7160
7498
  }
7161
7499
  async clear() {
7162
- const had = this.#record !== void 0;
7163
- this.#record = void 0;
7500
+ const had = this.#authority !== void 0;
7501
+ this.#authority = void 0;
7164
7502
  return had;
7165
7503
  }
7166
7504
  };
@@ -7430,23 +7768,22 @@ function signNonce(privateKey, nonce) {
7430
7768
  const signature = sign(null, nonceSigningBytes(nonce), privateKey);
7431
7769
  return signature.toString("base64url");
7432
7770
  }
7771
+
7772
+ // src/daemon/url.ts
7433
7773
  function toHttpBase(serverUrl) {
7434
7774
  const url = new URL(serverUrl);
7435
- if (url.protocol === "ws:") url.protocol = "http:";
7436
- else if (url.protocol === "wss:") url.protocol = "https:";
7437
7775
  url.pathname = "/";
7438
7776
  url.search = "";
7439
7777
  return url.toString();
7440
7778
  }
7441
- function toWsUrl(serverUrl) {
7442
- const url = new URL(BYOK_WS_PATH, toHttpBase(serverUrl));
7443
- url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
7444
- return url.toString();
7445
- }
7446
7779
  function describeEndpoint(transport, url) {
7447
7780
  const parsed = typeof url === "string" ? new URL(url) : url;
7448
7781
  return { transport, host: parsed.host, path: parsed.pathname };
7449
7782
  }
7783
+ function formatServerUrl(value) {
7784
+ const url = typeof value === "string" ? new URL(value) : value;
7785
+ return `${url.protocol}//${url.host}${url.pathname}`;
7786
+ }
7450
7787
  var InsecureServerUrlError = class extends Error {
7451
7788
  constructor(message) {
7452
7789
  super(message);
@@ -7465,22 +7802,21 @@ function assertServerUrlAllowed(rawUrl, opts = {}) {
7465
7802
  let url;
7466
7803
  try {
7467
7804
  url = new URL(rawUrl);
7468
- } catch (err) {
7469
- throw new InsecureServerUrlError(`invalid server URL "${rawUrl}": ${err instanceof Error ? err.message : String(err)}`);
7805
+ } catch {
7806
+ throw new InsecureServerUrlError("invalid server URL");
7470
7807
  }
7808
+ const endpoint = formatServerUrl(url);
7471
7809
  switch (url.protocol) {
7472
7810
  case "https:":
7473
- case "wss:":
7474
7811
  return;
7475
7812
  case "http:":
7476
- case "ws:":
7477
7813
  if (opts.dangerouslyAllowInsecureRemote || isLoopbackHostname(url.hostname)) return;
7478
7814
  throw new InsecureServerUrlError(
7479
- `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.`
7815
+ `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.`
7480
7816
  );
7481
7817
  default:
7482
7818
  throw new InsecureServerUrlError(
7483
- `refusing to connect to "${rawUrl}" \u2014 unsupported scheme "${url.protocol}" (expected http:, https:, ws:, or wss:).`
7819
+ `refusing to connect to "${endpoint}" \u2014 unsupported scheme "${url.protocol}" (expected http: or https:).`
7484
7820
  );
7485
7821
  }
7486
7822
  }
@@ -7492,12 +7828,22 @@ var DeviceRevokedError = class extends Error {
7492
7828
  this.name = "DeviceRevokedError";
7493
7829
  }
7494
7830
  };
7831
+ var AuthRequestAbortedError = class extends Error {
7832
+ constructor(reason) {
7833
+ super(reason === "deadline" ? "authentication request exceeded its deadline" : "authentication request was cancelled during shutdown");
7834
+ this.reason = reason;
7835
+ this.name = "AuthRequestAbortedError";
7836
+ }
7837
+ reason;
7838
+ };
7495
7839
  var ASSUMED_PAIR_TOKEN_TTL_MS = 45 * 60 * 1e3;
7496
7840
  var RENEW_MARGIN_MS = 60 * 1e3;
7841
+ var DEFAULT_AUTH_REQUEST_DEADLINE_MS = 15e3;
7497
7842
  var AuthManager = class {
7498
7843
  constructor(opts) {
7499
7844
  this.opts = opts;
7500
7845
  this.credentials = opts.credentials ?? opts.store.credentials;
7846
+ this.requestDeadlineMs = resolveRequestDeadlineMs(opts.authRequestDeadlineMs);
7501
7847
  }
7502
7848
  opts;
7503
7849
  record;
@@ -7507,7 +7853,10 @@ var AuthManager = class {
7507
7853
  stopped = false;
7508
7854
  pairing = false;
7509
7855
  credentialMutationTail = Promise.resolve();
7856
+ /** The sole cancellation authority for the request currently inside the serialized credential mutation. */
7857
+ activeRequest;
7510
7858
  credentials;
7859
+ requestDeadlineMs;
7511
7860
  get deviceId() {
7512
7861
  return this.record?.deviceId;
7513
7862
  }
@@ -7543,23 +7892,51 @@ var AuthManager = class {
7543
7892
  if (!(error instanceof DeviceRecordRePairRequiredError)) throw error;
7544
7893
  }
7545
7894
  }
7546
- const keyPair = existing ? { privateKey: importPrivateKeyPem(existing.devicePrivateKeyPem), publicKeyBase64Url: existing.devicePublicKey } : generateDeviceKeyPair();
7895
+ const observedDeviceName = this.opts.deviceName ?? os4__default.hostname();
7896
+ const observedMachineId = await this.opts.machineId?.();
7897
+ let keyPair;
7898
+ let pairingDeviceName = observedDeviceName;
7899
+ let pairingMachineId = observedMachineId;
7900
+ if (existing) {
7901
+ keyPair = { privateKey: importPrivateKeyPem(existing.devicePrivateKeyPem), publicKeyBase64Url: existing.devicePublicKey };
7902
+ } else {
7903
+ const firstAttempt = await this.credentials.readFirstPairingAttempt();
7904
+ if (firstAttempt) {
7905
+ keyPair = {
7906
+ privateKey: importPrivateKeyPem(firstAttempt.devicePrivateKeyPem),
7907
+ publicKeyBase64Url: firstAttempt.devicePublicKey
7908
+ };
7909
+ pairingDeviceName = firstAttempt.deviceName;
7910
+ pairingMachineId = firstAttempt.machineId;
7911
+ } else {
7912
+ keyPair = generateDeviceKeyPair();
7913
+ await this.credentials.saveFirstPairingAttempt({
7914
+ kind: "first-pairing-attempt-v1",
7915
+ deviceName: pairingDeviceName,
7916
+ devicePublicKey: keyPair.publicKeyBase64Url,
7917
+ devicePrivateKeyPem: exportPrivateKeyPem(keyPair.privateKey),
7918
+ ...pairingMachineId === void 0 ? {} : { machineId: pairingMachineId }
7919
+ });
7920
+ }
7921
+ }
7547
7922
  const url = new URL(BYOK_PAIR_PATH, toHttpBase(this.opts.serverUrl));
7548
- const machineId = await this.opts.machineId?.();
7549
- const res = await fetch(url, {
7550
- method: "POST",
7551
- headers: { "content-type": "application/json" },
7552
- body: JSON.stringify({
7553
- pairingCode,
7554
- deviceName: this.opts.deviceName ?? os4__default.hostname(),
7555
- devicePublicKey: keyPair.publicKeyBase64Url,
7556
- ...machineId === void 0 ? {} : { machineId }
7557
- })
7923
+ const body = await this.runRequest(async (signal) => {
7924
+ const res = await fetch(url, {
7925
+ method: "POST",
7926
+ headers: { "content-type": "application/json" },
7927
+ body: JSON.stringify({
7928
+ pairingCode,
7929
+ deviceName: pairingDeviceName,
7930
+ devicePublicKey: keyPair.publicKeyBase64Url,
7931
+ ...pairingMachineId === void 0 ? {} : { machineId: pairingMachineId }
7932
+ }),
7933
+ signal
7934
+ });
7935
+ if (!res.ok) {
7936
+ throw new Error(`pairing failed: HTTP ${res.status} ${await safeErrorText(res)}`.trimEnd());
7937
+ }
7938
+ return PairResponseSchema.parse(await res.json());
7558
7939
  });
7559
- if (!res.ok) {
7560
- throw new Error(`pairing failed: HTTP ${res.status} ${await safeErrorText(res)}`.trimEnd());
7561
- }
7562
- const body = PairResponseSchema.parse(await res.json());
7563
7940
  const metadata = {
7564
7941
  deviceId: body.deviceId,
7565
7942
  tenantId: body.tenantId,
@@ -7598,6 +7975,7 @@ var AuthManager = class {
7598
7975
  this.stopped = true;
7599
7976
  if (this.proactiveTimer) clearTimeout(this.proactiveTimer);
7600
7977
  this.proactiveTimer = void 0;
7978
+ this.activeRequest?.abort();
7601
7979
  await this.credentialMutationTail;
7602
7980
  }
7603
7981
  async renew() {
@@ -7614,29 +7992,35 @@ var AuthManager = class {
7614
7992
  const record3 = this.record;
7615
7993
  const base = toHttpBase(this.opts.serverUrl);
7616
7994
  const privateKey = importPrivateKeyPem(record3.devicePrivateKeyPem);
7617
- const challengeRes = await fetch(new URL(BYOK_CHALLENGE_PATH, base), {
7618
- method: "POST",
7619
- headers: { "content-type": "application/json" },
7620
- body: JSON.stringify({ deviceId: record3.deviceId })
7995
+ const { nonce } = await this.runRequest(async (signal) => {
7996
+ const challengeRes = await fetch(new URL(BYOK_CHALLENGE_PATH, base), {
7997
+ method: "POST",
7998
+ headers: { "content-type": "application/json" },
7999
+ body: JSON.stringify({ deviceId: record3.deviceId }),
8000
+ signal
8001
+ });
8002
+ if (challengeRes.status === 401) this.markRevoked();
8003
+ if (!challengeRes.ok) {
8004
+ throw new Error(
8005
+ `token renewal (challenge) failed: HTTP ${challengeRes.status} ${await safeErrorText(challengeRes)}`.trimEnd()
8006
+ );
8007
+ }
8008
+ return await challengeRes.json();
7621
8009
  });
7622
- if (challengeRes.status === 401) this.markRevoked();
7623
- if (!challengeRes.ok) {
7624
- throw new Error(
7625
- `token renewal (challenge) failed: HTTP ${challengeRes.status} ${await safeErrorText(challengeRes)}`.trimEnd()
7626
- );
7627
- }
7628
- const { nonce } = await challengeRes.json();
7629
8010
  const signature = signNonce(privateKey, nonce);
7630
- const tokenRes = await fetch(new URL(BYOK_TOKEN_PATH, base), {
7631
- method: "POST",
7632
- headers: { "content-type": "application/json" },
7633
- body: JSON.stringify({ deviceId: record3.deviceId, nonce, signature })
8011
+ const body = await this.runRequest(async (signal) => {
8012
+ const tokenRes = await fetch(new URL(BYOK_TOKEN_PATH, base), {
8013
+ method: "POST",
8014
+ headers: { "content-type": "application/json" },
8015
+ body: JSON.stringify({ deviceId: record3.deviceId, nonce, signature }),
8016
+ signal
8017
+ });
8018
+ if (tokenRes.status === 401) this.markRevoked();
8019
+ if (!tokenRes.ok) {
8020
+ throw new Error(`token renewal (token) failed: HTTP ${tokenRes.status} ${await safeErrorText(tokenRes)}`.trimEnd());
8021
+ }
8022
+ return await tokenRes.json();
7634
8023
  });
7635
- if (tokenRes.status === 401) this.markRevoked();
7636
- if (!tokenRes.ok) {
7637
- throw new Error(`token renewal (token) failed: HTTP ${tokenRes.status} ${await safeErrorText(tokenRes)}`.trimEnd());
7638
- }
7639
- const body = await tokenRes.json();
7640
8024
  const updated = {
7641
8025
  ...record3,
7642
8026
  accessToken: body.accessToken,
@@ -7666,6 +8050,37 @@ var AuthManager = class {
7666
8050
  timer.unref?.();
7667
8051
  this.proactiveTimer = timer;
7668
8052
  }
8053
+ /**
8054
+ * Bounds one complete auth exchange rather than fetch alone. Keeping the
8055
+ * controller active through `json()`/`text()` makes a non-cooperative or
8056
+ * partial response body cancellable by the same authority that owns fetch.
8057
+ */
8058
+ async runRequest(operation) {
8059
+ if (this.stopped) throw new AuthRequestAbortedError("stopped");
8060
+ const controller = new AbortController();
8061
+ this.activeRequest = controller;
8062
+ let deadlineElapsed = false;
8063
+ const deadline = setTimeout(() => {
8064
+ deadlineElapsed = true;
8065
+ controller.abort();
8066
+ }, this.requestDeadlineMs);
8067
+ let rejectOnAbort;
8068
+ const aborted = new Promise((_, reject) => {
8069
+ rejectOnAbort = reject;
8070
+ });
8071
+ const onAbort = () => rejectOnAbort(new AuthRequestAbortedError(deadlineElapsed ? "deadline" : "stopped"));
8072
+ controller.signal.addEventListener("abort", onAbort, { once: true });
8073
+ try {
8074
+ return await Promise.race([operation(controller.signal), aborted]);
8075
+ } catch (error) {
8076
+ if (controller.signal.aborted) throw new AuthRequestAbortedError(deadlineElapsed ? "deadline" : "stopped");
8077
+ throw error;
8078
+ } finally {
8079
+ clearTimeout(deadline);
8080
+ controller.signal.removeEventListener("abort", onAbort);
8081
+ if (this.activeRequest === controller) this.activeRequest = void 0;
8082
+ }
8083
+ }
7669
8084
  async runCredentialMutation(operation) {
7670
8085
  const predecessor = this.credentialMutationTail;
7671
8086
  let release;
@@ -7708,6 +8123,13 @@ function resolvePairExpiry(refreshHint) {
7708
8123
  }
7709
8124
  return new Date(Date.now() + ASSUMED_PAIR_TOKEN_TTL_MS).toISOString();
7710
8125
  }
8126
+ function resolveRequestDeadlineMs(value) {
8127
+ const deadline = value ?? DEFAULT_AUTH_REQUEST_DEADLINE_MS;
8128
+ if (!Number.isSafeInteger(deadline) || deadline <= 0) {
8129
+ throw new Error("authRequestDeadlineMs must be a positive safe integer");
8130
+ }
8131
+ return deadline;
8132
+ }
7711
8133
  async function safeErrorText(res) {
7712
8134
  try {
7713
8135
  return await res.text();
@@ -7729,37 +8151,48 @@ function withAuth(init, token) {
7729
8151
  }
7730
8152
 
7731
8153
  // src/daemon/blob-client.ts
7732
- async function safeErrorText2(res) {
7733
- try {
7734
- return await res.text();
7735
- } catch {
7736
- return "";
8154
+ var BlobRequestAbortedError = class extends Error {
8155
+ constructor(reason) {
8156
+ super(reason === "deadline" ? "blob request deadline elapsed" : "blob request cancelled");
8157
+ this.reason = reason;
8158
+ this.name = "BlobRequestAbortedError";
7737
8159
  }
7738
- }
8160
+ reason;
8161
+ };
7739
8162
  var BlobClient = class {
7740
- constructor(serverUrl, auth) {
8163
+ constructor(serverUrl, auth, options = {}) {
7741
8164
  this.serverUrl = serverUrl;
7742
8165
  this.auth = auth;
8166
+ this.options = options;
8167
+ const requestDeadlineMs = options.requestDeadlineMs ?? 15e3;
8168
+ if (!Number.isSafeInteger(requestDeadlineMs) || requestDeadlineMs <= 0) {
8169
+ throw new Error("BlobClient requestDeadlineMs must be a positive safe integer");
8170
+ }
8171
+ this.requestDeadlineMs = requestDeadlineMs;
7743
8172
  }
7744
8173
  serverUrl;
7745
8174
  auth;
8175
+ options;
8176
+ requestDeadlineMs;
7746
8177
  /** `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. */
7747
- async resolveInstruction(blobRef) {
8178
+ async resolveInstruction(blobRef, options = {}) {
7748
8179
  const base = toHttpBase(this.serverUrl);
7749
- const urlRes = await authedFetch(
7750
- new URL(byokBlobUrlPath(blobRef.blobId), base),
7751
- { method: "GET" },
7752
- this.auth
8180
+ const urlRes = await this.#request(
8181
+ (signal) => authedFetch(new URL(byokBlobUrlPath(blobRef.blobId), base), { method: "GET", signal }, this.auth),
8182
+ options.signal
7753
8183
  );
7754
8184
  if (!urlRes.ok) {
7755
- throw new Error(`failed to resolve blob download url: HTTP ${urlRes.status} ${await safeErrorText2(urlRes)}`.trimEnd());
8185
+ throw new Error(`failed to resolve blob download url: HTTP ${urlRes.status} ${await this.#safeErrorText(urlRes, options.signal)}`.trimEnd());
7756
8186
  }
7757
- const { downloadUrl } = await urlRes.json();
7758
- const contentRes = await fetch(new URL(downloadUrl, base));
8187
+ const { downloadUrl } = await this.#readJson(urlRes, options.signal);
8188
+ if (typeof downloadUrl !== "string" || downloadUrl.length === 0) {
8189
+ throw new Error("failed to resolve blob download url: response omitted downloadUrl");
8190
+ }
8191
+ const contentRes = await this.#request((signal) => fetch(new URL(downloadUrl, base), { signal }), options.signal);
7759
8192
  if (!contentRes.ok) {
7760
8193
  throw new Error(`failed to download blob content: HTTP ${contentRes.status}`);
7761
8194
  }
7762
- const bytes = new Uint8Array(await contentRes.arrayBuffer());
8195
+ const bytes = await this.#readBody(contentRes, options.signal);
7763
8196
  const observedHash = `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
7764
8197
  if (observedHash !== blobRef.contentHash || bytes.length !== blobRef.size) {
7765
8198
  throw new Error(
@@ -7768,63 +8201,71 @@ var BlobClient = class {
7768
8201
  }
7769
8202
  return new TextDecoder().decode(bytes);
7770
8203
  }
7771
- /** `POST /byok/blobs` (declares size/contentType/contentHash) -> PUT the bytes to the presigned upload URL -> a `BlobRef` for `task.artifact.blobRef`. */
8204
+ /** `POST /byok/blobs` -> PUT the bytes to the presigned URL -> finalize into a `BlobRef`. */
7772
8205
  async uploadArtifact(content, contentType, options = {}) {
7773
8206
  const bytes = typeof content === "string" ? new TextEncoder().encode(content) : content;
7774
8207
  const contentHash4 = `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
7775
8208
  const base = toHttpBase(this.serverUrl);
7776
8209
  const reservationId = options.idempotencyKey ?? `blob_${randomUUID()}`;
7777
- const createRes = await authedFetch(
7778
- new URL(BYOK_BLOBS_PATH, base),
7779
- {
7780
- method: "POST",
7781
- headers: {
7782
- "content-type": "application/json",
7783
- "idempotency-key": reservationId
8210
+ const createRes = await this.#request(
8211
+ (signal) => authedFetch(
8212
+ new URL(BYOK_BLOBS_PATH, base),
8213
+ {
8214
+ method: "POST",
8215
+ headers: { "content-type": "application/json", "idempotency-key": reservationId },
8216
+ body: JSON.stringify({ size: bytes.length, contentType, contentHash: contentHash4 }),
8217
+ signal
7784
8218
  },
7785
- body: JSON.stringify({ size: bytes.length, contentType, contentHash: contentHash4 })
7786
- },
7787
- this.auth
8219
+ this.auth
8220
+ ),
8221
+ options.signal
7788
8222
  );
7789
8223
  if (!createRes.ok) {
7790
- throw new Error(`failed to create blob: HTTP ${createRes.status} ${await safeErrorText2(createRes)}`.trimEnd());
8224
+ throw new Error(`failed to create blob: HTTP ${createRes.status} ${await this.#safeErrorText(createRes, options.signal)}`.trimEnd());
8225
+ }
8226
+ const { blobId, uploadUrl } = await this.#readJson(createRes, options.signal);
8227
+ if (typeof blobId !== "string" || blobId.length === 0 || typeof uploadUrl !== "string" || uploadUrl.length === 0) {
8228
+ throw new Error("failed to create blob: response omitted blobId or uploadUrl");
7791
8229
  }
7792
- const { blobId, uploadUrl } = await createRes.json();
7793
8230
  const blobRef = { blobId, contentHash: contentHash4, size: bytes.length, contentType };
7794
- if (options.idempotencyKey !== void 0 && await this.#hasExactCommittedBlob(base, blobRef)) {
8231
+ if (options.idempotencyKey !== void 0 && await this.#hasExactCommittedBlob(base, blobRef, options.signal)) {
7795
8232
  return blobRef;
7796
8233
  }
7797
- const putRes = await fetch(new URL(uploadUrl, base), {
7798
- method: "PUT",
7799
- headers: { "content-type": contentType },
7800
- body: bytes
7801
- });
8234
+ const putRes = await this.#request(
8235
+ (signal) => fetch(new URL(uploadUrl, base), {
8236
+ method: "PUT",
8237
+ headers: { "content-type": contentType },
8238
+ body: bytes,
8239
+ signal
8240
+ }),
8241
+ options.signal
8242
+ );
7802
8243
  if (!putRes.ok) {
7803
8244
  throw new Error(`failed to upload blob content: HTTP ${putRes.status}`);
7804
8245
  }
7805
- await this.#finalize(base, blobId, reservationId);
8246
+ this.#throwIfAborted(options.signal);
8247
+ await this.#finalize(base, blobId, reservationId, options.signal);
7806
8248
  return blobRef;
7807
8249
  }
7808
- async #hasExactCommittedBlob(base, blobRef) {
7809
- const urlRes = await authedFetch(
7810
- new URL(byokBlobUrlPath(blobRef.blobId), base),
7811
- { method: "GET" },
7812
- this.auth
8250
+ async #hasExactCommittedBlob(base, blobRef, signal) {
8251
+ const urlRes = await this.#request(
8252
+ (requestSignal) => authedFetch(new URL(byokBlobUrlPath(blobRef.blobId), base), { method: "GET", signal: requestSignal }, this.auth),
8253
+ signal
7813
8254
  );
7814
8255
  if (urlRes.status === 404) return false;
7815
8256
  if (!urlRes.ok) {
7816
- throw new Error(`failed to read back idempotent blob: HTTP ${urlRes.status} ${await safeErrorText2(urlRes)}`.trimEnd());
8257
+ throw new Error(`failed to read back idempotent blob: HTTP ${urlRes.status} ${await this.#safeErrorText(urlRes, signal)}`.trimEnd());
7817
8258
  }
7818
- const { downloadUrl } = await urlRes.json();
8259
+ const { downloadUrl } = await this.#readJson(urlRes, signal);
7819
8260
  if (typeof downloadUrl !== "string" || downloadUrl.length === 0) {
7820
8261
  throw new Error("failed to read back idempotent blob: response omitted downloadUrl");
7821
8262
  }
7822
- const contentRes = await fetch(new URL(downloadUrl, base));
8263
+ const contentRes = await this.#request((requestSignal) => fetch(new URL(downloadUrl, base), { signal: requestSignal }), signal);
7823
8264
  if (contentRes.status === 404) return false;
7824
8265
  if (!contentRes.ok) {
7825
8266
  throw new Error(`failed to read back idempotent blob content: HTTP ${contentRes.status}`);
7826
8267
  }
7827
- const bytes = new Uint8Array(await contentRes.arrayBuffer());
8268
+ const bytes = await this.#readBody(contentRes, signal);
7828
8269
  const observedHash = `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
7829
8270
  if (observedHash !== blobRef.contentHash || bytes.length !== blobRef.size) {
7830
8271
  throw new Error(
@@ -7833,18 +8274,19 @@ var BlobClient = class {
7833
8274
  }
7834
8275
  return true;
7835
8276
  }
7836
- async #finalize(base, blobId, reservationId) {
8277
+ async #finalize(base, blobId, reservationId, signal) {
7837
8278
  let lastFailure;
7838
8279
  for (let attempt = 1; attempt <= 2; attempt += 1) {
8280
+ this.#throwIfAborted(signal);
7839
8281
  let response;
7840
8282
  try {
7841
- response = await authedFetch(
7842
- new URL(byokBlobFinalizePath(blobId), base),
7843
- {
7844
- method: "POST",
7845
- headers: { "idempotency-key": reservationId }
7846
- },
7847
- this.auth
8283
+ response = await this.#request(
8284
+ (requestSignal) => authedFetch(
8285
+ new URL(byokBlobFinalizePath(blobId), base),
8286
+ { method: "POST", headers: { "idempotency-key": reservationId }, signal: requestSignal },
8287
+ this.auth
8288
+ ),
8289
+ signal
7848
8290
  );
7849
8291
  } catch (error) {
7850
8292
  lastFailure = error;
@@ -7854,13 +8296,105 @@ var BlobClient = class {
7854
8296
  if (response.ok) return;
7855
8297
  if (response.status < 500 || attempt === 2) {
7856
8298
  throw new Error(
7857
- `failed to finalize blob: HTTP ${response.status} ${await safeErrorText2(response)}`.trimEnd()
8299
+ `failed to finalize blob: HTTP ${response.status} ${await this.#safeErrorText(response, signal)}`.trimEnd()
7858
8300
  );
7859
8301
  }
7860
8302
  lastFailure = new Error(`failed to finalize blob: HTTP ${response.status}`);
7861
8303
  }
7862
8304
  throw lastFailure;
7863
8305
  }
8306
+ async #readJson(res, signal) {
8307
+ return JSON.parse(await this.#readText(res, signal));
8308
+ }
8309
+ async #safeErrorText(res, signal) {
8310
+ try {
8311
+ return await this.#readText(res, signal);
8312
+ } catch (error) {
8313
+ if (error instanceof BlobRequestAbortedError) throw error;
8314
+ return "";
8315
+ }
8316
+ }
8317
+ async #readText(res, signal) {
8318
+ return new TextDecoder().decode(await this.#readBody(res, signal));
8319
+ }
8320
+ /**
8321
+ * `Response.arrayBuffer()` only leaves cancellation observable through the
8322
+ * Fetch implementation. Read the body directly so our lifecycle/deadline
8323
+ * authority cancels the actual stream even when a fetch implementation has
8324
+ * already resolved at headers.
8325
+ */
8326
+ async #readBody(res, signal) {
8327
+ return this.#request(async (requestSignal) => {
8328
+ if (res.body === null) return new Uint8Array();
8329
+ const reader = res.body.getReader();
8330
+ const cancelBody = () => {
8331
+ void reader.cancel().catch(() => void 0);
8332
+ };
8333
+ requestSignal.addEventListener("abort", cancelBody, { once: true });
8334
+ try {
8335
+ const chunks = [];
8336
+ let length = 0;
8337
+ for (; ; ) {
8338
+ const { done, value } = await reader.read();
8339
+ if (done) break;
8340
+ chunks.push(value);
8341
+ length += value.byteLength;
8342
+ }
8343
+ const bytes = new Uint8Array(length);
8344
+ let offset = 0;
8345
+ for (const chunk of chunks) {
8346
+ bytes.set(chunk, offset);
8347
+ offset += chunk.byteLength;
8348
+ }
8349
+ return bytes;
8350
+ } finally {
8351
+ requestSignal.removeEventListener("abort", cancelBody);
8352
+ reader.releaseLock();
8353
+ }
8354
+ }, signal);
8355
+ }
8356
+ #throwIfAborted(signal) {
8357
+ if (this.options.signal?.aborted || signal?.aborted) {
8358
+ throw new BlobRequestAbortedError("cancelled");
8359
+ }
8360
+ }
8361
+ async #request(request, signal) {
8362
+ this.#throwIfAborted(signal);
8363
+ const controller = new AbortController();
8364
+ let abortReason = "cancelled";
8365
+ const abort = (reason) => {
8366
+ if (controller.signal.aborted) return;
8367
+ abortReason = reason;
8368
+ controller.abort();
8369
+ };
8370
+ const inheritedSignals = [this.options.signal, signal].filter((value) => value !== void 0);
8371
+ const abortForCancellation = () => abort("cancelled");
8372
+ for (const inheritedSignal of inheritedSignals) {
8373
+ inheritedSignal.addEventListener("abort", abortForCancellation, { once: true });
8374
+ }
8375
+ const deadline = setTimeout(() => abort("deadline"), this.requestDeadlineMs);
8376
+ deadline.unref?.();
8377
+ let rejectAbort = () => {
8378
+ };
8379
+ const aborted = new Promise((_resolve, reject) => {
8380
+ rejectAbort = reject;
8381
+ });
8382
+ const rejectOnAbort = () => rejectAbort(new BlobRequestAbortedError(abortReason));
8383
+ controller.signal.addEventListener("abort", rejectOnAbort, { once: true });
8384
+ try {
8385
+ return await Promise.race([request(controller.signal), aborted]);
8386
+ } catch (error) {
8387
+ if (error instanceof BlobRequestAbortedError) throw error;
8388
+ if (controller.signal.aborted) throw new BlobRequestAbortedError(abortReason);
8389
+ throw error;
8390
+ } finally {
8391
+ clearTimeout(deadline);
8392
+ controller.signal.removeEventListener("abort", rejectOnAbort);
8393
+ for (const inheritedSignal of inheritedSignals) {
8394
+ inheritedSignal.removeEventListener("abort", abortForCancellation);
8395
+ }
8396
+ }
8397
+ }
7864
8398
  };
7865
8399
  var DEFAULT_TIMEOUT_MS2 = 2e3;
7866
8400
  var DARWIN_UUID_RE = /"IOPlatformUUID"\s*=\s*"([^"]+)"/u;
@@ -8107,6 +8641,7 @@ var AnotherControlServerRunningError = class extends Error {
8107
8641
  }
8108
8642
  };
8109
8643
  var MAX_HALF_OPEN_CONNECTIONS = 8;
8644
+ var MAX_OUTBOUND_QUEUE_BYTES = 1024 * 1024;
8110
8645
  function errorMessage5(err) {
8111
8646
  return err instanceof Error ? err.message : String(err);
8112
8647
  }
@@ -8183,11 +8718,12 @@ async function bindControlEndpoint(server, endpoint) {
8183
8718
  }
8184
8719
  function handleConnection(socket, token, methods, handshakeTimeoutMs, onHandshakeSettled) {
8185
8720
  const reader = new NdjsonLineReader();
8186
- const activeStreams = /* @__PURE__ */ new Map();
8721
+ const activeRequests = /* @__PURE__ */ new Map();
8187
8722
  let phase = "client-hello";
8188
8723
  let serverNonce = "";
8189
8724
  let destroyed = false;
8190
8725
  let handshakeSettled = false;
8726
+ let disposeOutboundWriter;
8191
8727
  function settleHandshake() {
8192
8728
  if (handshakeSettled) return;
8193
8729
  handshakeSettled = true;
@@ -8197,8 +8733,83 @@ function handleConnection(socket, token, methods, handshakeTimeoutMs, onHandshak
8197
8733
  if (phase !== "ready") socket.destroy();
8198
8734
  }, handshakeTimeoutMs);
8199
8735
  handshakeTimer.unref?.();
8736
+ function terminateConnection() {
8737
+ if (destroyed) return;
8738
+ destroyed = true;
8739
+ clearTimeout(handshakeTimer);
8740
+ settleHandshake();
8741
+ disposeOutboundWriter?.();
8742
+ const requests = [...activeRequests.values()];
8743
+ activeRequests.clear();
8744
+ for (const request of requests) {
8745
+ if (request.kind === "stream" && !request.controller.signal.aborted) request.controller.abort();
8746
+ }
8747
+ if (!socket.destroyed) socket.destroy();
8748
+ }
8749
+ const queuedFrames = [];
8750
+ let queuedBytes = 0;
8751
+ let blocked = false;
8752
+ let terminal = false;
8753
+ let drainListening = false;
8754
+ function disposeQueuedFrames() {
8755
+ terminal = true;
8756
+ blocked = false;
8757
+ queuedFrames.length = 0;
8758
+ queuedBytes = 0;
8759
+ if (drainListening) {
8760
+ socket.removeListener("drain", onDrain);
8761
+ drainListening = false;
8762
+ }
8763
+ }
8764
+ function waitForDrain() {
8765
+ if (drainListening || terminal) return;
8766
+ drainListening = true;
8767
+ socket.once("drain", onDrain);
8768
+ }
8769
+ function writeEncodedFrame(encoded) {
8770
+ if (terminal) return;
8771
+ if (!socket.writable) {
8772
+ terminateConnection();
8773
+ return;
8774
+ }
8775
+ try {
8776
+ if (!socket.write(encoded)) {
8777
+ blocked = true;
8778
+ waitForDrain();
8779
+ }
8780
+ } catch {
8781
+ terminateConnection();
8782
+ }
8783
+ }
8784
+ function onDrain() {
8785
+ drainListening = false;
8786
+ if (terminal) return;
8787
+ blocked = false;
8788
+ while (!blocked && queuedFrames.length > 0) {
8789
+ const frame = queuedFrames.shift();
8790
+ queuedBytes -= frame.bytes;
8791
+ writeEncodedFrame(frame.encoded);
8792
+ }
8793
+ }
8794
+ disposeOutboundWriter = disposeQueuedFrames;
8200
8795
  function sendFrame(frame) {
8201
- if (!destroyed && socket.writable) socket.write(encodeFrame(frame));
8796
+ if (destroyed || terminal) return;
8797
+ const encoded = encodeFrame(frame);
8798
+ const bytes = Buffer.byteLength(encoded);
8799
+ if (bytes > MAX_OUTBOUND_QUEUE_BYTES) {
8800
+ terminateConnection();
8801
+ return;
8802
+ }
8803
+ if (blocked) {
8804
+ if (queuedBytes + bytes > MAX_OUTBOUND_QUEUE_BYTES) {
8805
+ terminateConnection();
8806
+ return;
8807
+ }
8808
+ queuedFrames.push({ encoded, bytes });
8809
+ queuedBytes += bytes;
8810
+ return;
8811
+ }
8812
+ writeEncodedFrame(encoded);
8202
8813
  }
8203
8814
  function handleClientHello(parsed) {
8204
8815
  const hello = parseClientHello(parsed);
@@ -8221,21 +8832,51 @@ function handleConnection(socket, token, methods, handshakeTimeoutMs, onHandshak
8221
8832
  settleHandshake();
8222
8833
  sendFrame({ v: 1, ready: true });
8223
8834
  }
8835
+ function rejectDuplicateRequest(id) {
8836
+ sendFrame({
8837
+ v: 1,
8838
+ id,
8839
+ ok: false,
8840
+ error: { code: "duplicate_request_id", message: `request id "${id}" is already active` }
8841
+ });
8842
+ }
8843
+ function registerRequest(id, record3) {
8844
+ if (activeRequests.has(id)) {
8845
+ rejectDuplicateRequest(id);
8846
+ return false;
8847
+ }
8848
+ activeRequests.set(id, record3);
8849
+ return true;
8850
+ }
8851
+ function releaseRequest(id, record3) {
8852
+ if (activeRequests.get(id) !== record3) return false;
8853
+ activeRequests.delete(id);
8854
+ return true;
8855
+ }
8224
8856
  function dispatch(id, method, params) {
8225
8857
  const unary = methods.unary[method];
8226
8858
  if (unary) {
8227
- 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) }));
8859
+ const record3 = { kind: "unary" };
8860
+ if (!registerRequest(id, record3)) return;
8861
+ Promise.resolve().then(() => unary(params)).then((result) => {
8862
+ if (!releaseRequest(id, record3)) return;
8863
+ sendFrame({ v: 1, id, ok: true, result });
8864
+ }).catch((err) => {
8865
+ if (!releaseRequest(id, record3)) return;
8866
+ sendFrame({ v: 1, id, ok: false, error: toControlErrorShape(err) });
8867
+ });
8228
8868
  return;
8229
8869
  }
8230
8870
  const stream = methods.stream[method];
8231
8871
  if (stream) {
8232
8872
  const controller = new AbortController();
8233
- activeStreams.set(id, controller);
8234
- stream(params, { emit: (event) => sendFrame({ v: 1, id, event }), signal: controller.signal }).then(() => {
8235
- activeStreams.delete(id);
8873
+ const record3 = { kind: "stream", controller };
8874
+ if (!registerRequest(id, record3)) return;
8875
+ Promise.resolve().then(() => stream(params, { emit: (event) => sendFrame({ v: 1, id, event }), signal: controller.signal })).then(() => {
8876
+ if (!releaseRequest(id, record3)) return;
8236
8877
  if (!controller.signal.aborted) sendFrame({ v: 1, id, ok: true, done: true });
8237
8878
  }).catch((err) => {
8238
- activeStreams.delete(id);
8879
+ if (!releaseRequest(id, record3)) return;
8239
8880
  sendFrame({ v: 1, id, ok: false, error: toControlErrorShape(err) });
8240
8881
  });
8241
8882
  return;
@@ -8277,13 +8918,10 @@ function handleConnection(socket, token, methods, handshakeTimeoutMs, onHandshak
8277
8918
  for (const line of lines) handleLine(line);
8278
8919
  });
8279
8920
  socket.on("error", () => {
8921
+ terminateConnection();
8280
8922
  });
8281
8923
  socket.on("close", () => {
8282
- destroyed = true;
8283
- clearTimeout(handshakeTimer);
8284
- settleHandshake();
8285
- for (const controller of activeStreams.values()) controller.abort();
8286
- activeStreams.clear();
8924
+ terminateConnection();
8287
8925
  });
8288
8926
  }
8289
8927
  async function startControlServer(opts) {
@@ -9201,6 +9839,20 @@ function createFleetJitter(productId, deviceId) {
9201
9839
  delay: (domain, sequence, baseMs) => deterministicJitterMs({ seed, domain, sequence, baseMs })
9202
9840
  };
9203
9841
  }
9842
+
9843
+ // src/daemon/replay-cursor.ts
9844
+ var ReplayCursorTooOldError = class extends Error {
9845
+ constructor(recoverableFrom) {
9846
+ super(
9847
+ 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}`
9848
+ );
9849
+ this.recoverableFrom = recoverableFrom;
9850
+ this.name = "ReplayCursorTooOldError";
9851
+ }
9852
+ recoverableFrom;
9853
+ };
9854
+
9855
+ // src/daemon/long-poll-transport.ts
9204
9856
  var LongPollRouteError = class extends Error {
9205
9857
  constructor(endpoint, status, cause) {
9206
9858
  super(
@@ -9216,7 +9868,7 @@ var LongPollRouteError = class extends Error {
9216
9868
  };
9217
9869
  var MAX_TRACKED_VALIDATION_FAILURE_WARNINGS = 1e3;
9218
9870
  var MAX_TRACKED_ROUTE_FAILURE_WARNINGS = 1e3;
9219
- function parseLooseEventsPollResponse(raw) {
9871
+ function parseLooseEventsPollResponse(raw, requestedCursor) {
9220
9872
  if (typeof raw !== "object" || raw === null) {
9221
9873
  throw new Error("events poll response is not an object");
9222
9874
  }
@@ -9224,19 +9876,48 @@ function parseLooseEventsPollResponse(raw) {
9224
9876
  if (!Array.isArray(events)) {
9225
9877
  throw new Error("events poll response.events is not an array");
9226
9878
  }
9227
- if (typeof cursor !== "number" || !Number.isInteger(cursor)) {
9228
- throw new Error("events poll response.cursor is not an integer");
9879
+ if (!isSafeNonnegativeInteger(cursor)) {
9880
+ throw new Error("events poll response.cursor is not a safe nonnegative integer");
9881
+ }
9882
+ if (cursor < requestedCursor) {
9883
+ throw new Error("events poll response.cursor regressed below the requested cursor");
9229
9884
  }
9230
9885
  if (capabilities !== void 0 && (!Array.isArray(capabilities) || capabilities.some((flag) => typeof flag !== "string"))) {
9231
9886
  throw new Error("events poll response.capabilities is not an array of strings");
9232
9887
  }
9233
9888
  return { events, cursor, capabilities: capabilities ?? [] };
9234
9889
  }
9890
+ function isSafeNonnegativeInteger(value) {
9891
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
9892
+ }
9893
+ function validateTrustedEventsPage(events, requestedCursor, pageCursor) {
9894
+ let previousTaskSeq;
9895
+ for (const raw of events) {
9896
+ if (typeof raw !== "object" || raw === null) continue;
9897
+ const { type, seq } = raw;
9898
+ if (typeof type !== "string" || !type.startsWith("task.")) continue;
9899
+ if (!isSafeNonnegativeInteger(seq)) {
9900
+ throw new Error("events poll task seq is not a safe nonnegative integer");
9901
+ }
9902
+ if (seq <= requestedCursor || seq > pageCursor) {
9903
+ throw new Error("events poll task seq lies outside the trusted page cursor range");
9904
+ }
9905
+ if (previousTaskSeq !== void 0 && seq <= previousTaskSeq) {
9906
+ throw new Error("events poll task seq is not strictly page ordered");
9907
+ }
9908
+ const knownTaskType = MESSAGE_TYPES.includes(type);
9909
+ const predecessor = previousTaskSeq ?? requestedCursor;
9910
+ if (!knownTaskType && seq !== predecessor + 1) {
9911
+ throw new Error("events poll unknown task seq would cross an untrusted gap");
9912
+ }
9913
+ previousTaskSeq = seq;
9914
+ }
9915
+ }
9235
9916
  function extractSkippableSeq(raw) {
9236
9917
  if (typeof raw !== "object" || raw === null) return void 0;
9237
9918
  const { type, seq } = raw;
9238
9919
  if (typeof type !== "string" || !type.startsWith("task.")) return void 0;
9239
- return typeof seq === "number" && Number.isInteger(seq) ? seq : void 0;
9920
+ return isSafeNonnegativeInteger(seq) ? seq : void 0;
9240
9921
  }
9241
9922
  var LongPollClient = class {
9242
9923
  constructor(opts) {
@@ -9247,6 +9928,8 @@ var LongPollClient = class {
9247
9928
  }
9248
9929
  opts;
9249
9930
  running = false;
9931
+ /** Owns exactly one active loop generation, including its held GET and retry delays. */
9932
+ loopAbortController;
9250
9933
  /**
9251
9934
  * Finding R1: seqs this loop has already `console.warn`'d about for a
9252
9935
  * validation-failed (recognized-type, invalid-payload) entry — a poison
@@ -9306,16 +9989,22 @@ var LongPollClient = class {
9306
9989
  noteRevoked(err) {
9307
9990
  if (!(err instanceof DeviceRevokedError)) return false;
9308
9991
  this.running = false;
9992
+ this.loopAbortController?.abort();
9309
9993
  this.opts.onRevoked?.();
9310
9994
  return true;
9311
9995
  }
9312
9996
  start() {
9313
9997
  if (this.running) return;
9314
9998
  this.running = true;
9315
- void this.loop();
9999
+ const controller = new AbortController();
10000
+ this.loopAbortController = controller;
10001
+ void this.loop(controller.signal).finally(() => {
10002
+ if (this.loopAbortController === controller) this.loopAbortController = void 0;
10003
+ });
9316
10004
  }
9317
10005
  stop() {
9318
10006
  this.running = false;
10007
+ this.loopAbortController?.abort();
9319
10008
  }
9320
10009
  /**
9321
10010
  * POST one batch of envelopes to `/byok/messages` (finding F6/protocol
@@ -9324,14 +10013,14 @@ var LongPollClient = class {
9324
10013
  * (`ConnectionHub.handleInbound`), so a resend of the SAME batch (same
9325
10014
  * envelope `id`s — the caller must never rebuild them) is deduped
9326
10015
  * server-side into a safe no-op rather than reprocessed (§9). Returns
9327
- * `true` once the server has accepted the batch.
10016
+ * validated frozen-v1 counts only after a readable response body.
9328
10017
  */
9329
10018
  async postBatch(envelopes) {
9330
10019
  try {
9331
10020
  await this.opts.auth.getValidAccessToken();
9332
10021
  } catch (err) {
9333
10022
  this.noteRevoked(err);
9334
- return false;
10023
+ return void 0;
9335
10024
  }
9336
10025
  let res;
9337
10026
  try {
@@ -9347,23 +10036,26 @@ var LongPollClient = class {
9347
10036
  );
9348
10037
  } catch (err) {
9349
10038
  if (!this.noteRevoked(err)) this.warnRouteFailure(this.messagesEndpoint, void 0, err);
9350
- return false;
10039
+ return void 0;
9351
10040
  }
9352
10041
  if (!res.ok) {
9353
10042
  this.warnRouteFailure(this.messagesEndpoint, res.status, void 0);
9354
- return false;
10043
+ return void 0;
9355
10044
  }
9356
10045
  try {
9357
- MessagesSendResponseSchema.parse(await res.json());
10046
+ const response = MessagesSendResponseSchema.parse(await res.json());
10047
+ if (response.accepted + (response.rejected ?? 0) !== envelopes.length) {
10048
+ throw new Error("messages response counts do not match the posted batch length");
10049
+ }
10050
+ return response;
9358
10051
  } catch (err) {
9359
10052
  this.warnRouteFailure(this.messagesEndpoint, res.status, err);
9360
- return false;
10053
+ return void 0;
9361
10054
  }
9362
- return true;
9363
10055
  }
9364
- async loop() {
10056
+ async loop(signal) {
9365
10057
  let retryAttempt = 0;
9366
- while (this.running) {
10058
+ while (this.running && !signal.aborted) {
9367
10059
  try {
9368
10060
  await this.opts.auth.getValidAccessToken();
9369
10061
  const base = toHttpBase(this.opts.serverUrl);
@@ -9372,30 +10064,40 @@ var LongPollClient = class {
9372
10064
  if (cursor !== void 0) url.searchParams.set("cursor", String(cursor));
9373
10065
  let res;
9374
10066
  try {
9375
- res = await authedFetch(url, { method: "GET" }, this.opts.auth);
10067
+ res = await authedFetch(url, { method: "GET", signal }, this.opts.auth);
9376
10068
  } catch (err) {
10069
+ if (signal.aborted) return;
9377
10070
  if (!(err instanceof DeviceRevokedError)) {
9378
10071
  this.warnRouteFailure(this.eventsEndpoint, void 0, err);
9379
10072
  }
9380
10073
  throw err;
9381
10074
  }
9382
10075
  if (!res.ok) {
10076
+ const replayCursorTooOld = await parseReplayCursorTooOld(res);
10077
+ if (replayCursorTooOld) {
10078
+ this.running = false;
10079
+ this.opts.onReplayCursorTooOld?.(replayCursorTooOld);
10080
+ return;
10081
+ }
9383
10082
  this.warnRouteFailure(this.eventsEndpoint, res.status, void 0);
9384
- this.opts.onServerCapabilities?.([]);
10083
+ this.opts.onServerCapabilitiesInvalidated?.();
10084
+ this.opts.onPollFailure?.();
9385
10085
  this.opts.onOperationalOutcome?.("failure");
9386
10086
  const baseMs = this.opts.retryDelayMs ?? 2e3;
9387
- await sleep(this.opts.retryDelayForAttempt?.(retryAttempt++, baseMs) ?? baseMs);
10087
+ await sleep(this.opts.retryDelayForAttempt?.(retryAttempt++, baseMs) ?? baseMs, signal);
9388
10088
  continue;
9389
10089
  }
9390
10090
  let parsed;
9391
10091
  try {
9392
- parsed = parseLooseEventsPollResponse(await res.json());
10092
+ parsed = parseLooseEventsPollResponse(await res.json(), cursor ?? 0);
10093
+ validateTrustedEventsPage(parsed.events, cursor ?? 0, parsed.cursor);
9393
10094
  } catch (err) {
9394
10095
  this.warnRouteFailure(this.eventsEndpoint, res.status, err);
9395
10096
  throw err;
9396
10097
  }
9397
10098
  this.opts.onServerCapabilities?.(parsed.capabilities);
9398
10099
  let hadValidationFailureThisBatch = false;
10100
+ let acceptedAnyEntry = false;
9399
10101
  for (const raw of parsed.events) {
9400
10102
  let envelope;
9401
10103
  try {
@@ -9403,7 +10105,10 @@ var LongPollClient = class {
9403
10105
  } catch (err) {
9404
10106
  if (err instanceof UnknownMessageTypeError) {
9405
10107
  const skippableSeq = extractSkippableSeq(raw);
9406
- if (skippableSeq !== void 0) this.opts.onSkippedSeq?.(skippableSeq);
10108
+ if (skippableSeq !== void 0) {
10109
+ this.opts.onSkippedSeq?.(skippableSeq);
10110
+ acceptedAnyEntry = true;
10111
+ }
9407
10112
  } else {
9408
10113
  const failedSeq = extractSkippableSeq(raw);
9409
10114
  if (failedSeq !== void 0) {
@@ -9423,45 +10128,65 @@ var LongPollClient = class {
9423
10128
  }
9424
10129
  continue;
9425
10130
  }
9426
- this.opts.onEnvelope(envelope);
10131
+ if (this.opts.onEnvelope(envelope) !== false) acceptedAnyEntry = true;
9427
10132
  }
9428
10133
  if (parsed.events.length === 0) {
9429
10134
  retryAttempt = 0;
9430
10135
  this.opts.onOperationalOutcome?.("success");
9431
- await sleep(this.opts.idleDelayMs ?? 250);
9432
- } else if (this.opts.isStalled?.() || hadValidationFailureThisBatch) {
10136
+ await sleep(this.opts.idleDelayMs ?? 250, signal);
10137
+ } else if (this.opts.isStalled?.() || hadValidationFailureThisBatch || !acceptedAnyEntry && this.opts.getCursor() === cursor) {
9433
10138
  this.opts.onOperationalOutcome?.("failure");
9434
10139
  const baseMs = this.opts.retryDelayMs ?? 2e3;
9435
- await sleep(this.opts.retryDelayForAttempt?.(retryAttempt++, baseMs) ?? baseMs);
10140
+ await sleep(this.opts.retryDelayForAttempt?.(retryAttempt++, baseMs) ?? baseMs, signal);
9436
10141
  } else {
9437
10142
  retryAttempt = 0;
9438
10143
  this.opts.onOperationalOutcome?.("success");
9439
10144
  }
9440
10145
  } catch (err) {
9441
- this.opts.onServerCapabilities?.([]);
10146
+ this.opts.onServerCapabilitiesInvalidated?.();
9442
10147
  if (this.noteRevoked(err)) return;
9443
- if (!this.running) return;
10148
+ if (!this.running || signal.aborted) return;
10149
+ this.opts.onPollFailure?.();
9444
10150
  this.opts.onOperationalOutcome?.("failure");
9445
10151
  const baseMs = this.opts.retryDelayMs ?? 2e3;
9446
- await sleep(this.opts.retryDelayForAttempt?.(retryAttempt++, baseMs) ?? baseMs);
10152
+ await sleep(this.opts.retryDelayForAttempt?.(retryAttempt++, baseMs) ?? baseMs, signal);
9447
10153
  }
9448
10154
  }
9449
10155
  }
9450
10156
  };
9451
- function sleep(ms) {
9452
- return new Promise((resolve) => setTimeout(resolve, ms));
9453
- }
9454
- var WsUnexpectedStatusError = class extends Error {
9455
- constructor(status, endpoint) {
9456
- super(`WS upgrade rejected with HTTP ${status} (ws ${endpoint.host}${endpoint.path})`);
9457
- this.status = status;
9458
- this.endpoint = endpoint;
9459
- this.name = "WsUnexpectedStatusError";
10157
+ async function parseReplayCursorTooOld(res) {
10158
+ if (res.status !== 409) return void 0;
10159
+ let body;
10160
+ try {
10161
+ body = await res.json();
10162
+ } catch {
10163
+ return void 0;
9460
10164
  }
9461
- status;
9462
- endpoint;
9463
- };
9464
- function createConnectionHelloEnvelope(opts) {
10165
+ if (typeof body !== "object" || body === null) return void 0;
10166
+ const { error, recoverableFrom } = body;
10167
+ if (error !== "cursor_too_old" || typeof recoverableFrom !== "number" || !Number.isSafeInteger(recoverableFrom) || recoverableFrom < 0) {
10168
+ return void 0;
10169
+ }
10170
+ return new ReplayCursorTooOldError(recoverableFrom);
10171
+ }
10172
+ function sleep(ms, signal) {
10173
+ if (signal.aborted) return Promise.resolve();
10174
+ return new Promise((resolve) => {
10175
+ const finish = () => {
10176
+ clearTimeout(timer);
10177
+ signal.removeEventListener("abort", finish);
10178
+ resolve();
10179
+ };
10180
+ const timer = setTimeout(finish, ms);
10181
+ signal.addEventListener("abort", finish, { once: true });
10182
+ });
10183
+ }
10184
+
10185
+ // src/daemon/connection-manager.ts
10186
+ function isCursorEnvelopeType(type) {
10187
+ return type.startsWith("task.") || type === "agent.egress.ack" || type === "agent.content.read" || type === "agent.home.projection";
10188
+ }
10189
+ function createConnectionHelloEnvelope(opts, getCursor) {
9465
10190
  const configuredToolsets = opts.getConfiguredToolsets?.();
9466
10191
  return createEnvelope("conn.hello", {
9467
10192
  protocolVersions: [PROTOCOL_VERSION],
@@ -9471,222 +10196,34 @@ function createConnectionHelloEnvelope(opts) {
9471
10196
  clientVersion: opts.clientVersion,
9472
10197
  runtimes: opts.runtimes,
9473
10198
  configuredToolsets: configuredToolsets === void 0 ? void 0 : [...configuredToolsets],
9474
- cursor: opts.getCursor?.()
10199
+ cursor: getCursor()
9475
10200
  });
9476
10201
  }
9477
- var WsTransport = class {
9478
- constructor(opts) {
9479
- this.opts = opts;
9480
- }
9481
- opts;
9482
- socket;
9483
- closedByUser = false;
9484
- autoReconnect = true;
9485
- acked = false;
9486
- everAckedThisAttempt = false;
9487
- reconnectAttempt = 0;
9488
- reconnectTimer;
9489
- livenessTimer;
9490
- lastActivity = 0;
9491
- lastUnexpectedStatus;
9492
- ackWaiters = [];
9493
- connect(opts = {}) {
9494
- this.autoReconnect = opts.auto ?? true;
9495
- this.closedByUser = false;
9496
- void this.openSocket();
9497
- }
9498
- /** 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). */
9499
- stopAutoReconnect() {
9500
- this.autoReconnect = false;
9501
- if (this.reconnectTimer) {
9502
- clearTimeout(this.reconnectTimer);
9503
- this.reconnectTimer = void 0;
9504
- }
9505
- }
9506
- /** Resume normal auto-reconnect-on-close behavior (does not itself trigger a connect — only affects future closes). */
9507
- resumeAutoReconnect() {
9508
- this.autoReconnect = true;
9509
- }
9510
- get isOpen() {
9511
- return this.acked;
9512
- }
9513
- /**
9514
- * Attempt to send ONE envelope right now; returns whether it actually went
9515
- * out. `false` means the socket isn't currently open+acked — the caller
9516
- * (`ConnectionManager.drainOutbox`, Design B/finding N4) owns re-queueing
9517
- * and retrying later (e.g. on the next `onAcked`), since this transport no
9518
- * longer buffers anything itself.
9519
- */
9520
- sendNow(envelope) {
9521
- if (this.socket && this.socket.readyState === WebSocket.OPEN && this.acked) {
9522
- this.socket.send(encodeEnvelope(envelope));
9523
- return true;
9524
- }
9525
- return false;
9526
- }
9527
- waitForAck(timeoutMs = 1e4) {
9528
- if (this.acked) return Promise.resolve();
9529
- return new Promise((resolve, reject) => {
9530
- const timer = setTimeout(() => reject(new Error("Timed out waiting for conn.ack")), timeoutMs);
9531
- this.ackWaiters.push({
9532
- resolve: () => {
9533
- clearTimeout(timer);
9534
- resolve();
9535
- },
9536
- reject: (err) => {
9537
- clearTimeout(timer);
9538
- reject(err);
9539
- }
9540
- });
9541
- });
9542
- }
9543
- close() {
9544
- this.closedByUser = true;
9545
- if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
9546
- this.socket?.close();
9547
- }
9548
- async openSocket() {
9549
- const url = toWsUrl(this.opts.serverUrl);
9550
- const endpoint = describeEndpoint("ws", url);
9551
- this.acked = false;
9552
- this.everAckedThisAttempt = false;
9553
- this.opts.onStateChange?.("connecting");
9554
- let token;
9555
- try {
9556
- token = await this.opts.getToken();
9557
- } catch (err) {
9558
- this.opts.onStateChange?.("closed");
9559
- this.opts.onConnectOutcome?.(false, err, endpoint);
9560
- if (!this.closedByUser && this.autoReconnect) this.scheduleReconnect();
9561
- return;
9562
- }
9563
- const socket = new WebSocket(url, {
9564
- headers: { Authorization: `Bearer ${token}` }
9565
- });
9566
- this.socket = socket;
9567
- this.lastActivity = Date.now();
9568
- this.startLivenessCheck(socket);
9569
- socket.on("open", () => {
9570
- const hello = createConnectionHelloEnvelope(this.opts);
9571
- socket.send(encodeEnvelope(hello));
9572
- });
9573
- socket.on("ping", () => {
9574
- this.lastActivity = Date.now();
9575
- });
9576
- socket.on("message", (data, isBinary) => {
9577
- this.lastActivity = Date.now();
9578
- const bytes = toBytes(data);
9579
- let envelope;
9580
- try {
9581
- envelope = decodeEnvelope(bytes);
9582
- } catch {
9583
- return;
9584
- }
9585
- if (envelope.type === "conn.ack") {
9586
- this.reconnectAttempt = 0;
9587
- this.acked = true;
9588
- this.everAckedThisAttempt = true;
9589
- this.opts.onStateChange?.("open");
9590
- for (const waiter of this.ackWaiters.splice(0)) waiter.resolve();
9591
- this.opts.onAcked?.(envelope.payload.capabilities);
9592
- }
9593
- this.opts.onEnvelope(envelope);
9594
- });
9595
- socket.on("close", () => {
9596
- this.socket = void 0;
9597
- this.stopLivenessCheck();
9598
- this.opts.onStateChange?.("closed");
9599
- const acked = this.everAckedThisAttempt;
9600
- const status = this.lastUnexpectedStatus;
9601
- this.lastUnexpectedStatus = void 0;
9602
- this.opts.onConnectOutcome?.(
9603
- acked,
9604
- status !== void 0 ? new WsUnexpectedStatusError(status, endpoint) : void 0,
9605
- endpoint
9606
- );
9607
- if (!this.closedByUser && this.autoReconnect) this.scheduleReconnect();
9608
- });
9609
- socket.on("error", () => {
9610
- });
9611
- socket.on("unexpected-response", (_req, res) => {
9612
- this.lastUnexpectedStatus = res.statusCode;
9613
- res.resume();
9614
- socket.terminate();
9615
- });
9616
- }
9617
- scheduleReconnect() {
9618
- const { baseMs = 1e3, maxMs = 3e4, factor = 2 } = this.opts.backoff ?? {};
9619
- const delay3 = Math.min(maxMs, baseMs * factor ** this.reconnectAttempt);
9620
- const jitter = this.opts.reconnectDelayMs?.(this.reconnectAttempt, delay3) ?? delay3;
9621
- this.reconnectAttempt += 1;
9622
- this.reconnectTimer = setTimeout(() => void this.openSocket(), jitter);
9623
- }
9624
- startLivenessCheck(socket) {
9625
- const { timeoutMs = 75e3, checkIntervalMs = Math.max(1e3, Math.floor(timeoutMs / 3)) } = this.opts.liveness ?? {};
9626
- this.livenessTimer = setInterval(() => {
9627
- if (Date.now() - this.lastActivity > timeoutMs) {
9628
- socket.terminate();
9629
- }
9630
- }, checkIntervalMs);
9631
- this.livenessTimer.unref?.();
9632
- }
9633
- stopLivenessCheck() {
9634
- if (this.livenessTimer) {
9635
- clearInterval(this.livenessTimer);
9636
- this.livenessTimer = void 0;
9637
- }
9638
- }
9639
- };
9640
- function toBytes(data, _isBinary) {
9641
- if (Buffer.isBuffer(data)) return data;
9642
- if (Array.isArray(data)) return Buffer.concat(data);
9643
- if (data instanceof ArrayBuffer) return new Uint8Array(data);
9644
- return Buffer.from(String(data), "utf8");
9645
- }
9646
-
9647
- // src/daemon/connection-manager.ts
9648
- function isCursorEnvelopeType(type) {
9649
- return type.startsWith("task.") || type === "agent.egress.ack" || type === "agent.content.read" || type === "agent.home.projection";
9650
- }
10202
+ var MAX_REJECTED_OUTBOX_ENTRIES = 1e3;
9651
10203
  var ConnectionManager = class {
9652
10204
  constructor(opts) {
9653
10205
  this.opts = opts;
9654
10206
  this.fleetJitter = opts.fleetJitter ?? createFleetJitter(opts.productId, opts.deviceId);
9655
- this.ws = new WsTransport({
9656
- serverUrl: opts.serverUrl,
9657
- getToken: () => opts.auth.getValidAccessToken(),
9658
- deviceId: opts.deviceId,
9659
- productId: opts.productId,
9660
- capabilities: opts.capabilities,
9661
- clientVersion: opts.clientVersion,
9662
- runtimes: opts.runtimes,
9663
- getConfiguredToolsets: opts.getConfiguredToolsets,
9664
- getCursor: () => this.cursor,
9665
- onEnvelope: (envelope) => this.deliver(envelope),
9666
- onStateChange: (state) => {
9667
- if (!this.stopped && !this.revoked) this.opts.onStateChange?.(state);
9668
- },
9669
- onAcked: (capabilities) => this.onAcked(capabilities),
9670
- onConnectOutcome: (acked, err) => this.onWsOutcome(acked, err),
9671
- backoff: opts.backoff,
9672
- liveness: opts.liveness,
9673
- reconnectDelayMs: (attempt, baseMs) => this.fleetJitter.delay("reconnect", attempt, baseMs)
9674
- });
9675
10207
  this.longPoll = new LongPollClient({
9676
10208
  serverUrl: opts.serverUrl,
9677
10209
  auth: opts.auth,
9678
- // Design A: the query cursor for the NEXT `GET /byok/events` is the
9679
- // same watermark `deliver()` dedupes against (see `dedupWatermark`) —
9680
- // normally the eager `deliveredSeq` (so an in-flight envelope isn't
9681
- // re-pulled), but the durable `cursor` while `stalledAtSeq` is set, so
9682
- // the failed envelope (and everything after it) IS re-pulled and
9683
- // re-attempted.
9684
- getCursor: () => this.dedupWatermark(),
10210
+ // The long-poll query cursor is also the kernel's irreversible ack.
10211
+ // Only report the successfully processed cursor here. `deliveredSeq`
10212
+ // remains a local dedup watermark; using it on the wire would ack an
10213
+ // in-flight envelope before its handler settles and make a later
10214
+ // failure impossible to redeliver.
10215
+ getCursor: () => this.cursor,
9685
10216
  onEnvelope: (envelope) => this.deliver(envelope),
9686
10217
  onServerCapabilities: (capabilities) => {
9687
- if (this.mode === "long-poll") this.serverCapabilities = capabilities;
10218
+ this.serverCapabilities = capabilities;
10219
+ this.noteConnected();
9688
10220
  },
10221
+ onServerCapabilitiesInvalidated: () => {
10222
+ this.serverCapabilities = [];
10223
+ },
10224
+ onPollFailure: () => this.noteDisconnected(),
9689
10225
  onRevoked: () => this.enterRevoked(),
10226
+ onReplayCursorTooOld: (error) => this.enterReplayCursorTooOld(error),
9690
10227
  // M4 Phase 4 (version-negotiation drill fix): a batch entry
9691
10228
  // LongPollClient couldn't parse into a known Envelope at all (an
9692
10229
  // unrecognized message type) still needs its cursor/watermark
@@ -9712,13 +10249,10 @@ var ConnectionManager = class {
9712
10249
  }
9713
10250
  opts;
9714
10251
  fleetJitter;
9715
- ws;
9716
10252
  longPoll;
9717
- mode = "ws";
9718
- consecutiveFailures = 0;
9719
- wsRetryTimer;
9720
- wsProbeSequence = 0;
9721
10253
  uploadRetryAttempt = 0;
10254
+ started = false;
10255
+ connected = false;
9722
10256
  cursor;
9723
10257
  /**
9724
10258
  * Finding F3 (at-most-once redelivery): the lowest `task.*` envelope `seq`
@@ -9740,10 +10274,9 @@ var ConnectionManager = class {
9740
10274
  * (see `advanceCursor`) — that semantics is unchanged. `deliveredSeq`
9741
10275
  * advances eagerly, the instant a `task.*` envelope is admitted past
9742
10276
  * dedup (see `deliver`/`noteDelivered`), independent of whether its
9743
- * handler has even started, let alone succeeded. It exists so a
9744
- * long-poll re-query (`LongPollClient`'s `getCursor`) doesn't re-pull an
9745
- * envelope that's already been delivered once and is still in flight
9746
- * `handleOffer` is NOT idempotent and must never be re-pulled while a
10277
+ * handler has even started, let alone succeeded. It exists so a repeated
10278
+ * read at the durable cursor does not re-dispatch an envelope already in
10279
+ * flight `handleOffer` must not start a second adapter session while a
9747
10280
  * first attempt is still running. On WS this same field is written the
9748
10281
  * same way, but since a live WS connection only ever pushes a given `seq`
9749
10282
  * once, it never has an observable effect there beyond mirroring
@@ -9754,18 +10287,18 @@ var ConnectionManager = class {
9754
10287
  /** Finding F3: serializes `onEnvelope` calls into a per-connection FIFO — one envelope's handler always fully settles before the next one starts. */
9755
10288
  processingChain = Promise.resolve();
9756
10289
  /**
9757
- * Design B (finding N4): the ONE outbound queue both transports drain
9758
- * from — holds `Envelope` OBJECTS, never re-encoded/rebuilt strings, so a
10290
+ * Design B (finding N4): the ONE outbound queue holds `Envelope` OBJECTS,
10291
+ * never re-encoded/rebuilt strings, so a
9759
10292
  * resend after a failed send attempt is byte-identical to the original
9760
10293
  * (same `id`), which is what lets the server's per-(deviceId,id) dedup
9761
10294
  * (Wave 1) recognize it as a safe no-op retry rather than a second
9762
- * application (protocol §9). A transport switch (long-poll <-> WS) never
9763
- * touches this queue — see `drainOutbox` — so nothing queued while one
9764
- * transport was active is ever stranded when the other takes over.
10295
+ * application (protocol §9).
9765
10296
  */
9766
10297
  outbox = [];
10298
+ /** Terminally rejected outbound envelopes, retained as a bounded observable quarantine. */
10299
+ rejectedOutboundEnvelopes = [];
9767
10300
  /**
9768
- * Finding F5(b): how many envelopes `drainOutbox`'s long-poll branch has
10301
+ * Finding F5(b): how many envelopes `drainOutbox` has
9769
10302
  * currently spliced OUT of `this.outbox` for an in-flight (not yet
9770
10303
  * confirmed delivered) `postBatch` call — 0 the rest of the time. See
9771
10304
  * `outboxLength`'s own doc comment for why this needs to be tracked
@@ -9775,6 +10308,7 @@ var ConnectionManager = class {
9775
10308
  draining = false;
9776
10309
  stopped = false;
9777
10310
  revoked = false;
10311
+ terminalError;
9778
10312
  settledWaiters = [];
9779
10313
  pendingCursorSave = Promise.resolve();
9780
10314
  /**
@@ -9817,38 +10351,22 @@ var ConnectionManager = class {
9817
10351
  */
9818
10352
  cancelPendingDrainRetry;
9819
10353
  /**
9820
- * The capabilities the CURRENT transport's server advertised — untyped
9821
- * `string[]` for forward compatibility. WS populates it from `conn.ack`;
9822
- * long-poll populates it from each successful events response. Empty until
9823
- * the active transport supplies an advertisement.
9824
- *
9825
- * Finding R2 (cross-model re-review — was P1): strictly PER-CONNECTION,
9826
- * not per-daemon-lifetime. Cleared to `[]` the instant the acked WS
9827
- * connection ends for ANY reason — an ordinary disconnect (`onWsOutcome`'s
9828
- * `acked` branch), `stop()`, or a transport switch to long-poll
9829
- * (`enterLongPoll`) — and only repopulated by a fresh advertisement from
9830
- * the transport that is still current.
9831
- * The previous version of this doc comment claimed long-poll mode simply
9832
- * "stays at whatever the last real WS `conn.ack` said" — that was the bug:
9833
- * a daemon that once learned e.g. `approval_resolved` from an earlier WS
9834
- * session kept believing it applied to whatever it's connected to NOW,
9835
- * even after a disconnect/degrade where nothing has actually confirmed
9836
- * that's still true (a reconnect could land on a DIFFERENT server behind a
9837
- * load balancer). Concretely, `TaskRunner.sendApprovalResolved` gates
9838
- * `task.approval_resolved` on this list — sending it to a server that
9839
- * doesn't actually understand it over the long-poll path would get a
9840
- * batch-level 400 from `MessagesSendRequestSchema` (protocol §8.2), which
9841
- * `drainOutbox`'s retry-the-same-batch-forever loop then head-of-line
9842
- * blocks EVERY envelope queued behind it on, permanently. Clearing this
9843
- * eagerly means that gate reliably fails closed (falls back to the
9844
- * pre-existing implicit-resume inference, unconditionally — see
9845
- * `sendApprovalResolved`'s own doc comment) the moment the connection that
9846
- * advertised the capability is no longer the one actually in use.
10354
+ * The capabilities the current server response advertised — untyped
10355
+ * `string[]` for forward compatibility. An advertisement is scoped to the
10356
+ * current long-poll response stream and is cleared after an HTTP failure,
10357
+ * terminal shutdown, or revocation. This keeps capability-gated outbound
10358
+ * messages fail-closed until the current server has explicitly advertised
10359
+ * support.
9847
10360
  */
9848
10361
  serverCapabilities = [];
9849
10362
  async start() {
10363
+ if (this.terminalError) throw this.terminalError;
9850
10364
  this.cursor = await this.opts.cursorStore.load(this.opts.serverUrl, this.opts.deviceId);
9851
- this.ws.connect({ auto: true });
10365
+ this.started = true;
10366
+ this.opts.onStateChange?.("connecting");
10367
+ this.longPoll.start();
10368
+ this.outbox.unshift(createConnectionHelloEnvelope(this.opts, () => this.cursor));
10369
+ void this.drainOutbox();
9852
10370
  }
9853
10371
  /**
9854
10372
  * Design B (finding N4): push onto the single shared outbox and try to
@@ -9859,17 +10377,13 @@ var ConnectionManager = class {
9859
10377
  this.outbox.push(envelope);
9860
10378
  void this.drainOutbox();
9861
10379
  }
10380
+ /** Publish a fresh local configuration snapshot while this daemon is running. */
10381
+ refreshHello() {
10382
+ if (!this.started || this.stopped || this.revoked || this.terminalError) return;
10383
+ this.send(createConnectionHelloEnvelope(this.opts, () => this.cursor));
10384
+ }
9862
10385
  /**
9863
- * Design B (finding N4): drain the shared outbox through whichever
9864
- * transport is currently active, re-checking `this.mode` fresh on every
9865
- * iteration so a transport switch mid-drain is picked up immediately
9866
- * rather than fighting a stale decision made before the switch.
9867
- *
9868
- * WS: a synchronous, one-at-a-time `sendNow` per envelope while open+
9869
- * acked; stops (without dropping anything — the remainder stays queued)
9870
- * the moment it isn't, and is re-invoked once `onAcked` fires.
9871
- *
9872
- * Long-poll: POSTs the outbox in chunks of at most
10386
+ * POSTs the outbox through long-poll in chunks of at most
9873
10387
  * `MAX_MESSAGES_PER_BATCH` (finding P1) — the server hard-caps a single
9874
10388
  * `/byok/messages` batch there (`MessagesSendRequestSchema`, protocol
9875
10389
  * §8.2) and 400s the WHOLE request if it's exceeded, which — before this
@@ -9881,43 +10395,48 @@ var ConnectionManager = class {
9881
10395
  * failure that SAME chunk is unshifted back (order-preserving, same
9882
10396
  * Envelope objects/ids — never rebuilt, so a retry is exactly the resend
9883
10397
  * Wave 1's server-side dedup expects) and retried after a short backoff,
9884
- * re-reading `this.mode` each time so a WS recovery that happens
9885
- * mid-retry is honored on the very next loop iteration instead of only
9886
- * after this attempt's backoff chain gives up.
9887
- *
9888
10398
  * Re-entrancy is guarded by `draining`: a call arriving while a drain is
9889
10399
  * already in progress just returns — the in-progress loop's own
9890
- * `while (this.outbox.length > 0)` check will pick up anything newly
9891
- * pushed (or left over after a mode switch) on its very next iteration.
10400
+ * `while (this.outbox.length > 0)` check picks up anything newly pushed.
9892
10401
  */
9893
10402
  async drainOutbox() {
9894
- if (this.draining) return;
10403
+ if (!this.started || this.draining) return;
9895
10404
  this.draining = true;
10405
+ const isolateRejectedBatch = async (batch) => {
10406
+ const pending = [[...batch]];
10407
+ while (pending.length > 0) {
10408
+ if (this.stopped || this.revoked) return pending.flat();
10409
+ const segment = pending.shift();
10410
+ const result = await this.longPoll.postBatch(segment);
10411
+ if (result === void 0) return [...segment, ...pending.flat()];
10412
+ if ((result.rejected ?? 0) === 0) continue;
10413
+ if (segment.length === 1) {
10414
+ this.quarantineRejectedOutbound(segment[0], "inbound_rejected");
10415
+ continue;
10416
+ }
10417
+ const midpoint = Math.floor(segment.length / 2);
10418
+ pending.unshift(segment.slice(0, midpoint), segment.slice(midpoint));
10419
+ }
10420
+ return void 0;
10421
+ };
9896
10422
  try {
9897
10423
  while (this.outbox.length > 0) {
9898
10424
  if (this.stopped || this.revoked) return;
9899
- if (this.mode === "ws") {
9900
- if (!this.ws.isOpen) return;
9901
- const envelope = this.outbox[0];
9902
- if (!this.ws.sendNow(envelope)) return;
9903
- this.outbox.shift();
9904
- continue;
9905
- }
9906
10425
  const batch = this.outbox.splice(0, MAX_MESSAGES_PER_BATCH);
9907
10426
  this.inFlightBatchSize = batch.length;
9908
- let ok;
10427
+ let retrySegments;
9909
10428
  try {
9910
- ok = await this.longPoll.postBatch(batch);
10429
+ retrySegments = await isolateRejectedBatch(batch);
9911
10430
  } finally {
9912
10431
  this.inFlightBatchSize = 0;
9913
10432
  }
9914
- if (ok) {
10433
+ if (retrySegments === void 0) {
9915
10434
  this.uploadRetryAttempt = 0;
9916
10435
  this.opts.onOperationalOutcome?.("success", "upload");
9917
10436
  continue;
9918
10437
  }
9919
10438
  this.opts.onOperationalOutcome?.("failure", "upload");
9920
- this.outbox.unshift(...batch);
10439
+ this.outbox.unshift(...retrySegments);
9921
10440
  if (this.stopped || this.revoked) return;
9922
10441
  const baseMs = this.opts.longPollRetryDelayMs ?? 2e3;
9923
10442
  await this.drainRetryDelay(this.fleetJitter.delay("upload", this.uploadRetryAttempt++, baseMs));
@@ -9933,8 +10452,7 @@ var ConnectionManager = class {
9933
10452
  * in-flight wait immediately instead of leaving `drainOutbox` parked here
9934
10453
  * for up to the rest of the delay before it next checks `this.revoked` —
9935
10454
  * and (b) unref'd, so the timer never keeps the Node process alive by
9936
- * itself while nothing else (a live long-poll GET, an open WS connection)
9937
- * legitimately is.
10455
+ * itself while nothing else (such as the live long-poll GET) legitimately is.
9938
10456
  */
9939
10457
  drainRetryDelay(ms) {
9940
10458
  return new Promise((resolve) => {
@@ -9950,45 +10468,41 @@ var ConnectionManager = class {
9950
10468
  };
9951
10469
  });
9952
10470
  }
9953
- isTransportDegraded() {
9954
- return this.mode === "long-poll";
9955
- }
9956
10471
  /**
9957
- * The capabilities the CURRENT transport's server advertised: from
9958
- * `conn.ack` on WS, or the latest successful `GET /byok/events` response
9959
- * on long-poll. Empty before either transport has supplied its current
9960
- * advertisement, and cleared across disconnect/switch boundaries.
10472
+ * The capabilities the latest successful `GET /byok/events` response
10473
+ * advertised. Empty before a successful response and after a failed one.
9961
10474
  */
9962
10475
  getServerCapabilities() {
9963
10476
  return this.serverCapabilities;
9964
10477
  }
10478
+ getTerminalError() {
10479
+ return this.terminalError;
10480
+ }
9965
10481
  isConnected() {
9966
- return this.mode === "ws" && this.ws.isOpen;
10482
+ return this.connected;
9967
10483
  }
9968
10484
  isRevoked() {
9969
10485
  return this.revoked;
9970
10486
  }
9971
10487
  /**
9972
- * Resolves once the connection has settled either a working, acked WS
9973
- * connection, or the long-poll fallback taking over (protocol §8). This
9974
- * lets `daemon.start()` return promptly even when WS is unavailable from
9975
- * the very first attempt, rather than hanging until a WS `conn.ack` that
9976
- * may never come.
10488
+ * Resolves after the first successful long-poll response establishes the
10489
+ * authenticated connection.
9977
10490
  *
9978
10491
  * Rejects with {@link DeviceRevokedError} — instead of hanging until
9979
10492
  * `timeoutMs` — if the device turns out to be revoked while settling (or
9980
10493
  * already was): a cold `daemon.start()` against an already-revoked device
9981
10494
  * must fail fast, not surface a generic timeout (protocol §6.3).
9982
10495
  */
9983
- waitForAck(timeoutMs = 1e4) {
9984
- if (this.ws.isOpen || this.mode === "long-poll") return Promise.resolve();
10496
+ waitForConnection(timeoutMs = 1e4) {
10497
+ if (this.connected) return Promise.resolve();
10498
+ if (this.terminalError) return Promise.reject(this.terminalError);
9985
10499
  if (this.revoked) return Promise.reject(new DeviceRevokedError());
9986
10500
  return new Promise((resolve, reject) => {
9987
10501
  let settle = () => {
9988
10502
  };
9989
10503
  const timer = setTimeout(() => {
9990
10504
  this.settledWaiters = this.settledWaiters.filter((w) => w !== settle);
9991
- reject(new Error("Timed out waiting for the connection to settle (WS ack or long-poll fallback)"));
10505
+ reject(new Error("Timed out waiting for the long-poll connection to settle"));
9992
10506
  }, timeoutMs);
9993
10507
  settle = (err) => {
9994
10508
  clearTimeout(timer);
@@ -9999,7 +10513,7 @@ var ConnectionManager = class {
9999
10513
  });
10000
10514
  }
10001
10515
  /**
10002
- * Stops both transports and waits for every in-flight envelope handler
10516
+ * Stops the long-poll transport and waits for every in-flight envelope handler
10003
10517
  * (the F3 FIFO chain) and the most recent cursor write to actually land on
10004
10518
  * disk — otherwise a `stop()` racing a just-processed envelope's
10005
10519
  * persistence could lose that cursor advance, or leave a handler running
@@ -10008,14 +10522,14 @@ var ConnectionManager = class {
10008
10522
  * Finding F5(b) (cross-model adversarial review): `drainTimeoutMs`, when
10009
10523
  * passed, bounds how long this waits for the shared outbox (`this.outbox`
10010
10524
  * — Design B) to actually finish draining BEFORE flipping `this.stopped`
10011
- * and closing the transports. Before this fix, `stop()` set `stopped`
10525
+ * and stopping the transport. Before this fix, `stop()` set `stopped`
10012
10526
  * synchronously and never waited for `drainOutbox` at all: an envelope
10013
10527
  * `send()` had just pushed moments earlier (e.g. `TaskRunner.shutdownTask`'s
10014
10528
  * own `task.fail`, sent right before `create-daemon.ts`'s
10015
10529
  * `performControlShutdown` calls this) could still be sitting UNSENT in
10016
10530
  * `this.outbox` — mid long-poll retry backoff, or simply not yet picked up
10017
10531
  * by the fire-and-forget `drainOutbox()` `send()` kicked off — and this
10018
- * method would happily proceed to `stopped = true` / `ws.close()` regardless,
10532
+ * method would happily proceed to `stopped = true` regardless,
10019
10533
  * after which NOTHING ever drains it again: silently lost, even though
10020
10534
  * `TaskRunner` believed it had been sent. `drainTimeoutMs` omitted (the
10021
10535
  * default) preserves the EXACT prior behavior for every other existing
@@ -10032,9 +10546,9 @@ var ConnectionManager = class {
10032
10546
  }
10033
10547
  this.stopped = true;
10034
10548
  this.serverCapabilities = [];
10035
- if (this.wsRetryTimer) clearInterval(this.wsRetryTimer);
10036
10549
  this.longPoll.stop();
10037
- this.ws.close();
10550
+ this.connected = false;
10551
+ this.opts.onStateChange?.("closed");
10038
10552
  await this.processingChain;
10039
10553
  await this.pendingCursorSave;
10040
10554
  }
@@ -10059,6 +10573,10 @@ var ConnectionManager = class {
10059
10573
  outboxLength() {
10060
10574
  return this.outbox.length + this.inFlightBatchSize;
10061
10575
  }
10576
+ /** A bounded terminal quarantine for operator inspection; these entries are never retried. */
10577
+ rejectedOutbox() {
10578
+ return this.rejectedOutboundEnvelopes;
10579
+ }
10062
10580
  /**
10063
10581
  * Finding F5(b): polls {@link outboxLength} (not `this.outbox.length`
10064
10582
  * alone — see that method's own doc comment for why a spliced-out,
@@ -10066,15 +10584,14 @@ var ConnectionManager = class {
10066
10584
  * a single `drainOutbox()` promise directly — a drain in progress can
10067
10585
  * itself loop through multiple retry/backoff cycles (`drainRetryDelay`)
10068
10586
  * while the server is unreachable, and a fresh, INDEPENDENT
10069
- * `drainOutbox()` call can also be triggered concurrently (`send()`, a
10070
- * mode switch's own `void this.drainOutbox()`) — polling the one thing
10587
+ * `drainOutbox()` call can also be triggered concurrently (`send()`)
10588
+ * polling the one thing
10071
10589
  * that actually matters (is anything still undelivered) can never go
10072
10590
  * stale the way capturing one specific in-flight promise reference
10073
10591
  * could. Kicks off one more `drainOutbox()` attempt itself first
10074
10592
  * (harmless no-op if one is already running — see its own re-entrancy
10075
- * guard) in case nothing is currently actively retrying (e.g. WS just
10076
- * dropped and long-poll hasn't taken over yet), so this bounded wait
10077
- * isn't just passively hoping something else happens to be making
10593
+ * guard) in case nothing is currently actively retrying, so this bounded
10594
+ * wait isn't just passively hoping something else happens to be making
10078
10595
  * progress.
10079
10596
  */
10080
10597
  waitForOutboxDrained(timeoutMs) {
@@ -10117,19 +10634,21 @@ var ConnectionManager = class {
10117
10634
  deliver(envelope) {
10118
10635
  const tracked = isCursorEnvelopeType(envelope.type) && typeof envelope.seq === "number";
10119
10636
  const watermark = this.dedupWatermark();
10120
- if (tracked && watermark !== void 0 && envelope.seq <= watermark) return;
10637
+ if (tracked && watermark !== void 0 && envelope.seq <= watermark) return false;
10121
10638
  if (tracked) {
10122
10639
  const seq = envelope.seq;
10123
- if (this.inFlightSeqs.has(seq) || this.processedSeqs.has(seq)) return;
10640
+ if (this.inFlightSeqs.has(seq) || this.processedSeqs.has(seq)) return false;
10124
10641
  this.inFlightSeqs.add(seq);
10125
10642
  this.noteDelivered(seq);
10126
10643
  }
10127
10644
  this.processingChain = this.processingChain.then(() => this.process(envelope, tracked));
10645
+ return true;
10128
10646
  }
10129
10647
  /**
10130
- * Design A: the watermark `deliver()` dedupes inbound `task.*` envelopes
10131
- * against, and the same value `LongPollClient` queries the next
10132
- * `GET /byok/events` cursor with (see the constructor). Normally this is
10648
+ * The local watermark `deliver()` dedupes inbound `task.*` envelopes
10649
+ * against. It is deliberately NOT the long-poll query cursor: that query
10650
+ * is the kernel acknowledgement and uses only the successfully processed
10651
+ * `cursor` (see the constructor). Normally this local watermark is
10133
10652
  * `deliveredSeq` — which is always >= `cursor` (every envelope that
10134
10653
  * reaches `advanceCursor` already passed through `noteDelivered` first,
10135
10654
  * see `deliver`) — so this is the literal `max(cursor, deliveredSeq)` the
@@ -10145,11 +10664,9 @@ var ConnectionManager = class {
10145
10664
  * whose outcome wasn't known yet. No separate "reset deliveredSeq on
10146
10665
  * reconnect" step is needed for this to be correct — collapsing to
10147
10666
  * `cursor` exactly while stalled already produces the right answer on
10148
- * every redelivery path (long-poll re-query AND a WS reconnect's
10149
- * backlog replay alike), and NOT resetting it unconditionally on every
10150
- * reconnect is what lets `deliveredSeq` keep doing its job of not
10151
- * re-pulling/re-dispatching something already in flight across a
10152
- * reconnect that happens to land while a handler is still running.
10667
+ * every long-poll retry path. NOT resetting it unconditionally on every
10668
+ * retry lets `deliveredSeq` keep doing its job of not re-dispatching
10669
+ * something already in flight while a handler is still running.
10153
10670
  */
10154
10671
  dedupWatermark() {
10155
10672
  return this.stalledAtSeq !== void 0 ? this.cursor : this.deliveredSeq ?? this.cursor;
@@ -10167,11 +10684,13 @@ var ConnectionManager = class {
10167
10684
  }
10168
10685
  await this.opts.onEnvelope(envelope);
10169
10686
  if (!tracked) return;
10170
- this.processedSeqs.add(seq);
10171
- if (this.stalledAtSeq !== void 0 && envelope.seq !== this.stalledAtSeq) return;
10687
+ if (this.stalledAtSeq !== void 0 && envelope.seq !== this.stalledAtSeq) {
10688
+ this.processedSeqs.add(seq);
10689
+ return;
10690
+ }
10172
10691
  this.stalledAtSeq = void 0;
10692
+ await this.advanceCursor(envelope.seq);
10173
10693
  this.processedSeqs.clear();
10174
- this.advanceCursor(envelope.seq);
10175
10694
  } catch (err) {
10176
10695
  if (tracked && this.stalledAtSeq === void 0) this.stalledAtSeq = envelope.seq;
10177
10696
  console.error(
@@ -10185,9 +10704,8 @@ var ConnectionManager = class {
10185
10704
  /**
10186
10705
  * M4 Phase 4 (version-negotiation drill fix): `LongPollClient` calls this
10187
10706
  * for a batch entry it could not parse into a known `Envelope` at all (an
10188
- * unrecognized message type mirrors `ws-transport.ts`'s identical
10189
- * per-frame tolerance, see `long-poll-transport.ts`'s own doc comment on
10190
- * `parseLooseEventsPollResponse`) but which still carried a numeric,
10707
+ * unrecognized message type (see `long-poll-transport.ts`'s own doc
10708
+ * comment on `parseLooseEventsPollResponse`) but which still carried a numeric,
10191
10709
  * task-class envelope-level `seq` (the caller only invokes this for a
10192
10710
  * `task.`-prefixed type — see `long-poll-transport.ts`'s own
10193
10711
  * `extractSkippableSeq`; `conn.*`-shaped or type-less entries never reach
@@ -10227,7 +10745,7 @@ var ConnectionManager = class {
10227
10745
  * `noteDelivered` (the eager, in-memory watermark) stays UNCHAINED —
10228
10746
  * called immediately, unconditionally, regardless of `stalledAtSeq` —
10229
10747
  * matching `deliver()`'s own eager, unconditional call for a real
10230
- * envelope: its only job is "don't re-pull something already handed off,"
10748
+ * envelope: its only job is "don't re-dispatch something already handed off,"
10231
10749
  * independent of outcome, and that property does not depend on FIFO
10232
10750
  * ordering the way the DURABLE cursor does.
10233
10751
  *
@@ -10247,9 +10765,18 @@ var ConnectionManager = class {
10247
10765
  */
10248
10766
  noteSkippedSeq(seq) {
10249
10767
  this.noteDelivered(seq);
10250
- this.processingChain = this.processingChain.then(() => {
10768
+ this.processingChain = this.processingChain.then(async () => {
10251
10769
  if (this.stalledAtSeq === void 0 || seq === this.stalledAtSeq) {
10252
- this.advanceCursor(seq);
10770
+ try {
10771
+ await this.advanceCursor(seq);
10772
+ if (this.stalledAtSeq === seq) this.stalledAtSeq = void 0;
10773
+ } catch (err) {
10774
+ if (this.stalledAtSeq === void 0) this.stalledAtSeq = seq;
10775
+ console.error(
10776
+ `[byok/client] unknown task seq=${seq} could not durably persist its cursor; cursor left unadvanced for redelivery:`,
10777
+ err
10778
+ );
10779
+ }
10253
10780
  }
10254
10781
  });
10255
10782
  }
@@ -10295,94 +10822,52 @@ var ConnectionManager = class {
10295
10822
  if (this.stalledAtSeq === void 0) this.stalledAtSeq = seq;
10296
10823
  });
10297
10824
  }
10298
- advanceCursor(seq) {
10825
+ async advanceCursor(seq) {
10299
10826
  if (this.cursor !== void 0 && seq <= this.cursor) return;
10827
+ const save = this.pendingCursorSave.catch(() => void 0).then(() => this.opts.cursorStore.save(this.opts.serverUrl, this.opts.deviceId, seq));
10828
+ this.pendingCursorSave = save;
10829
+ await save;
10300
10830
  this.cursor = seq;
10301
- this.pendingCursorSave = this.pendingCursorSave.catch(() => {
10302
- }).then(() => this.opts.cursorStore.save(this.opts.serverUrl, this.opts.deviceId, seq)).catch(() => {
10303
- });
10304
- }
10305
- /**
10306
- * Fires the moment a connection attempt reaches `conn.ack` — independent
10307
- * of whether/when it later closes. This is the ONLY place that can
10308
- * reliably detect "WS is back up" while long-polling: a healthy
10309
- * connection stays open indefinitely, so it never reaches `onWsOutcome`
10310
- * (which is close-only) at all.
10311
- */
10312
- onAcked(capabilities) {
10313
- this.serverCapabilities = capabilities;
10314
- this.consecutiveFailures = 0;
10315
- this.notifySettled();
10316
- this.opts.onOperationalOutcome?.("success", "reconnect");
10317
- if (this.mode === "long-poll") this.exitLongPoll();
10318
- void this.drainOutbox();
10319
10831
  }
10320
- onWsOutcome(acked, err) {
10321
- if (this.stopped || this.revoked) return;
10322
- if (acked) this.serverCapabilities = [];
10323
- if (err instanceof WsUnexpectedStatusError && err.status === 401) {
10324
- this.opts.auth.handleUnauthorized().catch((renewErr) => {
10325
- if (renewErr instanceof DeviceRevokedError) this.enterRevoked();
10326
- });
10327
- }
10328
- if (acked) return;
10329
- this.opts.onOperationalOutcome?.("failure", "reconnect");
10330
- this.consecutiveFailures += 1;
10331
- if (this.mode === "ws" && this.consecutiveFailures >= (this.opts.wsFailureThreshold ?? 3)) {
10332
- this.enterLongPoll();
10333
- }
10832
+ quarantineRejectedOutbound(envelope, reason) {
10833
+ if (this.rejectedOutboundEnvelopes.length === MAX_REJECTED_OUTBOX_ENTRIES) this.rejectedOutboundEnvelopes.shift();
10834
+ this.rejectedOutboundEnvelopes.push({ envelope, reason });
10835
+ console.error(
10836
+ `[byok/client] outbound envelope ${envelope.id} was permanently rejected (${reason}) and moved to the terminal quarantine.`
10837
+ );
10334
10838
  }
10335
10839
  notifySettled(err) {
10336
10840
  for (const waiter of this.settledWaiters.splice(0)) waiter(err);
10337
10841
  }
10338
- enterLongPoll() {
10339
- this.mode = "long-poll";
10340
- this.serverCapabilities = [];
10341
- this.ws.stopAutoReconnect();
10342
- this.opts.onStateChange?.("degraded");
10343
- this.longPoll.start();
10344
- this.outbox.unshift(createConnectionHelloEnvelope({
10345
- deviceId: this.opts.deviceId,
10346
- productId: this.opts.productId,
10347
- capabilities: this.opts.capabilities,
10348
- clientVersion: this.opts.clientVersion,
10349
- runtimes: this.opts.runtimes,
10350
- getConfiguredToolsets: this.opts.getConfiguredToolsets,
10351
- getCursor: () => this.cursor
10352
- }));
10842
+ noteConnected() {
10843
+ if (this.connected || this.stopped || this.revoked || this.terminalError) return;
10844
+ this.connected = true;
10845
+ this.opts.onStateChange?.("open");
10353
10846
  this.notifySettled();
10354
- void this.drainOutbox();
10355
- this.scheduleWsProbe();
10356
10847
  }
10357
- exitLongPoll() {
10358
- if (this.wsRetryTimer) {
10359
- clearInterval(this.wsRetryTimer);
10360
- this.wsRetryTimer = void 0;
10361
- }
10362
- this.longPoll.stop();
10363
- this.mode = "ws";
10364
- this.consecutiveFailures = 0;
10365
- this.ws.resumeAutoReconnect();
10366
- void this.drainOutbox();
10848
+ noteDisconnected() {
10849
+ if (!this.connected || this.stopped || this.revoked || this.terminalError) return;
10850
+ this.connected = false;
10851
+ this.opts.onStateChange?.("connecting");
10367
10852
  }
10368
- scheduleWsProbe() {
10369
- const baseMs = this.opts.wsRetryIntervalMs ?? 5 * 60 * 1e3;
10370
- const delayMs = this.fleetJitter.delay("reconnect", this.wsProbeSequence++, baseMs);
10371
- const timer = setTimeout(() => {
10372
- this.wsRetryTimer = void 0;
10373
- if (this.stopped || this.revoked || this.mode !== "long-poll") return;
10374
- this.ws.connect({ auto: false });
10375
- this.scheduleWsProbe();
10376
- }, delayMs);
10377
- timer.unref?.();
10378
- this.wsRetryTimer = timer;
10853
+ enterReplayCursorTooOld(error) {
10854
+ if (this.terminalError) return;
10855
+ this.terminalError = error;
10856
+ this.stopped = true;
10857
+ this.serverCapabilities = [];
10858
+ this.longPoll.stop();
10859
+ this.connected = false;
10860
+ this.cancelPendingDrainRetry?.();
10861
+ this.notifySettled(error);
10862
+ this.opts.onStateChange?.("closed");
10863
+ this.opts.onTerminalError?.(error);
10379
10864
  }
10380
10865
  enterRevoked() {
10381
10866
  if (this.revoked) return;
10382
10867
  this.revoked = true;
10383
- if (this.wsRetryTimer) clearInterval(this.wsRetryTimer);
10868
+ this.serverCapabilities = [];
10384
10869
  this.longPoll.stop();
10385
- this.ws.close();
10870
+ this.connected = false;
10386
10871
  this.cancelPendingDrainRetry?.();
10387
10872
  this.opts.onStateChange?.("revoked");
10388
10873
  this.notifySettled(new DeviceRevokedError());
@@ -13083,21 +13568,28 @@ async function recordAuditWarning(context, kind, values) {
13083
13568
  }
13084
13569
  }
13085
13570
  var agentMemoryHomeQueues = /* @__PURE__ */ new Map();
13086
- async function exclusiveAgentMemoryHome(home, fn) {
13087
- const previous = agentMemoryHomeQueues.get(home) ?? Promise.resolve();
13571
+ var agentMemoryProjectionTransactionQueues = /* @__PURE__ */ new Map();
13572
+ async function exclusiveAgentMemoryHomeQueue(queues2, home, fn) {
13573
+ const previous = queues2.get(home) ?? Promise.resolve();
13088
13574
  let release;
13089
13575
  const next = new Promise((resolve) => {
13090
13576
  release = resolve;
13091
13577
  });
13092
- agentMemoryHomeQueues.set(home, next);
13578
+ queues2.set(home, next);
13093
13579
  await previous;
13094
13580
  try {
13095
13581
  return await fn();
13096
13582
  } finally {
13097
13583
  release();
13098
- if (agentMemoryHomeQueues.get(home) === next) agentMemoryHomeQueues.delete(home);
13584
+ if (queues2.get(home) === next) queues2.delete(home);
13099
13585
  }
13100
13586
  }
13587
+ async function exclusiveAgentMemoryHome(home, fn) {
13588
+ return exclusiveAgentMemoryHomeQueue(agentMemoryHomeQueues, home, fn);
13589
+ }
13590
+ async function exclusiveAgentMemoryProjectionTransaction(home, fn) {
13591
+ return exclusiveAgentMemoryHomeQueue(agentMemoryProjectionTransactionQueues, home, fn);
13592
+ }
13101
13593
  var AgentMemoryService = class {
13102
13594
  constructor(input) {
13103
13595
  this.input = input;
@@ -13401,13 +13893,16 @@ var AgentMemoryRedactedOutbox = class _AgentMemoryRedactedOutbox {
13401
13893
  async function snapshotAndProjectAgentMemory(input, projection) {
13402
13894
  if (projection?.capability !== AGENT_MEMORY_PROJECTION_CAPABILITY || !projection.grant || !projection.redactor || !projection.port) return;
13403
13895
  const context = taskContext(input);
13404
- const outbox = await AgentMemoryRedactedOutbox.open(context, projection.grant);
13405
- const initialReplay = await outbox.replay(projection.port);
13406
- if (initialReplay.status === "pending") throw new AgentMemoryProjectionReplayPendingError(initialReplay);
13407
- const snapshot = await captureAgentMemorySnapshot(context);
13408
- await outbox.append(redactedBytes(snapshot, await projection.redactor.redact(snapshot)));
13409
- const trailingReplay = await outbox.replay(projection.port);
13410
- if (trailingReplay.status === "pending") throw new AgentMemoryProjectionReplayPendingError(trailingReplay);
13896
+ const { grant, redactor, port } = projection;
13897
+ await exclusiveAgentMemoryProjectionTransaction(context.canonicalHome, async () => {
13898
+ const outbox = await AgentMemoryRedactedOutbox.open(context, grant);
13899
+ const initialReplay = await outbox.replay(port);
13900
+ if (initialReplay.status === "pending") throw new AgentMemoryProjectionReplayPendingError(initialReplay);
13901
+ const snapshot = await captureAgentMemorySnapshot(context);
13902
+ await outbox.append(redactedBytes(snapshot, await redactor.redact(snapshot)));
13903
+ const trailingReplay = await outbox.replay(port);
13904
+ if (trailingReplay.status === "pending") throw new AgentMemoryProjectionReplayPendingError(trailingReplay);
13905
+ });
13411
13906
  }
13412
13907
  var AGENT_MEMORY_FILESYSTEM_HELPER_PROTOCOL = 2;
13413
13908
  var AGENT_MEMORY_FILESYSTEM_HELPER_VERSION = "2";
@@ -13706,6 +14201,7 @@ function resultDocumentRejectionDetail(check) {
13706
14201
  }
13707
14202
  }
13708
14203
  var DEFAULT_MAX_TASK_OUTPUT_BYTES = 64 * 1024 * 1024;
14204
+ var DEFAULT_MAX_CONCURRENT_MUTABLE_SESSIONS_PER_AGENT_HOME = 1;
13709
14205
  function isKnownRuntimeId(id) {
13710
14206
  return RuntimeIdSchema.safeParse(id).success;
13711
14207
  }
@@ -13815,6 +14311,7 @@ var TaskRunner = class {
13815
14311
  deps;
13816
14312
  tasks = /* @__PURE__ */ new Map();
13817
14313
  pendingMessageTasks = /* @__PURE__ */ new Map();
14314
+ messageOutboxesByHome = /* @__PURE__ */ new Map();
13818
14315
  messageContextByToken = /* @__PURE__ */ new Map();
13819
14316
  messageContextByTask = /* @__PURE__ */ new Map();
13820
14317
  memoryContextByToken = /* @__PURE__ */ new Map();
@@ -13875,6 +14372,8 @@ var TaskRunner = class {
13875
14372
  * costs nothing.
13876
14373
  */
13877
14374
  inFlightOffers = /* @__PURE__ */ new Set();
14375
+ /** Blob I/O before an offer becomes an active task still belongs to that offer's cancellation authority. */
14376
+ inFlightBlobAborts = /* @__PURE__ */ new Map();
13878
14377
  /**
13879
14378
  * Finding P2 (Fix 2c): taskIds that have reached a terminal outcome
13880
14379
  * (Complete/Failed/Cancelled) this session — populated in `finish()`.
@@ -13949,6 +14448,10 @@ var TaskRunner = class {
13949
14448
  get maxTaskOutputBytes() {
13950
14449
  return this.deps.maxTaskOutputBytes ?? DEFAULT_MAX_TASK_OUTPUT_BYTES;
13951
14450
  }
14451
+ /** WP0: effective per-canonical-Agent-home Attempt cap — see {@link DEFAULT_MAX_CONCURRENT_MUTABLE_SESSIONS_PER_AGENT_HOME}. */
14452
+ get maxConcurrentMutableSessionsPerAgentHome() {
14453
+ return this.deps.maxConcurrentMutableSessionsPerAgentHome ?? DEFAULT_MAX_CONCURRENT_MUTABLE_SESSIONS_PER_AGENT_HOME;
14454
+ }
13952
14455
  /**
13953
14456
  * M4 Phase 4 (part B.3, observability): per-active-task queue watermarks
13954
14457
  * for the control socket's `status` result — see
@@ -14001,6 +14504,7 @@ var TaskRunner = class {
14001
14504
  async recoverAgentMessageOutboxes(agentsRoot) {
14002
14505
  if (this.deps.tenantId === void 0) throw new Error("Agent message recovery requires authenticated tenant enrollment");
14003
14506
  for (const outbox of await AgentMessageOutbox.recover(agentsRoot, this.deps.tenantId)) {
14507
+ this.messageOutboxesByHome.set(outbox.homeDir, Promise.resolve(outbox));
14004
14508
  for (const record3 of outbox.retryableRecords()) {
14005
14509
  if (record3.sessionRef === void 0) continue;
14006
14510
  const existing = this.recoveredMessageOutboxes.get(record3.taskId);
@@ -14009,6 +14513,16 @@ var TaskRunner = class {
14009
14513
  }
14010
14514
  }
14011
14515
  }
14516
+ agentMessageOutbox(homeDir) {
14517
+ const existing = this.messageOutboxesByHome.get(homeDir);
14518
+ if (existing !== void 0) return existing;
14519
+ const opened = AgentMessageOutbox.open(homeDir);
14520
+ this.messageOutboxesByHome.set(homeDir, opened);
14521
+ void opened.catch(() => {
14522
+ if (this.messageOutboxesByHome.get(homeDir) === opened) this.messageOutboxesByHome.delete(homeDir);
14523
+ });
14524
+ return opened;
14525
+ }
14012
14526
  /** Retry stable recovered records after a transport handshake/re-handshake. */
14013
14527
  retryRecoveredAgentMessages() {
14014
14528
  for (const [taskId, outbox] of this.recoveredMessageOutboxes) {
@@ -14164,6 +14678,7 @@ var TaskRunner = class {
14164
14678
  if (active.finalizationStarted) return this.finish(active.taskId);
14165
14679
  if (!this.reserveSemanticTerminal(active)) return active.semanticTerminalSettled ?? false;
14166
14680
  active.beingTornDown = true;
14681
+ active.blobAbort.abort();
14167
14682
  await this.observeGit(active, "salvage");
14168
14683
  const timeoutMs = this.deps.shutdownInterruptTimeoutMs ?? DEFAULT_SHUTDOWN_INTERRUPT_TIMEOUT_MS;
14169
14684
  await raceSettleFirst(() => active.session.interrupt(), timeoutMs);
@@ -14310,6 +14825,8 @@ var TaskRunner = class {
14310
14825
  return;
14311
14826
  }
14312
14827
  this.inFlightOffers.add(taskId);
14828
+ const blobAbort = new AbortController();
14829
+ this.inFlightBlobAborts.set(taskId, blobAbort);
14313
14830
  let agentBinding;
14314
14831
  let agentLeaseTransferred = false;
14315
14832
  try {
@@ -14324,6 +14841,24 @@ var TaskRunner = class {
14324
14841
  decline("strict Agent-only daemon refuses legacy task offers", false);
14325
14842
  return;
14326
14843
  }
14844
+ if (agentRef !== void 0) {
14845
+ const limit = this.maxConcurrentMutableSessionsPerAgentHome;
14846
+ let canonicalHome;
14847
+ try {
14848
+ canonicalHome = await this.deps.agentHome.layout.canonicalHomePath(agentRef);
14849
+ } catch (error) {
14850
+ decline(
14851
+ `Agent home admission failed: ${errorMessage6(error)}`,
14852
+ !(error instanceof AgentHomeResolutionError)
14853
+ );
14854
+ return;
14855
+ }
14856
+ const active2 = this.deps.agentHome.executionLeaseManager.activeAttemptCount(canonicalHome);
14857
+ if (active2 >= limit) {
14858
+ decline(`agent home busy: ${active2} active attempt(s)`, true);
14859
+ return;
14860
+ }
14861
+ }
14327
14862
  const guarded = this.deps.admissionGuard?.({ taskId, payload });
14328
14863
  if (guarded !== void 0 && !guarded.admit) {
14329
14864
  decline(guarded.reason, guarded.retryable);
@@ -14468,7 +15003,7 @@ var TaskRunner = class {
14468
15003
  }
14469
15004
  if (agentRef !== void 0) {
14470
15005
  try {
14471
- agentBinding = await this.deps.agentHome.acquire(agentRef);
15006
+ agentBinding = await this.deps.agentHome.acquireExecution(agentRef, { taskId, sessionRef });
14472
15007
  } catch (error) {
14473
15008
  decline(
14474
15009
  `Agent home admission failed: ${errorMessage6(error)}`,
@@ -14503,7 +15038,7 @@ var TaskRunner = class {
14503
15038
  }
14504
15039
  }
14505
15040
  try {
14506
- await this.deps.agentHome.initialize(agentBinding);
15041
+ await this.deps.agentHome.initializeExecution(agentBinding);
14507
15042
  } catch (error) {
14508
15043
  await agentBinding.lease.release().catch(() => {
14509
15044
  });
@@ -14513,7 +15048,7 @@ var TaskRunner = class {
14513
15048
  }
14514
15049
  if (messageRequirement !== void 0) {
14515
15050
  try {
14516
- const outbox = await AgentMessageOutbox.open(agentBinding.resolution.canonicalHome);
15051
+ const outbox = await this.agentMessageOutbox(agentBinding.resolution.canonicalHome);
14517
15052
  this.pendingMessageTasks.set(taskId, {
14518
15053
  taskId,
14519
15054
  agentRef: agentBinding.resolution.agentRef,
@@ -14632,7 +15167,7 @@ var TaskRunner = class {
14632
15167
  );
14633
15168
  let resolvedInstruction;
14634
15169
  try {
14635
- resolvedInstruction = await this.resolveInstruction(payload.instruction);
15170
+ resolvedInstruction = await this.resolveInstruction(payload.instruction, blobAbort.signal);
14636
15171
  if (plainWorkspaceNeedsResolve) workspaceDir = await this.resolveWorkspaceDir(taskId, known?.workspaceDir);
14637
15172
  } catch (err) {
14638
15173
  gitLease?.release();
@@ -14745,9 +15280,29 @@ var TaskRunner = class {
14745
15280
  }
14746
15281
  return;
14747
15282
  }
15283
+ if (agentBinding !== void 0) {
15284
+ try {
15285
+ await agentBinding.lease.bindSession(session.sessionRef);
15286
+ } catch (error) {
15287
+ await session.close().catch(() => {
15288
+ });
15289
+ await this.failClaimedAgent(
15290
+ taskId,
15291
+ `Agent session execution lease could not bind the runtime session: ${errorMessage6(error)}`,
15292
+ false,
15293
+ {
15294
+ binding: agentBinding,
15295
+ runtimeId: pick.descriptor.id,
15296
+ sessionRef: session.sessionRef
15297
+ }
15298
+ );
15299
+ return;
15300
+ }
15301
+ }
14748
15302
  let active;
14749
15303
  active = {
14750
15304
  taskId,
15305
+ blobAbort,
14751
15306
  egressEnabled: "egressPolicy" in payload,
14752
15307
  adapter: pick.adapter,
14753
15308
  session,
@@ -14788,15 +15343,16 @@ var TaskRunner = class {
14788
15343
  ...terminalProjection === void 0 ? {} : { terminalProjection }
14789
15344
  };
14790
15345
  if (agentBinding !== void 0) {
15346
+ const binding = agentBinding;
14791
15347
  try {
14792
- await this.deps.agentSessionHandoffs.record({
14793
- agentRef: agentBinding.resolution.agentRef,
15348
+ await this.deps.agentHome.mutateExecution(binding, () => this.deps.agentSessionHandoffs.record({
15349
+ agentRef: binding.resolution.agentRef,
14794
15350
  taskId,
14795
15351
  sessionRef: session.sessionRef,
14796
15352
  runtimeId: pick.descriptor.id,
14797
15353
  cwd: workspaceDir,
14798
- leaseId: agentBinding.lease.leaseId
14799
- });
15354
+ leaseId: binding.lease.leaseId
15355
+ }));
14800
15356
  } catch (error) {
14801
15357
  await session.close().catch(() => {
14802
15358
  });
@@ -14838,6 +15394,7 @@ var TaskRunner = class {
14838
15394
  }
14839
15395
  this.deps.send(createEnvelope("task.started", {}, { taskId }));
14840
15396
  agentLeaseTransferred = agentBinding !== void 0;
15397
+ this.inFlightBlobAborts.delete(taskId);
14841
15398
  this.tasks.set(taskId, active);
14842
15399
  this.pendingMessageTasks.delete(taskId);
14843
15400
  if (active.messageOutbox !== void 0 && activatedMessageRecord !== void 0) {
@@ -14869,6 +15426,7 @@ var TaskRunner = class {
14869
15426
  await agentBinding.lease.release().catch(() => {
14870
15427
  });
14871
15428
  }
15429
+ this.inFlightBlobAborts.delete(taskId);
14872
15430
  this.inFlightOffers.delete(taskId);
14873
15431
  }
14874
15432
  }
@@ -15020,9 +15578,9 @@ var TaskRunner = class {
15020
15578
  void this.closeAgentMemoryFilesystem(taskId);
15021
15579
  }
15022
15580
  /** Protocol §7: an instruction too large to inline arrives as a `blobRef` — resolve it via the blob client rather than failing closed. */
15023
- async resolveInstruction(instruction) {
15581
+ async resolveInstruction(instruction, signal) {
15024
15582
  if (typeof instruction === "string") return instruction;
15025
- return this.deps.blobClient.resolveInstruction(instruction.blobRef);
15583
+ return this.deps.blobClient.resolveInstruction(instruction.blobRef, { signal });
15026
15584
  }
15027
15585
  /** Resolve every requested logical id locally and reject missing/colliding server authority before claim. */
15028
15586
  resolveMcpServers(requiredToolsets) {
@@ -15254,7 +15812,9 @@ var TaskRunner = class {
15254
15812
  }
15255
15813
  }
15256
15814
  try {
15257
- const blobRef = await this.deps.blobClient.uploadArtifact(bytes, contentType);
15815
+ const blobRef = await this.deps.blobClient.uploadArtifact(bytes, contentType, {
15816
+ signal: active.blobAbort.signal
15817
+ });
15258
15818
  this.deps.send(createEnvelope("task.artifact", { name, contentType, blobRef }, { taskId: active.taskId }));
15259
15819
  } catch (err) {
15260
15820
  this.reportArtifactError(active, name, `failed to upload artifact "${name}": ${errorMessage6(err)}`);
@@ -15269,6 +15829,7 @@ var TaskRunner = class {
15269
15829
  const active = this.tasks.get(taskId);
15270
15830
  if (!active) {
15271
15831
  this.setPendingCancelled(taskId, reason);
15832
+ this.inFlightBlobAborts.get(taskId)?.abort();
15272
15833
  return;
15273
15834
  }
15274
15835
  if (active.finalizationStarted) {
@@ -15279,6 +15840,7 @@ var TaskRunner = class {
15279
15840
  await active.semanticTerminalSettled;
15280
15841
  return;
15281
15842
  }
15843
+ active.blobAbort.abort();
15282
15844
  try {
15283
15845
  await active.session.interrupt();
15284
15846
  } catch {
@@ -15778,7 +16340,7 @@ var TaskRunner = class {
15778
16340
  async failClaimedAgent(taskId, reason, retryable, context) {
15779
16341
  const agentRef = context.binding.resolution.agentRef;
15780
16342
  const result = await this.retryAgentTerminalEvidence(
15781
- () => this.deps.agentSessionHandoffs.recordTaskTerminal({
16343
+ () => this.deps.agentHome.mutateExecution(context.binding, () => this.deps.agentSessionHandoffs.recordTaskTerminal({
15782
16344
  agentRef,
15783
16345
  taskId,
15784
16346
  runtimeId: context.runtimeId,
@@ -15786,7 +16348,7 @@ var TaskRunner = class {
15786
16348
  leaseId: context.binding.lease.leaseId,
15787
16349
  ...context.sessionRef === void 0 ? {} : { sessionRef: context.sessionRef },
15788
16350
  terminalReason: reason
15789
- })
16351
+ }))
15790
16352
  );
15791
16353
  if (!result.ok) {
15792
16354
  this.reportAgentTerminalEvidenceFailure({
@@ -16775,6 +17337,8 @@ var AgentEgressController = class {
16775
17337
  options;
16776
17338
  latest = new AgentLatestValueState();
16777
17339
  spools = /* @__PURE__ */ new Map();
17340
+ spoolOpens = /* @__PURE__ */ new Map();
17341
+ reliableAppendTail = Promise.resolve();
16778
17342
  latestStatus = emptyLane();
16779
17343
  reliableStatus = emptyLane();
16780
17344
  drops = [];
@@ -16829,7 +17393,8 @@ var AgentEgressController = class {
16829
17393
  return latest === void 0 ? [] : Object.freeze([latest]);
16830
17394
  }
16831
17395
  async appendReliable(input) {
16832
- if (!this.active || this.options.tenantId === void 0) {
17396
+ const tenantId = this.options.tenantId;
17397
+ if (!this.active || tenantId === void 0) {
16833
17398
  this.noteDrop("reliable", "policy_denied", input.agentRef);
16834
17399
  return { ok: false, reason: "policy_denied" };
16835
17400
  }
@@ -16844,17 +17409,21 @@ var AgentEgressController = class {
16844
17409
  return { ok: false, reason: "sanitizer_rejected" };
16845
17410
  }
16846
17411
  try {
16847
- const spool = await this.spoolFor(input.homeDir, input.agentRef);
16848
- const record3 = await spool.append({
16849
- agentRef: input.agentRef,
16850
- tenantId: this.options.tenantId,
16851
- policyRevision: this.options.policy.policyRevision,
16852
- payload,
16853
- sessionRef: input.sessionRef,
16854
- ...input.taskId === void 0 ? {} : { taskId: input.taskId },
16855
- ...input.eventId === void 0 ? {} : { eventId: input.eventId }
16856
- }, this.options.policy, this.tenantPendingBytes());
16857
- return { ok: true, record: record3 };
17412
+ const spoolOpen = this.spoolFor(input.homeDir, input.agentRef);
17413
+ return await this.withAppendTail(async () => {
17414
+ const spool = await spoolOpen;
17415
+ this.bindSpool(input.agentRef, spool, spoolOpen);
17416
+ const record3 = await spool.append({
17417
+ agentRef: input.agentRef,
17418
+ tenantId,
17419
+ policyRevision: this.options.policy.policyRevision,
17420
+ payload,
17421
+ sessionRef: input.sessionRef,
17422
+ ...input.taskId === void 0 ? {} : { taskId: input.taskId },
17423
+ ...input.eventId === void 0 ? {} : { eventId: input.eventId }
17424
+ }, this.options.policy, this.tenantPendingBytes());
17425
+ return { ok: true, record: record3 };
17426
+ });
16858
17427
  } catch (error) {
16859
17428
  const reason = error instanceof AgentReliableQuotaError ? error.reason : "backpressure";
16860
17429
  this.noteDrop("reliable", reason, input.agentRef);
@@ -16867,21 +17436,26 @@ var AgentEgressController = class {
16867
17436
  * with `wireType: agent.content.receipt` before any transport attempt.
16868
17437
  */
16869
17438
  async appendContentReceipt(input) {
16870
- if (!this.active || this.options.tenantId === void 0) {
17439
+ const tenantId = this.options.tenantId;
17440
+ if (!this.active || tenantId === void 0) {
16871
17441
  this.noteDrop("reliable", "policy_denied", input.agentRef);
16872
17442
  return { ok: false, reason: "policy_denied" };
16873
17443
  }
16874
17444
  try {
16875
- const spool = await this.spoolFor(input.homeDir, input.agentRef);
16876
- const record3 = await spool.appendContentReceipt({
16877
- agentRef: input.agentRef,
16878
- tenantId: this.options.tenantId,
16879
- policyRevision: this.options.policy.policyRevision,
16880
- sessionRef: input.payload.sessionRef,
16881
- payload: input.payload,
16882
- ...input.taskId === void 0 ? {} : { taskId: input.taskId }
16883
- }, this.options.policy, this.tenantPendingBytes());
16884
- return { ok: true, record: record3 };
17445
+ const spoolOpen = this.spoolFor(input.homeDir, input.agentRef);
17446
+ return await this.withAppendTail(async () => {
17447
+ const spool = await spoolOpen;
17448
+ this.bindSpool(input.agentRef, spool, spoolOpen);
17449
+ const record3 = await spool.appendContentReceipt({
17450
+ agentRef: input.agentRef,
17451
+ tenantId,
17452
+ policyRevision: this.options.policy.policyRevision,
17453
+ sessionRef: input.payload.sessionRef,
17454
+ payload: input.payload,
17455
+ ...input.taskId === void 0 ? {} : { taskId: input.taskId }
17456
+ }, this.options.policy, this.tenantPendingBytes());
17457
+ return { ok: true, record: record3 };
17458
+ });
16885
17459
  } catch (error) {
16886
17460
  const reason = error instanceof AgentReliableQuotaError ? error.reason : "backpressure";
16887
17461
  this.noteDrop("reliable", reason, input.agentRef);
@@ -16956,20 +17530,59 @@ var AgentEgressController = class {
16956
17530
  }
16957
17531
  return this.reliableRecords();
16958
17532
  }
16959
- async spoolFor(homeDir, agentRef) {
17533
+ spoolFor(homeDir, agentRef) {
16960
17534
  const key = agentKey(agentRef);
16961
17535
  const existing = this.spools.get(key);
16962
- if (existing) return existing;
16963
- const spool = await AgentReliableSpool.open(homeDir);
16964
- if (spool.records().some((record3) => !sameAgent(record3.agentRef, agentRef))) {
16965
- throw new Error("Agent-local reliable spool has a different AgentRef");
17536
+ if (existing) {
17537
+ if (existing.homeDir !== homeDir) throw new Error("Agent reliable spool is already bound to a different home");
17538
+ return Promise.resolve(existing);
17539
+ }
17540
+ const inFlight = this.spoolOpens.get(key);
17541
+ if (inFlight !== void 0) {
17542
+ if (inFlight.homeDir !== homeDir) throw new Error("Agent reliable spool opening is already bound to a different home");
17543
+ return inFlight.promise;
17544
+ }
17545
+ const opened = (async () => {
17546
+ const spool = await AgentReliableSpool.open(homeDir);
17547
+ if (spool.records().some((record3) => !sameAgent(record3.agentRef, agentRef))) {
17548
+ throw new Error("Agent-local reliable spool has a different AgentRef");
17549
+ }
17550
+ return spool;
17551
+ })();
17552
+ const slot = { homeDir, promise: opened };
17553
+ this.spoolOpens.set(key, slot);
17554
+ void opened.catch(() => {
17555
+ if (this.spoolOpens.get(key) === slot) this.spoolOpens.delete(key);
17556
+ });
17557
+ return opened;
17558
+ }
17559
+ bindSpool(agentRef, spool, spoolOpen) {
17560
+ const key = agentKey(agentRef);
17561
+ const existing = this.spools.get(key);
17562
+ try {
17563
+ if (existing && existing !== spool) throw new Error("multiple Agent egress spools claim the same AgentRef");
17564
+ this.spools.set(key, spool);
17565
+ } finally {
17566
+ const slot = this.spoolOpens.get(key);
17567
+ if (slot?.promise === spoolOpen) this.spoolOpens.delete(key);
16966
17568
  }
16967
- this.spools.set(key, spool);
16968
- return spool;
16969
17569
  }
16970
17570
  tenantPendingBytes() {
16971
17571
  return this.reliableRecords().reduce((total, record3) => total + record3.byteCount, 0);
16972
17572
  }
17573
+ async withAppendTail(operation) {
17574
+ const previous = this.reliableAppendTail;
17575
+ let release;
17576
+ this.reliableAppendTail = new Promise((resolve) => {
17577
+ release = resolve;
17578
+ });
17579
+ try {
17580
+ await previous;
17581
+ return await operation();
17582
+ } finally {
17583
+ release();
17584
+ }
17585
+ }
16973
17586
  noteDrop(lane, reason, agentRef, countDrop = true) {
16974
17587
  const laneStatus = lane === "latest-value" ? this.latestStatus : this.reliableStatus;
16975
17588
  if (countDrop) laneStatus.dropped += 1;
@@ -17957,7 +18570,7 @@ async function detectRuntimes(adapters) {
17957
18570
  }
17958
18571
  return runtimes;
17959
18572
  }
17960
- function computeCapabilities(adapters, agentHomeConfigured = false, strictAgentOnly = false, agentHomeProjectionConfigured = false, agentEgressConfigured = false, contentReadPolicies) {
18573
+ function computeCapabilities(adapters, agentHomeConfigured = false, strictAgentOnly = false, agentHomeProjectionConfigured = false, agentEgressConfigured = false, contentReadPolicies, providerProfileBindingConfigured = false) {
17961
18574
  const flags = [];
17962
18575
  if (adapters.some((adapter) => adapter.descriptor.capabilities.steer)) flags.push("steer");
17963
18576
  flags.push("blob-upload");
@@ -17969,6 +18582,9 @@ function computeCapabilities(adapters, agentHomeConfigured = false, strictAgentO
17969
18582
  if (selectionAdapters.length > 0 && selectionAdapters.every((adapter) => adapter.descriptor.supportsDispatchSelection === true)) {
17970
18583
  flags.push("dispatch-selection");
17971
18584
  }
18585
+ if (providerProfileBindingConfigured && adapters.some((adapter) => adapter.descriptor.id === "pi" && adapter.descriptor.supportsDispatchSelection)) {
18586
+ flags.push(PROVIDER_PROFILE_BINDING_CAPABILITY);
18587
+ }
17972
18588
  if (adapters.some((adapter) => adapter.descriptor.capabilities.mcpToolsets === true)) {
17973
18589
  flags.push("toolset-selection");
17974
18590
  }
@@ -18131,6 +18747,12 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
18131
18747
  if (config.strictAgentOnly === true && config.agentHome === void 0) {
18132
18748
  throw new Error("DaemonConfig.strictAgentOnly requires DaemonConfig.agentHome");
18133
18749
  }
18750
+ if (config.maxConcurrentMutableSessionsPerAgentHome !== void 0 && !(Number.isSafeInteger(config.maxConcurrentMutableSessionsPerAgentHome) && config.maxConcurrentMutableSessionsPerAgentHome > 0)) {
18751
+ throw new Error(
18752
+ `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".`
18753
+ );
18754
+ }
18755
+ const agentHomeAttemptLimit = config.maxConcurrentMutableSessionsPerAgentHome ?? DEFAULT_MAX_CONCURRENT_MUTABLE_SESSIONS_PER_AGENT_HOME;
18134
18756
  const egressPolicy = resolveAgentEgressPolicy(config.agentEgress?.policy);
18135
18757
  const egressBatcherOptions = egressPolicy.activity.mode === "contentful-trajectory" ? {
18136
18758
  ...config.progressBatch,
@@ -18241,6 +18863,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
18241
18863
  const observer = new DaemonObserver();
18242
18864
  const approvalRegistry = new ApprovalRegistry();
18243
18865
  let connection;
18866
+ let blobLifecycleAbort;
18244
18867
  let connectionState = "closed";
18245
18868
  let daemonStarted = false;
18246
18869
  let tenantRebinding = false;
@@ -18259,6 +18882,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
18259
18882
  function buildAuthManager() {
18260
18883
  return new AuthManager({
18261
18884
  serverUrl: config.serverUrl,
18885
+ authRequestDeadlineMs: config.authRequestDeadlineMs,
18262
18886
  store,
18263
18887
  deviceName: config.deviceName,
18264
18888
  machineId: config.machineId ?? (() => resolveMachineId({ productId: config.productId })),
@@ -18276,7 +18900,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
18276
18900
  assertServerUrlAllowed(config.serverUrl, { dangerouslyAllowInsecureRemote: true });
18277
18901
  const reason = err instanceof Error ? err.message : String(err);
18278
18902
  console.warn(
18279
- `[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})`
18903
+ `[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})`
18280
18904
  );
18281
18905
  }
18282
18906
  }
@@ -18436,9 +19060,10 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
18436
19060
  await gitWorkspaceStore?.initialize();
18437
19061
  await gitWorkspaceStore?.reconcile();
18438
19062
  }
19063
+ blobLifecycleAbort = new AbortController();
18439
19064
  const [runtimes, blobClient] = await Promise.all([
18440
19065
  detectRuntimes(adapters),
18441
- Promise.resolve(new BlobClient(config.serverUrl, auth))
19066
+ Promise.resolve(new BlobClient(config.serverUrl, auth, { signal: blobLifecycleAbort.signal }))
18442
19067
  ]);
18443
19068
  observer.noteRuntimesDetected(runtimes);
18444
19069
  detectedRuntimeFacts = runtimes;
@@ -18448,7 +19073,8 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
18448
19073
  config.strictAgentOnly === true,
18449
19074
  agentHomeManager?.supportsTaskFreeProjection() === true,
18450
19075
  config.agentEgress !== void 0,
18451
- agentContentReadPolicies
19076
+ agentContentReadPolicies,
19077
+ config.piByokLauncher !== void 0
18452
19078
  );
18453
19079
  const agentHomeProjectionCompletion = agentHomeManager?.supportsTaskFreeProjection() === true ? new AgentHomeProjectionCompletionClient({
18454
19080
  serverUrl: config.serverUrl,
@@ -18507,6 +19133,8 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
18507
19133
  workspaceRoot: config.workspaceRoot,
18508
19134
  ...agentHomeManager === void 0 ? {} : { agentHome: agentHomeManager },
18509
19135
  ...config.strictAgentOnly === true ? { strictAgentOnly: true } : {},
19136
+ // WP0: already validated up front — see `DaemonConfig.maxConcurrentMutableSessionsPerAgentHome`.
19137
+ maxConcurrentMutableSessionsPerAgentHome: agentHomeAttemptLimit,
18510
19138
  ...agentSessionHandoffs === void 0 ? {} : { agentSessionHandoffs },
18511
19139
  deviceId: record3.deviceId,
18512
19140
  // M5: see `DaemonConfig.runtimeEnvironment`'s own doc comment above.
@@ -18791,16 +19419,12 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
18791
19419
  return runner?.handleEnvelope(envelope) ?? Promise.resolve();
18792
19420
  },
18793
19421
  onStateChange: (state) => {
18794
- const wasSettled = connectionState === "open" || connectionState === "degraded";
19422
+ const wasSettled = connectionState === "open";
18795
19423
  connectionState = state;
18796
19424
  observer.noteConnectionState(state);
18797
- if (!wasSettled && (state === "open" || state === "degraded")) runner?.retryRecoveredAgentMessages();
18798
- if (!wasSettled && (state === "open" || state === "degraded")) runPresenceDiscovery();
19425
+ if (!wasSettled && state === "open") runner?.retryRecoveredAgentMessages();
19426
+ if (!wasSettled && state === "open") runPresenceDiscovery();
18799
19427
  },
18800
- backoff: overrides.backoff,
18801
- liveness: overrides.liveness,
18802
- wsFailureThreshold: overrides.longPoll?.wsFailureThreshold,
18803
- wsRetryIntervalMs: overrides.longPoll?.wsRetryIntervalMs,
18804
19428
  longPollRetryDelayMs: overrides.longPoll?.retryDelayMs,
18805
19429
  longPollIdleDelayMs: overrides.longPoll?.idleDelayMs,
18806
19430
  fleetJitter,
@@ -18812,7 +19436,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
18812
19436
  }
18813
19437
  });
18814
19438
  await connection.start();
18815
- await connection.waitForAck();
19439
+ await connection.waitForConnection();
18816
19440
  runner.retryRecoveredAgentMessages();
18817
19441
  for (const record4 of agentEgress.retryableReliableRecords(connection.getServerCapabilities())) {
18818
19442
  dispatchReliableRecord(record4);
@@ -18867,6 +19491,8 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
18867
19491
  }
18868
19492
  async function runShutdownSequence(reason, opts = {}) {
18869
19493
  shuttingDown = true;
19494
+ blobLifecycleAbort?.abort();
19495
+ blobLifecycleAbort = void 0;
18870
19496
  const errors = [];
18871
19497
  let mutationBarrierComplete = hostedStorageInitializationBarrierComplete;
18872
19498
  if (!hostedStorageInitializationBarrierComplete) {
@@ -19069,7 +19695,9 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
19069
19695
  // fine.
19070
19696
  ...storageStatus === void 0 ? {} : { storage: storageStatus },
19071
19697
  operationalHealth: operationalHealth.snapshot(),
19072
- toolsets: toolsetRegistry.status()
19698
+ toolsets: toolsetRegistry.status(),
19699
+ // WP0: same counts `Daemon.status()` reports, from the same reader.
19700
+ agentHomeExecution: agentHomeExecutionStatus()
19073
19701
  };
19074
19702
  }
19075
19703
  async function performControlShutdown(reason) {
@@ -19304,9 +19932,8 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
19304
19932
  const minted = mintDeviceAssertion({
19305
19933
  record: record3,
19306
19934
  // `toHttpBase` is the one place a configured serverUrl is normalized
19307
- // (ws:->http:, wss:->https:, path stripped), so an operator who
19308
- // configured the websocket spelling and one who configured the HTTP
19309
- // spelling of the same deployment produce the same issuer.
19935
+ // (path stripped), so the issuer ignores an operator-provided path
19936
+ // or query.
19310
19937
  issuer: new URL(toHttpBase(config.serverUrl)).origin,
19311
19938
  productId: config.productId,
19312
19939
  audience: parsed.audience,
@@ -19421,23 +20048,33 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
19421
20048
  await binding.lease.release();
19422
20049
  }
19423
20050
  }
20051
+ function agentHomeExecutionStatus() {
20052
+ const summary = agentHomeManager?.executionLeaseManager.activeAttemptSummary();
20053
+ return {
20054
+ maxConcurrentMutableSessionsPerAgentHome: agentHomeAttemptLimit,
20055
+ activeHomes: summary?.homes ?? 0,
20056
+ activeAttempts: summary?.attempts ?? 0
20057
+ };
20058
+ }
19424
20059
  function status() {
19425
20060
  return {
19426
20061
  localAgentRelease,
19427
20062
  paired: auth.deviceId !== void 0,
19428
20063
  connected: connectionState === "open",
19429
- degraded: connection?.isTransportDegraded() ?? false,
19430
20064
  revoked: connection?.isRevoked() ?? auth.isRevoked(),
19431
20065
  deviceId: auth.deviceId,
19432
20066
  activeTaskCount: runner?.activeTaskCount ?? 0,
19433
20067
  branding: config.branding,
19434
20068
  operationalHealth: operationalHealth.snapshot(),
19435
20069
  toolsets: toolsetRegistry.status(),
19436
- egress: agentEgress.status()
20070
+ egress: agentEgress.status(),
20071
+ agentHomeExecution: agentHomeExecutionStatus()
19437
20072
  };
19438
20073
  }
19439
20074
  function reloadMcpToolsets(mcpToolsets, expectedRevision) {
19440
- return toolsetRegistry.reload(mcpToolsets, expectedRevision);
20075
+ const receipt = toolsetRegistry.reload(mcpToolsets, expectedRevision);
20076
+ if (daemonStarted) connection?.refreshHello();
20077
+ return receipt;
19441
20078
  }
19442
20079
  function reportMcpToolsetObservation(toolsetId, expectedDefinitionRevision, observation) {
19443
20080
  toolsetRegistry.report(toolsetId, expectedDefinitionRevision, observation);
@@ -20687,6 +21324,6 @@ function createServiceLifecycle(def, opts = {}) {
20687
21324
  }
20688
21325
  }
20689
21326
 
20690
- export { AGENT_CONTENT_READ_CAPABILITIES, AGENT_CONTENT_READ_CAPABILITY_ARTIFACT, AGENT_CONTENT_READ_CAPABILITY_TRANSCRIPT, AGENT_CONTENT_READ_CAPABILITY_WORKSPACE, AGENT_HOME_PROJECTION_STATE_FILE, AGENT_MEMORY_AUDIT_FILENAME, AGENT_MEMORY_OUTBOX_FILENAME, AgentHomeBusyError, AgentHomeCollisionError, AgentHomeError, AgentHomeLayout, AgentHomeLeaseCorruptError, AgentHomeLeaseManager, AgentHomeManager, AgentHomeResolutionError, AgentMemoryError, AgentMemoryRevisionConflictError, AgentRefValidationError, AgentSessionHandoffCorruptError, AgentSessionHandoffMismatchError, AgentSessionHandoffStore, AgentSessionHandoffStoreError, BYOK_SDK_HELPER_SUBCOMMAND, BlobClient, ClaudeAdapter, CodexAdapter, DEFAULT_ACK_CRITICAL_RESERVE_BYTES, DEFAULT_CLEANUP_BATCH_LIMIT, DEFAULT_HARD_BUDGET_RATIO, DEFAULT_INCREMENTAL_VACUUM_PAGES, DEFAULT_LOG_ROTATION, DEFAULT_NORMAL_COMPACTION_INTERVAL_MS, DEFAULT_PRESSURE_COMPACTION_INTERVAL_MS, DEFAULT_RETENTION_MS, DEFAULT_SOFT_BUDGET_RATIO, DaemonObserver, GitWorkspaceError, GitWorkspaceManager, GitWorkspaceStore, JOURNAL_DB_FILENAME, JOURNAL_QUARANTINE_DIRNAME, JOURNAL_TASK_REF_PREFIX, JournalClosedError, JournalCorruptError, JournalRecordTooLargeError, JournalUnavailableError, JournalUnknownTaskError, LocalStateRelocationBusyError, LocalStateRelocationError, LocalStateRelocationIntegrityError, LocalStorageEmergencyError, LocalStoragePolicyError, LocalStoragePressureEngine, LocalTeamWorkspace, LocalTeamWorkspace as LocalTeamWorkspaceService, McpToolsetDefinitionRevisionConflictError, McpToolsetRevisionConflictError, PI_PACKAGE_NAME, PiAdapter, PolicyUnsupportedError, RUNTIME_ADAPTER_CONTRACT_VIOLATION_REASON, RuntimeDisposalFailure, RuntimeExecutionFailure, SKILL_PACKS_CAPABILITY, SKILL_PACKS_DIRNAME, SKILL_PACK_AUDIT_FILENAME, SKILL_PACK_INSTALL_ERROR_CODES, SKILL_PACK_LOCK_FILENAME, SKILL_PACK_LOCK_SCHEMA, SKILL_PACK_RESPONSE_MAX_BYTES, SecureDirHardeningError, SkillPackInstallError, SqliteLocalTaskJournal, SteerUnsupportedError, TeamTmuxViewError, TeamWorkspaceConflictError, TeamWorkspaceCorruptError, TeamWorkspaceError, TeamWorkspaceLeaseError, TeamWorkspaceNotFoundError, TeamWorkspaceQuotaError, TeamWorkspaceReceiptError, TeamWorkspaceValidationError, TruthMemoryClient, TruthMemoryClientError, UnsupportedServicePlatformError, buildIcaclsArgs, cleanupEligibleAt, cleanupOrderFor, computePressureState, createAgentHomeProjection, createAgentHomeProjectionConsumer, createDaemon, createDaemonWithAdapters, createFilesystemCleanupExecutor, createServiceLifecycle, createStatfsFreeBytesProvider, decodeTeamMemberContext, encodeTeamMemberContext, ensureSecureDir, freezeRuntimeAdapterDescriptor, generateLaunchdPlist, generateSystemdUnit, generateWinswXml, installSkillPacks, isAgentMemorySecureFilesystemAvailable, isGitWorkspaceConfig, isRuntimeDisposalFailure, isRuntimeExecutionFailure, journalHash, listInstalledSkillPacks, localStateRelocation, nodeAgentProgram, openTeamTmuxView, prependGitWorkspaceGuidance, projectRuntimeBoundaryFailure, projectRuntimeExecutionFailure, projectSkillPack, readDeviceEnrollmentStatus, requestDeviceAssertion, resolveLocalAgentReleaseIdentity, resolveLocalStoragePolicy, resolveSdkReservedHelperBin, runSdkReservedHelperCommand, sanitizeServiceName, sealRuntimeOperationManifest, skillPacksRoot, stableAgentHomeOwnerId, validateAgentRef };
21327
+ export { AGENT_CONTENT_READ_CAPABILITIES, AGENT_CONTENT_READ_CAPABILITY_ARTIFACT, AGENT_CONTENT_READ_CAPABILITY_TRANSCRIPT, AGENT_CONTENT_READ_CAPABILITY_WORKSPACE, AGENT_HOME_PROJECTION_STATE_FILE, AGENT_MEMORY_AUDIT_FILENAME, AGENT_MEMORY_OUTBOX_FILENAME, AgentHomeBusyError, AgentHomeCollisionError, AgentHomeError, AgentHomeLayout, AgentHomeLeaseCorruptError, AgentHomeLeaseManager, AgentHomeManager, AgentHomeResolutionError, AgentMemoryError, AgentMemoryRevisionConflictError, AgentRefValidationError, AgentSessionHandoffCorruptError, AgentSessionHandoffMismatchError, AgentSessionHandoffStore, AgentSessionHandoffStoreError, BYOK_SDK_HELPER_SUBCOMMAND, BlobClient, BlobRequestAbortedError, ClaudeAdapter, CodexAdapter, DEFAULT_ACK_CRITICAL_RESERVE_BYTES, DEFAULT_CLEANUP_BATCH_LIMIT, DEFAULT_HARD_BUDGET_RATIO, DEFAULT_INCREMENTAL_VACUUM_PAGES, DEFAULT_LOG_ROTATION, DEFAULT_NORMAL_COMPACTION_INTERVAL_MS, DEFAULT_PRESSURE_COMPACTION_INTERVAL_MS, DEFAULT_RETENTION_MS, DEFAULT_SOFT_BUDGET_RATIO, DaemonObserver, GitWorkspaceError, GitWorkspaceManager, GitWorkspaceStore, JOURNAL_DB_FILENAME, JOURNAL_QUARANTINE_DIRNAME, JOURNAL_TASK_REF_PREFIX, JournalClosedError, JournalCorruptError, JournalRecordTooLargeError, JournalUnavailableError, JournalUnknownTaskError, LocalStateRelocationBusyError, LocalStateRelocationError, LocalStateRelocationIntegrityError, LocalStorageEmergencyError, LocalStoragePolicyError, LocalStoragePressureEngine, LocalTeamWorkspace, LocalTeamWorkspace as LocalTeamWorkspaceService, McpToolsetDefinitionRevisionConflictError, McpToolsetRevisionConflictError, PI_PACKAGE_NAME, PiAdapter, PolicyUnsupportedError, RUNTIME_ADAPTER_CONTRACT_VIOLATION_REASON, ReplayCursorTooOldError, RuntimeDisposalFailure, RuntimeExecutionFailure, SKILL_PACKS_CAPABILITY, SKILL_PACKS_DIRNAME, SKILL_PACK_AUDIT_FILENAME, SKILL_PACK_INSTALL_ERROR_CODES, SKILL_PACK_LOCK_FILENAME, SKILL_PACK_LOCK_SCHEMA, SKILL_PACK_RESPONSE_MAX_BYTES, SecureDirHardeningError, SkillPackInstallError, SqliteLocalTaskJournal, SteerUnsupportedError, TeamTmuxViewError, TeamWorkspaceConflictError, TeamWorkspaceCorruptError, TeamWorkspaceError, TeamWorkspaceLeaseError, TeamWorkspaceNotFoundError, TeamWorkspaceQuotaError, TeamWorkspaceReceiptError, TeamWorkspaceValidationError, TruthMemoryClient, TruthMemoryClientError, UnsupportedServicePlatformError, buildIcaclsArgs, cleanupEligibleAt, cleanupOrderFor, computePressureState, createAgentHomeProjection, createAgentHomeProjectionConsumer, createDaemon, createDaemonWithAdapters, createFilesystemCleanupExecutor, createServiceLifecycle, createStatfsFreeBytesProvider, decodeTeamMemberContext, encodeTeamMemberContext, ensureSecureDir, freezeRuntimeAdapterDescriptor, generateLaunchdPlist, generateSystemdUnit, generateWinswXml, installSkillPacks, isAgentMemorySecureFilesystemAvailable, isGitWorkspaceConfig, isRuntimeDisposalFailure, isRuntimeExecutionFailure, journalHash, listInstalledSkillPacks, localStateRelocation, nodeAgentProgram, openTeamTmuxView, prependGitWorkspaceGuidance, projectRuntimeBoundaryFailure, projectRuntimeExecutionFailure, projectSkillPack, readDeviceEnrollmentStatus, requestDeviceAssertion, resolveLocalAgentReleaseIdentity, resolveLocalStoragePolicy, resolveSdkReservedHelperBin, runSdkReservedHelperCommand, sanitizeServiceName, sealRuntimeOperationManifest, skillPacksRoot, stableAgentHomeOwnerId, validateAgentRef };
20691
21328
  //# sourceMappingURL=index.js.map
20692
21329
  //# sourceMappingURL=index.js.map