@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/bin/byok-agent.js
CHANGED
|
@@ -3,7 +3,7 @@ import { randomUUID, createHash, randomBytes, timingSafeEqual, createHmac, creat
|
|
|
3
3
|
import { readFileSync, promises, linkSync, fstatSync, lstatSync, unlinkSync, constants, readSync, openSync, writeFileSync, fchmodSync, fsyncSync, closeSync, opendirSync, existsSync, realpathSync, mkdirSync, renameSync, chmodSync, statSync, readdirSync } from 'fs';
|
|
4
4
|
import * as path3 from 'path';
|
|
5
5
|
import path3__default, { isAbsolute, join } from 'path';
|
|
6
|
-
import { AGENT_CONTENT_ARTIFACT_READ_CAPABILITY, AGENT_CONTENT_TRANSCRIPT_READ_CAPABILITY, AGENT_CONTENT_WORKSPACE_READ_CAPABILITY, TASK_STATES, AgentEgressPolicySchema, AgentHomeProjectionPayloadSchema, CONFIGURED_TOOLSETS_MAX_ITEMS, ToolsetIdSchema, AgentRefSchema, AgentContentReceiptPayloadSchema, BYOK_PAIR_PATH, PairResponseSchema, BYOK_CHALLENGE_PATH, BYOK_TOKEN_PATH, partitionAgentEvents, TASK_TRANSITIONS, encodeEnvelope, PROTOCOL_VERSION, createEnvelope, AGENT_EGRESS_RELIABLE_ACK_CAPABILITY, byokBlobUrlPath, BYOK_BLOBS_PATH, byokBlobFinalizePath, TERMINAL_PROJECTION_SELECTION_CAPABILITY, STRICT_AGENT_ONLY_CAPABILITY, AGENT_HOME_PROJECTION_CAPABILITY, AGENT_EGRESS_POLICY_CAPABILITY, AGENT_EGRESS_FRESH_SESSION_CAPABILITY, AGENT_MESSAGE_EGRESS_CAPABILITY, AgentHomeProjectionCompletionRequestSchema, byokAgentHomeProjectionCompletionPath, AgentHomeProjectionReadbackSchema, parseMessage, checkResultDocument, MAX_MESSAGES_PER_BATCH, BYOK_CAPABILITIES_PATH, BYOK_PRESENCE_PATH, AgentMessagePublishPayloadSchema, AgentMessageDispositionPayloadSchema, RuntimeIdSchema, AGENT_MEMORY_PROJECTION_CAPABILITY, TERMINAL_INFERENCE_USAGE_MAX_DURATION_MS, TERMINAL_INFERENCE_USAGE_MAX_TOKENS, RESULT_DOCUMENT_MAX_BYTES,
|
|
6
|
+
import { AGENT_CONTENT_ARTIFACT_READ_CAPABILITY, AGENT_CONTENT_TRANSCRIPT_READ_CAPABILITY, AGENT_CONTENT_WORKSPACE_READ_CAPABILITY, TASK_STATES, AgentEgressPolicySchema, AgentHomeProjectionPayloadSchema, CONFIGURED_TOOLSETS_MAX_ITEMS, ToolsetIdSchema, AgentRefSchema, AgentContentReceiptPayloadSchema, BYOK_PAIR_PATH, PairResponseSchema, BYOK_CHALLENGE_PATH, BYOK_TOKEN_PATH, partitionAgentEvents, TASK_TRANSITIONS, encodeEnvelope, PROTOCOL_VERSION, createEnvelope, AGENT_EGRESS_RELIABLE_ACK_CAPABILITY, byokBlobUrlPath, BYOK_BLOBS_PATH, byokBlobFinalizePath, TERMINAL_PROJECTION_SELECTION_CAPABILITY, PROVIDER_PROFILE_BINDING_CAPABILITY, STRICT_AGENT_ONLY_CAPABILITY, AGENT_HOME_PROJECTION_CAPABILITY, AGENT_EGRESS_POLICY_CAPABILITY, AGENT_EGRESS_FRESH_SESSION_CAPABILITY, AGENT_MESSAGE_EGRESS_CAPABILITY, AgentHomeProjectionCompletionRequestSchema, byokAgentHomeProjectionCompletionPath, AgentHomeProjectionReadbackSchema, parseMessage, checkResultDocument, MAX_MESSAGES_PER_BATCH, BYOK_CAPABILITIES_PATH, BYOK_PRESENCE_PATH, AgentMessagePublishPayloadSchema, AgentMessageDispositionPayloadSchema, RuntimeIdSchema, AGENT_MEMORY_PROJECTION_CAPABILITY, TERMINAL_INFERENCE_USAGE_MAX_DURATION_MS, TERMINAL_INFERENCE_USAGE_MAX_TOKENS, RESULT_DOCUMENT_MAX_BYTES, BYOK_EVENTS_PATH, BYOK_MESSAGES_PATH, MessagesSendResponseSchema, UnknownMessageTypeError, MESSAGE_TYPES, AGENT_MEMORY_PROJECTION_MAX_ORDERING_VALUE, AgentMemoryProjectionMutationSchema, AGENT_MEMORY_PROJECTION_MAX_REDACTED_BYTES } from '@byok-sdk/protocol';
|
|
7
7
|
import net, { createServer, createConnection } from 'net';
|
|
8
8
|
import * as os from 'os';
|
|
9
9
|
import os__default from 'os';
|
|
@@ -13,7 +13,6 @@ import 'readline';
|
|
|
13
13
|
import { isTenantId, DEVICE_ASSERTION_AUDIENCE_MAX_BYTES, DEVICE_ASSERTION_DEFAULT_TTL_MS, DEVICE_ASSERTION_MAX_TTL_MS, nonceSigningBytes, CapabilityDeclarationSchema, hasCapability, DeviceAssertionClaimsSchema, deviceAssertionSigningInput, DEVICE_ASSERTION_SCHEMA_ID } from '@byok-sdk/core';
|
|
14
14
|
import { promisify } from 'util';
|
|
15
15
|
import * as fs13 from 'fs/promises';
|
|
16
|
-
import { WebSocket } from 'ws';
|
|
17
16
|
import { createRequire } from 'module';
|
|
18
17
|
import { createInterface } from 'readline/promises';
|
|
19
18
|
|
|
@@ -445,6 +444,23 @@ async function resolveExistingAncestor(inputPath) {
|
|
|
445
444
|
}
|
|
446
445
|
}
|
|
447
446
|
}
|
|
447
|
+
async function canonicalPath(inputPath) {
|
|
448
|
+
const { canonical: canonical2, tail } = await resolveExistingAncestor(inputPath);
|
|
449
|
+
return path3__default.resolve(canonical2, ...tail);
|
|
450
|
+
}
|
|
451
|
+
async function assertRealDirectoryIfPresent(target) {
|
|
452
|
+
let stat;
|
|
453
|
+
try {
|
|
454
|
+
stat = await promises.lstat(target);
|
|
455
|
+
} catch (error) {
|
|
456
|
+
const code = error.code;
|
|
457
|
+
if (code === "ENOENT" || code === "ENOTDIR") return;
|
|
458
|
+
throw error;
|
|
459
|
+
}
|
|
460
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
461
|
+
throw new AgentHomeResolutionError(`Agent home path component is not a real directory: ${target}`);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
448
464
|
async function materializeDirectory(inputPath) {
|
|
449
465
|
const { canonical: canonical2, tail } = await resolveExistingAncestor(inputPath);
|
|
450
466
|
let cursor = canonical2;
|
|
@@ -539,6 +555,39 @@ var AgentHomeLayout = class {
|
|
|
539
555
|
await gate.release();
|
|
540
556
|
}
|
|
541
557
|
}
|
|
558
|
+
/**
|
|
559
|
+
* Pure canonical-home derivation for read-only callers, such as the
|
|
560
|
+
* pre-admission single-writer count. It validates the AgentRef and joins
|
|
561
|
+
* exactly the same `<hostStorageRoot>/agents/<agentId>` segments
|
|
562
|
+
* {@link AgentHomeLayout.resolve} would, canonicalizing only the components
|
|
563
|
+
* that already exist.
|
|
564
|
+
*
|
|
565
|
+
* It deliberately creates no directory, takes no cross-process mutation
|
|
566
|
+
* gate and records no Agent binding, so an offer the host vetoes after the
|
|
567
|
+
* count leaves nothing behind on disk. `resolve()` stays the only path that
|
|
568
|
+
* may materialize a home or bind it to an Agent identity.
|
|
569
|
+
*
|
|
570
|
+
* An `agents` root or `agents/<agentId>` leaf that already exists but is a
|
|
571
|
+
* symlink (or any non-directory) is rejected here with the same error class
|
|
572
|
+
* and message `resolve()` raises for it, so an in-root `two -> one` link
|
|
573
|
+
* fails closed instead of silently keying the count of `one`. A leaf that
|
|
574
|
+
* does not exist yet is not an error: this derivation runs before the home
|
|
575
|
+
* is materialized. A home canonicalizing outside the `agents` root stays
|
|
576
|
+
* rejected as before.
|
|
577
|
+
*/
|
|
578
|
+
async canonicalHomePath(agentRefInput) {
|
|
579
|
+
const agentRef = validateAgentRef(agentRefInput);
|
|
580
|
+
const hostStorageRoot = this.canonicalRoot ?? await canonicalPath(this.hostStorageRootInput);
|
|
581
|
+
const agentsRoot = path3__default.join(hostStorageRoot, AGENT_HOME_DIRECTORY);
|
|
582
|
+
await assertRealDirectoryIfPresent(agentsRoot);
|
|
583
|
+
const lexicalHome = path3__default.join(agentsRoot, agentRef.agentId);
|
|
584
|
+
await assertRealDirectoryIfPresent(lexicalHome);
|
|
585
|
+
const canonicalHome = await canonicalPath(lexicalHome);
|
|
586
|
+
if (canonicalHome === agentsRoot || !isWithin(agentsRoot, canonicalHome)) {
|
|
587
|
+
throw new AgentHomeResolutionError(`Agent home resolves outside the Agent home root: ${canonicalHome}`);
|
|
588
|
+
}
|
|
589
|
+
return canonicalHome;
|
|
590
|
+
}
|
|
542
591
|
/**
|
|
543
592
|
* Prove the canonical root is materializable and writable before the daemon
|
|
544
593
|
* advertises Agent-home capability. No Agent identity or persistent Agent
|
|
@@ -855,6 +904,44 @@ var AgentHomeExecutionLeaseManager = class _AgentHomeExecutionLeaseManager {
|
|
|
855
904
|
});
|
|
856
905
|
});
|
|
857
906
|
}
|
|
907
|
+
/**
|
|
908
|
+
* WP0: Attempts currently holding an execution lease on this exact
|
|
909
|
+
* canonical home, across every lane and every session. This is the number
|
|
910
|
+
* the daemon's admission gate reads before any side effect — see
|
|
911
|
+
* `TaskRunner.handleOffer`'s per-home busy gate.
|
|
912
|
+
*
|
|
913
|
+
* Derived from the one lease registry above rather than a second tally, so
|
|
914
|
+
* it inherits the lease lifecycle exactly: an entry appears at `acquire()`,
|
|
915
|
+
* survives `bindSession()` (which rekeys in place), and disappears only at
|
|
916
|
+
* `release()`, which the task runner calls after the attempt is terminal
|
|
917
|
+
* AND `Session.close()` resolved. A failed disposal never reaches
|
|
918
|
+
* `release()`, so the slot stays held — fail closed, the same posture as
|
|
919
|
+
* `runtime-disposal-failed`. Crash residue needs nothing extra here: a
|
|
920
|
+
* restarted daemon starts with an empty registry and reclaims the on-disk
|
|
921
|
+
* marker only under the same stable owner identity (`openLeaseMarker`).
|
|
922
|
+
*
|
|
923
|
+
* Counted regardless of which lease manager owns the group: the invariant
|
|
924
|
+
* being protected is the filesystem path (`MEMORY.md`, `notes/`, `.git`),
|
|
925
|
+
* not the owner identity.
|
|
926
|
+
*/
|
|
927
|
+
activeAttemptCount(canonicalHome) {
|
|
928
|
+
return _AgentHomeExecutionLeaseManager.groups.get(canonicalHome)?.leasesByKey.size ?? 0;
|
|
929
|
+
}
|
|
930
|
+
/**
|
|
931
|
+
* Counts-only readback for daemon/control status. Scoped to this manager's
|
|
932
|
+
* own leases, so the number describes this daemon rather than every home
|
|
933
|
+
* any manager in the process happens to hold. Never exposes a home path.
|
|
934
|
+
*/
|
|
935
|
+
activeAttemptSummary() {
|
|
936
|
+
let homes = 0;
|
|
937
|
+
let attempts = 0;
|
|
938
|
+
for (const group of _AgentHomeExecutionLeaseManager.groups.values()) {
|
|
939
|
+
if (group.manager !== this.manager) continue;
|
|
940
|
+
homes += 1;
|
|
941
|
+
attempts += group.leasesByKey.size;
|
|
942
|
+
}
|
|
943
|
+
return { homes, attempts };
|
|
944
|
+
}
|
|
858
945
|
async mutate(binding, operation) {
|
|
859
946
|
return this.exclusive(binding.resolution.canonicalHome, async () => {
|
|
860
947
|
const group = _AgentHomeExecutionLeaseManager.groups.get(binding.resolution.canonicalHome);
|
|
@@ -1818,13 +1905,22 @@ function freezeRuntimeAdapterDescriptor(descriptor) {
|
|
|
1818
1905
|
});
|
|
1819
1906
|
}
|
|
1820
1907
|
function sealRuntimeOperationManifest(manifest) {
|
|
1908
|
+
const dispatchSelection = manifest.dispatchSelection === void 0 ? void 0 : manifest.dispatchSelection.lane === "byok-profile" ? Object.freeze({
|
|
1909
|
+
...manifest.dispatchSelection,
|
|
1910
|
+
providerProfile: Object.freeze({
|
|
1911
|
+
...manifest.dispatchSelection.providerProfile,
|
|
1912
|
+
requiredCapabilities: Object.freeze([
|
|
1913
|
+
...manifest.dispatchSelection.providerProfile.requiredCapabilities
|
|
1914
|
+
])
|
|
1915
|
+
})
|
|
1916
|
+
}) : Object.freeze({ ...manifest.dispatchSelection });
|
|
1821
1917
|
return Object.freeze({
|
|
1822
1918
|
taskId: manifest.taskId,
|
|
1823
1919
|
runtimeId: manifest.runtimeId,
|
|
1824
1920
|
descriptor: freezeRuntimeAdapterDescriptor(manifest.descriptor),
|
|
1825
1921
|
policy: frozenPolicy(manifest.policy),
|
|
1826
1922
|
requiredToolsetIds: Object.freeze([...manifest.requiredToolsetIds]),
|
|
1827
|
-
...
|
|
1923
|
+
...dispatchSelection === void 0 ? {} : { dispatchSelection },
|
|
1828
1924
|
...manifest.sessionRef === void 0 ? {} : { sessionRef: manifest.sessionRef },
|
|
1829
1925
|
...manifest.agentRef === void 0 ? {} : { agentRef: Object.freeze({ agentId: manifest.agentRef.agentId, profileRevision: manifest.agentRef.profileRevision }) },
|
|
1830
1926
|
cwd: manifest.cwd ?? manifest.workspace.workspaceDir,
|
|
@@ -3852,7 +3948,11 @@ function validatePiByokLauncherConfig(launcher) {
|
|
|
3852
3948
|
"--macos-keychain-path",
|
|
3853
3949
|
"--secret-service-prefix",
|
|
3854
3950
|
"--provider",
|
|
3855
|
-
"--model"
|
|
3951
|
+
"--model",
|
|
3952
|
+
"--profile-revision",
|
|
3953
|
+
"--profile-hash",
|
|
3954
|
+
"--required-capabilities",
|
|
3955
|
+
"--validate-only"
|
|
3856
3956
|
]);
|
|
3857
3957
|
const conflicting = launcher.args?.find((arg) => reserved.has(arg));
|
|
3858
3958
|
if (conflicting !== void 0) {
|
|
@@ -3914,11 +4014,17 @@ var PiAdapter = class {
|
|
|
3914
4014
|
const bin = this.resolveBin();
|
|
3915
4015
|
const extensions = (this.options.resolveExtensions ?? resolvePiExtensions)();
|
|
3916
4016
|
const selection = input.offer.dispatchSelection;
|
|
3917
|
-
const pinnedSelection = selection === void 0 ? void 0 : Object.freeze({
|
|
4017
|
+
const pinnedSelection = selection === void 0 ? void 0 : selection.lane === "byok-profile" ? Object.freeze({
|
|
4018
|
+
...selection,
|
|
4019
|
+
providerProfile: Object.freeze({
|
|
4020
|
+
...selection.providerProfile,
|
|
4021
|
+
requiredCapabilities: Object.freeze([...selection.providerProfile.requiredCapabilities])
|
|
4022
|
+
})
|
|
4023
|
+
}) : Object.freeze({ ...selection });
|
|
3918
4024
|
let command = bin.command;
|
|
3919
4025
|
let launcherArgs;
|
|
3920
4026
|
if (pinnedSelection !== void 0) {
|
|
3921
|
-
if (pinnedSelection.lane !== "byok" || pinnedSelection.runtimeId !== "pi") {
|
|
4027
|
+
if (pinnedSelection.lane !== "byok" && pinnedSelection.lane !== "byok-profile" || pinnedSelection.runtimeId !== "pi") {
|
|
3922
4028
|
return { kind: "reject", reason: `pi adapter cannot execute ${pinnedSelection.lane} selection for runtime ${pinnedSelection.runtimeId}`, retryable: false };
|
|
3923
4029
|
}
|
|
3924
4030
|
const launcher = this.options.byokLauncher;
|
|
@@ -3926,6 +4032,21 @@ var PiAdapter = class {
|
|
|
3926
4032
|
return { kind: "reject", reason: "pi BYOK selection requires a configured credential-custody launcher", retryable: false };
|
|
3927
4033
|
}
|
|
3928
4034
|
command = launcher.command;
|
|
4035
|
+
const providerProfile = pinnedSelection.lane === "byok-profile" ? pinnedSelection.providerProfile : void 0;
|
|
4036
|
+
if (providerProfile !== void 0) {
|
|
4037
|
+
try {
|
|
4038
|
+
await (this.options.validateProviderProfileBinding ?? validateProviderProfileBindingWithLauncher)(
|
|
4039
|
+
providerProfile,
|
|
4040
|
+
launcher
|
|
4041
|
+
);
|
|
4042
|
+
} catch (error) {
|
|
4043
|
+
return {
|
|
4044
|
+
kind: "reject",
|
|
4045
|
+
reason: `provider profile admission failed: ${errorMessage2(error)}`,
|
|
4046
|
+
retryable: false
|
|
4047
|
+
};
|
|
4048
|
+
}
|
|
4049
|
+
}
|
|
3929
4050
|
launcherArgs = [
|
|
3930
4051
|
...launcher.args ?? [],
|
|
3931
4052
|
"--pi-bin",
|
|
@@ -3937,9 +4058,19 @@ var PiAdapter = class {
|
|
|
3937
4058
|
...launcher.macosKeychainPath !== void 0 ? ["--macos-keychain-path", launcher.macosKeychainPath] : [],
|
|
3938
4059
|
...launcher.secretServicePrefix ? ["--secret-service-prefix", launcher.secretServicePrefix] : [],
|
|
3939
4060
|
"--provider",
|
|
3940
|
-
pinnedSelection.providerId,
|
|
4061
|
+
providerProfile?.profileRef ?? (pinnedSelection.lane === "byok" ? pinnedSelection.providerId : ""),
|
|
3941
4062
|
"--model",
|
|
3942
|
-
pinnedSelection.modelId
|
|
4063
|
+
providerProfile?.modelId ?? (pinnedSelection.lane === "byok" ? pinnedSelection.modelId : ""),
|
|
4064
|
+
...providerProfile === void 0 ? [] : [
|
|
4065
|
+
"--profile-revision",
|
|
4066
|
+
providerProfile.profileRevision,
|
|
4067
|
+
"--profile-hash",
|
|
4068
|
+
providerProfile.profileHash,
|
|
4069
|
+
"--required-capabilities",
|
|
4070
|
+
JSON.stringify(providerProfile.requiredCapabilities),
|
|
4071
|
+
"--validate-only",
|
|
4072
|
+
"false"
|
|
4073
|
+
]
|
|
3943
4074
|
];
|
|
3944
4075
|
}
|
|
3945
4076
|
return {
|
|
@@ -4082,8 +4213,42 @@ var PiAdapter = class {
|
|
|
4082
4213
|
return (this.options.resolveBin ?? resolvePiBin)();
|
|
4083
4214
|
}
|
|
4084
4215
|
};
|
|
4216
|
+
async function validateProviderProfileBindingWithLauncher(binding, launcher) {
|
|
4217
|
+
await execFileAsync(launcher.command, [
|
|
4218
|
+
...launcher.args ?? [],
|
|
4219
|
+
"--pi-bin",
|
|
4220
|
+
process.execPath,
|
|
4221
|
+
"--profile-db",
|
|
4222
|
+
launcher.profileDbPath,
|
|
4223
|
+
"--session-dir",
|
|
4224
|
+
launcher.sessionDir,
|
|
4225
|
+
"--provider",
|
|
4226
|
+
binding.profileRef,
|
|
4227
|
+
"--model",
|
|
4228
|
+
binding.modelId,
|
|
4229
|
+
"--profile-revision",
|
|
4230
|
+
binding.profileRevision,
|
|
4231
|
+
"--profile-hash",
|
|
4232
|
+
binding.profileHash,
|
|
4233
|
+
"--required-capabilities",
|
|
4234
|
+
JSON.stringify(binding.requiredCapabilities),
|
|
4235
|
+
"--validate-only",
|
|
4236
|
+
"true",
|
|
4237
|
+
...launcher.macosKeychainPath !== void 0 ? ["--macos-keychain-path", launcher.macosKeychainPath] : [],
|
|
4238
|
+
...launcher.secretServicePrefix ? ["--secret-service-prefix", launcher.secretServicePrefix] : []
|
|
4239
|
+
], { timeout: DETECT_TIMEOUT_MS });
|
|
4240
|
+
}
|
|
4085
4241
|
function sameDispatchSelection(left, right) {
|
|
4086
4242
|
if (left === void 0 || right === void 0) return left === right;
|
|
4243
|
+
if (left.lane === "byok-profile" || right.lane === "byok-profile") {
|
|
4244
|
+
if (left.lane !== "byok-profile" || right.lane !== "byok-profile") return false;
|
|
4245
|
+
if (left.runtimeId !== right.runtimeId) return false;
|
|
4246
|
+
const leftProfile = left.providerProfile;
|
|
4247
|
+
const rightProfile = right.providerProfile;
|
|
4248
|
+
return leftProfile.profileRef === rightProfile.profileRef && leftProfile.profileRevision === rightProfile.profileRevision && leftProfile.profileHash === rightProfile.profileHash && leftProfile.modelId === rightProfile.modelId && leftProfile.requiredCapabilities.length === rightProfile.requiredCapabilities.length && leftProfile.requiredCapabilities.every(
|
|
4249
|
+
(capability, index) => capability === rightProfile.requiredCapabilities[index]
|
|
4250
|
+
);
|
|
4251
|
+
}
|
|
4087
4252
|
return left.lane === right.lane && left.runtimeId === right.runtimeId && left.providerId === right.providerId && left.modelId === right.modelId;
|
|
4088
4253
|
}
|
|
4089
4254
|
async function resolveAuthoritativeSessionId(rpc) {
|
|
@@ -6976,19 +7141,14 @@ function signNonce(privateKey, nonce) {
|
|
|
6976
7141
|
const signature = sign(null, nonceSigningBytes(nonce), privateKey);
|
|
6977
7142
|
return signature.toString("base64url");
|
|
6978
7143
|
}
|
|
7144
|
+
|
|
7145
|
+
// src/daemon/url.ts
|
|
6979
7146
|
function toHttpBase(serverUrl) {
|
|
6980
7147
|
const url = new URL(serverUrl);
|
|
6981
|
-
if (url.protocol === "ws:") url.protocol = "http:";
|
|
6982
|
-
else if (url.protocol === "wss:") url.protocol = "https:";
|
|
6983
7148
|
url.pathname = "/";
|
|
6984
7149
|
url.search = "";
|
|
6985
7150
|
return url.toString();
|
|
6986
7151
|
}
|
|
6987
|
-
function toWsUrl(serverUrl) {
|
|
6988
|
-
const url = new URL(BYOK_WS_PATH, toHttpBase(serverUrl));
|
|
6989
|
-
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
6990
|
-
return url.toString();
|
|
6991
|
-
}
|
|
6992
7152
|
function describeEndpoint(transport, url) {
|
|
6993
7153
|
const parsed = typeof url === "string" ? new URL(url) : url;
|
|
6994
7154
|
return { transport, host: parsed.host, path: parsed.pathname };
|
|
@@ -7021,17 +7181,15 @@ function assertServerUrlAllowed(rawUrl, opts = {}) {
|
|
|
7021
7181
|
const endpoint = formatServerUrl(url);
|
|
7022
7182
|
switch (url.protocol) {
|
|
7023
7183
|
case "https:":
|
|
7024
|
-
case "wss:":
|
|
7025
7184
|
return;
|
|
7026
7185
|
case "http:":
|
|
7027
|
-
case "ws:":
|
|
7028
7186
|
if (opts.dangerouslyAllowInsecureRemote || isLoopbackHostname(url.hostname)) return;
|
|
7029
7187
|
throw new InsecureServerUrlError(
|
|
7030
|
-
`refusing to connect to "${endpoint}" over plaintext ${url.protocol.replace(":", "")} \u2014 "${url.hostname}" is not a loopback host. Use
|
|
7188
|
+
`refusing to connect to "${endpoint}" over plaintext ${url.protocol.replace(":", "")} \u2014 "${url.hostname}" is not a loopback host. Use https: for any non-loopback server, or pass { dangerouslyAllowInsecureRemote: true } (DaemonConfig) if you understand and accept the risk of sending device credentials over an unencrypted connection.`
|
|
7031
7189
|
);
|
|
7032
7190
|
default:
|
|
7033
7191
|
throw new InsecureServerUrlError(
|
|
7034
|
-
`refusing to connect to "${endpoint}" \u2014 unsupported scheme "${url.protocol}" (expected http
|
|
7192
|
+
`refusing to connect to "${endpoint}" \u2014 unsupported scheme "${url.protocol}" (expected http: or https:).`
|
|
7035
7193
|
);
|
|
7036
7194
|
}
|
|
7037
7195
|
}
|
|
@@ -9083,7 +9241,7 @@ var LongPollRouteError = class extends Error {
|
|
|
9083
9241
|
};
|
|
9084
9242
|
var MAX_TRACKED_VALIDATION_FAILURE_WARNINGS = 1e3;
|
|
9085
9243
|
var MAX_TRACKED_ROUTE_FAILURE_WARNINGS = 1e3;
|
|
9086
|
-
function parseLooseEventsPollResponse(raw) {
|
|
9244
|
+
function parseLooseEventsPollResponse(raw, requestedCursor) {
|
|
9087
9245
|
if (typeof raw !== "object" || raw === null) {
|
|
9088
9246
|
throw new Error("events poll response is not an object");
|
|
9089
9247
|
}
|
|
@@ -9091,19 +9249,48 @@ function parseLooseEventsPollResponse(raw) {
|
|
|
9091
9249
|
if (!Array.isArray(events)) {
|
|
9092
9250
|
throw new Error("events poll response.events is not an array");
|
|
9093
9251
|
}
|
|
9094
|
-
if (
|
|
9095
|
-
throw new Error("events poll response.cursor is not
|
|
9252
|
+
if (!isSafeNonnegativeInteger(cursor)) {
|
|
9253
|
+
throw new Error("events poll response.cursor is not a safe nonnegative integer");
|
|
9254
|
+
}
|
|
9255
|
+
if (cursor < requestedCursor) {
|
|
9256
|
+
throw new Error("events poll response.cursor regressed below the requested cursor");
|
|
9096
9257
|
}
|
|
9097
9258
|
if (capabilities !== void 0 && (!Array.isArray(capabilities) || capabilities.some((flag) => typeof flag !== "string"))) {
|
|
9098
9259
|
throw new Error("events poll response.capabilities is not an array of strings");
|
|
9099
9260
|
}
|
|
9100
9261
|
return { events, cursor, capabilities: capabilities ?? [] };
|
|
9101
9262
|
}
|
|
9263
|
+
function isSafeNonnegativeInteger(value) {
|
|
9264
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
9265
|
+
}
|
|
9266
|
+
function validateTrustedEventsPage(events, requestedCursor, pageCursor) {
|
|
9267
|
+
let previousTaskSeq;
|
|
9268
|
+
for (const raw of events) {
|
|
9269
|
+
if (typeof raw !== "object" || raw === null) continue;
|
|
9270
|
+
const { type, seq } = raw;
|
|
9271
|
+
if (typeof type !== "string" || !type.startsWith("task.")) continue;
|
|
9272
|
+
if (!isSafeNonnegativeInteger(seq)) {
|
|
9273
|
+
throw new Error("events poll task seq is not a safe nonnegative integer");
|
|
9274
|
+
}
|
|
9275
|
+
if (seq <= requestedCursor || seq > pageCursor) {
|
|
9276
|
+
throw new Error("events poll task seq lies outside the trusted page cursor range");
|
|
9277
|
+
}
|
|
9278
|
+
if (previousTaskSeq !== void 0 && seq <= previousTaskSeq) {
|
|
9279
|
+
throw new Error("events poll task seq is not strictly page ordered");
|
|
9280
|
+
}
|
|
9281
|
+
const knownTaskType = MESSAGE_TYPES.includes(type);
|
|
9282
|
+
const predecessor = previousTaskSeq ?? requestedCursor;
|
|
9283
|
+
if (!knownTaskType && seq !== predecessor + 1) {
|
|
9284
|
+
throw new Error("events poll unknown task seq would cross an untrusted gap");
|
|
9285
|
+
}
|
|
9286
|
+
previousTaskSeq = seq;
|
|
9287
|
+
}
|
|
9288
|
+
}
|
|
9102
9289
|
function extractSkippableSeq(raw) {
|
|
9103
9290
|
if (typeof raw !== "object" || raw === null) return void 0;
|
|
9104
9291
|
const { type, seq } = raw;
|
|
9105
9292
|
if (typeof type !== "string" || !type.startsWith("task.")) return void 0;
|
|
9106
|
-
return
|
|
9293
|
+
return isSafeNonnegativeInteger(seq) ? seq : void 0;
|
|
9107
9294
|
}
|
|
9108
9295
|
var LongPollClient = class {
|
|
9109
9296
|
constructor(opts) {
|
|
@@ -9114,6 +9301,8 @@ var LongPollClient = class {
|
|
|
9114
9301
|
}
|
|
9115
9302
|
opts;
|
|
9116
9303
|
running = false;
|
|
9304
|
+
/** Owns exactly one active loop generation, including its held GET and retry delays. */
|
|
9305
|
+
loopAbortController;
|
|
9117
9306
|
/**
|
|
9118
9307
|
* Finding R1: seqs this loop has already `console.warn`'d about for a
|
|
9119
9308
|
* validation-failed (recognized-type, invalid-payload) entry — a poison
|
|
@@ -9173,16 +9362,22 @@ var LongPollClient = class {
|
|
|
9173
9362
|
noteRevoked(err) {
|
|
9174
9363
|
if (!(err instanceof DeviceRevokedError)) return false;
|
|
9175
9364
|
this.running = false;
|
|
9365
|
+
this.loopAbortController?.abort();
|
|
9176
9366
|
this.opts.onRevoked?.();
|
|
9177
9367
|
return true;
|
|
9178
9368
|
}
|
|
9179
9369
|
start() {
|
|
9180
9370
|
if (this.running) return;
|
|
9181
9371
|
this.running = true;
|
|
9182
|
-
|
|
9372
|
+
const controller = new AbortController();
|
|
9373
|
+
this.loopAbortController = controller;
|
|
9374
|
+
void this.loop(controller.signal).finally(() => {
|
|
9375
|
+
if (this.loopAbortController === controller) this.loopAbortController = void 0;
|
|
9376
|
+
});
|
|
9183
9377
|
}
|
|
9184
9378
|
stop() {
|
|
9185
9379
|
this.running = false;
|
|
9380
|
+
this.loopAbortController?.abort();
|
|
9186
9381
|
}
|
|
9187
9382
|
/**
|
|
9188
9383
|
* POST one batch of envelopes to `/byok/messages` (finding F6/protocol
|
|
@@ -9191,14 +9386,14 @@ var LongPollClient = class {
|
|
|
9191
9386
|
* (`ConnectionHub.handleInbound`), so a resend of the SAME batch (same
|
|
9192
9387
|
* envelope `id`s — the caller must never rebuild them) is deduped
|
|
9193
9388
|
* server-side into a safe no-op rather than reprocessed (§9). Returns
|
|
9194
|
-
*
|
|
9389
|
+
* validated frozen-v1 counts only after a readable response body.
|
|
9195
9390
|
*/
|
|
9196
9391
|
async postBatch(envelopes) {
|
|
9197
9392
|
try {
|
|
9198
9393
|
await this.opts.auth.getValidAccessToken();
|
|
9199
9394
|
} catch (err) {
|
|
9200
9395
|
this.noteRevoked(err);
|
|
9201
|
-
return
|
|
9396
|
+
return void 0;
|
|
9202
9397
|
}
|
|
9203
9398
|
let res;
|
|
9204
9399
|
try {
|
|
@@ -9214,23 +9409,26 @@ var LongPollClient = class {
|
|
|
9214
9409
|
);
|
|
9215
9410
|
} catch (err) {
|
|
9216
9411
|
if (!this.noteRevoked(err)) this.warnRouteFailure(this.messagesEndpoint, void 0, err);
|
|
9217
|
-
return
|
|
9412
|
+
return void 0;
|
|
9218
9413
|
}
|
|
9219
9414
|
if (!res.ok) {
|
|
9220
9415
|
this.warnRouteFailure(this.messagesEndpoint, res.status, void 0);
|
|
9221
|
-
return
|
|
9416
|
+
return void 0;
|
|
9222
9417
|
}
|
|
9223
9418
|
try {
|
|
9224
|
-
MessagesSendResponseSchema.parse(await res.json());
|
|
9419
|
+
const response = MessagesSendResponseSchema.parse(await res.json());
|
|
9420
|
+
if (response.accepted + (response.rejected ?? 0) !== envelopes.length) {
|
|
9421
|
+
throw new Error("messages response counts do not match the posted batch length");
|
|
9422
|
+
}
|
|
9423
|
+
return response;
|
|
9225
9424
|
} catch (err) {
|
|
9226
9425
|
this.warnRouteFailure(this.messagesEndpoint, res.status, err);
|
|
9227
|
-
return
|
|
9426
|
+
return void 0;
|
|
9228
9427
|
}
|
|
9229
|
-
return true;
|
|
9230
9428
|
}
|
|
9231
|
-
async loop() {
|
|
9429
|
+
async loop(signal) {
|
|
9232
9430
|
let retryAttempt = 0;
|
|
9233
|
-
while (this.running) {
|
|
9431
|
+
while (this.running && !signal.aborted) {
|
|
9234
9432
|
try {
|
|
9235
9433
|
await this.opts.auth.getValidAccessToken();
|
|
9236
9434
|
const base = toHttpBase(this.opts.serverUrl);
|
|
@@ -9239,8 +9437,9 @@ var LongPollClient = class {
|
|
|
9239
9437
|
if (cursor !== void 0) url.searchParams.set("cursor", String(cursor));
|
|
9240
9438
|
let res;
|
|
9241
9439
|
try {
|
|
9242
|
-
res = await authedFetch(url, { method: "GET" }, this.opts.auth);
|
|
9440
|
+
res = await authedFetch(url, { method: "GET", signal }, this.opts.auth);
|
|
9243
9441
|
} catch (err) {
|
|
9442
|
+
if (signal.aborted) return;
|
|
9244
9443
|
if (!(err instanceof DeviceRevokedError)) {
|
|
9245
9444
|
this.warnRouteFailure(this.eventsEndpoint, void 0, err);
|
|
9246
9445
|
}
|
|
@@ -9254,21 +9453,24 @@ var LongPollClient = class {
|
|
|
9254
9453
|
return;
|
|
9255
9454
|
}
|
|
9256
9455
|
this.warnRouteFailure(this.eventsEndpoint, res.status, void 0);
|
|
9257
|
-
this.opts.
|
|
9456
|
+
this.opts.onServerCapabilitiesInvalidated?.();
|
|
9457
|
+
this.opts.onPollFailure?.();
|
|
9258
9458
|
this.opts.onOperationalOutcome?.("failure");
|
|
9259
9459
|
const baseMs = this.opts.retryDelayMs ?? 2e3;
|
|
9260
|
-
await sleep(this.opts.retryDelayForAttempt?.(retryAttempt++, baseMs) ?? baseMs);
|
|
9460
|
+
await sleep(this.opts.retryDelayForAttempt?.(retryAttempt++, baseMs) ?? baseMs, signal);
|
|
9261
9461
|
continue;
|
|
9262
9462
|
}
|
|
9263
9463
|
let parsed;
|
|
9264
9464
|
try {
|
|
9265
|
-
parsed = parseLooseEventsPollResponse(await res.json());
|
|
9465
|
+
parsed = parseLooseEventsPollResponse(await res.json(), cursor ?? 0);
|
|
9466
|
+
validateTrustedEventsPage(parsed.events, cursor ?? 0, parsed.cursor);
|
|
9266
9467
|
} catch (err) {
|
|
9267
9468
|
this.warnRouteFailure(this.eventsEndpoint, res.status, err);
|
|
9268
9469
|
throw err;
|
|
9269
9470
|
}
|
|
9270
9471
|
this.opts.onServerCapabilities?.(parsed.capabilities);
|
|
9271
9472
|
let hadValidationFailureThisBatch = false;
|
|
9473
|
+
let acceptedAnyEntry = false;
|
|
9272
9474
|
for (const raw of parsed.events) {
|
|
9273
9475
|
let envelope;
|
|
9274
9476
|
try {
|
|
@@ -9276,7 +9478,10 @@ var LongPollClient = class {
|
|
|
9276
9478
|
} catch (err) {
|
|
9277
9479
|
if (err instanceof UnknownMessageTypeError) {
|
|
9278
9480
|
const skippableSeq = extractSkippableSeq(raw);
|
|
9279
|
-
if (skippableSeq !== void 0)
|
|
9481
|
+
if (skippableSeq !== void 0) {
|
|
9482
|
+
this.opts.onSkippedSeq?.(skippableSeq);
|
|
9483
|
+
acceptedAnyEntry = true;
|
|
9484
|
+
}
|
|
9280
9485
|
} else {
|
|
9281
9486
|
const failedSeq = extractSkippableSeq(raw);
|
|
9282
9487
|
if (failedSeq !== void 0) {
|
|
@@ -9296,27 +9501,28 @@ var LongPollClient = class {
|
|
|
9296
9501
|
}
|
|
9297
9502
|
continue;
|
|
9298
9503
|
}
|
|
9299
|
-
this.opts.onEnvelope(envelope);
|
|
9504
|
+
if (this.opts.onEnvelope(envelope) !== false) acceptedAnyEntry = true;
|
|
9300
9505
|
}
|
|
9301
9506
|
if (parsed.events.length === 0) {
|
|
9302
9507
|
retryAttempt = 0;
|
|
9303
9508
|
this.opts.onOperationalOutcome?.("success");
|
|
9304
|
-
await sleep(this.opts.idleDelayMs ?? 250);
|
|
9305
|
-
} else if (this.opts.isStalled?.() || hadValidationFailureThisBatch) {
|
|
9509
|
+
await sleep(this.opts.idleDelayMs ?? 250, signal);
|
|
9510
|
+
} else if (this.opts.isStalled?.() || hadValidationFailureThisBatch || !acceptedAnyEntry && this.opts.getCursor() === cursor) {
|
|
9306
9511
|
this.opts.onOperationalOutcome?.("failure");
|
|
9307
9512
|
const baseMs = this.opts.retryDelayMs ?? 2e3;
|
|
9308
|
-
await sleep(this.opts.retryDelayForAttempt?.(retryAttempt++, baseMs) ?? baseMs);
|
|
9513
|
+
await sleep(this.opts.retryDelayForAttempt?.(retryAttempt++, baseMs) ?? baseMs, signal);
|
|
9309
9514
|
} else {
|
|
9310
9515
|
retryAttempt = 0;
|
|
9311
9516
|
this.opts.onOperationalOutcome?.("success");
|
|
9312
9517
|
}
|
|
9313
9518
|
} catch (err) {
|
|
9314
|
-
this.opts.
|
|
9519
|
+
this.opts.onServerCapabilitiesInvalidated?.();
|
|
9315
9520
|
if (this.noteRevoked(err)) return;
|
|
9316
|
-
if (!this.running) return;
|
|
9521
|
+
if (!this.running || signal.aborted) return;
|
|
9522
|
+
this.opts.onPollFailure?.();
|
|
9317
9523
|
this.opts.onOperationalOutcome?.("failure");
|
|
9318
9524
|
const baseMs = this.opts.retryDelayMs ?? 2e3;
|
|
9319
|
-
await sleep(this.opts.retryDelayForAttempt?.(retryAttempt++, baseMs) ?? baseMs);
|
|
9525
|
+
await sleep(this.opts.retryDelayForAttempt?.(retryAttempt++, baseMs) ?? baseMs, signal);
|
|
9320
9526
|
}
|
|
9321
9527
|
}
|
|
9322
9528
|
}
|
|
@@ -9336,20 +9542,24 @@ async function parseReplayCursorTooOld(res) {
|
|
|
9336
9542
|
}
|
|
9337
9543
|
return new ReplayCursorTooOldError(recoverableFrom);
|
|
9338
9544
|
}
|
|
9339
|
-
function sleep(ms) {
|
|
9340
|
-
|
|
9545
|
+
function sleep(ms, signal) {
|
|
9546
|
+
if (signal.aborted) return Promise.resolve();
|
|
9547
|
+
return new Promise((resolve) => {
|
|
9548
|
+
const finish = () => {
|
|
9549
|
+
clearTimeout(timer);
|
|
9550
|
+
signal.removeEventListener("abort", finish);
|
|
9551
|
+
resolve();
|
|
9552
|
+
};
|
|
9553
|
+
const timer = setTimeout(finish, ms);
|
|
9554
|
+
signal.addEventListener("abort", finish, { once: true });
|
|
9555
|
+
});
|
|
9341
9556
|
}
|
|
9342
|
-
|
|
9343
|
-
|
|
9344
|
-
|
|
9345
|
-
|
|
9346
|
-
|
|
9347
|
-
|
|
9348
|
-
}
|
|
9349
|
-
status;
|
|
9350
|
-
endpoint;
|
|
9351
|
-
};
|
|
9352
|
-
function createConnectionHelloEnvelope(opts) {
|
|
9557
|
+
|
|
9558
|
+
// src/daemon/connection-manager.ts
|
|
9559
|
+
function isCursorEnvelopeType(type) {
|
|
9560
|
+
return type.startsWith("task.") || type === "agent.egress.ack" || type === "agent.content.read" || type === "agent.home.projection";
|
|
9561
|
+
}
|
|
9562
|
+
function createConnectionHelloEnvelope(opts, getCursor) {
|
|
9353
9563
|
const configuredToolsets = opts.getConfiguredToolsets?.();
|
|
9354
9564
|
return createEnvelope("conn.hello", {
|
|
9355
9565
|
protocolVersions: [PROTOCOL_VERSION],
|
|
@@ -9359,222 +9569,32 @@ function createConnectionHelloEnvelope(opts) {
|
|
|
9359
9569
|
clientVersion: opts.clientVersion,
|
|
9360
9570
|
runtimes: opts.runtimes,
|
|
9361
9571
|
configuredToolsets: configuredToolsets === void 0 ? void 0 : [...configuredToolsets],
|
|
9362
|
-
cursor:
|
|
9572
|
+
cursor: getCursor()
|
|
9363
9573
|
});
|
|
9364
9574
|
}
|
|
9365
|
-
var
|
|
9366
|
-
constructor(opts) {
|
|
9367
|
-
this.opts = opts;
|
|
9368
|
-
}
|
|
9369
|
-
opts;
|
|
9370
|
-
socket;
|
|
9371
|
-
closedByUser = false;
|
|
9372
|
-
autoReconnect = true;
|
|
9373
|
-
acked = false;
|
|
9374
|
-
everAckedThisAttempt = false;
|
|
9375
|
-
reconnectAttempt = 0;
|
|
9376
|
-
reconnectTimer;
|
|
9377
|
-
livenessTimer;
|
|
9378
|
-
lastActivity = 0;
|
|
9379
|
-
lastUnexpectedStatus;
|
|
9380
|
-
ackWaiters = [];
|
|
9381
|
-
connect(opts = {}) {
|
|
9382
|
-
this.autoReconnect = opts.auto ?? true;
|
|
9383
|
-
this.closedByUser = false;
|
|
9384
|
-
void this.openSocket();
|
|
9385
|
-
}
|
|
9386
|
-
/** 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). */
|
|
9387
|
-
stopAutoReconnect() {
|
|
9388
|
-
this.autoReconnect = false;
|
|
9389
|
-
if (this.reconnectTimer) {
|
|
9390
|
-
clearTimeout(this.reconnectTimer);
|
|
9391
|
-
this.reconnectTimer = void 0;
|
|
9392
|
-
}
|
|
9393
|
-
}
|
|
9394
|
-
/** Resume normal auto-reconnect-on-close behavior (does not itself trigger a connect — only affects future closes). */
|
|
9395
|
-
resumeAutoReconnect() {
|
|
9396
|
-
this.autoReconnect = true;
|
|
9397
|
-
}
|
|
9398
|
-
get isOpen() {
|
|
9399
|
-
return this.acked;
|
|
9400
|
-
}
|
|
9401
|
-
/**
|
|
9402
|
-
* Attempt to send ONE envelope right now; returns whether it actually went
|
|
9403
|
-
* out. `false` means the socket isn't currently open+acked — the caller
|
|
9404
|
-
* (`ConnectionManager.drainOutbox`, Design B/finding N4) owns re-queueing
|
|
9405
|
-
* and retrying later (e.g. on the next `onAcked`), since this transport no
|
|
9406
|
-
* longer buffers anything itself.
|
|
9407
|
-
*/
|
|
9408
|
-
sendNow(envelope) {
|
|
9409
|
-
if (this.socket && this.socket.readyState === WebSocket.OPEN && this.acked) {
|
|
9410
|
-
this.socket.send(encodeEnvelope(envelope));
|
|
9411
|
-
return true;
|
|
9412
|
-
}
|
|
9413
|
-
return false;
|
|
9414
|
-
}
|
|
9415
|
-
waitForAck(timeoutMs = 1e4) {
|
|
9416
|
-
if (this.acked) return Promise.resolve();
|
|
9417
|
-
return new Promise((resolve, reject) => {
|
|
9418
|
-
const timer = setTimeout(() => reject(new Error("Timed out waiting for conn.ack")), timeoutMs);
|
|
9419
|
-
this.ackWaiters.push({
|
|
9420
|
-
resolve: () => {
|
|
9421
|
-
clearTimeout(timer);
|
|
9422
|
-
resolve();
|
|
9423
|
-
},
|
|
9424
|
-
reject: (err) => {
|
|
9425
|
-
clearTimeout(timer);
|
|
9426
|
-
reject(err);
|
|
9427
|
-
}
|
|
9428
|
-
});
|
|
9429
|
-
});
|
|
9430
|
-
}
|
|
9431
|
-
close() {
|
|
9432
|
-
this.closedByUser = true;
|
|
9433
|
-
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
9434
|
-
this.socket?.close();
|
|
9435
|
-
}
|
|
9436
|
-
async openSocket() {
|
|
9437
|
-
const url = toWsUrl(this.opts.serverUrl);
|
|
9438
|
-
const endpoint = describeEndpoint("ws", url);
|
|
9439
|
-
this.acked = false;
|
|
9440
|
-
this.everAckedThisAttempt = false;
|
|
9441
|
-
this.opts.onStateChange?.("connecting");
|
|
9442
|
-
let token;
|
|
9443
|
-
try {
|
|
9444
|
-
token = await this.opts.getToken();
|
|
9445
|
-
} catch (err) {
|
|
9446
|
-
this.opts.onStateChange?.("closed");
|
|
9447
|
-
this.opts.onConnectOutcome?.(false, err, endpoint);
|
|
9448
|
-
if (!this.closedByUser && this.autoReconnect) this.scheduleReconnect();
|
|
9449
|
-
return;
|
|
9450
|
-
}
|
|
9451
|
-
const socket = new WebSocket(url, {
|
|
9452
|
-
headers: { Authorization: `Bearer ${token}` }
|
|
9453
|
-
});
|
|
9454
|
-
this.socket = socket;
|
|
9455
|
-
this.lastActivity = Date.now();
|
|
9456
|
-
this.startLivenessCheck(socket);
|
|
9457
|
-
socket.on("open", () => {
|
|
9458
|
-
const hello = createConnectionHelloEnvelope(this.opts);
|
|
9459
|
-
socket.send(encodeEnvelope(hello));
|
|
9460
|
-
});
|
|
9461
|
-
socket.on("ping", () => {
|
|
9462
|
-
this.lastActivity = Date.now();
|
|
9463
|
-
});
|
|
9464
|
-
socket.on("message", (data, isBinary) => {
|
|
9465
|
-
this.lastActivity = Date.now();
|
|
9466
|
-
const bytes = toBytes(data);
|
|
9467
|
-
let envelope;
|
|
9468
|
-
try {
|
|
9469
|
-
envelope = decodeEnvelope(bytes);
|
|
9470
|
-
} catch {
|
|
9471
|
-
return;
|
|
9472
|
-
}
|
|
9473
|
-
if (envelope.type === "conn.ack") {
|
|
9474
|
-
this.reconnectAttempt = 0;
|
|
9475
|
-
this.acked = true;
|
|
9476
|
-
this.everAckedThisAttempt = true;
|
|
9477
|
-
this.opts.onStateChange?.("open");
|
|
9478
|
-
for (const waiter of this.ackWaiters.splice(0)) waiter.resolve();
|
|
9479
|
-
this.opts.onAcked?.(envelope.payload.capabilities);
|
|
9480
|
-
}
|
|
9481
|
-
this.opts.onEnvelope(envelope);
|
|
9482
|
-
});
|
|
9483
|
-
socket.on("close", (code, reason) => {
|
|
9484
|
-
this.socket = void 0;
|
|
9485
|
-
this.stopLivenessCheck();
|
|
9486
|
-
this.opts.onStateChange?.("closed");
|
|
9487
|
-
const acked = this.everAckedThisAttempt;
|
|
9488
|
-
const status = this.lastUnexpectedStatus;
|
|
9489
|
-
this.lastUnexpectedStatus = void 0;
|
|
9490
|
-
const replayCursorTooOld = code === 1008 && reason.toString("utf8") === "cursor_too_old" ? new ReplayCursorTooOldError() : void 0;
|
|
9491
|
-
this.opts.onConnectOutcome?.(
|
|
9492
|
-
acked,
|
|
9493
|
-
replayCursorTooOld ?? (status !== void 0 ? new WsUnexpectedStatusError(status, endpoint) : void 0),
|
|
9494
|
-
endpoint
|
|
9495
|
-
);
|
|
9496
|
-
if (!this.closedByUser && this.autoReconnect) this.scheduleReconnect();
|
|
9497
|
-
});
|
|
9498
|
-
socket.on("error", () => {
|
|
9499
|
-
});
|
|
9500
|
-
socket.on("unexpected-response", (_req, res) => {
|
|
9501
|
-
this.lastUnexpectedStatus = res.statusCode;
|
|
9502
|
-
res.resume();
|
|
9503
|
-
socket.terminate();
|
|
9504
|
-
});
|
|
9505
|
-
}
|
|
9506
|
-
scheduleReconnect() {
|
|
9507
|
-
const { baseMs = 1e3, maxMs = 3e4, factor = 2 } = this.opts.backoff ?? {};
|
|
9508
|
-
const delay5 = Math.min(maxMs, baseMs * factor ** this.reconnectAttempt);
|
|
9509
|
-
const jitter = this.opts.reconnectDelayMs?.(this.reconnectAttempt, delay5) ?? delay5;
|
|
9510
|
-
this.reconnectAttempt += 1;
|
|
9511
|
-
this.reconnectTimer = setTimeout(() => void this.openSocket(), jitter);
|
|
9512
|
-
}
|
|
9513
|
-
startLivenessCheck(socket) {
|
|
9514
|
-
const { timeoutMs = 75e3, checkIntervalMs = Math.max(1e3, Math.floor(timeoutMs / 3)) } = this.opts.liveness ?? {};
|
|
9515
|
-
this.livenessTimer = setInterval(() => {
|
|
9516
|
-
if (Date.now() - this.lastActivity > timeoutMs) {
|
|
9517
|
-
socket.terminate();
|
|
9518
|
-
}
|
|
9519
|
-
}, checkIntervalMs);
|
|
9520
|
-
this.livenessTimer.unref?.();
|
|
9521
|
-
}
|
|
9522
|
-
stopLivenessCheck() {
|
|
9523
|
-
if (this.livenessTimer) {
|
|
9524
|
-
clearInterval(this.livenessTimer);
|
|
9525
|
-
this.livenessTimer = void 0;
|
|
9526
|
-
}
|
|
9527
|
-
}
|
|
9528
|
-
};
|
|
9529
|
-
function toBytes(data, _isBinary) {
|
|
9530
|
-
if (Buffer.isBuffer(data)) return data;
|
|
9531
|
-
if (Array.isArray(data)) return Buffer.concat(data);
|
|
9532
|
-
if (data instanceof ArrayBuffer) return new Uint8Array(data);
|
|
9533
|
-
return Buffer.from(String(data), "utf8");
|
|
9534
|
-
}
|
|
9535
|
-
|
|
9536
|
-
// src/daemon/connection-manager.ts
|
|
9537
|
-
function isCursorEnvelopeType(type) {
|
|
9538
|
-
return type.startsWith("task.") || type === "agent.egress.ack" || type === "agent.content.read" || type === "agent.home.projection";
|
|
9539
|
-
}
|
|
9575
|
+
var MAX_REJECTED_OUTBOX_ENTRIES = 1e3;
|
|
9540
9576
|
var ConnectionManager = class {
|
|
9541
9577
|
constructor(opts) {
|
|
9542
9578
|
this.opts = opts;
|
|
9543
9579
|
this.fleetJitter = opts.fleetJitter ?? createFleetJitter(opts.productId, opts.deviceId);
|
|
9544
|
-
this.ws = new WsTransport({
|
|
9545
|
-
serverUrl: opts.serverUrl,
|
|
9546
|
-
getToken: () => opts.auth.getValidAccessToken(),
|
|
9547
|
-
deviceId: opts.deviceId,
|
|
9548
|
-
productId: opts.productId,
|
|
9549
|
-
capabilities: opts.capabilities,
|
|
9550
|
-
clientVersion: opts.clientVersion,
|
|
9551
|
-
runtimes: opts.runtimes,
|
|
9552
|
-
getConfiguredToolsets: opts.getConfiguredToolsets,
|
|
9553
|
-
getCursor: () => this.cursor,
|
|
9554
|
-
onEnvelope: (envelope) => this.deliver(envelope),
|
|
9555
|
-
onStateChange: (state) => {
|
|
9556
|
-
if (!this.stopped && !this.revoked) this.opts.onStateChange?.(state);
|
|
9557
|
-
},
|
|
9558
|
-
onAcked: (capabilities) => this.onAcked(capabilities),
|
|
9559
|
-
onConnectOutcome: (acked, err) => this.onWsOutcome(acked, err),
|
|
9560
|
-
backoff: opts.backoff,
|
|
9561
|
-
liveness: opts.liveness,
|
|
9562
|
-
reconnectDelayMs: (attempt, baseMs) => this.fleetJitter.delay("reconnect", attempt, baseMs)
|
|
9563
|
-
});
|
|
9564
9580
|
this.longPoll = new LongPollClient({
|
|
9565
9581
|
serverUrl: opts.serverUrl,
|
|
9566
9582
|
auth: opts.auth,
|
|
9567
|
-
//
|
|
9568
|
-
//
|
|
9569
|
-
//
|
|
9570
|
-
//
|
|
9571
|
-
//
|
|
9572
|
-
|
|
9573
|
-
getCursor: () => this.dedupWatermark(),
|
|
9583
|
+
// The long-poll query cursor is also the kernel's irreversible ack.
|
|
9584
|
+
// Only report the successfully processed cursor here. `deliveredSeq`
|
|
9585
|
+
// remains a local dedup watermark; using it on the wire would ack an
|
|
9586
|
+
// in-flight envelope before its handler settles and make a later
|
|
9587
|
+
// failure impossible to redeliver.
|
|
9588
|
+
getCursor: () => this.cursor,
|
|
9574
9589
|
onEnvelope: (envelope) => this.deliver(envelope),
|
|
9575
9590
|
onServerCapabilities: (capabilities) => {
|
|
9576
|
-
|
|
9591
|
+
this.serverCapabilities = capabilities;
|
|
9592
|
+
this.noteConnected();
|
|
9577
9593
|
},
|
|
9594
|
+
onServerCapabilitiesInvalidated: () => {
|
|
9595
|
+
this.serverCapabilities = [];
|
|
9596
|
+
},
|
|
9597
|
+
onPollFailure: () => this.noteDisconnected(),
|
|
9578
9598
|
onRevoked: () => this.enterRevoked(),
|
|
9579
9599
|
onReplayCursorTooOld: (error) => this.enterReplayCursorTooOld(error),
|
|
9580
9600
|
// M4 Phase 4 (version-negotiation drill fix): a batch entry
|
|
@@ -9602,13 +9622,10 @@ var ConnectionManager = class {
|
|
|
9602
9622
|
}
|
|
9603
9623
|
opts;
|
|
9604
9624
|
fleetJitter;
|
|
9605
|
-
ws;
|
|
9606
9625
|
longPoll;
|
|
9607
|
-
mode = "ws";
|
|
9608
|
-
consecutiveFailures = 0;
|
|
9609
|
-
wsRetryTimer;
|
|
9610
|
-
wsProbeSequence = 0;
|
|
9611
9626
|
uploadRetryAttempt = 0;
|
|
9627
|
+
started = false;
|
|
9628
|
+
connected = false;
|
|
9612
9629
|
cursor;
|
|
9613
9630
|
/**
|
|
9614
9631
|
* Finding F3 (at-most-once redelivery): the lowest `task.*` envelope `seq`
|
|
@@ -9630,10 +9647,9 @@ var ConnectionManager = class {
|
|
|
9630
9647
|
* (see `advanceCursor`) — that semantics is unchanged. `deliveredSeq`
|
|
9631
9648
|
* advances eagerly, the instant a `task.*` envelope is admitted past
|
|
9632
9649
|
* dedup (see `deliver`/`noteDelivered`), independent of whether its
|
|
9633
|
-
* handler has even started, let alone succeeded. It exists so a
|
|
9634
|
-
*
|
|
9635
|
-
*
|
|
9636
|
-
* `handleOffer` is NOT idempotent and must never be re-pulled while a
|
|
9650
|
+
* handler has even started, let alone succeeded. It exists so a repeated
|
|
9651
|
+
* read at the durable cursor does not re-dispatch an envelope already in
|
|
9652
|
+
* flight — `handleOffer` must not start a second adapter session while a
|
|
9637
9653
|
* first attempt is still running. On WS this same field is written the
|
|
9638
9654
|
* same way, but since a live WS connection only ever pushes a given `seq`
|
|
9639
9655
|
* once, it never has an observable effect there beyond mirroring
|
|
@@ -9644,18 +9660,18 @@ var ConnectionManager = class {
|
|
|
9644
9660
|
/** Finding F3: serializes `onEnvelope` calls into a per-connection FIFO — one envelope's handler always fully settles before the next one starts. */
|
|
9645
9661
|
processingChain = Promise.resolve();
|
|
9646
9662
|
/**
|
|
9647
|
-
* Design B (finding N4): the ONE outbound queue
|
|
9648
|
-
*
|
|
9663
|
+
* Design B (finding N4): the ONE outbound queue holds `Envelope` OBJECTS,
|
|
9664
|
+
* never re-encoded/rebuilt strings, so a
|
|
9649
9665
|
* resend after a failed send attempt is byte-identical to the original
|
|
9650
9666
|
* (same `id`), which is what lets the server's per-(deviceId,id) dedup
|
|
9651
9667
|
* (Wave 1) recognize it as a safe no-op retry rather than a second
|
|
9652
|
-
* application (protocol §9).
|
|
9653
|
-
* touches this queue — see `drainOutbox` — so nothing queued while one
|
|
9654
|
-
* transport was active is ever stranded when the other takes over.
|
|
9668
|
+
* application (protocol §9).
|
|
9655
9669
|
*/
|
|
9656
9670
|
outbox = [];
|
|
9671
|
+
/** Terminally rejected outbound envelopes, retained as a bounded observable quarantine. */
|
|
9672
|
+
rejectedOutboundEnvelopes = [];
|
|
9657
9673
|
/**
|
|
9658
|
-
* Finding F5(b): how many envelopes `drainOutbox`
|
|
9674
|
+
* Finding F5(b): how many envelopes `drainOutbox` has
|
|
9659
9675
|
* currently spliced OUT of `this.outbox` for an in-flight (not yet
|
|
9660
9676
|
* confirmed delivered) `postBatch` call — 0 the rest of the time. See
|
|
9661
9677
|
* `outboxLength`'s own doc comment for why this needs to be tracked
|
|
@@ -9708,39 +9724,22 @@ var ConnectionManager = class {
|
|
|
9708
9724
|
*/
|
|
9709
9725
|
cancelPendingDrainRetry;
|
|
9710
9726
|
/**
|
|
9711
|
-
* The capabilities the
|
|
9712
|
-
* `string[]` for forward compatibility.
|
|
9713
|
-
* long-poll
|
|
9714
|
-
*
|
|
9715
|
-
*
|
|
9716
|
-
*
|
|
9717
|
-
* not per-daemon-lifetime. Cleared to `[]` the instant the acked WS
|
|
9718
|
-
* connection ends for ANY reason — an ordinary disconnect (`onWsOutcome`'s
|
|
9719
|
-
* `acked` branch), `stop()`, or a transport switch to long-poll
|
|
9720
|
-
* (`enterLongPoll`) — and only repopulated by a fresh advertisement from
|
|
9721
|
-
* the transport that is still current.
|
|
9722
|
-
* The previous version of this doc comment claimed long-poll mode simply
|
|
9723
|
-
* "stays at whatever the last real WS `conn.ack` said" — that was the bug:
|
|
9724
|
-
* a daemon that once learned e.g. `approval_resolved` from an earlier WS
|
|
9725
|
-
* session kept believing it applied to whatever it's connected to NOW,
|
|
9726
|
-
* even after a disconnect/degrade where nothing has actually confirmed
|
|
9727
|
-
* that's still true (a reconnect could land on a DIFFERENT server behind a
|
|
9728
|
-
* load balancer). Concretely, `TaskRunner.sendApprovalResolved` gates
|
|
9729
|
-
* `task.approval_resolved` on this list — sending it to a server that
|
|
9730
|
-
* doesn't actually understand it over the long-poll path would get a
|
|
9731
|
-
* batch-level 400 from `MessagesSendRequestSchema` (protocol §8.2), which
|
|
9732
|
-
* `drainOutbox`'s retry-the-same-batch-forever loop then head-of-line
|
|
9733
|
-
* blocks EVERY envelope queued behind it on, permanently. Clearing this
|
|
9734
|
-
* eagerly means that gate reliably fails closed (falls back to the
|
|
9735
|
-
* pre-existing implicit-resume inference, unconditionally — see
|
|
9736
|
-
* `sendApprovalResolved`'s own doc comment) the moment the connection that
|
|
9737
|
-
* advertised the capability is no longer the one actually in use.
|
|
9727
|
+
* The capabilities the current server response advertised — untyped
|
|
9728
|
+
* `string[]` for forward compatibility. An advertisement is scoped to the
|
|
9729
|
+
* current long-poll response stream and is cleared after an HTTP failure,
|
|
9730
|
+
* terminal shutdown, or revocation. This keeps capability-gated outbound
|
|
9731
|
+
* messages fail-closed until the current server has explicitly advertised
|
|
9732
|
+
* support.
|
|
9738
9733
|
*/
|
|
9739
9734
|
serverCapabilities = [];
|
|
9740
9735
|
async start() {
|
|
9741
9736
|
if (this.terminalError) throw this.terminalError;
|
|
9742
9737
|
this.cursor = await this.opts.cursorStore.load(this.opts.serverUrl, this.opts.deviceId);
|
|
9743
|
-
this.
|
|
9738
|
+
this.started = true;
|
|
9739
|
+
this.opts.onStateChange?.("connecting");
|
|
9740
|
+
this.longPoll.start();
|
|
9741
|
+
this.outbox.unshift(createConnectionHelloEnvelope(this.opts, () => this.cursor));
|
|
9742
|
+
void this.drainOutbox();
|
|
9744
9743
|
}
|
|
9745
9744
|
/**
|
|
9746
9745
|
* Design B (finding N4): push onto the single shared outbox and try to
|
|
@@ -9751,17 +9750,13 @@ var ConnectionManager = class {
|
|
|
9751
9750
|
this.outbox.push(envelope);
|
|
9752
9751
|
void this.drainOutbox();
|
|
9753
9752
|
}
|
|
9753
|
+
/** Publish a fresh local configuration snapshot while this daemon is running. */
|
|
9754
|
+
refreshHello() {
|
|
9755
|
+
if (!this.started || this.stopped || this.revoked || this.terminalError) return;
|
|
9756
|
+
this.send(createConnectionHelloEnvelope(this.opts, () => this.cursor));
|
|
9757
|
+
}
|
|
9754
9758
|
/**
|
|
9755
|
-
*
|
|
9756
|
-
* transport is currently active, re-checking `this.mode` fresh on every
|
|
9757
|
-
* iteration so a transport switch mid-drain is picked up immediately
|
|
9758
|
-
* rather than fighting a stale decision made before the switch.
|
|
9759
|
-
*
|
|
9760
|
-
* WS: a synchronous, one-at-a-time `sendNow` per envelope while open+
|
|
9761
|
-
* acked; stops (without dropping anything — the remainder stays queued)
|
|
9762
|
-
* the moment it isn't, and is re-invoked once `onAcked` fires.
|
|
9763
|
-
*
|
|
9764
|
-
* Long-poll: POSTs the outbox in chunks of at most
|
|
9759
|
+
* POSTs the outbox through long-poll in chunks of at most
|
|
9765
9760
|
* `MAX_MESSAGES_PER_BATCH` (finding P1) — the server hard-caps a single
|
|
9766
9761
|
* `/byok/messages` batch there (`MessagesSendRequestSchema`, protocol
|
|
9767
9762
|
* §8.2) and 400s the WHOLE request if it's exceeded, which — before this
|
|
@@ -9773,43 +9768,48 @@ var ConnectionManager = class {
|
|
|
9773
9768
|
* failure that SAME chunk is unshifted back (order-preserving, same
|
|
9774
9769
|
* Envelope objects/ids — never rebuilt, so a retry is exactly the resend
|
|
9775
9770
|
* Wave 1's server-side dedup expects) and retried after a short backoff,
|
|
9776
|
-
* re-reading `this.mode` each time so a WS recovery that happens
|
|
9777
|
-
* mid-retry is honored on the very next loop iteration instead of only
|
|
9778
|
-
* after this attempt's backoff chain gives up.
|
|
9779
|
-
*
|
|
9780
9771
|
* Re-entrancy is guarded by `draining`: a call arriving while a drain is
|
|
9781
9772
|
* already in progress just returns — the in-progress loop's own
|
|
9782
|
-
* `while (this.outbox.length > 0)` check
|
|
9783
|
-
* pushed (or left over after a mode switch) on its very next iteration.
|
|
9773
|
+
* `while (this.outbox.length > 0)` check picks up anything newly pushed.
|
|
9784
9774
|
*/
|
|
9785
9775
|
async drainOutbox() {
|
|
9786
|
-
if (this.draining) return;
|
|
9776
|
+
if (!this.started || this.draining) return;
|
|
9787
9777
|
this.draining = true;
|
|
9778
|
+
const isolateRejectedBatch = async (batch) => {
|
|
9779
|
+
const pending = [[...batch]];
|
|
9780
|
+
while (pending.length > 0) {
|
|
9781
|
+
if (this.stopped || this.revoked) return pending.flat();
|
|
9782
|
+
const segment = pending.shift();
|
|
9783
|
+
const result = await this.longPoll.postBatch(segment);
|
|
9784
|
+
if (result === void 0) return [...segment, ...pending.flat()];
|
|
9785
|
+
if ((result.rejected ?? 0) === 0) continue;
|
|
9786
|
+
if (segment.length === 1) {
|
|
9787
|
+
this.quarantineRejectedOutbound(segment[0], "inbound_rejected");
|
|
9788
|
+
continue;
|
|
9789
|
+
}
|
|
9790
|
+
const midpoint = Math.floor(segment.length / 2);
|
|
9791
|
+
pending.unshift(segment.slice(0, midpoint), segment.slice(midpoint));
|
|
9792
|
+
}
|
|
9793
|
+
return void 0;
|
|
9794
|
+
};
|
|
9788
9795
|
try {
|
|
9789
9796
|
while (this.outbox.length > 0) {
|
|
9790
9797
|
if (this.stopped || this.revoked) return;
|
|
9791
|
-
if (this.mode === "ws") {
|
|
9792
|
-
if (!this.ws.isOpen) return;
|
|
9793
|
-
const envelope = this.outbox[0];
|
|
9794
|
-
if (!this.ws.sendNow(envelope)) return;
|
|
9795
|
-
this.outbox.shift();
|
|
9796
|
-
continue;
|
|
9797
|
-
}
|
|
9798
9798
|
const batch = this.outbox.splice(0, MAX_MESSAGES_PER_BATCH);
|
|
9799
9799
|
this.inFlightBatchSize = batch.length;
|
|
9800
|
-
let
|
|
9800
|
+
let retrySegments;
|
|
9801
9801
|
try {
|
|
9802
|
-
|
|
9802
|
+
retrySegments = await isolateRejectedBatch(batch);
|
|
9803
9803
|
} finally {
|
|
9804
9804
|
this.inFlightBatchSize = 0;
|
|
9805
9805
|
}
|
|
9806
|
-
if (
|
|
9806
|
+
if (retrySegments === void 0) {
|
|
9807
9807
|
this.uploadRetryAttempt = 0;
|
|
9808
9808
|
this.opts.onOperationalOutcome?.("success", "upload");
|
|
9809
9809
|
continue;
|
|
9810
9810
|
}
|
|
9811
9811
|
this.opts.onOperationalOutcome?.("failure", "upload");
|
|
9812
|
-
this.outbox.unshift(...
|
|
9812
|
+
this.outbox.unshift(...retrySegments);
|
|
9813
9813
|
if (this.stopped || this.revoked) return;
|
|
9814
9814
|
const baseMs = this.opts.longPollRetryDelayMs ?? 2e3;
|
|
9815
9815
|
await this.drainRetryDelay(this.fleetJitter.delay("upload", this.uploadRetryAttempt++, baseMs));
|
|
@@ -9825,8 +9825,7 @@ var ConnectionManager = class {
|
|
|
9825
9825
|
* in-flight wait immediately instead of leaving `drainOutbox` parked here
|
|
9826
9826
|
* for up to the rest of the delay before it next checks `this.revoked` —
|
|
9827
9827
|
* and (b) unref'd, so the timer never keeps the Node process alive by
|
|
9828
|
-
* itself while nothing else (
|
|
9829
|
-
* legitimately is.
|
|
9828
|
+
* itself while nothing else (such as the live long-poll GET) legitimately is.
|
|
9830
9829
|
*/
|
|
9831
9830
|
drainRetryDelay(ms) {
|
|
9832
9831
|
return new Promise((resolve) => {
|
|
@@ -9842,14 +9841,9 @@ var ConnectionManager = class {
|
|
|
9842
9841
|
};
|
|
9843
9842
|
});
|
|
9844
9843
|
}
|
|
9845
|
-
isTransportDegraded() {
|
|
9846
|
-
return this.mode === "long-poll";
|
|
9847
|
-
}
|
|
9848
9844
|
/**
|
|
9849
|
-
* The capabilities the
|
|
9850
|
-
*
|
|
9851
|
-
* on long-poll. Empty before either transport has supplied its current
|
|
9852
|
-
* advertisement, and cleared across disconnect/switch boundaries.
|
|
9845
|
+
* The capabilities the latest successful `GET /byok/events` response
|
|
9846
|
+
* advertised. Empty before a successful response and after a failed one.
|
|
9853
9847
|
*/
|
|
9854
9848
|
getServerCapabilities() {
|
|
9855
9849
|
return this.serverCapabilities;
|
|
@@ -9857,29 +9851,23 @@ var ConnectionManager = class {
|
|
|
9857
9851
|
getTerminalError() {
|
|
9858
9852
|
return this.terminalError;
|
|
9859
9853
|
}
|
|
9860
|
-
getMode() {
|
|
9861
|
-
return this.mode;
|
|
9862
|
-
}
|
|
9863
9854
|
isConnected() {
|
|
9864
|
-
return this.
|
|
9855
|
+
return this.connected;
|
|
9865
9856
|
}
|
|
9866
9857
|
isRevoked() {
|
|
9867
9858
|
return this.revoked;
|
|
9868
9859
|
}
|
|
9869
9860
|
/**
|
|
9870
|
-
* Resolves
|
|
9871
|
-
* connection
|
|
9872
|
-
* lets `daemon.start()` return promptly even when WS is unavailable from
|
|
9873
|
-
* the very first attempt, rather than hanging until a WS `conn.ack` that
|
|
9874
|
-
* may never come.
|
|
9861
|
+
* Resolves after the first successful long-poll response establishes the
|
|
9862
|
+
* authenticated connection.
|
|
9875
9863
|
*
|
|
9876
9864
|
* Rejects with {@link DeviceRevokedError} — instead of hanging until
|
|
9877
9865
|
* `timeoutMs` — if the device turns out to be revoked while settling (or
|
|
9878
9866
|
* already was): a cold `daemon.start()` against an already-revoked device
|
|
9879
9867
|
* must fail fast, not surface a generic timeout (protocol §6.3).
|
|
9880
9868
|
*/
|
|
9881
|
-
|
|
9882
|
-
if (this.
|
|
9869
|
+
waitForConnection(timeoutMs = 1e4) {
|
|
9870
|
+
if (this.connected) return Promise.resolve();
|
|
9883
9871
|
if (this.terminalError) return Promise.reject(this.terminalError);
|
|
9884
9872
|
if (this.revoked) return Promise.reject(new DeviceRevokedError());
|
|
9885
9873
|
return new Promise((resolve, reject) => {
|
|
@@ -9887,7 +9875,7 @@ var ConnectionManager = class {
|
|
|
9887
9875
|
};
|
|
9888
9876
|
const timer = setTimeout(() => {
|
|
9889
9877
|
this.settledWaiters = this.settledWaiters.filter((w) => w !== settle);
|
|
9890
|
-
reject(new Error("Timed out waiting for the connection to settle
|
|
9878
|
+
reject(new Error("Timed out waiting for the long-poll connection to settle"));
|
|
9891
9879
|
}, timeoutMs);
|
|
9892
9880
|
settle = (err) => {
|
|
9893
9881
|
clearTimeout(timer);
|
|
@@ -9898,7 +9886,7 @@ var ConnectionManager = class {
|
|
|
9898
9886
|
});
|
|
9899
9887
|
}
|
|
9900
9888
|
/**
|
|
9901
|
-
* Stops
|
|
9889
|
+
* Stops the long-poll transport and waits for every in-flight envelope handler
|
|
9902
9890
|
* (the F3 FIFO chain) and the most recent cursor write to actually land on
|
|
9903
9891
|
* disk — otherwise a `stop()` racing a just-processed envelope's
|
|
9904
9892
|
* persistence could lose that cursor advance, or leave a handler running
|
|
@@ -9907,14 +9895,14 @@ var ConnectionManager = class {
|
|
|
9907
9895
|
* Finding F5(b) (cross-model adversarial review): `drainTimeoutMs`, when
|
|
9908
9896
|
* passed, bounds how long this waits for the shared outbox (`this.outbox`
|
|
9909
9897
|
* — Design B) to actually finish draining BEFORE flipping `this.stopped`
|
|
9910
|
-
* and
|
|
9898
|
+
* and stopping the transport. Before this fix, `stop()` set `stopped`
|
|
9911
9899
|
* synchronously and never waited for `drainOutbox` at all: an envelope
|
|
9912
9900
|
* `send()` had just pushed moments earlier (e.g. `TaskRunner.shutdownTask`'s
|
|
9913
9901
|
* own `task.fail`, sent right before `create-daemon.ts`'s
|
|
9914
9902
|
* `performControlShutdown` calls this) could still be sitting UNSENT in
|
|
9915
9903
|
* `this.outbox` — mid long-poll retry backoff, or simply not yet picked up
|
|
9916
9904
|
* by the fire-and-forget `drainOutbox()` `send()` kicked off — and this
|
|
9917
|
-
* method would happily proceed to `stopped = true`
|
|
9905
|
+
* method would happily proceed to `stopped = true` regardless,
|
|
9918
9906
|
* after which NOTHING ever drains it again: silently lost, even though
|
|
9919
9907
|
* `TaskRunner` believed it had been sent. `drainTimeoutMs` omitted (the
|
|
9920
9908
|
* default) preserves the EXACT prior behavior for every other existing
|
|
@@ -9931,9 +9919,9 @@ var ConnectionManager = class {
|
|
|
9931
9919
|
}
|
|
9932
9920
|
this.stopped = true;
|
|
9933
9921
|
this.serverCapabilities = [];
|
|
9934
|
-
if (this.wsRetryTimer) clearInterval(this.wsRetryTimer);
|
|
9935
9922
|
this.longPoll.stop();
|
|
9936
|
-
this.
|
|
9923
|
+
this.connected = false;
|
|
9924
|
+
this.opts.onStateChange?.("closed");
|
|
9937
9925
|
await this.processingChain;
|
|
9938
9926
|
await this.pendingCursorSave;
|
|
9939
9927
|
}
|
|
@@ -9958,6 +9946,10 @@ var ConnectionManager = class {
|
|
|
9958
9946
|
outboxLength() {
|
|
9959
9947
|
return this.outbox.length + this.inFlightBatchSize;
|
|
9960
9948
|
}
|
|
9949
|
+
/** A bounded terminal quarantine for operator inspection; these entries are never retried. */
|
|
9950
|
+
rejectedOutbox() {
|
|
9951
|
+
return this.rejectedOutboundEnvelopes;
|
|
9952
|
+
}
|
|
9961
9953
|
/**
|
|
9962
9954
|
* Finding F5(b): polls {@link outboxLength} (not `this.outbox.length`
|
|
9963
9955
|
* alone — see that method's own doc comment for why a spliced-out,
|
|
@@ -9965,15 +9957,14 @@ var ConnectionManager = class {
|
|
|
9965
9957
|
* a single `drainOutbox()` promise directly — a drain in progress can
|
|
9966
9958
|
* itself loop through multiple retry/backoff cycles (`drainRetryDelay`)
|
|
9967
9959
|
* while the server is unreachable, and a fresh, INDEPENDENT
|
|
9968
|
-
* `drainOutbox()` call can also be triggered concurrently (`send()
|
|
9969
|
-
*
|
|
9960
|
+
* `drainOutbox()` call can also be triggered concurrently (`send()`) —
|
|
9961
|
+
* polling the one thing
|
|
9970
9962
|
* that actually matters (is anything still undelivered) can never go
|
|
9971
9963
|
* stale the way capturing one specific in-flight promise reference
|
|
9972
9964
|
* could. Kicks off one more `drainOutbox()` attempt itself first
|
|
9973
9965
|
* (harmless no-op if one is already running — see its own re-entrancy
|
|
9974
|
-
* guard) in case nothing is currently actively retrying
|
|
9975
|
-
*
|
|
9976
|
-
* isn't just passively hoping something else happens to be making
|
|
9966
|
+
* guard) in case nothing is currently actively retrying, so this bounded
|
|
9967
|
+
* wait isn't just passively hoping something else happens to be making
|
|
9977
9968
|
* progress.
|
|
9978
9969
|
*/
|
|
9979
9970
|
waitForOutboxDrained(timeoutMs) {
|
|
@@ -10016,19 +10007,21 @@ var ConnectionManager = class {
|
|
|
10016
10007
|
deliver(envelope) {
|
|
10017
10008
|
const tracked = isCursorEnvelopeType(envelope.type) && typeof envelope.seq === "number";
|
|
10018
10009
|
const watermark = this.dedupWatermark();
|
|
10019
|
-
if (tracked && watermark !== void 0 && envelope.seq <= watermark) return;
|
|
10010
|
+
if (tracked && watermark !== void 0 && envelope.seq <= watermark) return false;
|
|
10020
10011
|
if (tracked) {
|
|
10021
10012
|
const seq = envelope.seq;
|
|
10022
|
-
if (this.inFlightSeqs.has(seq) || this.processedSeqs.has(seq)) return;
|
|
10013
|
+
if (this.inFlightSeqs.has(seq) || this.processedSeqs.has(seq)) return false;
|
|
10023
10014
|
this.inFlightSeqs.add(seq);
|
|
10024
10015
|
this.noteDelivered(seq);
|
|
10025
10016
|
}
|
|
10026
10017
|
this.processingChain = this.processingChain.then(() => this.process(envelope, tracked));
|
|
10018
|
+
return true;
|
|
10027
10019
|
}
|
|
10028
10020
|
/**
|
|
10029
|
-
*
|
|
10030
|
-
* against
|
|
10031
|
-
*
|
|
10021
|
+
* The local watermark `deliver()` dedupes inbound `task.*` envelopes
|
|
10022
|
+
* against. It is deliberately NOT the long-poll query cursor: that query
|
|
10023
|
+
* is the kernel acknowledgement and uses only the successfully processed
|
|
10024
|
+
* `cursor` (see the constructor). Normally this local watermark is
|
|
10032
10025
|
* `deliveredSeq` — which is always >= `cursor` (every envelope that
|
|
10033
10026
|
* reaches `advanceCursor` already passed through `noteDelivered` first,
|
|
10034
10027
|
* see `deliver`) — so this is the literal `max(cursor, deliveredSeq)` the
|
|
@@ -10044,11 +10037,9 @@ var ConnectionManager = class {
|
|
|
10044
10037
|
* whose outcome wasn't known yet. No separate "reset deliveredSeq on
|
|
10045
10038
|
* reconnect" step is needed for this to be correct — collapsing to
|
|
10046
10039
|
* `cursor` exactly while stalled already produces the right answer on
|
|
10047
|
-
* every
|
|
10048
|
-
*
|
|
10049
|
-
*
|
|
10050
|
-
* re-pulling/re-dispatching something already in flight across a
|
|
10051
|
-
* reconnect that happens to land while a handler is still running.
|
|
10040
|
+
* every long-poll retry path. NOT resetting it unconditionally on every
|
|
10041
|
+
* retry lets `deliveredSeq` keep doing its job of not re-dispatching
|
|
10042
|
+
* something already in flight while a handler is still running.
|
|
10052
10043
|
*/
|
|
10053
10044
|
dedupWatermark() {
|
|
10054
10045
|
return this.stalledAtSeq !== void 0 ? this.cursor : this.deliveredSeq ?? this.cursor;
|
|
@@ -10066,11 +10057,13 @@ var ConnectionManager = class {
|
|
|
10066
10057
|
}
|
|
10067
10058
|
await this.opts.onEnvelope(envelope);
|
|
10068
10059
|
if (!tracked) return;
|
|
10069
|
-
this.
|
|
10070
|
-
|
|
10060
|
+
if (this.stalledAtSeq !== void 0 && envelope.seq !== this.stalledAtSeq) {
|
|
10061
|
+
this.processedSeqs.add(seq);
|
|
10062
|
+
return;
|
|
10063
|
+
}
|
|
10071
10064
|
this.stalledAtSeq = void 0;
|
|
10065
|
+
await this.advanceCursor(envelope.seq);
|
|
10072
10066
|
this.processedSeqs.clear();
|
|
10073
|
-
this.advanceCursor(envelope.seq);
|
|
10074
10067
|
} catch (err) {
|
|
10075
10068
|
if (tracked && this.stalledAtSeq === void 0) this.stalledAtSeq = envelope.seq;
|
|
10076
10069
|
console.error(
|
|
@@ -10084,9 +10077,8 @@ var ConnectionManager = class {
|
|
|
10084
10077
|
/**
|
|
10085
10078
|
* M4 Phase 4 (version-negotiation drill fix): `LongPollClient` calls this
|
|
10086
10079
|
* for a batch entry it could not parse into a known `Envelope` at all (an
|
|
10087
|
-
* unrecognized message type
|
|
10088
|
-
*
|
|
10089
|
-
* `parseLooseEventsPollResponse`) but which still carried a numeric,
|
|
10080
|
+
* unrecognized message type (see `long-poll-transport.ts`'s own doc
|
|
10081
|
+
* comment on `parseLooseEventsPollResponse`) but which still carried a numeric,
|
|
10090
10082
|
* task-class envelope-level `seq` (the caller only invokes this for a
|
|
10091
10083
|
* `task.`-prefixed type — see `long-poll-transport.ts`'s own
|
|
10092
10084
|
* `extractSkippableSeq`; `conn.*`-shaped or type-less entries never reach
|
|
@@ -10126,7 +10118,7 @@ var ConnectionManager = class {
|
|
|
10126
10118
|
* `noteDelivered` (the eager, in-memory watermark) stays UNCHAINED —
|
|
10127
10119
|
* called immediately, unconditionally, regardless of `stalledAtSeq` —
|
|
10128
10120
|
* matching `deliver()`'s own eager, unconditional call for a real
|
|
10129
|
-
* envelope: its only job is "don't re-
|
|
10121
|
+
* envelope: its only job is "don't re-dispatch something already handed off,"
|
|
10130
10122
|
* independent of outcome, and that property does not depend on FIFO
|
|
10131
10123
|
* ordering the way the DURABLE cursor does.
|
|
10132
10124
|
*
|
|
@@ -10146,9 +10138,18 @@ var ConnectionManager = class {
|
|
|
10146
10138
|
*/
|
|
10147
10139
|
noteSkippedSeq(seq) {
|
|
10148
10140
|
this.noteDelivered(seq);
|
|
10149
|
-
this.processingChain = this.processingChain.then(() => {
|
|
10141
|
+
this.processingChain = this.processingChain.then(async () => {
|
|
10150
10142
|
if (this.stalledAtSeq === void 0 || seq === this.stalledAtSeq) {
|
|
10151
|
-
|
|
10143
|
+
try {
|
|
10144
|
+
await this.advanceCursor(seq);
|
|
10145
|
+
if (this.stalledAtSeq === seq) this.stalledAtSeq = void 0;
|
|
10146
|
+
} catch (err) {
|
|
10147
|
+
if (this.stalledAtSeq === void 0) this.stalledAtSeq = seq;
|
|
10148
|
+
console.error(
|
|
10149
|
+
`[byok/client] unknown task seq=${seq} could not durably persist its cursor; cursor left unadvanced for redelivery:`,
|
|
10150
|
+
err
|
|
10151
|
+
);
|
|
10152
|
+
}
|
|
10152
10153
|
}
|
|
10153
10154
|
});
|
|
10154
10155
|
}
|
|
@@ -10194,113 +10195,52 @@ var ConnectionManager = class {
|
|
|
10194
10195
|
if (this.stalledAtSeq === void 0) this.stalledAtSeq = seq;
|
|
10195
10196
|
});
|
|
10196
10197
|
}
|
|
10197
|
-
advanceCursor(seq) {
|
|
10198
|
+
async advanceCursor(seq) {
|
|
10198
10199
|
if (this.cursor !== void 0 && seq <= this.cursor) return;
|
|
10200
|
+
const save = this.pendingCursorSave.catch(() => void 0).then(() => this.opts.cursorStore.save(this.opts.serverUrl, this.opts.deviceId, seq));
|
|
10201
|
+
this.pendingCursorSave = save;
|
|
10202
|
+
await save;
|
|
10199
10203
|
this.cursor = seq;
|
|
10200
|
-
this.pendingCursorSave = this.pendingCursorSave.catch(() => {
|
|
10201
|
-
}).then(() => this.opts.cursorStore.save(this.opts.serverUrl, this.opts.deviceId, seq)).catch(() => {
|
|
10202
|
-
});
|
|
10203
|
-
}
|
|
10204
|
-
/**
|
|
10205
|
-
* Fires the moment a connection attempt reaches `conn.ack` — independent
|
|
10206
|
-
* of whether/when it later closes. This is the ONLY place that can
|
|
10207
|
-
* reliably detect "WS is back up" while long-polling: a healthy
|
|
10208
|
-
* connection stays open indefinitely, so it never reaches `onWsOutcome`
|
|
10209
|
-
* (which is close-only) at all.
|
|
10210
|
-
*/
|
|
10211
|
-
onAcked(capabilities) {
|
|
10212
|
-
if (this.terminalError) return;
|
|
10213
|
-
this.serverCapabilities = capabilities;
|
|
10214
|
-
this.consecutiveFailures = 0;
|
|
10215
|
-
this.notifySettled();
|
|
10216
|
-
this.opts.onOperationalOutcome?.("success", "reconnect");
|
|
10217
|
-
if (this.mode === "long-poll") this.exitLongPoll();
|
|
10218
|
-
void this.drainOutbox();
|
|
10219
10204
|
}
|
|
10220
|
-
|
|
10221
|
-
if (
|
|
10222
|
-
|
|
10223
|
-
|
|
10224
|
-
|
|
10225
|
-
|
|
10226
|
-
if (acked) this.serverCapabilities = [];
|
|
10227
|
-
if (err instanceof WsUnexpectedStatusError && err.status === 401) {
|
|
10228
|
-
this.opts.auth.handleUnauthorized().catch((renewErr) => {
|
|
10229
|
-
if (renewErr instanceof DeviceRevokedError) this.enterRevoked();
|
|
10230
|
-
});
|
|
10231
|
-
}
|
|
10232
|
-
if (acked) return;
|
|
10233
|
-
this.opts.onOperationalOutcome?.("failure", "reconnect");
|
|
10234
|
-
this.consecutiveFailures += 1;
|
|
10235
|
-
if (this.mode === "ws" && this.consecutiveFailures >= (this.opts.wsFailureThreshold ?? 3)) {
|
|
10236
|
-
this.enterLongPoll();
|
|
10237
|
-
}
|
|
10205
|
+
quarantineRejectedOutbound(envelope, reason) {
|
|
10206
|
+
if (this.rejectedOutboundEnvelopes.length === MAX_REJECTED_OUTBOX_ENTRIES) this.rejectedOutboundEnvelopes.shift();
|
|
10207
|
+
this.rejectedOutboundEnvelopes.push({ envelope, reason });
|
|
10208
|
+
console.error(
|
|
10209
|
+
`[byok/client] outbound envelope ${envelope.id} was permanently rejected (${reason}) and moved to the terminal quarantine.`
|
|
10210
|
+
);
|
|
10238
10211
|
}
|
|
10239
10212
|
notifySettled(err) {
|
|
10240
10213
|
for (const waiter of this.settledWaiters.splice(0)) waiter(err);
|
|
10241
10214
|
}
|
|
10242
|
-
|
|
10243
|
-
this.
|
|
10244
|
-
this.
|
|
10245
|
-
this.
|
|
10246
|
-
this.opts.onStateChange?.("degraded");
|
|
10247
|
-
this.longPoll.start();
|
|
10248
|
-
this.outbox.unshift(createConnectionHelloEnvelope({
|
|
10249
|
-
deviceId: this.opts.deviceId,
|
|
10250
|
-
productId: this.opts.productId,
|
|
10251
|
-
capabilities: this.opts.capabilities,
|
|
10252
|
-
clientVersion: this.opts.clientVersion,
|
|
10253
|
-
runtimes: this.opts.runtimes,
|
|
10254
|
-
getConfiguredToolsets: this.opts.getConfiguredToolsets,
|
|
10255
|
-
getCursor: () => this.cursor
|
|
10256
|
-
}));
|
|
10215
|
+
noteConnected() {
|
|
10216
|
+
if (this.connected || this.stopped || this.revoked || this.terminalError) return;
|
|
10217
|
+
this.connected = true;
|
|
10218
|
+
this.opts.onStateChange?.("open");
|
|
10257
10219
|
this.notifySettled();
|
|
10258
|
-
|
|
10259
|
-
|
|
10220
|
+
}
|
|
10221
|
+
noteDisconnected() {
|
|
10222
|
+
if (!this.connected || this.stopped || this.revoked || this.terminalError) return;
|
|
10223
|
+
this.connected = false;
|
|
10224
|
+
this.opts.onStateChange?.("connecting");
|
|
10260
10225
|
}
|
|
10261
10226
|
enterReplayCursorTooOld(error) {
|
|
10262
10227
|
if (this.terminalError) return;
|
|
10263
10228
|
this.terminalError = error;
|
|
10264
10229
|
this.stopped = true;
|
|
10265
10230
|
this.serverCapabilities = [];
|
|
10266
|
-
if (this.wsRetryTimer) clearInterval(this.wsRetryTimer);
|
|
10267
10231
|
this.longPoll.stop();
|
|
10268
|
-
this.
|
|
10269
|
-
this.ws.close();
|
|
10232
|
+
this.connected = false;
|
|
10270
10233
|
this.cancelPendingDrainRetry?.();
|
|
10271
10234
|
this.notifySettled(error);
|
|
10272
10235
|
this.opts.onStateChange?.("closed");
|
|
10273
10236
|
this.opts.onTerminalError?.(error);
|
|
10274
10237
|
}
|
|
10275
|
-
exitLongPoll() {
|
|
10276
|
-
if (this.wsRetryTimer) {
|
|
10277
|
-
clearInterval(this.wsRetryTimer);
|
|
10278
|
-
this.wsRetryTimer = void 0;
|
|
10279
|
-
}
|
|
10280
|
-
this.longPoll.stop();
|
|
10281
|
-
this.mode = "ws";
|
|
10282
|
-
this.consecutiveFailures = 0;
|
|
10283
|
-
this.ws.resumeAutoReconnect();
|
|
10284
|
-
void this.drainOutbox();
|
|
10285
|
-
}
|
|
10286
|
-
scheduleWsProbe() {
|
|
10287
|
-
const baseMs = this.opts.wsRetryIntervalMs ?? 5 * 60 * 1e3;
|
|
10288
|
-
const delayMs = this.fleetJitter.delay("reconnect", this.wsProbeSequence++, baseMs);
|
|
10289
|
-
const timer = setTimeout(() => {
|
|
10290
|
-
this.wsRetryTimer = void 0;
|
|
10291
|
-
if (this.stopped || this.revoked || this.mode !== "long-poll") return;
|
|
10292
|
-
this.ws.connect({ auto: false });
|
|
10293
|
-
this.scheduleWsProbe();
|
|
10294
|
-
}, delayMs);
|
|
10295
|
-
timer.unref?.();
|
|
10296
|
-
this.wsRetryTimer = timer;
|
|
10297
|
-
}
|
|
10298
10238
|
enterRevoked() {
|
|
10299
10239
|
if (this.revoked) return;
|
|
10300
10240
|
this.revoked = true;
|
|
10301
|
-
|
|
10241
|
+
this.serverCapabilities = [];
|
|
10302
10242
|
this.longPoll.stop();
|
|
10303
|
-
this.
|
|
10243
|
+
this.connected = false;
|
|
10304
10244
|
this.cancelPendingDrainRetry?.();
|
|
10305
10245
|
this.opts.onStateChange?.("revoked");
|
|
10306
10246
|
this.notifySettled(new DeviceRevokedError());
|
|
@@ -13668,6 +13608,7 @@ function resultDocumentRejectionDetail(check) {
|
|
|
13668
13608
|
}
|
|
13669
13609
|
}
|
|
13670
13610
|
var DEFAULT_MAX_TASK_OUTPUT_BYTES = 64 * 1024 * 1024;
|
|
13611
|
+
var DEFAULT_MAX_CONCURRENT_MUTABLE_SESSIONS_PER_AGENT_HOME = 1;
|
|
13671
13612
|
function isKnownRuntimeId(id) {
|
|
13672
13613
|
return RuntimeIdSchema.safeParse(id).success;
|
|
13673
13614
|
}
|
|
@@ -13914,6 +13855,10 @@ var TaskRunner = class {
|
|
|
13914
13855
|
get maxTaskOutputBytes() {
|
|
13915
13856
|
return this.deps.maxTaskOutputBytes ?? DEFAULT_MAX_TASK_OUTPUT_BYTES;
|
|
13916
13857
|
}
|
|
13858
|
+
/** WP0: effective per-canonical-Agent-home Attempt cap — see {@link DEFAULT_MAX_CONCURRENT_MUTABLE_SESSIONS_PER_AGENT_HOME}. */
|
|
13859
|
+
get maxConcurrentMutableSessionsPerAgentHome() {
|
|
13860
|
+
return this.deps.maxConcurrentMutableSessionsPerAgentHome ?? DEFAULT_MAX_CONCURRENT_MUTABLE_SESSIONS_PER_AGENT_HOME;
|
|
13861
|
+
}
|
|
13917
13862
|
/**
|
|
13918
13863
|
* M4 Phase 4 (part B.3, observability): per-active-task queue watermarks
|
|
13919
13864
|
* for the control socket's `status` result — see
|
|
@@ -14303,6 +14248,24 @@ var TaskRunner = class {
|
|
|
14303
14248
|
decline("strict Agent-only daemon refuses legacy task offers", false);
|
|
14304
14249
|
return;
|
|
14305
14250
|
}
|
|
14251
|
+
if (agentRef !== void 0) {
|
|
14252
|
+
const limit = this.maxConcurrentMutableSessionsPerAgentHome;
|
|
14253
|
+
let canonicalHome;
|
|
14254
|
+
try {
|
|
14255
|
+
canonicalHome = await this.deps.agentHome.layout.canonicalHomePath(agentRef);
|
|
14256
|
+
} catch (error) {
|
|
14257
|
+
decline(
|
|
14258
|
+
`Agent home admission failed: ${errorMessage5(error)}`,
|
|
14259
|
+
!(error instanceof AgentHomeResolutionError)
|
|
14260
|
+
);
|
|
14261
|
+
return;
|
|
14262
|
+
}
|
|
14263
|
+
const active2 = this.deps.agentHome.executionLeaseManager.activeAttemptCount(canonicalHome);
|
|
14264
|
+
if (active2 >= limit) {
|
|
14265
|
+
decline(`agent home busy: ${active2} active attempt(s)`, true);
|
|
14266
|
+
return;
|
|
14267
|
+
}
|
|
14268
|
+
}
|
|
14306
14269
|
const guarded = this.deps.admissionGuard?.({ taskId, payload });
|
|
14307
14270
|
if (guarded !== void 0 && !guarded.admit) {
|
|
14308
14271
|
decline(guarded.reason, guarded.retryable);
|
|
@@ -18014,7 +17977,7 @@ async function detectRuntimes(adapters) {
|
|
|
18014
17977
|
}
|
|
18015
17978
|
return runtimes;
|
|
18016
17979
|
}
|
|
18017
|
-
function computeCapabilities(adapters, agentHomeConfigured = false, strictAgentOnly = false, agentHomeProjectionConfigured = false, agentEgressConfigured = false, contentReadPolicies) {
|
|
17980
|
+
function computeCapabilities(adapters, agentHomeConfigured = false, strictAgentOnly = false, agentHomeProjectionConfigured = false, agentEgressConfigured = false, contentReadPolicies, providerProfileBindingConfigured = false) {
|
|
18018
17981
|
const flags = [];
|
|
18019
17982
|
if (adapters.some((adapter) => adapter.descriptor.capabilities.steer)) flags.push("steer");
|
|
18020
17983
|
flags.push("blob-upload");
|
|
@@ -18026,6 +17989,9 @@ function computeCapabilities(adapters, agentHomeConfigured = false, strictAgentO
|
|
|
18026
17989
|
if (selectionAdapters.length > 0 && selectionAdapters.every((adapter) => adapter.descriptor.supportsDispatchSelection === true)) {
|
|
18027
17990
|
flags.push("dispatch-selection");
|
|
18028
17991
|
}
|
|
17992
|
+
if (providerProfileBindingConfigured && adapters.some((adapter) => adapter.descriptor.id === "pi" && adapter.descriptor.supportsDispatchSelection)) {
|
|
17993
|
+
flags.push(PROVIDER_PROFILE_BINDING_CAPABILITY);
|
|
17994
|
+
}
|
|
18029
17995
|
if (adapters.some((adapter) => adapter.descriptor.capabilities.mcpToolsets === true)) {
|
|
18030
17996
|
flags.push("toolset-selection");
|
|
18031
17997
|
}
|
|
@@ -18188,6 +18154,12 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
18188
18154
|
if (config.strictAgentOnly === true && config.agentHome === void 0) {
|
|
18189
18155
|
throw new Error("DaemonConfig.strictAgentOnly requires DaemonConfig.agentHome");
|
|
18190
18156
|
}
|
|
18157
|
+
if (config.maxConcurrentMutableSessionsPerAgentHome !== void 0 && !(Number.isSafeInteger(config.maxConcurrentMutableSessionsPerAgentHome) && config.maxConcurrentMutableSessionsPerAgentHome > 0)) {
|
|
18158
|
+
throw new Error(
|
|
18159
|
+
`DaemonConfig.maxConcurrentMutableSessionsPerAgentHome must be a positive safe integer (or omitted to use the default of ${DEFAULT_MAX_CONCURRENT_MUTABLE_SESSIONS_PER_AGENT_HOME}) \u2014 got ${config.maxConcurrentMutableSessionsPerAgentHome}. Raising it above 1 lets that many Attempts co-write one canonical Agent home; 0, a negative number, NaN, or a non-integer is rejected rather than silently treated as "uncapped".`
|
|
18160
|
+
);
|
|
18161
|
+
}
|
|
18162
|
+
const agentHomeAttemptLimit = config.maxConcurrentMutableSessionsPerAgentHome ?? DEFAULT_MAX_CONCURRENT_MUTABLE_SESSIONS_PER_AGENT_HOME;
|
|
18191
18163
|
const egressPolicy = resolveAgentEgressPolicy(config.agentEgress?.policy);
|
|
18192
18164
|
const egressBatcherOptions = egressPolicy.activity.mode === "contentful-trajectory" ? {
|
|
18193
18165
|
...config.progressBatch,
|
|
@@ -18508,7 +18480,8 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
18508
18480
|
config.strictAgentOnly === true,
|
|
18509
18481
|
agentHomeManager?.supportsTaskFreeProjection() === true,
|
|
18510
18482
|
config.agentEgress !== void 0,
|
|
18511
|
-
agentContentReadPolicies
|
|
18483
|
+
agentContentReadPolicies,
|
|
18484
|
+
config.piByokLauncher !== void 0
|
|
18512
18485
|
);
|
|
18513
18486
|
const agentHomeProjectionCompletion = agentHomeManager?.supportsTaskFreeProjection() === true ? new AgentHomeProjectionCompletionClient({
|
|
18514
18487
|
serverUrl: config.serverUrl,
|
|
@@ -18567,6 +18540,8 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
18567
18540
|
workspaceRoot: config.workspaceRoot,
|
|
18568
18541
|
...agentHomeManager === void 0 ? {} : { agentHome: agentHomeManager },
|
|
18569
18542
|
...config.strictAgentOnly === true ? { strictAgentOnly: true } : {},
|
|
18543
|
+
// WP0: already validated up front — see `DaemonConfig.maxConcurrentMutableSessionsPerAgentHome`.
|
|
18544
|
+
maxConcurrentMutableSessionsPerAgentHome: agentHomeAttemptLimit,
|
|
18570
18545
|
...agentSessionHandoffs === void 0 ? {} : { agentSessionHandoffs },
|
|
18571
18546
|
deviceId: record.deviceId,
|
|
18572
18547
|
// M5: see `DaemonConfig.runtimeEnvironment`'s own doc comment above.
|
|
@@ -18851,16 +18826,12 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
18851
18826
|
return runner?.handleEnvelope(envelope) ?? Promise.resolve();
|
|
18852
18827
|
},
|
|
18853
18828
|
onStateChange: (state) => {
|
|
18854
|
-
const wasSettled = connectionState === "open"
|
|
18829
|
+
const wasSettled = connectionState === "open";
|
|
18855
18830
|
connectionState = state;
|
|
18856
18831
|
observer.noteConnectionState(state);
|
|
18857
|
-
if (!wasSettled &&
|
|
18858
|
-
if (!wasSettled &&
|
|
18832
|
+
if (!wasSettled && state === "open") runner?.retryRecoveredAgentMessages();
|
|
18833
|
+
if (!wasSettled && state === "open") runPresenceDiscovery();
|
|
18859
18834
|
},
|
|
18860
|
-
backoff: overrides.backoff,
|
|
18861
|
-
liveness: overrides.liveness,
|
|
18862
|
-
wsFailureThreshold: overrides.longPoll?.wsFailureThreshold,
|
|
18863
|
-
wsRetryIntervalMs: overrides.longPoll?.wsRetryIntervalMs,
|
|
18864
18835
|
longPollRetryDelayMs: overrides.longPoll?.retryDelayMs,
|
|
18865
18836
|
longPollIdleDelayMs: overrides.longPoll?.idleDelayMs,
|
|
18866
18837
|
fleetJitter,
|
|
@@ -18872,7 +18843,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
18872
18843
|
}
|
|
18873
18844
|
});
|
|
18874
18845
|
await connection.start();
|
|
18875
|
-
await connection.
|
|
18846
|
+
await connection.waitForConnection();
|
|
18876
18847
|
runner.retryRecoveredAgentMessages();
|
|
18877
18848
|
for (const record2 of agentEgress.retryableReliableRecords(connection.getServerCapabilities())) {
|
|
18878
18849
|
dispatchReliableRecord(record2);
|
|
@@ -19131,7 +19102,9 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
19131
19102
|
// fine.
|
|
19132
19103
|
...storageStatus === void 0 ? {} : { storage: storageStatus },
|
|
19133
19104
|
operationalHealth: operationalHealth.snapshot(),
|
|
19134
|
-
toolsets: toolsetRegistry.status()
|
|
19105
|
+
toolsets: toolsetRegistry.status(),
|
|
19106
|
+
// WP0: same counts `Daemon.status()` reports, from the same reader.
|
|
19107
|
+
agentHomeExecution: agentHomeExecutionStatus()
|
|
19135
19108
|
};
|
|
19136
19109
|
}
|
|
19137
19110
|
async function performControlShutdown(reason) {
|
|
@@ -19366,9 +19339,8 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
19366
19339
|
const minted = mintDeviceAssertion({
|
|
19367
19340
|
record,
|
|
19368
19341
|
// `toHttpBase` is the one place a configured serverUrl is normalized
|
|
19369
|
-
// (
|
|
19370
|
-
//
|
|
19371
|
-
// spelling of the same deployment produce the same issuer.
|
|
19342
|
+
// (path stripped), so the issuer ignores an operator-provided path
|
|
19343
|
+
// or query.
|
|
19372
19344
|
issuer: new URL(toHttpBase(config.serverUrl)).origin,
|
|
19373
19345
|
productId: config.productId,
|
|
19374
19346
|
audience: parsed.audience,
|
|
@@ -19483,23 +19455,33 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
19483
19455
|
await binding.lease.release();
|
|
19484
19456
|
}
|
|
19485
19457
|
}
|
|
19458
|
+
function agentHomeExecutionStatus() {
|
|
19459
|
+
const summary = agentHomeManager?.executionLeaseManager.activeAttemptSummary();
|
|
19460
|
+
return {
|
|
19461
|
+
maxConcurrentMutableSessionsPerAgentHome: agentHomeAttemptLimit,
|
|
19462
|
+
activeHomes: summary?.homes ?? 0,
|
|
19463
|
+
activeAttempts: summary?.attempts ?? 0
|
|
19464
|
+
};
|
|
19465
|
+
}
|
|
19486
19466
|
function status() {
|
|
19487
19467
|
return {
|
|
19488
19468
|
localAgentRelease,
|
|
19489
19469
|
paired: auth.deviceId !== void 0,
|
|
19490
19470
|
connected: connectionState === "open",
|
|
19491
|
-
degraded: connection?.isTransportDegraded() ?? false,
|
|
19492
19471
|
revoked: connection?.isRevoked() ?? auth.isRevoked(),
|
|
19493
19472
|
deviceId: auth.deviceId,
|
|
19494
19473
|
activeTaskCount: runner?.activeTaskCount ?? 0,
|
|
19495
19474
|
branding: config.branding,
|
|
19496
19475
|
operationalHealth: operationalHealth.snapshot(),
|
|
19497
19476
|
toolsets: toolsetRegistry.status(),
|
|
19498
|
-
egress: agentEgress.status()
|
|
19477
|
+
egress: agentEgress.status(),
|
|
19478
|
+
agentHomeExecution: agentHomeExecutionStatus()
|
|
19499
19479
|
};
|
|
19500
19480
|
}
|
|
19501
19481
|
function reloadMcpToolsets(mcpToolsets, expectedRevision) {
|
|
19502
|
-
|
|
19482
|
+
const receipt = toolsetRegistry.reload(mcpToolsets, expectedRevision);
|
|
19483
|
+
if (daemonStarted) connection?.refreshHello();
|
|
19484
|
+
return receipt;
|
|
19503
19485
|
}
|
|
19504
19486
|
function reportMcpToolsetObservation(toolsetId, expectedDefinitionRevision, observation) {
|
|
19505
19487
|
toolsetRegistry.report(toolsetId, expectedDefinitionRevision, observation);
|
|
@@ -19908,7 +19890,7 @@ function createServiceLifecycle(def, opts = {}) {
|
|
|
19908
19890
|
|
|
19909
19891
|
// src/bin/official-release.ts
|
|
19910
19892
|
var OFFICIAL_LOCAL_AGENT_RELEASE = resolveLocalAgentReleaseIdentity({
|
|
19911
|
-
version: "0.
|
|
19893
|
+
version: "0.13.0"
|
|
19912
19894
|
});
|
|
19913
19895
|
|
|
19914
19896
|
// src/bin/config.ts
|
|
@@ -21988,7 +21970,7 @@ async function recentAuditFacts(storeDir) {
|
|
|
21988
21970
|
}
|
|
21989
21971
|
function projectControl(control) {
|
|
21990
21972
|
if (control.status === "offline") return { status: "offline" };
|
|
21991
|
-
const transport = ["connecting", "open", "closed", "
|
|
21973
|
+
const transport = ["connecting", "open", "closed", "revoked"].includes(control.transport) ? control.transport : "unavailable";
|
|
21992
21974
|
const operationalHealth = control.operationalHealth.availability === "available" ? {
|
|
21993
21975
|
availability: "available",
|
|
21994
21976
|
state: control.operationalHealth.state,
|