@byok-sdk/client 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -0
- package/dist/adapters/index.js +74 -5
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/pi/pi-adapter.d.ts +3 -0
- package/dist/adapters/pi/subagents-policy-extension.js +1 -1
- package/dist/adapters/pi/subagents-policy-extension.js.map +1 -1
- package/dist/agent-home.d.ts +74 -0
- package/dist/agent-memory/index.d.ts +1 -1
- package/dist/agent-memory/index.js.map +1 -1
- package/dist/bin/byok-agent-memory-mcp.js.map +1 -1
- package/dist/bin/byok-agent-message-mcp.js.map +1 -1
- package/dist/bin/byok-agent-team-mcp.js.map +1 -1
- package/dist/bin/byok-agent.js +467 -485
- package/dist/bin/byok-agent.js.map +1 -1
- package/dist/bin/byok-approval-mcp.js.map +1 -1
- package/dist/daemon/connection-manager.d.ts +63 -127
- package/dist/daemon/control-protocol.d.ts +8 -1
- package/dist/daemon/create-daemon.d.ts +26 -13
- package/dist/daemon/long-poll-transport.d.ts +29 -23
- package/dist/daemon/observer.d.ts +1 -1
- package/dist/daemon/task-runner.d.ts +18 -0
- package/dist/daemon/url.d.ts +10 -12
- package/dist/diagnostics/support-bundle.d.ts +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +465 -483
- package/dist/index.js.map +1 -1
- package/package.json +5 -7
- 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,
|
|
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
|
|
@@ -833,6 +882,44 @@ var AgentHomeExecutionLeaseManager = class _AgentHomeExecutionLeaseManager {
|
|
|
833
882
|
});
|
|
834
883
|
});
|
|
835
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
|
+
}
|
|
836
923
|
async mutate(binding, operation) {
|
|
837
924
|
return this.exclusive(binding.resolution.canonicalHome, async () => {
|
|
838
925
|
const group = _AgentHomeExecutionLeaseManager.groups.get(binding.resolution.canonicalHome);
|
|
@@ -2036,13 +2123,22 @@ function freezeRuntimeAdapterDescriptor(descriptor) {
|
|
|
2036
2123
|
});
|
|
2037
2124
|
}
|
|
2038
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 });
|
|
2039
2135
|
return Object.freeze({
|
|
2040
2136
|
taskId: manifest.taskId,
|
|
2041
2137
|
runtimeId: manifest.runtimeId,
|
|
2042
2138
|
descriptor: freezeRuntimeAdapterDescriptor(manifest.descriptor),
|
|
2043
2139
|
policy: frozenPolicy(manifest.policy),
|
|
2044
2140
|
requiredToolsetIds: Object.freeze([...manifest.requiredToolsetIds]),
|
|
2045
|
-
...
|
|
2141
|
+
...dispatchSelection === void 0 ? {} : { dispatchSelection },
|
|
2046
2142
|
...manifest.sessionRef === void 0 ? {} : { sessionRef: manifest.sessionRef },
|
|
2047
2143
|
...manifest.agentRef === void 0 ? {} : { agentRef: Object.freeze({ agentId: manifest.agentRef.agentId, profileRevision: manifest.agentRef.profileRevision }) },
|
|
2048
2144
|
cwd: manifest.cwd ?? manifest.workspace.workspaceDir,
|
|
@@ -4464,7 +4560,11 @@ function validatePiByokLauncherConfig(launcher) {
|
|
|
4464
4560
|
"--macos-keychain-path",
|
|
4465
4561
|
"--secret-service-prefix",
|
|
4466
4562
|
"--provider",
|
|
4467
|
-
"--model"
|
|
4563
|
+
"--model",
|
|
4564
|
+
"--profile-revision",
|
|
4565
|
+
"--profile-hash",
|
|
4566
|
+
"--required-capabilities",
|
|
4567
|
+
"--validate-only"
|
|
4468
4568
|
]);
|
|
4469
4569
|
const conflicting = launcher.args?.find((arg) => reserved.has(arg));
|
|
4470
4570
|
if (conflicting !== void 0) {
|
|
@@ -4526,11 +4626,17 @@ var PiAdapter = class {
|
|
|
4526
4626
|
const bin = this.resolveBin();
|
|
4527
4627
|
const extensions = (this.options.resolveExtensions ?? resolvePiExtensions)();
|
|
4528
4628
|
const selection = input.offer.dispatchSelection;
|
|
4529
|
-
const pinnedSelection = selection === void 0 ? void 0 : Object.freeze({
|
|
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 });
|
|
4530
4636
|
let command = bin.command;
|
|
4531
4637
|
let launcherArgs;
|
|
4532
4638
|
if (pinnedSelection !== void 0) {
|
|
4533
|
-
if (pinnedSelection.lane !== "byok" || pinnedSelection.runtimeId !== "pi") {
|
|
4639
|
+
if (pinnedSelection.lane !== "byok" && pinnedSelection.lane !== "byok-profile" || pinnedSelection.runtimeId !== "pi") {
|
|
4534
4640
|
return { kind: "reject", reason: `pi adapter cannot execute ${pinnedSelection.lane} selection for runtime ${pinnedSelection.runtimeId}`, retryable: false };
|
|
4535
4641
|
}
|
|
4536
4642
|
const launcher = this.options.byokLauncher;
|
|
@@ -4538,6 +4644,21 @@ var PiAdapter = class {
|
|
|
4538
4644
|
return { kind: "reject", reason: "pi BYOK selection requires a configured credential-custody launcher", retryable: false };
|
|
4539
4645
|
}
|
|
4540
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
|
+
}
|
|
4541
4662
|
launcherArgs = [
|
|
4542
4663
|
...launcher.args ?? [],
|
|
4543
4664
|
"--pi-bin",
|
|
@@ -4549,9 +4670,19 @@ var PiAdapter = class {
|
|
|
4549
4670
|
...launcher.macosKeychainPath !== void 0 ? ["--macos-keychain-path", launcher.macosKeychainPath] : [],
|
|
4550
4671
|
...launcher.secretServicePrefix ? ["--secret-service-prefix", launcher.secretServicePrefix] : [],
|
|
4551
4672
|
"--provider",
|
|
4552
|
-
pinnedSelection.providerId,
|
|
4673
|
+
providerProfile?.profileRef ?? (pinnedSelection.lane === "byok" ? pinnedSelection.providerId : ""),
|
|
4553
4674
|
"--model",
|
|
4554
|
-
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
|
+
]
|
|
4555
4686
|
];
|
|
4556
4687
|
}
|
|
4557
4688
|
return {
|
|
@@ -4694,8 +4825,42 @@ var PiAdapter = class {
|
|
|
4694
4825
|
return (this.options.resolveBin ?? resolvePiBin)();
|
|
4695
4826
|
}
|
|
4696
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
|
+
}
|
|
4697
4853
|
function sameDispatchSelection(left, right) {
|
|
4698
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
|
+
}
|
|
4699
4864
|
return left.lane === right.lane && left.runtimeId === right.runtimeId && left.providerId === right.providerId && left.modelId === right.modelId;
|
|
4700
4865
|
}
|
|
4701
4866
|
async function resolveAuthoritativeSessionId(rpc) {
|
|
@@ -7603,19 +7768,14 @@ function signNonce(privateKey, nonce) {
|
|
|
7603
7768
|
const signature = sign(null, nonceSigningBytes(nonce), privateKey);
|
|
7604
7769
|
return signature.toString("base64url");
|
|
7605
7770
|
}
|
|
7771
|
+
|
|
7772
|
+
// src/daemon/url.ts
|
|
7606
7773
|
function toHttpBase(serverUrl) {
|
|
7607
7774
|
const url = new URL(serverUrl);
|
|
7608
|
-
if (url.protocol === "ws:") url.protocol = "http:";
|
|
7609
|
-
else if (url.protocol === "wss:") url.protocol = "https:";
|
|
7610
7775
|
url.pathname = "/";
|
|
7611
7776
|
url.search = "";
|
|
7612
7777
|
return url.toString();
|
|
7613
7778
|
}
|
|
7614
|
-
function toWsUrl(serverUrl) {
|
|
7615
|
-
const url = new URL(BYOK_WS_PATH, toHttpBase(serverUrl));
|
|
7616
|
-
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
7617
|
-
return url.toString();
|
|
7618
|
-
}
|
|
7619
7779
|
function describeEndpoint(transport, url) {
|
|
7620
7780
|
const parsed = typeof url === "string" ? new URL(url) : url;
|
|
7621
7781
|
return { transport, host: parsed.host, path: parsed.pathname };
|
|
@@ -7648,17 +7808,15 @@ function assertServerUrlAllowed(rawUrl, opts = {}) {
|
|
|
7648
7808
|
const endpoint = formatServerUrl(url);
|
|
7649
7809
|
switch (url.protocol) {
|
|
7650
7810
|
case "https:":
|
|
7651
|
-
case "wss:":
|
|
7652
7811
|
return;
|
|
7653
7812
|
case "http:":
|
|
7654
|
-
case "ws:":
|
|
7655
7813
|
if (opts.dangerouslyAllowInsecureRemote || isLoopbackHostname(url.hostname)) return;
|
|
7656
7814
|
throw new InsecureServerUrlError(
|
|
7657
|
-
`refusing to connect to "${endpoint}" over plaintext ${url.protocol.replace(":", "")} \u2014 "${url.hostname}" is not a loopback host. Use
|
|
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.`
|
|
7658
7816
|
);
|
|
7659
7817
|
default:
|
|
7660
7818
|
throw new InsecureServerUrlError(
|
|
7661
|
-
`refusing to connect to "${endpoint}" \u2014 unsupported scheme "${url.protocol}" (expected http
|
|
7819
|
+
`refusing to connect to "${endpoint}" \u2014 unsupported scheme "${url.protocol}" (expected http: or https:).`
|
|
7662
7820
|
);
|
|
7663
7821
|
}
|
|
7664
7822
|
}
|
|
@@ -9710,7 +9868,7 @@ var LongPollRouteError = class extends Error {
|
|
|
9710
9868
|
};
|
|
9711
9869
|
var MAX_TRACKED_VALIDATION_FAILURE_WARNINGS = 1e3;
|
|
9712
9870
|
var MAX_TRACKED_ROUTE_FAILURE_WARNINGS = 1e3;
|
|
9713
|
-
function parseLooseEventsPollResponse(raw) {
|
|
9871
|
+
function parseLooseEventsPollResponse(raw, requestedCursor) {
|
|
9714
9872
|
if (typeof raw !== "object" || raw === null) {
|
|
9715
9873
|
throw new Error("events poll response is not an object");
|
|
9716
9874
|
}
|
|
@@ -9718,19 +9876,48 @@ function parseLooseEventsPollResponse(raw) {
|
|
|
9718
9876
|
if (!Array.isArray(events)) {
|
|
9719
9877
|
throw new Error("events poll response.events is not an array");
|
|
9720
9878
|
}
|
|
9721
|
-
if (
|
|
9722
|
-
throw new Error("events poll response.cursor is not
|
|
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");
|
|
9723
9884
|
}
|
|
9724
9885
|
if (capabilities !== void 0 && (!Array.isArray(capabilities) || capabilities.some((flag) => typeof flag !== "string"))) {
|
|
9725
9886
|
throw new Error("events poll response.capabilities is not an array of strings");
|
|
9726
9887
|
}
|
|
9727
9888
|
return { events, cursor, capabilities: capabilities ?? [] };
|
|
9728
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
|
+
}
|
|
9729
9916
|
function extractSkippableSeq(raw) {
|
|
9730
9917
|
if (typeof raw !== "object" || raw === null) return void 0;
|
|
9731
9918
|
const { type, seq } = raw;
|
|
9732
9919
|
if (typeof type !== "string" || !type.startsWith("task.")) return void 0;
|
|
9733
|
-
return
|
|
9920
|
+
return isSafeNonnegativeInteger(seq) ? seq : void 0;
|
|
9734
9921
|
}
|
|
9735
9922
|
var LongPollClient = class {
|
|
9736
9923
|
constructor(opts) {
|
|
@@ -9741,6 +9928,8 @@ var LongPollClient = class {
|
|
|
9741
9928
|
}
|
|
9742
9929
|
opts;
|
|
9743
9930
|
running = false;
|
|
9931
|
+
/** Owns exactly one active loop generation, including its held GET and retry delays. */
|
|
9932
|
+
loopAbortController;
|
|
9744
9933
|
/**
|
|
9745
9934
|
* Finding R1: seqs this loop has already `console.warn`'d about for a
|
|
9746
9935
|
* validation-failed (recognized-type, invalid-payload) entry — a poison
|
|
@@ -9800,16 +9989,22 @@ var LongPollClient = class {
|
|
|
9800
9989
|
noteRevoked(err) {
|
|
9801
9990
|
if (!(err instanceof DeviceRevokedError)) return false;
|
|
9802
9991
|
this.running = false;
|
|
9992
|
+
this.loopAbortController?.abort();
|
|
9803
9993
|
this.opts.onRevoked?.();
|
|
9804
9994
|
return true;
|
|
9805
9995
|
}
|
|
9806
9996
|
start() {
|
|
9807
9997
|
if (this.running) return;
|
|
9808
9998
|
this.running = true;
|
|
9809
|
-
|
|
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
|
+
});
|
|
9810
10004
|
}
|
|
9811
10005
|
stop() {
|
|
9812
10006
|
this.running = false;
|
|
10007
|
+
this.loopAbortController?.abort();
|
|
9813
10008
|
}
|
|
9814
10009
|
/**
|
|
9815
10010
|
* POST one batch of envelopes to `/byok/messages` (finding F6/protocol
|
|
@@ -9818,14 +10013,14 @@ var LongPollClient = class {
|
|
|
9818
10013
|
* (`ConnectionHub.handleInbound`), so a resend of the SAME batch (same
|
|
9819
10014
|
* envelope `id`s — the caller must never rebuild them) is deduped
|
|
9820
10015
|
* server-side into a safe no-op rather than reprocessed (§9). Returns
|
|
9821
|
-
*
|
|
10016
|
+
* validated frozen-v1 counts only after a readable response body.
|
|
9822
10017
|
*/
|
|
9823
10018
|
async postBatch(envelopes) {
|
|
9824
10019
|
try {
|
|
9825
10020
|
await this.opts.auth.getValidAccessToken();
|
|
9826
10021
|
} catch (err) {
|
|
9827
10022
|
this.noteRevoked(err);
|
|
9828
|
-
return
|
|
10023
|
+
return void 0;
|
|
9829
10024
|
}
|
|
9830
10025
|
let res;
|
|
9831
10026
|
try {
|
|
@@ -9841,23 +10036,26 @@ var LongPollClient = class {
|
|
|
9841
10036
|
);
|
|
9842
10037
|
} catch (err) {
|
|
9843
10038
|
if (!this.noteRevoked(err)) this.warnRouteFailure(this.messagesEndpoint, void 0, err);
|
|
9844
|
-
return
|
|
10039
|
+
return void 0;
|
|
9845
10040
|
}
|
|
9846
10041
|
if (!res.ok) {
|
|
9847
10042
|
this.warnRouteFailure(this.messagesEndpoint, res.status, void 0);
|
|
9848
|
-
return
|
|
10043
|
+
return void 0;
|
|
9849
10044
|
}
|
|
9850
10045
|
try {
|
|
9851
|
-
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;
|
|
9852
10051
|
} catch (err) {
|
|
9853
10052
|
this.warnRouteFailure(this.messagesEndpoint, res.status, err);
|
|
9854
|
-
return
|
|
10053
|
+
return void 0;
|
|
9855
10054
|
}
|
|
9856
|
-
return true;
|
|
9857
10055
|
}
|
|
9858
|
-
async loop() {
|
|
10056
|
+
async loop(signal) {
|
|
9859
10057
|
let retryAttempt = 0;
|
|
9860
|
-
while (this.running) {
|
|
10058
|
+
while (this.running && !signal.aborted) {
|
|
9861
10059
|
try {
|
|
9862
10060
|
await this.opts.auth.getValidAccessToken();
|
|
9863
10061
|
const base = toHttpBase(this.opts.serverUrl);
|
|
@@ -9866,8 +10064,9 @@ var LongPollClient = class {
|
|
|
9866
10064
|
if (cursor !== void 0) url.searchParams.set("cursor", String(cursor));
|
|
9867
10065
|
let res;
|
|
9868
10066
|
try {
|
|
9869
|
-
res = await authedFetch(url, { method: "GET" }, this.opts.auth);
|
|
10067
|
+
res = await authedFetch(url, { method: "GET", signal }, this.opts.auth);
|
|
9870
10068
|
} catch (err) {
|
|
10069
|
+
if (signal.aborted) return;
|
|
9871
10070
|
if (!(err instanceof DeviceRevokedError)) {
|
|
9872
10071
|
this.warnRouteFailure(this.eventsEndpoint, void 0, err);
|
|
9873
10072
|
}
|
|
@@ -9881,21 +10080,24 @@ var LongPollClient = class {
|
|
|
9881
10080
|
return;
|
|
9882
10081
|
}
|
|
9883
10082
|
this.warnRouteFailure(this.eventsEndpoint, res.status, void 0);
|
|
9884
|
-
this.opts.
|
|
10083
|
+
this.opts.onServerCapabilitiesInvalidated?.();
|
|
10084
|
+
this.opts.onPollFailure?.();
|
|
9885
10085
|
this.opts.onOperationalOutcome?.("failure");
|
|
9886
10086
|
const baseMs = this.opts.retryDelayMs ?? 2e3;
|
|
9887
|
-
await sleep(this.opts.retryDelayForAttempt?.(retryAttempt++, baseMs) ?? baseMs);
|
|
10087
|
+
await sleep(this.opts.retryDelayForAttempt?.(retryAttempt++, baseMs) ?? baseMs, signal);
|
|
9888
10088
|
continue;
|
|
9889
10089
|
}
|
|
9890
10090
|
let parsed;
|
|
9891
10091
|
try {
|
|
9892
|
-
parsed = parseLooseEventsPollResponse(await res.json());
|
|
10092
|
+
parsed = parseLooseEventsPollResponse(await res.json(), cursor ?? 0);
|
|
10093
|
+
validateTrustedEventsPage(parsed.events, cursor ?? 0, parsed.cursor);
|
|
9893
10094
|
} catch (err) {
|
|
9894
10095
|
this.warnRouteFailure(this.eventsEndpoint, res.status, err);
|
|
9895
10096
|
throw err;
|
|
9896
10097
|
}
|
|
9897
10098
|
this.opts.onServerCapabilities?.(parsed.capabilities);
|
|
9898
10099
|
let hadValidationFailureThisBatch = false;
|
|
10100
|
+
let acceptedAnyEntry = false;
|
|
9899
10101
|
for (const raw of parsed.events) {
|
|
9900
10102
|
let envelope;
|
|
9901
10103
|
try {
|
|
@@ -9903,7 +10105,10 @@ var LongPollClient = class {
|
|
|
9903
10105
|
} catch (err) {
|
|
9904
10106
|
if (err instanceof UnknownMessageTypeError) {
|
|
9905
10107
|
const skippableSeq = extractSkippableSeq(raw);
|
|
9906
|
-
if (skippableSeq !== void 0)
|
|
10108
|
+
if (skippableSeq !== void 0) {
|
|
10109
|
+
this.opts.onSkippedSeq?.(skippableSeq);
|
|
10110
|
+
acceptedAnyEntry = true;
|
|
10111
|
+
}
|
|
9907
10112
|
} else {
|
|
9908
10113
|
const failedSeq = extractSkippableSeq(raw);
|
|
9909
10114
|
if (failedSeq !== void 0) {
|
|
@@ -9923,27 +10128,28 @@ var LongPollClient = class {
|
|
|
9923
10128
|
}
|
|
9924
10129
|
continue;
|
|
9925
10130
|
}
|
|
9926
|
-
this.opts.onEnvelope(envelope);
|
|
10131
|
+
if (this.opts.onEnvelope(envelope) !== false) acceptedAnyEntry = true;
|
|
9927
10132
|
}
|
|
9928
10133
|
if (parsed.events.length === 0) {
|
|
9929
10134
|
retryAttempt = 0;
|
|
9930
10135
|
this.opts.onOperationalOutcome?.("success");
|
|
9931
|
-
await sleep(this.opts.idleDelayMs ?? 250);
|
|
9932
|
-
} 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) {
|
|
9933
10138
|
this.opts.onOperationalOutcome?.("failure");
|
|
9934
10139
|
const baseMs = this.opts.retryDelayMs ?? 2e3;
|
|
9935
|
-
await sleep(this.opts.retryDelayForAttempt?.(retryAttempt++, baseMs) ?? baseMs);
|
|
10140
|
+
await sleep(this.opts.retryDelayForAttempt?.(retryAttempt++, baseMs) ?? baseMs, signal);
|
|
9936
10141
|
} else {
|
|
9937
10142
|
retryAttempt = 0;
|
|
9938
10143
|
this.opts.onOperationalOutcome?.("success");
|
|
9939
10144
|
}
|
|
9940
10145
|
} catch (err) {
|
|
9941
|
-
this.opts.
|
|
10146
|
+
this.opts.onServerCapabilitiesInvalidated?.();
|
|
9942
10147
|
if (this.noteRevoked(err)) return;
|
|
9943
|
-
if (!this.running) return;
|
|
10148
|
+
if (!this.running || signal.aborted) return;
|
|
10149
|
+
this.opts.onPollFailure?.();
|
|
9944
10150
|
this.opts.onOperationalOutcome?.("failure");
|
|
9945
10151
|
const baseMs = this.opts.retryDelayMs ?? 2e3;
|
|
9946
|
-
await sleep(this.opts.retryDelayForAttempt?.(retryAttempt++, baseMs) ?? baseMs);
|
|
10152
|
+
await sleep(this.opts.retryDelayForAttempt?.(retryAttempt++, baseMs) ?? baseMs, signal);
|
|
9947
10153
|
}
|
|
9948
10154
|
}
|
|
9949
10155
|
}
|
|
@@ -9963,20 +10169,24 @@ async function parseReplayCursorTooOld(res) {
|
|
|
9963
10169
|
}
|
|
9964
10170
|
return new ReplayCursorTooOldError(recoverableFrom);
|
|
9965
10171
|
}
|
|
9966
|
-
function sleep(ms) {
|
|
9967
|
-
|
|
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
|
+
});
|
|
9968
10183
|
}
|
|
9969
|
-
|
|
9970
|
-
|
|
9971
|
-
|
|
9972
|
-
|
|
9973
|
-
|
|
9974
|
-
|
|
9975
|
-
}
|
|
9976
|
-
status;
|
|
9977
|
-
endpoint;
|
|
9978
|
-
};
|
|
9979
|
-
function createConnectionHelloEnvelope(opts) {
|
|
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) {
|
|
9980
10190
|
const configuredToolsets = opts.getConfiguredToolsets?.();
|
|
9981
10191
|
return createEnvelope("conn.hello", {
|
|
9982
10192
|
protocolVersions: [PROTOCOL_VERSION],
|
|
@@ -9986,222 +10196,32 @@ function createConnectionHelloEnvelope(opts) {
|
|
|
9986
10196
|
clientVersion: opts.clientVersion,
|
|
9987
10197
|
runtimes: opts.runtimes,
|
|
9988
10198
|
configuredToolsets: configuredToolsets === void 0 ? void 0 : [...configuredToolsets],
|
|
9989
|
-
cursor:
|
|
10199
|
+
cursor: getCursor()
|
|
9990
10200
|
});
|
|
9991
10201
|
}
|
|
9992
|
-
var
|
|
9993
|
-
constructor(opts) {
|
|
9994
|
-
this.opts = opts;
|
|
9995
|
-
}
|
|
9996
|
-
opts;
|
|
9997
|
-
socket;
|
|
9998
|
-
closedByUser = false;
|
|
9999
|
-
autoReconnect = true;
|
|
10000
|
-
acked = false;
|
|
10001
|
-
everAckedThisAttempt = false;
|
|
10002
|
-
reconnectAttempt = 0;
|
|
10003
|
-
reconnectTimer;
|
|
10004
|
-
livenessTimer;
|
|
10005
|
-
lastActivity = 0;
|
|
10006
|
-
lastUnexpectedStatus;
|
|
10007
|
-
ackWaiters = [];
|
|
10008
|
-
connect(opts = {}) {
|
|
10009
|
-
this.autoReconnect = opts.auto ?? true;
|
|
10010
|
-
this.closedByUser = false;
|
|
10011
|
-
void this.openSocket();
|
|
10012
|
-
}
|
|
10013
|
-
/** 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). */
|
|
10014
|
-
stopAutoReconnect() {
|
|
10015
|
-
this.autoReconnect = false;
|
|
10016
|
-
if (this.reconnectTimer) {
|
|
10017
|
-
clearTimeout(this.reconnectTimer);
|
|
10018
|
-
this.reconnectTimer = void 0;
|
|
10019
|
-
}
|
|
10020
|
-
}
|
|
10021
|
-
/** Resume normal auto-reconnect-on-close behavior (does not itself trigger a connect — only affects future closes). */
|
|
10022
|
-
resumeAutoReconnect() {
|
|
10023
|
-
this.autoReconnect = true;
|
|
10024
|
-
}
|
|
10025
|
-
get isOpen() {
|
|
10026
|
-
return this.acked;
|
|
10027
|
-
}
|
|
10028
|
-
/**
|
|
10029
|
-
* Attempt to send ONE envelope right now; returns whether it actually went
|
|
10030
|
-
* out. `false` means the socket isn't currently open+acked — the caller
|
|
10031
|
-
* (`ConnectionManager.drainOutbox`, Design B/finding N4) owns re-queueing
|
|
10032
|
-
* and retrying later (e.g. on the next `onAcked`), since this transport no
|
|
10033
|
-
* longer buffers anything itself.
|
|
10034
|
-
*/
|
|
10035
|
-
sendNow(envelope) {
|
|
10036
|
-
if (this.socket && this.socket.readyState === WebSocket.OPEN && this.acked) {
|
|
10037
|
-
this.socket.send(encodeEnvelope(envelope));
|
|
10038
|
-
return true;
|
|
10039
|
-
}
|
|
10040
|
-
return false;
|
|
10041
|
-
}
|
|
10042
|
-
waitForAck(timeoutMs = 1e4) {
|
|
10043
|
-
if (this.acked) return Promise.resolve();
|
|
10044
|
-
return new Promise((resolve, reject) => {
|
|
10045
|
-
const timer = setTimeout(() => reject(new Error("Timed out waiting for conn.ack")), timeoutMs);
|
|
10046
|
-
this.ackWaiters.push({
|
|
10047
|
-
resolve: () => {
|
|
10048
|
-
clearTimeout(timer);
|
|
10049
|
-
resolve();
|
|
10050
|
-
},
|
|
10051
|
-
reject: (err) => {
|
|
10052
|
-
clearTimeout(timer);
|
|
10053
|
-
reject(err);
|
|
10054
|
-
}
|
|
10055
|
-
});
|
|
10056
|
-
});
|
|
10057
|
-
}
|
|
10058
|
-
close() {
|
|
10059
|
-
this.closedByUser = true;
|
|
10060
|
-
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
10061
|
-
this.socket?.close();
|
|
10062
|
-
}
|
|
10063
|
-
async openSocket() {
|
|
10064
|
-
const url = toWsUrl(this.opts.serverUrl);
|
|
10065
|
-
const endpoint = describeEndpoint("ws", url);
|
|
10066
|
-
this.acked = false;
|
|
10067
|
-
this.everAckedThisAttempt = false;
|
|
10068
|
-
this.opts.onStateChange?.("connecting");
|
|
10069
|
-
let token;
|
|
10070
|
-
try {
|
|
10071
|
-
token = await this.opts.getToken();
|
|
10072
|
-
} catch (err) {
|
|
10073
|
-
this.opts.onStateChange?.("closed");
|
|
10074
|
-
this.opts.onConnectOutcome?.(false, err, endpoint);
|
|
10075
|
-
if (!this.closedByUser && this.autoReconnect) this.scheduleReconnect();
|
|
10076
|
-
return;
|
|
10077
|
-
}
|
|
10078
|
-
const socket = new WebSocket(url, {
|
|
10079
|
-
headers: { Authorization: `Bearer ${token}` }
|
|
10080
|
-
});
|
|
10081
|
-
this.socket = socket;
|
|
10082
|
-
this.lastActivity = Date.now();
|
|
10083
|
-
this.startLivenessCheck(socket);
|
|
10084
|
-
socket.on("open", () => {
|
|
10085
|
-
const hello = createConnectionHelloEnvelope(this.opts);
|
|
10086
|
-
socket.send(encodeEnvelope(hello));
|
|
10087
|
-
});
|
|
10088
|
-
socket.on("ping", () => {
|
|
10089
|
-
this.lastActivity = Date.now();
|
|
10090
|
-
});
|
|
10091
|
-
socket.on("message", (data, isBinary) => {
|
|
10092
|
-
this.lastActivity = Date.now();
|
|
10093
|
-
const bytes = toBytes(data);
|
|
10094
|
-
let envelope;
|
|
10095
|
-
try {
|
|
10096
|
-
envelope = decodeEnvelope(bytes);
|
|
10097
|
-
} catch {
|
|
10098
|
-
return;
|
|
10099
|
-
}
|
|
10100
|
-
if (envelope.type === "conn.ack") {
|
|
10101
|
-
this.reconnectAttempt = 0;
|
|
10102
|
-
this.acked = true;
|
|
10103
|
-
this.everAckedThisAttempt = true;
|
|
10104
|
-
this.opts.onStateChange?.("open");
|
|
10105
|
-
for (const waiter of this.ackWaiters.splice(0)) waiter.resolve();
|
|
10106
|
-
this.opts.onAcked?.(envelope.payload.capabilities);
|
|
10107
|
-
}
|
|
10108
|
-
this.opts.onEnvelope(envelope);
|
|
10109
|
-
});
|
|
10110
|
-
socket.on("close", (code, reason) => {
|
|
10111
|
-
this.socket = void 0;
|
|
10112
|
-
this.stopLivenessCheck();
|
|
10113
|
-
this.opts.onStateChange?.("closed");
|
|
10114
|
-
const acked = this.everAckedThisAttempt;
|
|
10115
|
-
const status = this.lastUnexpectedStatus;
|
|
10116
|
-
this.lastUnexpectedStatus = void 0;
|
|
10117
|
-
const replayCursorTooOld = code === 1008 && reason.toString("utf8") === "cursor_too_old" ? new ReplayCursorTooOldError() : void 0;
|
|
10118
|
-
this.opts.onConnectOutcome?.(
|
|
10119
|
-
acked,
|
|
10120
|
-
replayCursorTooOld ?? (status !== void 0 ? new WsUnexpectedStatusError(status, endpoint) : void 0),
|
|
10121
|
-
endpoint
|
|
10122
|
-
);
|
|
10123
|
-
if (!this.closedByUser && this.autoReconnect) this.scheduleReconnect();
|
|
10124
|
-
});
|
|
10125
|
-
socket.on("error", () => {
|
|
10126
|
-
});
|
|
10127
|
-
socket.on("unexpected-response", (_req, res) => {
|
|
10128
|
-
this.lastUnexpectedStatus = res.statusCode;
|
|
10129
|
-
res.resume();
|
|
10130
|
-
socket.terminate();
|
|
10131
|
-
});
|
|
10132
|
-
}
|
|
10133
|
-
scheduleReconnect() {
|
|
10134
|
-
const { baseMs = 1e3, maxMs = 3e4, factor = 2 } = this.opts.backoff ?? {};
|
|
10135
|
-
const delay3 = Math.min(maxMs, baseMs * factor ** this.reconnectAttempt);
|
|
10136
|
-
const jitter = this.opts.reconnectDelayMs?.(this.reconnectAttempt, delay3) ?? delay3;
|
|
10137
|
-
this.reconnectAttempt += 1;
|
|
10138
|
-
this.reconnectTimer = setTimeout(() => void this.openSocket(), jitter);
|
|
10139
|
-
}
|
|
10140
|
-
startLivenessCheck(socket) {
|
|
10141
|
-
const { timeoutMs = 75e3, checkIntervalMs = Math.max(1e3, Math.floor(timeoutMs / 3)) } = this.opts.liveness ?? {};
|
|
10142
|
-
this.livenessTimer = setInterval(() => {
|
|
10143
|
-
if (Date.now() - this.lastActivity > timeoutMs) {
|
|
10144
|
-
socket.terminate();
|
|
10145
|
-
}
|
|
10146
|
-
}, checkIntervalMs);
|
|
10147
|
-
this.livenessTimer.unref?.();
|
|
10148
|
-
}
|
|
10149
|
-
stopLivenessCheck() {
|
|
10150
|
-
if (this.livenessTimer) {
|
|
10151
|
-
clearInterval(this.livenessTimer);
|
|
10152
|
-
this.livenessTimer = void 0;
|
|
10153
|
-
}
|
|
10154
|
-
}
|
|
10155
|
-
};
|
|
10156
|
-
function toBytes(data, _isBinary) {
|
|
10157
|
-
if (Buffer.isBuffer(data)) return data;
|
|
10158
|
-
if (Array.isArray(data)) return Buffer.concat(data);
|
|
10159
|
-
if (data instanceof ArrayBuffer) return new Uint8Array(data);
|
|
10160
|
-
return Buffer.from(String(data), "utf8");
|
|
10161
|
-
}
|
|
10162
|
-
|
|
10163
|
-
// src/daemon/connection-manager.ts
|
|
10164
|
-
function isCursorEnvelopeType(type) {
|
|
10165
|
-
return type.startsWith("task.") || type === "agent.egress.ack" || type === "agent.content.read" || type === "agent.home.projection";
|
|
10166
|
-
}
|
|
10202
|
+
var MAX_REJECTED_OUTBOX_ENTRIES = 1e3;
|
|
10167
10203
|
var ConnectionManager = class {
|
|
10168
10204
|
constructor(opts) {
|
|
10169
10205
|
this.opts = opts;
|
|
10170
10206
|
this.fleetJitter = opts.fleetJitter ?? createFleetJitter(opts.productId, opts.deviceId);
|
|
10171
|
-
this.ws = new WsTransport({
|
|
10172
|
-
serverUrl: opts.serverUrl,
|
|
10173
|
-
getToken: () => opts.auth.getValidAccessToken(),
|
|
10174
|
-
deviceId: opts.deviceId,
|
|
10175
|
-
productId: opts.productId,
|
|
10176
|
-
capabilities: opts.capabilities,
|
|
10177
|
-
clientVersion: opts.clientVersion,
|
|
10178
|
-
runtimes: opts.runtimes,
|
|
10179
|
-
getConfiguredToolsets: opts.getConfiguredToolsets,
|
|
10180
|
-
getCursor: () => this.cursor,
|
|
10181
|
-
onEnvelope: (envelope) => this.deliver(envelope),
|
|
10182
|
-
onStateChange: (state) => {
|
|
10183
|
-
if (!this.stopped && !this.revoked) this.opts.onStateChange?.(state);
|
|
10184
|
-
},
|
|
10185
|
-
onAcked: (capabilities) => this.onAcked(capabilities),
|
|
10186
|
-
onConnectOutcome: (acked, err) => this.onWsOutcome(acked, err),
|
|
10187
|
-
backoff: opts.backoff,
|
|
10188
|
-
liveness: opts.liveness,
|
|
10189
|
-
reconnectDelayMs: (attempt, baseMs) => this.fleetJitter.delay("reconnect", attempt, baseMs)
|
|
10190
|
-
});
|
|
10191
10207
|
this.longPoll = new LongPollClient({
|
|
10192
10208
|
serverUrl: opts.serverUrl,
|
|
10193
10209
|
auth: opts.auth,
|
|
10194
|
-
//
|
|
10195
|
-
//
|
|
10196
|
-
//
|
|
10197
|
-
//
|
|
10198
|
-
//
|
|
10199
|
-
|
|
10200
|
-
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,
|
|
10201
10216
|
onEnvelope: (envelope) => this.deliver(envelope),
|
|
10202
10217
|
onServerCapabilities: (capabilities) => {
|
|
10203
|
-
|
|
10218
|
+
this.serverCapabilities = capabilities;
|
|
10219
|
+
this.noteConnected();
|
|
10204
10220
|
},
|
|
10221
|
+
onServerCapabilitiesInvalidated: () => {
|
|
10222
|
+
this.serverCapabilities = [];
|
|
10223
|
+
},
|
|
10224
|
+
onPollFailure: () => this.noteDisconnected(),
|
|
10205
10225
|
onRevoked: () => this.enterRevoked(),
|
|
10206
10226
|
onReplayCursorTooOld: (error) => this.enterReplayCursorTooOld(error),
|
|
10207
10227
|
// M4 Phase 4 (version-negotiation drill fix): a batch entry
|
|
@@ -10229,13 +10249,10 @@ var ConnectionManager = class {
|
|
|
10229
10249
|
}
|
|
10230
10250
|
opts;
|
|
10231
10251
|
fleetJitter;
|
|
10232
|
-
ws;
|
|
10233
10252
|
longPoll;
|
|
10234
|
-
mode = "ws";
|
|
10235
|
-
consecutiveFailures = 0;
|
|
10236
|
-
wsRetryTimer;
|
|
10237
|
-
wsProbeSequence = 0;
|
|
10238
10253
|
uploadRetryAttempt = 0;
|
|
10254
|
+
started = false;
|
|
10255
|
+
connected = false;
|
|
10239
10256
|
cursor;
|
|
10240
10257
|
/**
|
|
10241
10258
|
* Finding F3 (at-most-once redelivery): the lowest `task.*` envelope `seq`
|
|
@@ -10257,10 +10274,9 @@ var ConnectionManager = class {
|
|
|
10257
10274
|
* (see `advanceCursor`) — that semantics is unchanged. `deliveredSeq`
|
|
10258
10275
|
* advances eagerly, the instant a `task.*` envelope is admitted past
|
|
10259
10276
|
* dedup (see `deliver`/`noteDelivered`), independent of whether its
|
|
10260
|
-
* handler has even started, let alone succeeded. It exists so a
|
|
10261
|
-
*
|
|
10262
|
-
*
|
|
10263
|
-
* `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
|
|
10264
10280
|
* first attempt is still running. On WS this same field is written the
|
|
10265
10281
|
* same way, but since a live WS connection only ever pushes a given `seq`
|
|
10266
10282
|
* once, it never has an observable effect there beyond mirroring
|
|
@@ -10271,18 +10287,18 @@ var ConnectionManager = class {
|
|
|
10271
10287
|
/** Finding F3: serializes `onEnvelope` calls into a per-connection FIFO — one envelope's handler always fully settles before the next one starts. */
|
|
10272
10288
|
processingChain = Promise.resolve();
|
|
10273
10289
|
/**
|
|
10274
|
-
* Design B (finding N4): the ONE outbound queue
|
|
10275
|
-
*
|
|
10290
|
+
* Design B (finding N4): the ONE outbound queue holds `Envelope` OBJECTS,
|
|
10291
|
+
* never re-encoded/rebuilt strings, so a
|
|
10276
10292
|
* resend after a failed send attempt is byte-identical to the original
|
|
10277
10293
|
* (same `id`), which is what lets the server's per-(deviceId,id) dedup
|
|
10278
10294
|
* (Wave 1) recognize it as a safe no-op retry rather than a second
|
|
10279
|
-
* application (protocol §9).
|
|
10280
|
-
* touches this queue — see `drainOutbox` — so nothing queued while one
|
|
10281
|
-
* transport was active is ever stranded when the other takes over.
|
|
10295
|
+
* application (protocol §9).
|
|
10282
10296
|
*/
|
|
10283
10297
|
outbox = [];
|
|
10298
|
+
/** Terminally rejected outbound envelopes, retained as a bounded observable quarantine. */
|
|
10299
|
+
rejectedOutboundEnvelopes = [];
|
|
10284
10300
|
/**
|
|
10285
|
-
* Finding F5(b): how many envelopes `drainOutbox`
|
|
10301
|
+
* Finding F5(b): how many envelopes `drainOutbox` has
|
|
10286
10302
|
* currently spliced OUT of `this.outbox` for an in-flight (not yet
|
|
10287
10303
|
* confirmed delivered) `postBatch` call — 0 the rest of the time. See
|
|
10288
10304
|
* `outboxLength`'s own doc comment for why this needs to be tracked
|
|
@@ -10335,39 +10351,22 @@ var ConnectionManager = class {
|
|
|
10335
10351
|
*/
|
|
10336
10352
|
cancelPendingDrainRetry;
|
|
10337
10353
|
/**
|
|
10338
|
-
* The capabilities the
|
|
10339
|
-
* `string[]` for forward compatibility.
|
|
10340
|
-
* long-poll
|
|
10341
|
-
*
|
|
10342
|
-
*
|
|
10343
|
-
*
|
|
10344
|
-
* not per-daemon-lifetime. Cleared to `[]` the instant the acked WS
|
|
10345
|
-
* connection ends for ANY reason — an ordinary disconnect (`onWsOutcome`'s
|
|
10346
|
-
* `acked` branch), `stop()`, or a transport switch to long-poll
|
|
10347
|
-
* (`enterLongPoll`) — and only repopulated by a fresh advertisement from
|
|
10348
|
-
* the transport that is still current.
|
|
10349
|
-
* The previous version of this doc comment claimed long-poll mode simply
|
|
10350
|
-
* "stays at whatever the last real WS `conn.ack` said" — that was the bug:
|
|
10351
|
-
* a daemon that once learned e.g. `approval_resolved` from an earlier WS
|
|
10352
|
-
* session kept believing it applied to whatever it's connected to NOW,
|
|
10353
|
-
* even after a disconnect/degrade where nothing has actually confirmed
|
|
10354
|
-
* that's still true (a reconnect could land on a DIFFERENT server behind a
|
|
10355
|
-
* load balancer). Concretely, `TaskRunner.sendApprovalResolved` gates
|
|
10356
|
-
* `task.approval_resolved` on this list — sending it to a server that
|
|
10357
|
-
* doesn't actually understand it over the long-poll path would get a
|
|
10358
|
-
* batch-level 400 from `MessagesSendRequestSchema` (protocol §8.2), which
|
|
10359
|
-
* `drainOutbox`'s retry-the-same-batch-forever loop then head-of-line
|
|
10360
|
-
* blocks EVERY envelope queued behind it on, permanently. Clearing this
|
|
10361
|
-
* eagerly means that gate reliably fails closed (falls back to the
|
|
10362
|
-
* pre-existing implicit-resume inference, unconditionally — see
|
|
10363
|
-
* `sendApprovalResolved`'s own doc comment) the moment the connection that
|
|
10364
|
-
* 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.
|
|
10365
10360
|
*/
|
|
10366
10361
|
serverCapabilities = [];
|
|
10367
10362
|
async start() {
|
|
10368
10363
|
if (this.terminalError) throw this.terminalError;
|
|
10369
10364
|
this.cursor = await this.opts.cursorStore.load(this.opts.serverUrl, this.opts.deviceId);
|
|
10370
|
-
this.
|
|
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();
|
|
10371
10370
|
}
|
|
10372
10371
|
/**
|
|
10373
10372
|
* Design B (finding N4): push onto the single shared outbox and try to
|
|
@@ -10378,17 +10377,13 @@ var ConnectionManager = class {
|
|
|
10378
10377
|
this.outbox.push(envelope);
|
|
10379
10378
|
void this.drainOutbox();
|
|
10380
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
|
+
}
|
|
10381
10385
|
/**
|
|
10382
|
-
*
|
|
10383
|
-
* transport is currently active, re-checking `this.mode` fresh on every
|
|
10384
|
-
* iteration so a transport switch mid-drain is picked up immediately
|
|
10385
|
-
* rather than fighting a stale decision made before the switch.
|
|
10386
|
-
*
|
|
10387
|
-
* WS: a synchronous, one-at-a-time `sendNow` per envelope while open+
|
|
10388
|
-
* acked; stops (without dropping anything — the remainder stays queued)
|
|
10389
|
-
* the moment it isn't, and is re-invoked once `onAcked` fires.
|
|
10390
|
-
*
|
|
10391
|
-
* Long-poll: POSTs the outbox in chunks of at most
|
|
10386
|
+
* POSTs the outbox through long-poll in chunks of at most
|
|
10392
10387
|
* `MAX_MESSAGES_PER_BATCH` (finding P1) — the server hard-caps a single
|
|
10393
10388
|
* `/byok/messages` batch there (`MessagesSendRequestSchema`, protocol
|
|
10394
10389
|
* §8.2) and 400s the WHOLE request if it's exceeded, which — before this
|
|
@@ -10400,43 +10395,48 @@ var ConnectionManager = class {
|
|
|
10400
10395
|
* failure that SAME chunk is unshifted back (order-preserving, same
|
|
10401
10396
|
* Envelope objects/ids — never rebuilt, so a retry is exactly the resend
|
|
10402
10397
|
* Wave 1's server-side dedup expects) and retried after a short backoff,
|
|
10403
|
-
* re-reading `this.mode` each time so a WS recovery that happens
|
|
10404
|
-
* mid-retry is honored on the very next loop iteration instead of only
|
|
10405
|
-
* after this attempt's backoff chain gives up.
|
|
10406
|
-
*
|
|
10407
10398
|
* Re-entrancy is guarded by `draining`: a call arriving while a drain is
|
|
10408
10399
|
* already in progress just returns — the in-progress loop's own
|
|
10409
|
-
* `while (this.outbox.length > 0)` check
|
|
10410
|
-
* 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.
|
|
10411
10401
|
*/
|
|
10412
10402
|
async drainOutbox() {
|
|
10413
|
-
if (this.draining) return;
|
|
10403
|
+
if (!this.started || this.draining) return;
|
|
10414
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
|
+
};
|
|
10415
10422
|
try {
|
|
10416
10423
|
while (this.outbox.length > 0) {
|
|
10417
10424
|
if (this.stopped || this.revoked) return;
|
|
10418
|
-
if (this.mode === "ws") {
|
|
10419
|
-
if (!this.ws.isOpen) return;
|
|
10420
|
-
const envelope = this.outbox[0];
|
|
10421
|
-
if (!this.ws.sendNow(envelope)) return;
|
|
10422
|
-
this.outbox.shift();
|
|
10423
|
-
continue;
|
|
10424
|
-
}
|
|
10425
10425
|
const batch = this.outbox.splice(0, MAX_MESSAGES_PER_BATCH);
|
|
10426
10426
|
this.inFlightBatchSize = batch.length;
|
|
10427
|
-
let
|
|
10427
|
+
let retrySegments;
|
|
10428
10428
|
try {
|
|
10429
|
-
|
|
10429
|
+
retrySegments = await isolateRejectedBatch(batch);
|
|
10430
10430
|
} finally {
|
|
10431
10431
|
this.inFlightBatchSize = 0;
|
|
10432
10432
|
}
|
|
10433
|
-
if (
|
|
10433
|
+
if (retrySegments === void 0) {
|
|
10434
10434
|
this.uploadRetryAttempt = 0;
|
|
10435
10435
|
this.opts.onOperationalOutcome?.("success", "upload");
|
|
10436
10436
|
continue;
|
|
10437
10437
|
}
|
|
10438
10438
|
this.opts.onOperationalOutcome?.("failure", "upload");
|
|
10439
|
-
this.outbox.unshift(...
|
|
10439
|
+
this.outbox.unshift(...retrySegments);
|
|
10440
10440
|
if (this.stopped || this.revoked) return;
|
|
10441
10441
|
const baseMs = this.opts.longPollRetryDelayMs ?? 2e3;
|
|
10442
10442
|
await this.drainRetryDelay(this.fleetJitter.delay("upload", this.uploadRetryAttempt++, baseMs));
|
|
@@ -10452,8 +10452,7 @@ var ConnectionManager = class {
|
|
|
10452
10452
|
* in-flight wait immediately instead of leaving `drainOutbox` parked here
|
|
10453
10453
|
* for up to the rest of the delay before it next checks `this.revoked` —
|
|
10454
10454
|
* and (b) unref'd, so the timer never keeps the Node process alive by
|
|
10455
|
-
* itself while nothing else (
|
|
10456
|
-
* legitimately is.
|
|
10455
|
+
* itself while nothing else (such as the live long-poll GET) legitimately is.
|
|
10457
10456
|
*/
|
|
10458
10457
|
drainRetryDelay(ms) {
|
|
10459
10458
|
return new Promise((resolve) => {
|
|
@@ -10469,14 +10468,9 @@ var ConnectionManager = class {
|
|
|
10469
10468
|
};
|
|
10470
10469
|
});
|
|
10471
10470
|
}
|
|
10472
|
-
isTransportDegraded() {
|
|
10473
|
-
return this.mode === "long-poll";
|
|
10474
|
-
}
|
|
10475
10471
|
/**
|
|
10476
|
-
* The capabilities the
|
|
10477
|
-
*
|
|
10478
|
-
* on long-poll. Empty before either transport has supplied its current
|
|
10479
|
-
* 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.
|
|
10480
10474
|
*/
|
|
10481
10475
|
getServerCapabilities() {
|
|
10482
10476
|
return this.serverCapabilities;
|
|
@@ -10484,29 +10478,23 @@ var ConnectionManager = class {
|
|
|
10484
10478
|
getTerminalError() {
|
|
10485
10479
|
return this.terminalError;
|
|
10486
10480
|
}
|
|
10487
|
-
getMode() {
|
|
10488
|
-
return this.mode;
|
|
10489
|
-
}
|
|
10490
10481
|
isConnected() {
|
|
10491
|
-
return this.
|
|
10482
|
+
return this.connected;
|
|
10492
10483
|
}
|
|
10493
10484
|
isRevoked() {
|
|
10494
10485
|
return this.revoked;
|
|
10495
10486
|
}
|
|
10496
10487
|
/**
|
|
10497
|
-
* Resolves
|
|
10498
|
-
* connection
|
|
10499
|
-
* lets `daemon.start()` return promptly even when WS is unavailable from
|
|
10500
|
-
* the very first attempt, rather than hanging until a WS `conn.ack` that
|
|
10501
|
-
* may never come.
|
|
10488
|
+
* Resolves after the first successful long-poll response establishes the
|
|
10489
|
+
* authenticated connection.
|
|
10502
10490
|
*
|
|
10503
10491
|
* Rejects with {@link DeviceRevokedError} — instead of hanging until
|
|
10504
10492
|
* `timeoutMs` — if the device turns out to be revoked while settling (or
|
|
10505
10493
|
* already was): a cold `daemon.start()` against an already-revoked device
|
|
10506
10494
|
* must fail fast, not surface a generic timeout (protocol §6.3).
|
|
10507
10495
|
*/
|
|
10508
|
-
|
|
10509
|
-
if (this.
|
|
10496
|
+
waitForConnection(timeoutMs = 1e4) {
|
|
10497
|
+
if (this.connected) return Promise.resolve();
|
|
10510
10498
|
if (this.terminalError) return Promise.reject(this.terminalError);
|
|
10511
10499
|
if (this.revoked) return Promise.reject(new DeviceRevokedError());
|
|
10512
10500
|
return new Promise((resolve, reject) => {
|
|
@@ -10514,7 +10502,7 @@ var ConnectionManager = class {
|
|
|
10514
10502
|
};
|
|
10515
10503
|
const timer = setTimeout(() => {
|
|
10516
10504
|
this.settledWaiters = this.settledWaiters.filter((w) => w !== settle);
|
|
10517
|
-
reject(new Error("Timed out waiting for the connection to settle
|
|
10505
|
+
reject(new Error("Timed out waiting for the long-poll connection to settle"));
|
|
10518
10506
|
}, timeoutMs);
|
|
10519
10507
|
settle = (err) => {
|
|
10520
10508
|
clearTimeout(timer);
|
|
@@ -10525,7 +10513,7 @@ var ConnectionManager = class {
|
|
|
10525
10513
|
});
|
|
10526
10514
|
}
|
|
10527
10515
|
/**
|
|
10528
|
-
* Stops
|
|
10516
|
+
* Stops the long-poll transport and waits for every in-flight envelope handler
|
|
10529
10517
|
* (the F3 FIFO chain) and the most recent cursor write to actually land on
|
|
10530
10518
|
* disk — otherwise a `stop()` racing a just-processed envelope's
|
|
10531
10519
|
* persistence could lose that cursor advance, or leave a handler running
|
|
@@ -10534,14 +10522,14 @@ var ConnectionManager = class {
|
|
|
10534
10522
|
* Finding F5(b) (cross-model adversarial review): `drainTimeoutMs`, when
|
|
10535
10523
|
* passed, bounds how long this waits for the shared outbox (`this.outbox`
|
|
10536
10524
|
* — Design B) to actually finish draining BEFORE flipping `this.stopped`
|
|
10537
|
-
* and
|
|
10525
|
+
* and stopping the transport. Before this fix, `stop()` set `stopped`
|
|
10538
10526
|
* synchronously and never waited for `drainOutbox` at all: an envelope
|
|
10539
10527
|
* `send()` had just pushed moments earlier (e.g. `TaskRunner.shutdownTask`'s
|
|
10540
10528
|
* own `task.fail`, sent right before `create-daemon.ts`'s
|
|
10541
10529
|
* `performControlShutdown` calls this) could still be sitting UNSENT in
|
|
10542
10530
|
* `this.outbox` — mid long-poll retry backoff, or simply not yet picked up
|
|
10543
10531
|
* by the fire-and-forget `drainOutbox()` `send()` kicked off — and this
|
|
10544
|
-
* method would happily proceed to `stopped = true`
|
|
10532
|
+
* method would happily proceed to `stopped = true` regardless,
|
|
10545
10533
|
* after which NOTHING ever drains it again: silently lost, even though
|
|
10546
10534
|
* `TaskRunner` believed it had been sent. `drainTimeoutMs` omitted (the
|
|
10547
10535
|
* default) preserves the EXACT prior behavior for every other existing
|
|
@@ -10558,9 +10546,9 @@ var ConnectionManager = class {
|
|
|
10558
10546
|
}
|
|
10559
10547
|
this.stopped = true;
|
|
10560
10548
|
this.serverCapabilities = [];
|
|
10561
|
-
if (this.wsRetryTimer) clearInterval(this.wsRetryTimer);
|
|
10562
10549
|
this.longPoll.stop();
|
|
10563
|
-
this.
|
|
10550
|
+
this.connected = false;
|
|
10551
|
+
this.opts.onStateChange?.("closed");
|
|
10564
10552
|
await this.processingChain;
|
|
10565
10553
|
await this.pendingCursorSave;
|
|
10566
10554
|
}
|
|
@@ -10585,6 +10573,10 @@ var ConnectionManager = class {
|
|
|
10585
10573
|
outboxLength() {
|
|
10586
10574
|
return this.outbox.length + this.inFlightBatchSize;
|
|
10587
10575
|
}
|
|
10576
|
+
/** A bounded terminal quarantine for operator inspection; these entries are never retried. */
|
|
10577
|
+
rejectedOutbox() {
|
|
10578
|
+
return this.rejectedOutboundEnvelopes;
|
|
10579
|
+
}
|
|
10588
10580
|
/**
|
|
10589
10581
|
* Finding F5(b): polls {@link outboxLength} (not `this.outbox.length`
|
|
10590
10582
|
* alone — see that method's own doc comment for why a spliced-out,
|
|
@@ -10592,15 +10584,14 @@ var ConnectionManager = class {
|
|
|
10592
10584
|
* a single `drainOutbox()` promise directly — a drain in progress can
|
|
10593
10585
|
* itself loop through multiple retry/backoff cycles (`drainRetryDelay`)
|
|
10594
10586
|
* while the server is unreachable, and a fresh, INDEPENDENT
|
|
10595
|
-
* `drainOutbox()` call can also be triggered concurrently (`send()
|
|
10596
|
-
*
|
|
10587
|
+
* `drainOutbox()` call can also be triggered concurrently (`send()`) —
|
|
10588
|
+
* polling the one thing
|
|
10597
10589
|
* that actually matters (is anything still undelivered) can never go
|
|
10598
10590
|
* stale the way capturing one specific in-flight promise reference
|
|
10599
10591
|
* could. Kicks off one more `drainOutbox()` attempt itself first
|
|
10600
10592
|
* (harmless no-op if one is already running — see its own re-entrancy
|
|
10601
|
-
* guard) in case nothing is currently actively retrying
|
|
10602
|
-
*
|
|
10603
|
-
* 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
|
|
10604
10595
|
* progress.
|
|
10605
10596
|
*/
|
|
10606
10597
|
waitForOutboxDrained(timeoutMs) {
|
|
@@ -10643,19 +10634,21 @@ var ConnectionManager = class {
|
|
|
10643
10634
|
deliver(envelope) {
|
|
10644
10635
|
const tracked = isCursorEnvelopeType(envelope.type) && typeof envelope.seq === "number";
|
|
10645
10636
|
const watermark = this.dedupWatermark();
|
|
10646
|
-
if (tracked && watermark !== void 0 && envelope.seq <= watermark) return;
|
|
10637
|
+
if (tracked && watermark !== void 0 && envelope.seq <= watermark) return false;
|
|
10647
10638
|
if (tracked) {
|
|
10648
10639
|
const seq = envelope.seq;
|
|
10649
|
-
if (this.inFlightSeqs.has(seq) || this.processedSeqs.has(seq)) return;
|
|
10640
|
+
if (this.inFlightSeqs.has(seq) || this.processedSeqs.has(seq)) return false;
|
|
10650
10641
|
this.inFlightSeqs.add(seq);
|
|
10651
10642
|
this.noteDelivered(seq);
|
|
10652
10643
|
}
|
|
10653
10644
|
this.processingChain = this.processingChain.then(() => this.process(envelope, tracked));
|
|
10645
|
+
return true;
|
|
10654
10646
|
}
|
|
10655
10647
|
/**
|
|
10656
|
-
*
|
|
10657
|
-
* against
|
|
10658
|
-
*
|
|
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
|
|
10659
10652
|
* `deliveredSeq` — which is always >= `cursor` (every envelope that
|
|
10660
10653
|
* reaches `advanceCursor` already passed through `noteDelivered` first,
|
|
10661
10654
|
* see `deliver`) — so this is the literal `max(cursor, deliveredSeq)` the
|
|
@@ -10671,11 +10664,9 @@ var ConnectionManager = class {
|
|
|
10671
10664
|
* whose outcome wasn't known yet. No separate "reset deliveredSeq on
|
|
10672
10665
|
* reconnect" step is needed for this to be correct — collapsing to
|
|
10673
10666
|
* `cursor` exactly while stalled already produces the right answer on
|
|
10674
|
-
* every
|
|
10675
|
-
*
|
|
10676
|
-
*
|
|
10677
|
-
* re-pulling/re-dispatching something already in flight across a
|
|
10678
|
-
* 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.
|
|
10679
10670
|
*/
|
|
10680
10671
|
dedupWatermark() {
|
|
10681
10672
|
return this.stalledAtSeq !== void 0 ? this.cursor : this.deliveredSeq ?? this.cursor;
|
|
@@ -10693,11 +10684,13 @@ var ConnectionManager = class {
|
|
|
10693
10684
|
}
|
|
10694
10685
|
await this.opts.onEnvelope(envelope);
|
|
10695
10686
|
if (!tracked) return;
|
|
10696
|
-
this.
|
|
10697
|
-
|
|
10687
|
+
if (this.stalledAtSeq !== void 0 && envelope.seq !== this.stalledAtSeq) {
|
|
10688
|
+
this.processedSeqs.add(seq);
|
|
10689
|
+
return;
|
|
10690
|
+
}
|
|
10698
10691
|
this.stalledAtSeq = void 0;
|
|
10692
|
+
await this.advanceCursor(envelope.seq);
|
|
10699
10693
|
this.processedSeqs.clear();
|
|
10700
|
-
this.advanceCursor(envelope.seq);
|
|
10701
10694
|
} catch (err) {
|
|
10702
10695
|
if (tracked && this.stalledAtSeq === void 0) this.stalledAtSeq = envelope.seq;
|
|
10703
10696
|
console.error(
|
|
@@ -10711,9 +10704,8 @@ var ConnectionManager = class {
|
|
|
10711
10704
|
/**
|
|
10712
10705
|
* M4 Phase 4 (version-negotiation drill fix): `LongPollClient` calls this
|
|
10713
10706
|
* for a batch entry it could not parse into a known `Envelope` at all (an
|
|
10714
|
-
* unrecognized message type
|
|
10715
|
-
*
|
|
10716
|
-
* `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,
|
|
10717
10709
|
* task-class envelope-level `seq` (the caller only invokes this for a
|
|
10718
10710
|
* `task.`-prefixed type — see `long-poll-transport.ts`'s own
|
|
10719
10711
|
* `extractSkippableSeq`; `conn.*`-shaped or type-less entries never reach
|
|
@@ -10753,7 +10745,7 @@ var ConnectionManager = class {
|
|
|
10753
10745
|
* `noteDelivered` (the eager, in-memory watermark) stays UNCHAINED —
|
|
10754
10746
|
* called immediately, unconditionally, regardless of `stalledAtSeq` —
|
|
10755
10747
|
* matching `deliver()`'s own eager, unconditional call for a real
|
|
10756
|
-
* envelope: its only job is "don't re-
|
|
10748
|
+
* envelope: its only job is "don't re-dispatch something already handed off,"
|
|
10757
10749
|
* independent of outcome, and that property does not depend on FIFO
|
|
10758
10750
|
* ordering the way the DURABLE cursor does.
|
|
10759
10751
|
*
|
|
@@ -10773,9 +10765,18 @@ var ConnectionManager = class {
|
|
|
10773
10765
|
*/
|
|
10774
10766
|
noteSkippedSeq(seq) {
|
|
10775
10767
|
this.noteDelivered(seq);
|
|
10776
|
-
this.processingChain = this.processingChain.then(() => {
|
|
10768
|
+
this.processingChain = this.processingChain.then(async () => {
|
|
10777
10769
|
if (this.stalledAtSeq === void 0 || seq === this.stalledAtSeq) {
|
|
10778
|
-
|
|
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
|
+
}
|
|
10779
10780
|
}
|
|
10780
10781
|
});
|
|
10781
10782
|
}
|
|
@@ -10821,113 +10822,52 @@ var ConnectionManager = class {
|
|
|
10821
10822
|
if (this.stalledAtSeq === void 0) this.stalledAtSeq = seq;
|
|
10822
10823
|
});
|
|
10823
10824
|
}
|
|
10824
|
-
advanceCursor(seq) {
|
|
10825
|
+
async advanceCursor(seq) {
|
|
10825
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;
|
|
10826
10830
|
this.cursor = seq;
|
|
10827
|
-
this.pendingCursorSave = this.pendingCursorSave.catch(() => {
|
|
10828
|
-
}).then(() => this.opts.cursorStore.save(this.opts.serverUrl, this.opts.deviceId, seq)).catch(() => {
|
|
10829
|
-
});
|
|
10830
|
-
}
|
|
10831
|
-
/**
|
|
10832
|
-
* Fires the moment a connection attempt reaches `conn.ack` — independent
|
|
10833
|
-
* of whether/when it later closes. This is the ONLY place that can
|
|
10834
|
-
* reliably detect "WS is back up" while long-polling: a healthy
|
|
10835
|
-
* connection stays open indefinitely, so it never reaches `onWsOutcome`
|
|
10836
|
-
* (which is close-only) at all.
|
|
10837
|
-
*/
|
|
10838
|
-
onAcked(capabilities) {
|
|
10839
|
-
if (this.terminalError) return;
|
|
10840
|
-
this.serverCapabilities = capabilities;
|
|
10841
|
-
this.consecutiveFailures = 0;
|
|
10842
|
-
this.notifySettled();
|
|
10843
|
-
this.opts.onOperationalOutcome?.("success", "reconnect");
|
|
10844
|
-
if (this.mode === "long-poll") this.exitLongPoll();
|
|
10845
|
-
void this.drainOutbox();
|
|
10846
10831
|
}
|
|
10847
|
-
|
|
10848
|
-
if (
|
|
10849
|
-
|
|
10850
|
-
|
|
10851
|
-
|
|
10852
|
-
|
|
10853
|
-
if (acked) this.serverCapabilities = [];
|
|
10854
|
-
if (err instanceof WsUnexpectedStatusError && err.status === 401) {
|
|
10855
|
-
this.opts.auth.handleUnauthorized().catch((renewErr) => {
|
|
10856
|
-
if (renewErr instanceof DeviceRevokedError) this.enterRevoked();
|
|
10857
|
-
});
|
|
10858
|
-
}
|
|
10859
|
-
if (acked) return;
|
|
10860
|
-
this.opts.onOperationalOutcome?.("failure", "reconnect");
|
|
10861
|
-
this.consecutiveFailures += 1;
|
|
10862
|
-
if (this.mode === "ws" && this.consecutiveFailures >= (this.opts.wsFailureThreshold ?? 3)) {
|
|
10863
|
-
this.enterLongPoll();
|
|
10864
|
-
}
|
|
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
|
+
);
|
|
10865
10838
|
}
|
|
10866
10839
|
notifySettled(err) {
|
|
10867
10840
|
for (const waiter of this.settledWaiters.splice(0)) waiter(err);
|
|
10868
10841
|
}
|
|
10869
|
-
|
|
10870
|
-
this.
|
|
10871
|
-
this.
|
|
10872
|
-
this.
|
|
10873
|
-
this.opts.onStateChange?.("degraded");
|
|
10874
|
-
this.longPoll.start();
|
|
10875
|
-
this.outbox.unshift(createConnectionHelloEnvelope({
|
|
10876
|
-
deviceId: this.opts.deviceId,
|
|
10877
|
-
productId: this.opts.productId,
|
|
10878
|
-
capabilities: this.opts.capabilities,
|
|
10879
|
-
clientVersion: this.opts.clientVersion,
|
|
10880
|
-
runtimes: this.opts.runtimes,
|
|
10881
|
-
getConfiguredToolsets: this.opts.getConfiguredToolsets,
|
|
10882
|
-
getCursor: () => this.cursor
|
|
10883
|
-
}));
|
|
10842
|
+
noteConnected() {
|
|
10843
|
+
if (this.connected || this.stopped || this.revoked || this.terminalError) return;
|
|
10844
|
+
this.connected = true;
|
|
10845
|
+
this.opts.onStateChange?.("open");
|
|
10884
10846
|
this.notifySettled();
|
|
10885
|
-
|
|
10886
|
-
|
|
10847
|
+
}
|
|
10848
|
+
noteDisconnected() {
|
|
10849
|
+
if (!this.connected || this.stopped || this.revoked || this.terminalError) return;
|
|
10850
|
+
this.connected = false;
|
|
10851
|
+
this.opts.onStateChange?.("connecting");
|
|
10887
10852
|
}
|
|
10888
10853
|
enterReplayCursorTooOld(error) {
|
|
10889
10854
|
if (this.terminalError) return;
|
|
10890
10855
|
this.terminalError = error;
|
|
10891
10856
|
this.stopped = true;
|
|
10892
10857
|
this.serverCapabilities = [];
|
|
10893
|
-
if (this.wsRetryTimer) clearInterval(this.wsRetryTimer);
|
|
10894
10858
|
this.longPoll.stop();
|
|
10895
|
-
this.
|
|
10896
|
-
this.ws.close();
|
|
10859
|
+
this.connected = false;
|
|
10897
10860
|
this.cancelPendingDrainRetry?.();
|
|
10898
10861
|
this.notifySettled(error);
|
|
10899
10862
|
this.opts.onStateChange?.("closed");
|
|
10900
10863
|
this.opts.onTerminalError?.(error);
|
|
10901
10864
|
}
|
|
10902
|
-
exitLongPoll() {
|
|
10903
|
-
if (this.wsRetryTimer) {
|
|
10904
|
-
clearInterval(this.wsRetryTimer);
|
|
10905
|
-
this.wsRetryTimer = void 0;
|
|
10906
|
-
}
|
|
10907
|
-
this.longPoll.stop();
|
|
10908
|
-
this.mode = "ws";
|
|
10909
|
-
this.consecutiveFailures = 0;
|
|
10910
|
-
this.ws.resumeAutoReconnect();
|
|
10911
|
-
void this.drainOutbox();
|
|
10912
|
-
}
|
|
10913
|
-
scheduleWsProbe() {
|
|
10914
|
-
const baseMs = this.opts.wsRetryIntervalMs ?? 5 * 60 * 1e3;
|
|
10915
|
-
const delayMs = this.fleetJitter.delay("reconnect", this.wsProbeSequence++, baseMs);
|
|
10916
|
-
const timer = setTimeout(() => {
|
|
10917
|
-
this.wsRetryTimer = void 0;
|
|
10918
|
-
if (this.stopped || this.revoked || this.mode !== "long-poll") return;
|
|
10919
|
-
this.ws.connect({ auto: false });
|
|
10920
|
-
this.scheduleWsProbe();
|
|
10921
|
-
}, delayMs);
|
|
10922
|
-
timer.unref?.();
|
|
10923
|
-
this.wsRetryTimer = timer;
|
|
10924
|
-
}
|
|
10925
10865
|
enterRevoked() {
|
|
10926
10866
|
if (this.revoked) return;
|
|
10927
10867
|
this.revoked = true;
|
|
10928
|
-
|
|
10868
|
+
this.serverCapabilities = [];
|
|
10929
10869
|
this.longPoll.stop();
|
|
10930
|
-
this.
|
|
10870
|
+
this.connected = false;
|
|
10931
10871
|
this.cancelPendingDrainRetry?.();
|
|
10932
10872
|
this.opts.onStateChange?.("revoked");
|
|
10933
10873
|
this.notifySettled(new DeviceRevokedError());
|
|
@@ -14261,6 +14201,7 @@ function resultDocumentRejectionDetail(check) {
|
|
|
14261
14201
|
}
|
|
14262
14202
|
}
|
|
14263
14203
|
var DEFAULT_MAX_TASK_OUTPUT_BYTES = 64 * 1024 * 1024;
|
|
14204
|
+
var DEFAULT_MAX_CONCURRENT_MUTABLE_SESSIONS_PER_AGENT_HOME = 1;
|
|
14264
14205
|
function isKnownRuntimeId(id) {
|
|
14265
14206
|
return RuntimeIdSchema.safeParse(id).success;
|
|
14266
14207
|
}
|
|
@@ -14507,6 +14448,10 @@ var TaskRunner = class {
|
|
|
14507
14448
|
get maxTaskOutputBytes() {
|
|
14508
14449
|
return this.deps.maxTaskOutputBytes ?? DEFAULT_MAX_TASK_OUTPUT_BYTES;
|
|
14509
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
|
+
}
|
|
14510
14455
|
/**
|
|
14511
14456
|
* M4 Phase 4 (part B.3, observability): per-active-task queue watermarks
|
|
14512
14457
|
* for the control socket's `status` result — see
|
|
@@ -14896,6 +14841,24 @@ var TaskRunner = class {
|
|
|
14896
14841
|
decline("strict Agent-only daemon refuses legacy task offers", false);
|
|
14897
14842
|
return;
|
|
14898
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
|
+
}
|
|
14899
14862
|
const guarded = this.deps.admissionGuard?.({ taskId, payload });
|
|
14900
14863
|
if (guarded !== void 0 && !guarded.admit) {
|
|
14901
14864
|
decline(guarded.reason, guarded.retryable);
|
|
@@ -18607,7 +18570,7 @@ async function detectRuntimes(adapters) {
|
|
|
18607
18570
|
}
|
|
18608
18571
|
return runtimes;
|
|
18609
18572
|
}
|
|
18610
|
-
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) {
|
|
18611
18574
|
const flags = [];
|
|
18612
18575
|
if (adapters.some((adapter) => adapter.descriptor.capabilities.steer)) flags.push("steer");
|
|
18613
18576
|
flags.push("blob-upload");
|
|
@@ -18619,6 +18582,9 @@ function computeCapabilities(adapters, agentHomeConfigured = false, strictAgentO
|
|
|
18619
18582
|
if (selectionAdapters.length > 0 && selectionAdapters.every((adapter) => adapter.descriptor.supportsDispatchSelection === true)) {
|
|
18620
18583
|
flags.push("dispatch-selection");
|
|
18621
18584
|
}
|
|
18585
|
+
if (providerProfileBindingConfigured && adapters.some((adapter) => adapter.descriptor.id === "pi" && adapter.descriptor.supportsDispatchSelection)) {
|
|
18586
|
+
flags.push(PROVIDER_PROFILE_BINDING_CAPABILITY);
|
|
18587
|
+
}
|
|
18622
18588
|
if (adapters.some((adapter) => adapter.descriptor.capabilities.mcpToolsets === true)) {
|
|
18623
18589
|
flags.push("toolset-selection");
|
|
18624
18590
|
}
|
|
@@ -18781,6 +18747,12 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
18781
18747
|
if (config.strictAgentOnly === true && config.agentHome === void 0) {
|
|
18782
18748
|
throw new Error("DaemonConfig.strictAgentOnly requires DaemonConfig.agentHome");
|
|
18783
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;
|
|
18784
18756
|
const egressPolicy = resolveAgentEgressPolicy(config.agentEgress?.policy);
|
|
18785
18757
|
const egressBatcherOptions = egressPolicy.activity.mode === "contentful-trajectory" ? {
|
|
18786
18758
|
...config.progressBatch,
|
|
@@ -19101,7 +19073,8 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
19101
19073
|
config.strictAgentOnly === true,
|
|
19102
19074
|
agentHomeManager?.supportsTaskFreeProjection() === true,
|
|
19103
19075
|
config.agentEgress !== void 0,
|
|
19104
|
-
agentContentReadPolicies
|
|
19076
|
+
agentContentReadPolicies,
|
|
19077
|
+
config.piByokLauncher !== void 0
|
|
19105
19078
|
);
|
|
19106
19079
|
const agentHomeProjectionCompletion = agentHomeManager?.supportsTaskFreeProjection() === true ? new AgentHomeProjectionCompletionClient({
|
|
19107
19080
|
serverUrl: config.serverUrl,
|
|
@@ -19160,6 +19133,8 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
19160
19133
|
workspaceRoot: config.workspaceRoot,
|
|
19161
19134
|
...agentHomeManager === void 0 ? {} : { agentHome: agentHomeManager },
|
|
19162
19135
|
...config.strictAgentOnly === true ? { strictAgentOnly: true } : {},
|
|
19136
|
+
// WP0: already validated up front — see `DaemonConfig.maxConcurrentMutableSessionsPerAgentHome`.
|
|
19137
|
+
maxConcurrentMutableSessionsPerAgentHome: agentHomeAttemptLimit,
|
|
19163
19138
|
...agentSessionHandoffs === void 0 ? {} : { agentSessionHandoffs },
|
|
19164
19139
|
deviceId: record3.deviceId,
|
|
19165
19140
|
// M5: see `DaemonConfig.runtimeEnvironment`'s own doc comment above.
|
|
@@ -19444,16 +19419,12 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
19444
19419
|
return runner?.handleEnvelope(envelope) ?? Promise.resolve();
|
|
19445
19420
|
},
|
|
19446
19421
|
onStateChange: (state) => {
|
|
19447
|
-
const wasSettled = connectionState === "open"
|
|
19422
|
+
const wasSettled = connectionState === "open";
|
|
19448
19423
|
connectionState = state;
|
|
19449
19424
|
observer.noteConnectionState(state);
|
|
19450
|
-
if (!wasSettled &&
|
|
19451
|
-
if (!wasSettled &&
|
|
19425
|
+
if (!wasSettled && state === "open") runner?.retryRecoveredAgentMessages();
|
|
19426
|
+
if (!wasSettled && state === "open") runPresenceDiscovery();
|
|
19452
19427
|
},
|
|
19453
|
-
backoff: overrides.backoff,
|
|
19454
|
-
liveness: overrides.liveness,
|
|
19455
|
-
wsFailureThreshold: overrides.longPoll?.wsFailureThreshold,
|
|
19456
|
-
wsRetryIntervalMs: overrides.longPoll?.wsRetryIntervalMs,
|
|
19457
19428
|
longPollRetryDelayMs: overrides.longPoll?.retryDelayMs,
|
|
19458
19429
|
longPollIdleDelayMs: overrides.longPoll?.idleDelayMs,
|
|
19459
19430
|
fleetJitter,
|
|
@@ -19465,7 +19436,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
19465
19436
|
}
|
|
19466
19437
|
});
|
|
19467
19438
|
await connection.start();
|
|
19468
|
-
await connection.
|
|
19439
|
+
await connection.waitForConnection();
|
|
19469
19440
|
runner.retryRecoveredAgentMessages();
|
|
19470
19441
|
for (const record4 of agentEgress.retryableReliableRecords(connection.getServerCapabilities())) {
|
|
19471
19442
|
dispatchReliableRecord(record4);
|
|
@@ -19724,7 +19695,9 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
19724
19695
|
// fine.
|
|
19725
19696
|
...storageStatus === void 0 ? {} : { storage: storageStatus },
|
|
19726
19697
|
operationalHealth: operationalHealth.snapshot(),
|
|
19727
|
-
toolsets: toolsetRegistry.status()
|
|
19698
|
+
toolsets: toolsetRegistry.status(),
|
|
19699
|
+
// WP0: same counts `Daemon.status()` reports, from the same reader.
|
|
19700
|
+
agentHomeExecution: agentHomeExecutionStatus()
|
|
19728
19701
|
};
|
|
19729
19702
|
}
|
|
19730
19703
|
async function performControlShutdown(reason) {
|
|
@@ -19959,9 +19932,8 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
19959
19932
|
const minted = mintDeviceAssertion({
|
|
19960
19933
|
record: record3,
|
|
19961
19934
|
// `toHttpBase` is the one place a configured serverUrl is normalized
|
|
19962
|
-
// (
|
|
19963
|
-
//
|
|
19964
|
-
// spelling of the same deployment produce the same issuer.
|
|
19935
|
+
// (path stripped), so the issuer ignores an operator-provided path
|
|
19936
|
+
// or query.
|
|
19965
19937
|
issuer: new URL(toHttpBase(config.serverUrl)).origin,
|
|
19966
19938
|
productId: config.productId,
|
|
19967
19939
|
audience: parsed.audience,
|
|
@@ -20076,23 +20048,33 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
20076
20048
|
await binding.lease.release();
|
|
20077
20049
|
}
|
|
20078
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
|
+
}
|
|
20079
20059
|
function status() {
|
|
20080
20060
|
return {
|
|
20081
20061
|
localAgentRelease,
|
|
20082
20062
|
paired: auth.deviceId !== void 0,
|
|
20083
20063
|
connected: connectionState === "open",
|
|
20084
|
-
degraded: connection?.isTransportDegraded() ?? false,
|
|
20085
20064
|
revoked: connection?.isRevoked() ?? auth.isRevoked(),
|
|
20086
20065
|
deviceId: auth.deviceId,
|
|
20087
20066
|
activeTaskCount: runner?.activeTaskCount ?? 0,
|
|
20088
20067
|
branding: config.branding,
|
|
20089
20068
|
operationalHealth: operationalHealth.snapshot(),
|
|
20090
20069
|
toolsets: toolsetRegistry.status(),
|
|
20091
|
-
egress: agentEgress.status()
|
|
20070
|
+
egress: agentEgress.status(),
|
|
20071
|
+
agentHomeExecution: agentHomeExecutionStatus()
|
|
20092
20072
|
};
|
|
20093
20073
|
}
|
|
20094
20074
|
function reloadMcpToolsets(mcpToolsets, expectedRevision) {
|
|
20095
|
-
|
|
20075
|
+
const receipt = toolsetRegistry.reload(mcpToolsets, expectedRevision);
|
|
20076
|
+
if (daemonStarted) connection?.refreshHello();
|
|
20077
|
+
return receipt;
|
|
20096
20078
|
}
|
|
20097
20079
|
function reportMcpToolsetObservation(toolsetId, expectedDefinitionRevision, observation) {
|
|
20098
20080
|
toolsetRegistry.report(toolsetId, expectedDefinitionRevision, observation);
|