@byok-sdk/client 0.12.0 → 0.14.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 +93 -0
- package/dist/adapters/claude/process-client.d.ts +24 -0
- package/dist/adapters/codex/process-runner.d.ts +46 -1
- package/dist/adapters/index.js +433 -33
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/pi/events.d.ts +1 -1
- package/dist/adapters/pi/pi-adapter.d.ts +3 -0
- package/dist/adapters/pi/rpc-client.d.ts +47 -1
- package/dist/adapters/pi/subagents-policy-extension.js +1 -1
- package/dist/adapters/pi/subagents-policy-extension.js.map +1 -1
- package/dist/adapters/pi/team-interaction-extension.d.ts +24 -0
- package/dist/adapters/pi/team-interaction-extension.js +81 -0
- package/dist/adapters/pi/team-interaction-extension.js.map +1 -0
- package/dist/adapters/process-tree.d.ts +74 -4
- package/dist/adapters/provider-credential-environment.d.ts +1 -1
- package/dist/adapters/win32-job-object.d.ts +101 -0
- 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 +9571 -8272
- package/dist/bin/byok-agent.js.map +1 -1
- package/dist/bin/byok-approval-mcp.js.map +1 -1
- package/dist/bin/commands/team-pi-relay.d.ts +24 -0
- package/dist/bin/commands/team-relay.d.ts +11 -0
- package/dist/bin/team-codex-relay.d.ts +32 -0
- package/dist/bin/team-notification-relay.d.ts +40 -0
- package/dist/bin/team-pi-session.d.ts +59 -0
- package/dist/daemon/agent-egress-policy.d.ts +8 -0
- package/dist/daemon/connection-manager.d.ts +66 -224
- package/dist/daemon/control-protocol.d.ts +10 -1
- package/dist/daemon/create-daemon.d.ts +58 -13
- package/dist/daemon/event-spill.d.ts +90 -0
- package/dist/daemon/journal/journal.d.ts +15 -3
- package/dist/daemon/journal/sqlite-journal.d.ts +7 -2
- package/dist/daemon/long-poll-transport.d.ts +28 -70
- package/dist/daemon/observer.d.ts +1 -1
- package/dist/daemon/task-runner.d.ts +31 -0
- package/dist/daemon/team-workspace.d.ts +9 -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 +1334 -696
- package/dist/index.js.map +1 -1
- package/package.json +9 -8
- package/dist/daemon/ws-transport.d.ts +0 -139
package/dist/index.js
CHANGED
|
@@ -2,20 +2,142 @@ 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,
|
|
5
|
+
import { AGENT_CONTENT_ARTIFACT_READ_CAPABILITY, AGENT_CONTENT_TRANSCRIPT_READ_CAPABILITY, AGENT_CONTENT_WORKSPACE_READ_CAPABILITY, AgentRefSchema, AgentHomeProjectionPayloadSchema, byokBlobUrlPath, BYOK_BLOBS_PATH, byokBlobFinalizePath, isTaskOfferType, 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, decodeEnvelope, createEnvelope, encodeEnvelope, PROTOCOL_VERSION, 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, isServerToDaemonType, 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, 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';
|
|
9
|
-
import { execFile, spawn } from 'child_process';
|
|
9
|
+
import { execFile, spawn, spawnSync } from 'child_process';
|
|
10
10
|
import { fileURLToPath } from 'url';
|
|
11
11
|
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
|
+
var __defProp = Object.defineProperty;
|
|
18
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
19
|
+
var __esm = (fn, res) => function __init() {
|
|
20
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
21
|
+
};
|
|
22
|
+
var __export = (target, all) => {
|
|
23
|
+
for (var name in all)
|
|
24
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// src/adapters/win32-job-object.ts
|
|
28
|
+
var win32_job_object_exports = {};
|
|
29
|
+
__export(win32_job_object_exports, {
|
|
30
|
+
Win32JobObjectFailure: () => Win32JobObjectFailure,
|
|
31
|
+
assignOwnedProcessToJob: () => assignOwnedProcessToJob,
|
|
32
|
+
isWin32JobObjectFailure: () => isWin32JobObjectFailure,
|
|
33
|
+
loadBindings: () => loadBindings
|
|
34
|
+
});
|
|
35
|
+
function isWin32JobObjectFailure(value) {
|
|
36
|
+
return value instanceof Error && value.name === "Win32JobObjectFailure";
|
|
37
|
+
}
|
|
38
|
+
function isNullHandle(value) {
|
|
39
|
+
return value === null || value === void 0 || value === 0n || value === 0;
|
|
40
|
+
}
|
|
41
|
+
async function loadBindings() {
|
|
42
|
+
if (cachedBindings !== void 0) return cachedBindings;
|
|
43
|
+
let koffi;
|
|
44
|
+
try {
|
|
45
|
+
const imported = await import('koffi');
|
|
46
|
+
koffi = imported.default ?? imported;
|
|
47
|
+
} catch (cause) {
|
|
48
|
+
throw new Win32JobObjectFailure({
|
|
49
|
+
step: "loadKoffi",
|
|
50
|
+
reason: "win32 runtime process ownership requires the koffi optional dependency, which could not be loaded"
|
|
51
|
+
}, { cause });
|
|
52
|
+
}
|
|
53
|
+
const kernel32 = koffi.load("kernel32.dll");
|
|
54
|
+
const PVOID = koffi.pointer("void");
|
|
55
|
+
const bind = (name, result, args) => kernel32.func("__stdcall", name, result, args);
|
|
56
|
+
cachedBindings = {
|
|
57
|
+
createJobObjectW: bind("CreateJobObjectW", PVOID, [PVOID, "str16"]),
|
|
58
|
+
setInformationJobObject: bind("SetInformationJobObject", "int", [PVOID, "int", PVOID, "uint32"]),
|
|
59
|
+
openProcess: bind("OpenProcess", PVOID, ["uint32", "int", "uint32"]),
|
|
60
|
+
assignProcessToJobObject: bind("AssignProcessToJobObject", "int", [PVOID, PVOID]),
|
|
61
|
+
closeHandle: bind("CloseHandle", "int", [PVOID]),
|
|
62
|
+
getLastError: bind("GetLastError", "uint32", [])
|
|
63
|
+
};
|
|
64
|
+
return cachedBindings;
|
|
65
|
+
}
|
|
66
|
+
function killOnCloseLimitInformation() {
|
|
67
|
+
const information = Buffer.alloc(JOBOBJECT_EXTENDED_LIMIT_SIZE);
|
|
68
|
+
information.writeUInt32LE(JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, JOBOBJECT_EXTENDED_LIMIT_FLAGS_OFFSET);
|
|
69
|
+
return information;
|
|
70
|
+
}
|
|
71
|
+
function daemonJob(bindings) {
|
|
72
|
+
const existing = jobsByBindings.get(bindings);
|
|
73
|
+
if (existing !== void 0) return existing;
|
|
74
|
+
const job = bindings.createJobObjectW(null, null);
|
|
75
|
+
if (isNullHandle(job)) {
|
|
76
|
+
throw new Win32JobObjectFailure({
|
|
77
|
+
step: "CreateJobObjectW",
|
|
78
|
+
reason: "win32 runtime process ownership could not create the daemon kill-on-close job object",
|
|
79
|
+
win32Code: bindings.getLastError()
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
const information = killOnCloseLimitInformation();
|
|
83
|
+
if (bindings.setInformationJobObject(job, JobObjectExtendedLimitInformation, information, information.length) === 0) {
|
|
84
|
+
const win32Code = bindings.getLastError();
|
|
85
|
+
bindings.closeHandle(job);
|
|
86
|
+
throw new Win32JobObjectFailure({
|
|
87
|
+
step: "SetInformationJobObject",
|
|
88
|
+
reason: "win32 runtime process ownership could not make the daemon job object kill-on-close",
|
|
89
|
+
win32Code
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
jobsByBindings.set(bindings, job);
|
|
93
|
+
return job;
|
|
94
|
+
}
|
|
95
|
+
async function assignOwnedProcessToJob(pid, options) {
|
|
96
|
+
const bindings = options?.bindings ?? await loadBindings();
|
|
97
|
+
const job = daemonJob(bindings);
|
|
98
|
+
const processHandle = bindings.openProcess(OWNED_PROCESS_ACCESS, 0, pid);
|
|
99
|
+
if (isNullHandle(processHandle)) {
|
|
100
|
+
throw new Win32JobObjectFailure({
|
|
101
|
+
step: "OpenProcess",
|
|
102
|
+
reason: `win32 runtime process ownership could not open process ${pid} for job assignment`,
|
|
103
|
+
win32Code: bindings.getLastError()
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
if (bindings.assignProcessToJobObject(job, processHandle) === 0) {
|
|
108
|
+
throw new Win32JobObjectFailure({
|
|
109
|
+
step: "AssignProcessToJobObject",
|
|
110
|
+
reason: `win32 runtime process ownership could not assign process ${pid} to the daemon kill-on-close job object`,
|
|
111
|
+
win32Code: bindings.getLastError()
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
} finally {
|
|
115
|
+
bindings.closeHandle(processHandle);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
var JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, JobObjectExtendedLimitInformation, JOBOBJECT_EXTENDED_LIMIT_SIZE, JOBOBJECT_EXTENDED_LIMIT_FLAGS_OFFSET, PROCESS_SET_QUOTA, PROCESS_TERMINATE, OWNED_PROCESS_ACCESS, Win32JobObjectFailure, cachedBindings, jobsByBindings;
|
|
119
|
+
var init_win32_job_object = __esm({
|
|
120
|
+
"src/adapters/win32-job-object.ts"() {
|
|
121
|
+
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 8192;
|
|
122
|
+
JobObjectExtendedLimitInformation = 9;
|
|
123
|
+
JOBOBJECT_EXTENDED_LIMIT_SIZE = 144;
|
|
124
|
+
JOBOBJECT_EXTENDED_LIMIT_FLAGS_OFFSET = 16;
|
|
125
|
+
PROCESS_SET_QUOTA = 256;
|
|
126
|
+
PROCESS_TERMINATE = 1;
|
|
127
|
+
OWNED_PROCESS_ACCESS = PROCESS_SET_QUOTA | PROCESS_TERMINATE;
|
|
128
|
+
Win32JobObjectFailure = class extends Error {
|
|
129
|
+
step;
|
|
130
|
+
win32Code;
|
|
131
|
+
constructor(input, options) {
|
|
132
|
+
super(input.reason, options);
|
|
133
|
+
this.name = "Win32JobObjectFailure";
|
|
134
|
+
this.step = input.step;
|
|
135
|
+
this.win32Code = input.win32Code;
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
jobsByBindings = /* @__PURE__ */ new WeakMap();
|
|
139
|
+
}
|
|
140
|
+
});
|
|
19
141
|
var tmpSeq = 0;
|
|
20
142
|
async function atomicWriteFile(filePath, data, options = {}) {
|
|
21
143
|
const tmpPath = `${filePath}.${process.pid}-${tmpSeq++}.tmp`;
|
|
@@ -423,6 +545,23 @@ async function resolveExistingAncestor(inputPath) {
|
|
|
423
545
|
}
|
|
424
546
|
}
|
|
425
547
|
}
|
|
548
|
+
async function canonicalPath(inputPath) {
|
|
549
|
+
const { canonical: canonical2, tail } = await resolveExistingAncestor(inputPath);
|
|
550
|
+
return path3__default.resolve(canonical2, ...tail);
|
|
551
|
+
}
|
|
552
|
+
async function assertRealDirectoryIfPresent(target) {
|
|
553
|
+
let stat;
|
|
554
|
+
try {
|
|
555
|
+
stat = await promises.lstat(target);
|
|
556
|
+
} catch (error) {
|
|
557
|
+
const code = error.code;
|
|
558
|
+
if (code === "ENOENT" || code === "ENOTDIR") return;
|
|
559
|
+
throw error;
|
|
560
|
+
}
|
|
561
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
562
|
+
throw new AgentHomeResolutionError(`Agent home path component is not a real directory: ${target}`);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
426
565
|
async function materializeDirectory(inputPath) {
|
|
427
566
|
const { canonical: canonical2, tail } = await resolveExistingAncestor(inputPath);
|
|
428
567
|
let cursor = canonical2;
|
|
@@ -517,6 +656,39 @@ var AgentHomeLayout = class {
|
|
|
517
656
|
await gate.release();
|
|
518
657
|
}
|
|
519
658
|
}
|
|
659
|
+
/**
|
|
660
|
+
* Pure canonical-home derivation for read-only callers, such as the
|
|
661
|
+
* pre-admission single-writer count. It validates the AgentRef and joins
|
|
662
|
+
* exactly the same `<hostStorageRoot>/agents/<agentId>` segments
|
|
663
|
+
* {@link AgentHomeLayout.resolve} would, canonicalizing only the components
|
|
664
|
+
* that already exist.
|
|
665
|
+
*
|
|
666
|
+
* It deliberately creates no directory, takes no cross-process mutation
|
|
667
|
+
* gate and records no Agent binding, so an offer the host vetoes after the
|
|
668
|
+
* count leaves nothing behind on disk. `resolve()` stays the only path that
|
|
669
|
+
* may materialize a home or bind it to an Agent identity.
|
|
670
|
+
*
|
|
671
|
+
* An `agents` root or `agents/<agentId>` leaf that already exists but is a
|
|
672
|
+
* symlink (or any non-directory) is rejected here with the same error class
|
|
673
|
+
* and message `resolve()` raises for it, so an in-root `two -> one` link
|
|
674
|
+
* fails closed instead of silently keying the count of `one`. A leaf that
|
|
675
|
+
* does not exist yet is not an error: this derivation runs before the home
|
|
676
|
+
* is materialized. A home canonicalizing outside the `agents` root stays
|
|
677
|
+
* rejected as before.
|
|
678
|
+
*/
|
|
679
|
+
async canonicalHomePath(agentRefInput) {
|
|
680
|
+
const agentRef = validateAgentRef(agentRefInput);
|
|
681
|
+
const hostStorageRoot = this.canonicalRoot ?? await canonicalPath(this.hostStorageRootInput);
|
|
682
|
+
const agentsRoot = path3__default.join(hostStorageRoot, AGENT_HOME_DIRECTORY);
|
|
683
|
+
await assertRealDirectoryIfPresent(agentsRoot);
|
|
684
|
+
const lexicalHome = path3__default.join(agentsRoot, agentRef.agentId);
|
|
685
|
+
await assertRealDirectoryIfPresent(lexicalHome);
|
|
686
|
+
const canonicalHome = await canonicalPath(lexicalHome);
|
|
687
|
+
if (canonicalHome === agentsRoot || !isWithin(agentsRoot, canonicalHome)) {
|
|
688
|
+
throw new AgentHomeResolutionError(`Agent home resolves outside the Agent home root: ${canonicalHome}`);
|
|
689
|
+
}
|
|
690
|
+
return canonicalHome;
|
|
691
|
+
}
|
|
520
692
|
/**
|
|
521
693
|
* Prove the canonical root is materializable and writable before the daemon
|
|
522
694
|
* advertises Agent-home capability. No Agent identity or persistent Agent
|
|
@@ -833,6 +1005,44 @@ var AgentHomeExecutionLeaseManager = class _AgentHomeExecutionLeaseManager {
|
|
|
833
1005
|
});
|
|
834
1006
|
});
|
|
835
1007
|
}
|
|
1008
|
+
/**
|
|
1009
|
+
* WP0: Attempts currently holding an execution lease on this exact
|
|
1010
|
+
* canonical home, across every lane and every session. This is the number
|
|
1011
|
+
* the daemon's admission gate reads before any side effect — see
|
|
1012
|
+
* `TaskRunner.handleOffer`'s per-home busy gate.
|
|
1013
|
+
*
|
|
1014
|
+
* Derived from the one lease registry above rather than a second tally, so
|
|
1015
|
+
* it inherits the lease lifecycle exactly: an entry appears at `acquire()`,
|
|
1016
|
+
* survives `bindSession()` (which rekeys in place), and disappears only at
|
|
1017
|
+
* `release()`, which the task runner calls after the attempt is terminal
|
|
1018
|
+
* AND `Session.close()` resolved. A failed disposal never reaches
|
|
1019
|
+
* `release()`, so the slot stays held — fail closed, the same posture as
|
|
1020
|
+
* `runtime-disposal-failed`. Crash residue needs nothing extra here: a
|
|
1021
|
+
* restarted daemon starts with an empty registry and reclaims the on-disk
|
|
1022
|
+
* marker only under the same stable owner identity (`openLeaseMarker`).
|
|
1023
|
+
*
|
|
1024
|
+
* Counted regardless of which lease manager owns the group: the invariant
|
|
1025
|
+
* being protected is the filesystem path (`MEMORY.md`, `notes/`, `.git`),
|
|
1026
|
+
* not the owner identity.
|
|
1027
|
+
*/
|
|
1028
|
+
activeAttemptCount(canonicalHome) {
|
|
1029
|
+
return _AgentHomeExecutionLeaseManager.groups.get(canonicalHome)?.leasesByKey.size ?? 0;
|
|
1030
|
+
}
|
|
1031
|
+
/**
|
|
1032
|
+
* Counts-only readback for daemon/control status. Scoped to this manager's
|
|
1033
|
+
* own leases, so the number describes this daemon rather than every home
|
|
1034
|
+
* any manager in the process happens to hold. Never exposes a home path.
|
|
1035
|
+
*/
|
|
1036
|
+
activeAttemptSummary() {
|
|
1037
|
+
let homes = 0;
|
|
1038
|
+
let attempts = 0;
|
|
1039
|
+
for (const group of _AgentHomeExecutionLeaseManager.groups.values()) {
|
|
1040
|
+
if (group.manager !== this.manager) continue;
|
|
1041
|
+
homes += 1;
|
|
1042
|
+
attempts += group.leasesByKey.size;
|
|
1043
|
+
}
|
|
1044
|
+
return { homes, attempts };
|
|
1045
|
+
}
|
|
836
1046
|
async mutate(binding, operation) {
|
|
837
1047
|
return this.exclusive(binding.resolution.canonicalHome, async () => {
|
|
838
1048
|
const group = _AgentHomeExecutionLeaseManager.groups.get(binding.resolution.canonicalHome);
|
|
@@ -2036,13 +2246,22 @@ function freezeRuntimeAdapterDescriptor(descriptor) {
|
|
|
2036
2246
|
});
|
|
2037
2247
|
}
|
|
2038
2248
|
function sealRuntimeOperationManifest(manifest) {
|
|
2249
|
+
const dispatchSelection = manifest.dispatchSelection === void 0 ? void 0 : manifest.dispatchSelection.lane === "byok-profile" ? Object.freeze({
|
|
2250
|
+
...manifest.dispatchSelection,
|
|
2251
|
+
providerProfile: Object.freeze({
|
|
2252
|
+
...manifest.dispatchSelection.providerProfile,
|
|
2253
|
+
requiredCapabilities: Object.freeze([
|
|
2254
|
+
...manifest.dispatchSelection.providerProfile.requiredCapabilities
|
|
2255
|
+
])
|
|
2256
|
+
})
|
|
2257
|
+
}) : Object.freeze({ ...manifest.dispatchSelection });
|
|
2039
2258
|
return Object.freeze({
|
|
2040
2259
|
taskId: manifest.taskId,
|
|
2041
2260
|
runtimeId: manifest.runtimeId,
|
|
2042
2261
|
descriptor: freezeRuntimeAdapterDescriptor(manifest.descriptor),
|
|
2043
2262
|
policy: frozenPolicy(manifest.policy),
|
|
2044
2263
|
requiredToolsetIds: Object.freeze([...manifest.requiredToolsetIds]),
|
|
2045
|
-
...
|
|
2264
|
+
...dispatchSelection === void 0 ? {} : { dispatchSelection },
|
|
2046
2265
|
...manifest.sessionRef === void 0 ? {} : { sessionRef: manifest.sessionRef },
|
|
2047
2266
|
...manifest.agentRef === void 0 ? {} : { agentRef: Object.freeze({ agentId: manifest.agentRef.agentId, profileRevision: manifest.agentRef.profileRevision }) },
|
|
2048
2267
|
cwd: manifest.cwd ?? manifest.workspace.workspaceDir,
|
|
@@ -2282,6 +2501,9 @@ function parseTeamMessageReadParams(value) {
|
|
|
2282
2501
|
if (!isRecord2(value) || !exactKeys(value, ["context", "afterSeq"]) || !contextString(value.context) || value.afterSeq !== void 0 && !safeInteger(value.afterSeq)) return void 0;
|
|
2283
2502
|
return { context: value.context, ...value.afterSeq === void 0 ? {} : { afterSeq: value.afterSeq } };
|
|
2284
2503
|
}
|
|
2504
|
+
function parseTeamNotificationSnapshotParams(value) {
|
|
2505
|
+
return parseTeamMessageReadParams(value);
|
|
2506
|
+
}
|
|
2285
2507
|
function parseTeamMessageAckParams(value) {
|
|
2286
2508
|
if (!isRecord2(value) || !exactKeys(value, ["context", "throughSeq"]) || !contextString(value.context) || !safeInteger(value.throughSeq)) return void 0;
|
|
2287
2509
|
return { context: value.context, throughSeq: value.throughSeq };
|
|
@@ -3942,6 +4164,55 @@ function withOwnedProcessTree(options) {
|
|
|
3942
4164
|
...process.platform === "win32" ? { windowsHide: true } : { detached: true }
|
|
3943
4165
|
};
|
|
3944
4166
|
}
|
|
4167
|
+
var hostExitTargets = /* @__PURE__ */ new Map();
|
|
4168
|
+
var hostExitHandlerInstalled = false;
|
|
4169
|
+
function terminateOwnedTreesForHostExit() {
|
|
4170
|
+
for (const target of hostExitTargets.values()) {
|
|
4171
|
+
try {
|
|
4172
|
+
if (target.platform === "win32") {
|
|
4173
|
+
target.spawnSyncFn("taskkill", ["/PID", String(target.pid), "/T", "/F"], {
|
|
4174
|
+
stdio: "ignore",
|
|
4175
|
+
windowsHide: true
|
|
4176
|
+
});
|
|
4177
|
+
} else {
|
|
4178
|
+
target.kill(-target.pid, "SIGKILL");
|
|
4179
|
+
}
|
|
4180
|
+
} catch {
|
|
4181
|
+
}
|
|
4182
|
+
}
|
|
4183
|
+
hostExitTargets.clear();
|
|
4184
|
+
}
|
|
4185
|
+
function ensureHostExitHandler() {
|
|
4186
|
+
if (hostExitHandlerInstalled) return;
|
|
4187
|
+
hostExitHandlerInstalled = true;
|
|
4188
|
+
process.prependListener("exit", terminateOwnedTreesForHostExit);
|
|
4189
|
+
}
|
|
4190
|
+
var defaultJobObject = {
|
|
4191
|
+
async assign(pid) {
|
|
4192
|
+
const { assignOwnedProcessToJob: assignOwnedProcessToJob2 } = await Promise.resolve().then(() => (init_win32_job_object(), win32_job_object_exports));
|
|
4193
|
+
await assignOwnedProcessToJob2(pid);
|
|
4194
|
+
}
|
|
4195
|
+
};
|
|
4196
|
+
async function adoptOwnedProcessTree(options) {
|
|
4197
|
+
const pid = positivePid(options.child, options.label);
|
|
4198
|
+
if (pid === void 0) return;
|
|
4199
|
+
const platform = options.platform ?? process.platform;
|
|
4200
|
+
ensureHostExitHandler();
|
|
4201
|
+
hostExitTargets.set(options.child, {
|
|
4202
|
+
pid,
|
|
4203
|
+
label: options.label,
|
|
4204
|
+
platform,
|
|
4205
|
+
kill: options.killFn ?? defaultKill,
|
|
4206
|
+
spawnSyncFn: options.spawnSyncFn ?? spawnSync
|
|
4207
|
+
});
|
|
4208
|
+
const forget = () => {
|
|
4209
|
+
hostExitTargets.delete(options.child);
|
|
4210
|
+
};
|
|
4211
|
+
options.child.once("close", forget);
|
|
4212
|
+
options.child.once("exit", forget);
|
|
4213
|
+
if (platform !== "win32") return;
|
|
4214
|
+
await (options.jobObject ?? defaultJobObject).assign(pid);
|
|
4215
|
+
}
|
|
3945
4216
|
function positivePid(child, label) {
|
|
3946
4217
|
const pid = child.pid;
|
|
3947
4218
|
if (pid === void 0) return void 0;
|
|
@@ -4153,21 +4424,8 @@ async function disposeOwnedProcessTree(options) {
|
|
|
4153
4424
|
var STDERR_RING_CAPACITY = 20;
|
|
4154
4425
|
var DIALOG_UI_METHODS = /* @__PURE__ */ new Set(["select", "confirm", "input", "editor"]);
|
|
4155
4426
|
var PiRpcClient = class {
|
|
4156
|
-
child;
|
|
4157
|
-
buffer = "";
|
|
4158
|
-
nextId = 1;
|
|
4159
|
-
pending = /* @__PURE__ */ new Map();
|
|
4160
|
-
eventQueue = new AsyncQueue();
|
|
4161
|
-
closed = false;
|
|
4162
|
-
exitError;
|
|
4163
|
-
closedPromise;
|
|
4164
|
-
resolveClosed;
|
|
4165
|
-
disposalAttempt;
|
|
4166
|
-
/** Bounded tail of recent stderr lines — pi discarded this entirely before (nothing ever read `child.stderr`), which is exactly why finding #1 (`Error: Unknown option: --session-id`, exit 1) had to be root-caused by hand instead of reading it off a thrown error. See `buildExitError`. */
|
|
4167
|
-
stderrRing = [];
|
|
4168
|
-
/** Count of pi RPC message types `PiSession` (pi-adapter.ts) has told us have no `AgentEvent` mapping and aren't routine bookkeeping — see `recordUnmappedFrame`. */
|
|
4169
|
-
unmappedFrameCounts = /* @__PURE__ */ new Map();
|
|
4170
4427
|
constructor(options) {
|
|
4428
|
+
this.options = options;
|
|
4171
4429
|
const spawnFn = options.spawnFn ?? spawn;
|
|
4172
4430
|
this.child = spawnFn(options.command, options.args, withOwnedProcessTree({
|
|
4173
4431
|
cwd: options.cwd,
|
|
@@ -4177,6 +4435,9 @@ var PiRpcClient = class {
|
|
|
4177
4435
|
this.closedPromise = new Promise((resolve) => {
|
|
4178
4436
|
this.resolveClosed = resolve;
|
|
4179
4437
|
});
|
|
4438
|
+
this.adopted = this.adoptOwnedTree(options);
|
|
4439
|
+
this.adopted.catch(() => {
|
|
4440
|
+
});
|
|
4180
4441
|
this.child.stdout.setEncoding("utf8");
|
|
4181
4442
|
this.child.stdout.on("data", (chunk) => this.onData(chunk));
|
|
4182
4443
|
this.child.stderr.setEncoding("utf8");
|
|
@@ -4188,10 +4449,37 @@ var PiRpcClient = class {
|
|
|
4188
4449
|
this.onClosed(err);
|
|
4189
4450
|
});
|
|
4190
4451
|
}
|
|
4191
|
-
|
|
4192
|
-
|
|
4452
|
+
options;
|
|
4453
|
+
child;
|
|
4454
|
+
buffer = "";
|
|
4455
|
+
nextId = 1;
|
|
4456
|
+
pending = /* @__PURE__ */ new Map();
|
|
4457
|
+
eventQueue = new AsyncQueue();
|
|
4458
|
+
closed = false;
|
|
4459
|
+
exitError;
|
|
4460
|
+
closedPromise;
|
|
4461
|
+
resolveClosed;
|
|
4462
|
+
disposalAttempt;
|
|
4463
|
+
/** Resolves once this tree is backstopped (see `adoptOwnedProcessTree`); rejects with the adoption failure, having already terminated the tree. */
|
|
4464
|
+
adopted;
|
|
4465
|
+
/** Set before the fail-closed termination starts, so it — not the exit code of the kill we ourselves requested — becomes this client's exit error. */
|
|
4466
|
+
adoptionFailure;
|
|
4467
|
+
/** Bounded tail of recent stderr lines — pi discarded this entirely before (nothing ever read `child.stderr`), which is exactly why finding #1 (`Error: Unknown option: --session-id`, exit 1) had to be root-caused by hand instead of reading it off a thrown error. See `buildExitError`. */
|
|
4468
|
+
stderrRing = [];
|
|
4469
|
+
/** Count of pi RPC message types `PiSession` (pi-adapter.ts) has told us have no `AgentEvent` mapping and aren't routine bookkeeping — see `recordUnmappedFrame`. */
|
|
4470
|
+
unmappedFrameCounts = /* @__PURE__ */ new Map();
|
|
4471
|
+
/**
|
|
4472
|
+
* Send a command, resolved with its correlated `response` message.
|
|
4473
|
+
*
|
|
4474
|
+
* Adoption is awaited FIRST — this is `PiAdapter.start()`'s first awaited
|
|
4475
|
+
* operation, so an unbackstopped tree never receives the initial prompt and
|
|
4476
|
+
* never yields a session. The wait costs one settled-promise turn once the
|
|
4477
|
+
* tree is adopted; every later command sees it already resolved.
|
|
4478
|
+
*/
|
|
4479
|
+
async send(command) {
|
|
4480
|
+
await this.adopted;
|
|
4193
4481
|
if (this.closed) {
|
|
4194
|
-
|
|
4482
|
+
throw this.exitError ?? new Error("pi process is closed");
|
|
4195
4483
|
}
|
|
4196
4484
|
const id = command.id ?? `req-${this.nextId++}`;
|
|
4197
4485
|
const full = { ...command, id };
|
|
@@ -4264,6 +4552,32 @@ var PiRpcClient = class {
|
|
|
4264
4552
|
label: "pi"
|
|
4265
4553
|
};
|
|
4266
4554
|
}
|
|
4555
|
+
/**
|
|
4556
|
+
* Backstop this tree, or tear it down. Adoption failure is a start-time
|
|
4557
|
+
* precondition, not a degraded mode: the child is terminated through the one
|
|
4558
|
+
* disposal authority and the failure is re-thrown, which is what makes the
|
|
4559
|
+
* first `send()` — and therefore `PiAdapter.start()` — fail before any
|
|
4560
|
+
* session is published. Both cleanup attempts are best-effort because the
|
|
4561
|
+
* adoption failure, not a terminator's own complaint, is the reason to report.
|
|
4562
|
+
*/
|
|
4563
|
+
async adoptOwnedTree(options) {
|
|
4564
|
+
try {
|
|
4565
|
+
await adoptOwnedProcessTree({
|
|
4566
|
+
child: this.child,
|
|
4567
|
+
label: "pi",
|
|
4568
|
+
platform: options.platform,
|
|
4569
|
+
jobObject: options.jobObject
|
|
4570
|
+
});
|
|
4571
|
+
} catch (cause) {
|
|
4572
|
+
const failure = cause instanceof Error ? cause : new Error(String(cause));
|
|
4573
|
+
this.adoptionFailure = failure;
|
|
4574
|
+
await requestOwnedProcessTreeTermination(this.processTreeOptions()).catch(() => {
|
|
4575
|
+
});
|
|
4576
|
+
await disposeOwnedProcessTree(this.processTreeOptions()).catch(() => {
|
|
4577
|
+
});
|
|
4578
|
+
throw failure;
|
|
4579
|
+
}
|
|
4580
|
+
}
|
|
4267
4581
|
onData(chunk) {
|
|
4268
4582
|
this.buffer += chunk;
|
|
4269
4583
|
let newlineIndex = this.buffer.indexOf("\n");
|
|
@@ -4282,6 +4596,7 @@ var PiRpcClient = class {
|
|
|
4282
4596
|
} catch {
|
|
4283
4597
|
return;
|
|
4284
4598
|
}
|
|
4599
|
+
this.options.onFrame?.(msg);
|
|
4285
4600
|
if (msg.type === "response" && typeof msg.id === "string" && this.pending.has(msg.id)) {
|
|
4286
4601
|
const waiter = this.pending.get(msg.id);
|
|
4287
4602
|
this.pending.delete(msg.id);
|
|
@@ -4289,11 +4604,20 @@ var PiRpcClient = class {
|
|
|
4289
4604
|
return;
|
|
4290
4605
|
}
|
|
4291
4606
|
if (msg.type === "extension_ui_request" && typeof msg.id === "string" && typeof msg.method === "string") {
|
|
4292
|
-
this.
|
|
4607
|
+
if (this.options.extensionUi?.mode === "hold") this.options.extensionUi.onRequest(msg);
|
|
4608
|
+
else this.respondToExtensionUiRequest(msg.id, msg.method);
|
|
4293
4609
|
return;
|
|
4294
4610
|
}
|
|
4295
4611
|
this.eventQueue.push(msg);
|
|
4296
4612
|
}
|
|
4613
|
+
/** Write an explicit host-owned response; completion is a transport receipt only. */
|
|
4614
|
+
async respondExtensionUi(response) {
|
|
4615
|
+
if (this.options.extensionUi?.mode !== "hold") throw new Error("Pi interactions are not host-owned");
|
|
4616
|
+
await this.adopted;
|
|
4617
|
+
if (this.closed) throw new Error("Pi process is closed");
|
|
4618
|
+
await new Promise((resolve, reject) => this.child.stdin.write(`${JSON.stringify({ type: "extension_ui_response", ...response })}
|
|
4619
|
+
`, (error) => error ? reject(error) : resolve()));
|
|
4620
|
+
}
|
|
4297
4621
|
/**
|
|
4298
4622
|
* Answer pi's extension-UI blocking protocol headlessly (rpc.md's
|
|
4299
4623
|
* "Extension UI Protocol"). Fail-closed policy, stated explicitly because
|
|
@@ -4347,9 +4671,10 @@ var PiRpcClient = class {
|
|
|
4347
4671
|
onClosed(err) {
|
|
4348
4672
|
if (this.closed) return;
|
|
4349
4673
|
this.closed = true;
|
|
4350
|
-
this.
|
|
4674
|
+
const terminal = this.adoptionFailure ?? err;
|
|
4675
|
+
this.exitError = terminal;
|
|
4351
4676
|
this.resolveClosed();
|
|
4352
|
-
for (const [, waiter] of this.pending) waiter.reject(
|
|
4677
|
+
for (const [, waiter] of this.pending) waiter.reject(terminal);
|
|
4353
4678
|
this.pending.clear();
|
|
4354
4679
|
this.eventQueue.end();
|
|
4355
4680
|
}
|
|
@@ -4383,6 +4708,8 @@ var PROVIDER_CREDENTIAL_ENV_DENY_NAMES = [
|
|
|
4383
4708
|
"TOGETHER_API_KEY",
|
|
4384
4709
|
"BASETEN_API_KEY",
|
|
4385
4710
|
"KIMI_API_KEY",
|
|
4711
|
+
"HF_TOKEN",
|
|
4712
|
+
"MOONSHOT_API_KEY",
|
|
4386
4713
|
"MINIMAX_API_KEY",
|
|
4387
4714
|
"MINIMAX_CN_API_KEY",
|
|
4388
4715
|
"QWEN_TOKEN_PLAN_API_KEY",
|
|
@@ -4464,7 +4791,11 @@ function validatePiByokLauncherConfig(launcher) {
|
|
|
4464
4791
|
"--macos-keychain-path",
|
|
4465
4792
|
"--secret-service-prefix",
|
|
4466
4793
|
"--provider",
|
|
4467
|
-
"--model"
|
|
4794
|
+
"--model",
|
|
4795
|
+
"--profile-revision",
|
|
4796
|
+
"--profile-hash",
|
|
4797
|
+
"--required-capabilities",
|
|
4798
|
+
"--validate-only"
|
|
4468
4799
|
]);
|
|
4469
4800
|
const conflicting = launcher.args?.find((arg) => reserved.has(arg));
|
|
4470
4801
|
if (conflicting !== void 0) {
|
|
@@ -4526,11 +4857,17 @@ var PiAdapter = class {
|
|
|
4526
4857
|
const bin = this.resolveBin();
|
|
4527
4858
|
const extensions = (this.options.resolveExtensions ?? resolvePiExtensions)();
|
|
4528
4859
|
const selection = input.offer.dispatchSelection;
|
|
4529
|
-
const pinnedSelection = selection === void 0 ? void 0 : Object.freeze({
|
|
4860
|
+
const pinnedSelection = selection === void 0 ? void 0 : selection.lane === "byok-profile" ? Object.freeze({
|
|
4861
|
+
...selection,
|
|
4862
|
+
providerProfile: Object.freeze({
|
|
4863
|
+
...selection.providerProfile,
|
|
4864
|
+
requiredCapabilities: Object.freeze([...selection.providerProfile.requiredCapabilities])
|
|
4865
|
+
})
|
|
4866
|
+
}) : Object.freeze({ ...selection });
|
|
4530
4867
|
let command = bin.command;
|
|
4531
4868
|
let launcherArgs;
|
|
4532
4869
|
if (pinnedSelection !== void 0) {
|
|
4533
|
-
if (pinnedSelection.lane !== "byok" || pinnedSelection.runtimeId !== "pi") {
|
|
4870
|
+
if (pinnedSelection.lane !== "byok" && pinnedSelection.lane !== "byok-profile" || pinnedSelection.runtimeId !== "pi") {
|
|
4534
4871
|
return { kind: "reject", reason: `pi adapter cannot execute ${pinnedSelection.lane} selection for runtime ${pinnedSelection.runtimeId}`, retryable: false };
|
|
4535
4872
|
}
|
|
4536
4873
|
const launcher = this.options.byokLauncher;
|
|
@@ -4538,6 +4875,21 @@ var PiAdapter = class {
|
|
|
4538
4875
|
return { kind: "reject", reason: "pi BYOK selection requires a configured credential-custody launcher", retryable: false };
|
|
4539
4876
|
}
|
|
4540
4877
|
command = launcher.command;
|
|
4878
|
+
const providerProfile = pinnedSelection.lane === "byok-profile" ? pinnedSelection.providerProfile : void 0;
|
|
4879
|
+
if (providerProfile !== void 0) {
|
|
4880
|
+
try {
|
|
4881
|
+
await (this.options.validateProviderProfileBinding ?? validateProviderProfileBindingWithLauncher)(
|
|
4882
|
+
providerProfile,
|
|
4883
|
+
launcher
|
|
4884
|
+
);
|
|
4885
|
+
} catch (error) {
|
|
4886
|
+
return {
|
|
4887
|
+
kind: "reject",
|
|
4888
|
+
reason: `provider profile admission failed: ${errorMessage3(error)}`,
|
|
4889
|
+
retryable: false
|
|
4890
|
+
};
|
|
4891
|
+
}
|
|
4892
|
+
}
|
|
4541
4893
|
launcherArgs = [
|
|
4542
4894
|
...launcher.args ?? [],
|
|
4543
4895
|
"--pi-bin",
|
|
@@ -4549,9 +4901,19 @@ var PiAdapter = class {
|
|
|
4549
4901
|
...launcher.macosKeychainPath !== void 0 ? ["--macos-keychain-path", launcher.macosKeychainPath] : [],
|
|
4550
4902
|
...launcher.secretServicePrefix ? ["--secret-service-prefix", launcher.secretServicePrefix] : [],
|
|
4551
4903
|
"--provider",
|
|
4552
|
-
pinnedSelection.providerId,
|
|
4904
|
+
providerProfile?.profileRef ?? (pinnedSelection.lane === "byok" ? pinnedSelection.providerId : ""),
|
|
4553
4905
|
"--model",
|
|
4554
|
-
pinnedSelection.modelId
|
|
4906
|
+
providerProfile?.modelId ?? (pinnedSelection.lane === "byok" ? pinnedSelection.modelId : ""),
|
|
4907
|
+
...providerProfile === void 0 ? [] : [
|
|
4908
|
+
"--profile-revision",
|
|
4909
|
+
providerProfile.profileRevision,
|
|
4910
|
+
"--profile-hash",
|
|
4911
|
+
providerProfile.profileHash,
|
|
4912
|
+
"--required-capabilities",
|
|
4913
|
+
JSON.stringify(providerProfile.requiredCapabilities),
|
|
4914
|
+
"--validate-only",
|
|
4915
|
+
"false"
|
|
4916
|
+
]
|
|
4555
4917
|
];
|
|
4556
4918
|
}
|
|
4557
4919
|
return {
|
|
@@ -4694,8 +5056,42 @@ var PiAdapter = class {
|
|
|
4694
5056
|
return (this.options.resolveBin ?? resolvePiBin)();
|
|
4695
5057
|
}
|
|
4696
5058
|
};
|
|
5059
|
+
async function validateProviderProfileBindingWithLauncher(binding, launcher) {
|
|
5060
|
+
await execFileAsync(launcher.command, [
|
|
5061
|
+
...launcher.args ?? [],
|
|
5062
|
+
"--pi-bin",
|
|
5063
|
+
process.execPath,
|
|
5064
|
+
"--profile-db",
|
|
5065
|
+
launcher.profileDbPath,
|
|
5066
|
+
"--session-dir",
|
|
5067
|
+
launcher.sessionDir,
|
|
5068
|
+
"--provider",
|
|
5069
|
+
binding.profileRef,
|
|
5070
|
+
"--model",
|
|
5071
|
+
binding.modelId,
|
|
5072
|
+
"--profile-revision",
|
|
5073
|
+
binding.profileRevision,
|
|
5074
|
+
"--profile-hash",
|
|
5075
|
+
binding.profileHash,
|
|
5076
|
+
"--required-capabilities",
|
|
5077
|
+
JSON.stringify(binding.requiredCapabilities),
|
|
5078
|
+
"--validate-only",
|
|
5079
|
+
"true",
|
|
5080
|
+
...launcher.macosKeychainPath !== void 0 ? ["--macos-keychain-path", launcher.macosKeychainPath] : [],
|
|
5081
|
+
...launcher.secretServicePrefix ? ["--secret-service-prefix", launcher.secretServicePrefix] : []
|
|
5082
|
+
], { timeout: DETECT_TIMEOUT_MS });
|
|
5083
|
+
}
|
|
4697
5084
|
function sameDispatchSelection(left, right) {
|
|
4698
5085
|
if (left === void 0 || right === void 0) return left === right;
|
|
5086
|
+
if (left.lane === "byok-profile" || right.lane === "byok-profile") {
|
|
5087
|
+
if (left.lane !== "byok-profile" || right.lane !== "byok-profile") return false;
|
|
5088
|
+
if (left.runtimeId !== right.runtimeId) return false;
|
|
5089
|
+
const leftProfile = left.providerProfile;
|
|
5090
|
+
const rightProfile = right.providerProfile;
|
|
5091
|
+
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(
|
|
5092
|
+
(capability, index) => capability === rightProfile.requiredCapabilities[index]
|
|
5093
|
+
);
|
|
5094
|
+
}
|
|
4699
5095
|
return left.lane === right.lane && left.runtimeId === right.runtimeId && left.providerId === right.providerId && left.modelId === right.modelId;
|
|
4700
5096
|
}
|
|
4701
5097
|
async function resolveAuthoritativeSessionId(rpc) {
|
|
@@ -5135,6 +5531,10 @@ var ClaudeProcessClient = class {
|
|
|
5135
5531
|
closedPromise;
|
|
5136
5532
|
resolveClosed;
|
|
5137
5533
|
disposalAttempt;
|
|
5534
|
+
/** Resolves once this tree is backstopped (see `adoptOwnedProcessTree`); rejects with the adoption failure, having already terminated the tree. */
|
|
5535
|
+
adopted;
|
|
5536
|
+
/** Set before the fail-closed termination starts, so it — not the exit code of the kill we ourselves requested — becomes this client's exit error. */
|
|
5537
|
+
adoptionFailure;
|
|
5138
5538
|
stderrRing = [];
|
|
5139
5539
|
unmappedFrameCounts = /* @__PURE__ */ new Map();
|
|
5140
5540
|
sessionId;
|
|
@@ -5149,6 +5549,9 @@ var ClaudeProcessClient = class {
|
|
|
5149
5549
|
this.closedPromise = new Promise((resolve) => {
|
|
5150
5550
|
this.resolveClosed = resolve;
|
|
5151
5551
|
});
|
|
5552
|
+
this.adopted = this.adoptOwnedTree(options);
|
|
5553
|
+
this.adopted.catch(() => {
|
|
5554
|
+
});
|
|
5152
5555
|
this.child.stdout.setEncoding("utf8");
|
|
5153
5556
|
this.child.stdout.on("data", (chunk) => this.onData(chunk));
|
|
5154
5557
|
this.child.stderr.setEncoding("utf8");
|
|
@@ -5192,9 +5595,10 @@ var ClaudeProcessClient = class {
|
|
|
5192
5595
|
* same id; if the process already closed before init ever arrived,
|
|
5193
5596
|
* every call rejects with that same exit error.
|
|
5194
5597
|
*/
|
|
5195
|
-
waitForInit() {
|
|
5196
|
-
|
|
5197
|
-
if (this.
|
|
5598
|
+
async waitForInit() {
|
|
5599
|
+
await this.adopted;
|
|
5600
|
+
if (this.sessionId !== void 0) return this.sessionId;
|
|
5601
|
+
if (this.closed) throw this.exitError ?? new Error("claude process is closed");
|
|
5198
5602
|
return new Promise((resolve, reject) => {
|
|
5199
5603
|
this.initWaiter = { resolve, reject };
|
|
5200
5604
|
});
|
|
@@ -5258,6 +5662,32 @@ var ClaudeProcessClient = class {
|
|
|
5258
5662
|
label: "claude"
|
|
5259
5663
|
};
|
|
5260
5664
|
}
|
|
5665
|
+
/**
|
|
5666
|
+
* Backstop this tree, or tear it down. Adoption failure is a start-time
|
|
5667
|
+
* precondition, not a degraded mode: the child is terminated through the one
|
|
5668
|
+
* disposal authority and the failure is re-thrown, which is what makes
|
|
5669
|
+
* `waitForInit()` — and therefore `ClaudeAdapter.start()` — fail before any
|
|
5670
|
+
* session is published. Both cleanup attempts are best-effort because the
|
|
5671
|
+
* adoption failure, not a terminator's own complaint, is the reason to report.
|
|
5672
|
+
*/
|
|
5673
|
+
async adoptOwnedTree(options) {
|
|
5674
|
+
try {
|
|
5675
|
+
await adoptOwnedProcessTree({
|
|
5676
|
+
child: this.child,
|
|
5677
|
+
label: "claude",
|
|
5678
|
+
platform: options.platform,
|
|
5679
|
+
jobObject: options.jobObject
|
|
5680
|
+
});
|
|
5681
|
+
} catch (cause) {
|
|
5682
|
+
const failure = cause instanceof Error ? cause : new Error(String(cause));
|
|
5683
|
+
this.adoptionFailure = failure;
|
|
5684
|
+
await requestOwnedProcessTreeTermination(this.processTreeOptions()).catch(() => {
|
|
5685
|
+
});
|
|
5686
|
+
await disposeOwnedProcessTree(this.processTreeOptions()).catch(() => {
|
|
5687
|
+
});
|
|
5688
|
+
throw failure;
|
|
5689
|
+
}
|
|
5690
|
+
}
|
|
5261
5691
|
onData(chunk) {
|
|
5262
5692
|
this.buffer += chunk;
|
|
5263
5693
|
let newlineIndex = this.buffer.indexOf("\n");
|
|
@@ -5306,9 +5736,10 @@ var ClaudeProcessClient = class {
|
|
|
5306
5736
|
onClosed(err) {
|
|
5307
5737
|
if (this.closed) return;
|
|
5308
5738
|
this.closed = true;
|
|
5309
|
-
this.
|
|
5739
|
+
const terminal = this.adoptionFailure ?? err;
|
|
5740
|
+
this.exitError = terminal;
|
|
5310
5741
|
this.resolveClosed();
|
|
5311
|
-
this.initWaiter?.reject(
|
|
5742
|
+
this.initWaiter?.reject(terminal);
|
|
5312
5743
|
this.initWaiter = void 0;
|
|
5313
5744
|
this.eventQueue.end();
|
|
5314
5745
|
}
|
|
@@ -6175,6 +6606,19 @@ var CodexProcessRunner = class {
|
|
|
6175
6606
|
closedPromise;
|
|
6176
6607
|
resolveClosed;
|
|
6177
6608
|
disposalAttempt;
|
|
6609
|
+
/** Resolves once this tree is backstopped (see `adoptOwnedProcessTree`); rejects with the adoption failure, having already terminated the tree. */
|
|
6610
|
+
adopted;
|
|
6611
|
+
/** Set before the fail-closed termination starts; `buildExitError` reports it instead of the exit status of the kill we ourselves requested. */
|
|
6612
|
+
adoptionFailure;
|
|
6613
|
+
adoption = "pending";
|
|
6614
|
+
/**
|
|
6615
|
+
* Lines parsed before adoption settled. Unlike pi and claude, this runner has
|
|
6616
|
+
* no first awaited operation of its own to gate on — its caller reads the
|
|
6617
|
+
* FIRST event as the authoritative thread id. Holding events until the tree
|
|
6618
|
+
* is backstopped is what keeps that caller from publishing a session for a
|
|
6619
|
+
* tree the job object never took.
|
|
6620
|
+
*/
|
|
6621
|
+
deferredEvents = [];
|
|
6178
6622
|
constructor(options) {
|
|
6179
6623
|
this.onEvent = options.onEvent;
|
|
6180
6624
|
const spawnFn = options.spawnFn ?? spawn;
|
|
@@ -6186,6 +6630,9 @@ var CodexProcessRunner = class {
|
|
|
6186
6630
|
this.closedPromise = new Promise((resolve) => {
|
|
6187
6631
|
this.resolveClosed = resolve;
|
|
6188
6632
|
});
|
|
6633
|
+
this.adopted = this.adoptOwnedTree(options);
|
|
6634
|
+
this.adopted.catch(() => {
|
|
6635
|
+
});
|
|
6189
6636
|
this.child.stdout.setEncoding("utf8");
|
|
6190
6637
|
this.child.stdout.on("data", (chunk) => this.onData(chunk));
|
|
6191
6638
|
this.child.stderr.setEncoding("utf8");
|
|
@@ -6251,8 +6698,58 @@ var CodexProcessRunner = class {
|
|
|
6251
6698
|
label: "codex"
|
|
6252
6699
|
};
|
|
6253
6700
|
}
|
|
6254
|
-
/**
|
|
6701
|
+
/**
|
|
6702
|
+
* Backstop this tree, or tear it down. Adoption failure is a start-time
|
|
6703
|
+
* precondition, not a degraded mode: the child is terminated through the one
|
|
6704
|
+
* disposal authority, every parsed line is dropped instead of delivered, and
|
|
6705
|
+
* the resulting close makes the caller's own `waitClosed()` race reject with
|
|
6706
|
+
* the adoption failure (`buildExitError`) before a thread id is published.
|
|
6707
|
+
* Both cleanup attempts are best-effort because the adoption failure, not a
|
|
6708
|
+
* terminator's own complaint, is the reason to report.
|
|
6709
|
+
*/
|
|
6710
|
+
async adoptOwnedTree(options) {
|
|
6711
|
+
try {
|
|
6712
|
+
await adoptOwnedProcessTree({
|
|
6713
|
+
child: this.child,
|
|
6714
|
+
label: "codex",
|
|
6715
|
+
platform: options.platform,
|
|
6716
|
+
jobObject: options.jobObject
|
|
6717
|
+
});
|
|
6718
|
+
} catch (cause) {
|
|
6719
|
+
const failure = cause instanceof Error ? cause : new Error(String(cause));
|
|
6720
|
+
this.adoptionFailure = failure;
|
|
6721
|
+
this.adoption = "failed";
|
|
6722
|
+
this.deferredEvents.length = 0;
|
|
6723
|
+
await requestOwnedProcessTreeTermination(this.processTreeOptions()).catch(() => {
|
|
6724
|
+
});
|
|
6725
|
+
await disposeOwnedProcessTree(this.processTreeOptions()).catch(() => {
|
|
6726
|
+
});
|
|
6727
|
+
throw failure;
|
|
6728
|
+
}
|
|
6729
|
+
this.adoption = "adopted";
|
|
6730
|
+
for (const evt of this.deferredEvents.splice(0)) this.onEvent(evt);
|
|
6731
|
+
}
|
|
6732
|
+
/** Arrival-order delivery, held back until the tree is backstopped (see `deferredEvents`). */
|
|
6733
|
+
deliver(evt) {
|
|
6734
|
+
if (this.adoption === "failed") return;
|
|
6735
|
+
if (this.adoption === "pending") {
|
|
6736
|
+
this.deferredEvents.push(evt);
|
|
6737
|
+
return;
|
|
6738
|
+
}
|
|
6739
|
+
this.onEvent(evt);
|
|
6740
|
+
}
|
|
6741
|
+
/**
|
|
6742
|
+
* Builds a descriptive error folding in the exit code/signal and the stderr
|
|
6743
|
+
* tail — mirrors `PiRpcClient.buildExitError`'s reasoning: a post-mortem on a
|
|
6744
|
+
* failed start/resume should never need separately re-running codex by hand
|
|
6745
|
+
* with a raw JSONL logger to learn why.
|
|
6746
|
+
*
|
|
6747
|
+
* A tree this runner could not backstop is the one exception: that process
|
|
6748
|
+
* exited because THIS runner killed it, so `exit code=null, signal=SIGKILL`
|
|
6749
|
+
* plus an empty stderr tail would bury the only reason anyone can act on.
|
|
6750
|
+
*/
|
|
6255
6751
|
buildExitError(context) {
|
|
6752
|
+
if (this.adoptionFailure !== void 0) return this.adoptionFailure;
|
|
6256
6753
|
const parts = [`${context} (exit code=${this.exitCode}, signal=${this.exitSignal})`];
|
|
6257
6754
|
if (this.stderrRing.length > 0) {
|
|
6258
6755
|
parts.push(`stderr: ${this.stderrRing.join(" | ")}`);
|
|
@@ -6280,7 +6777,7 @@ var CodexProcessRunner = class {
|
|
|
6280
6777
|
if (!parsed || typeof parsed !== "object" || typeof parsed.type !== "string") {
|
|
6281
6778
|
return;
|
|
6282
6779
|
}
|
|
6283
|
-
this.
|
|
6780
|
+
this.deliver(parsed);
|
|
6284
6781
|
}
|
|
6285
6782
|
onStderr(chunk) {
|
|
6286
6783
|
for (const rawLine of chunk.split("\n")) {
|
|
@@ -7603,19 +8100,14 @@ function signNonce(privateKey, nonce) {
|
|
|
7603
8100
|
const signature = sign(null, nonceSigningBytes(nonce), privateKey);
|
|
7604
8101
|
return signature.toString("base64url");
|
|
7605
8102
|
}
|
|
8103
|
+
|
|
8104
|
+
// src/daemon/url.ts
|
|
7606
8105
|
function toHttpBase(serverUrl) {
|
|
7607
8106
|
const url = new URL(serverUrl);
|
|
7608
|
-
if (url.protocol === "ws:") url.protocol = "http:";
|
|
7609
|
-
else if (url.protocol === "wss:") url.protocol = "https:";
|
|
7610
8107
|
url.pathname = "/";
|
|
7611
8108
|
url.search = "";
|
|
7612
8109
|
return url.toString();
|
|
7613
8110
|
}
|
|
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
8111
|
function describeEndpoint(transport, url) {
|
|
7620
8112
|
const parsed = typeof url === "string" ? new URL(url) : url;
|
|
7621
8113
|
return { transport, host: parsed.host, path: parsed.pathname };
|
|
@@ -7648,17 +8140,15 @@ function assertServerUrlAllowed(rawUrl, opts = {}) {
|
|
|
7648
8140
|
const endpoint = formatServerUrl(url);
|
|
7649
8141
|
switch (url.protocol) {
|
|
7650
8142
|
case "https:":
|
|
7651
|
-
case "wss:":
|
|
7652
8143
|
return;
|
|
7653
8144
|
case "http:":
|
|
7654
|
-
case "ws:":
|
|
7655
8145
|
if (opts.dangerouslyAllowInsecureRemote || isLoopbackHostname(url.hostname)) return;
|
|
7656
8146
|
throw new InsecureServerUrlError(
|
|
7657
|
-
`refusing to connect to "${endpoint}" over plaintext ${url.protocol.replace(":", "")} \u2014 "${url.hostname}" is not a loopback host. Use
|
|
8147
|
+
`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
8148
|
);
|
|
7659
8149
|
default:
|
|
7660
8150
|
throw new InsecureServerUrlError(
|
|
7661
|
-
`refusing to connect to "${endpoint}" \u2014 unsupported scheme "${url.protocol}" (expected http
|
|
8151
|
+
`refusing to connect to "${endpoint}" \u2014 unsupported scheme "${url.protocol}" (expected http: or https:).`
|
|
7662
8152
|
);
|
|
7663
8153
|
}
|
|
7664
8154
|
}
|
|
@@ -9320,6 +9810,28 @@ var LocalTeamWorkspace = class {
|
|
|
9320
9810
|
return Object.freeze({ accepted: true, durable: true, workspaceId: state.definition.workspaceId, memberId: input.lease.memberId, seq, messageId: message.messageId, message: publicValue });
|
|
9321
9811
|
});
|
|
9322
9812
|
}
|
|
9813
|
+
/** Metadata-only operator notification view. Never advances delivery or acknowledgement. */
|
|
9814
|
+
async notificationSnapshot(input) {
|
|
9815
|
+
assertLeaseShape(input?.lease);
|
|
9816
|
+
if (input.afterSeq !== void 0) assertAfterSeq(input.afterSeq);
|
|
9817
|
+
return this.enqueue(async () => {
|
|
9818
|
+
const state = await this.resolveLease(await this.load(), input.lease);
|
|
9819
|
+
const acknowledgedThroughSeq = state.receipts[input.lease.memberId].acknowledgedThroughSeq;
|
|
9820
|
+
const after = Math.max(input.afterSeq ?? 0, acknowledgedThroughSeq);
|
|
9821
|
+
let latestPeerSeq = null;
|
|
9822
|
+
for (const message of state.messages) {
|
|
9823
|
+
if (message.seq > after && message.senderMemberId !== input.lease.memberId) latestPeerSeq = message.seq;
|
|
9824
|
+
}
|
|
9825
|
+
return Object.freeze({
|
|
9826
|
+
workspaceId: state.definition.workspaceId,
|
|
9827
|
+
memberId: input.lease.memberId,
|
|
9828
|
+
registryRevision: state.definition.revision,
|
|
9829
|
+
expiresAt: input.lease.expiresAt,
|
|
9830
|
+
acknowledgedThroughSeq,
|
|
9831
|
+
latestPeerSeq
|
|
9832
|
+
});
|
|
9833
|
+
});
|
|
9834
|
+
}
|
|
9323
9835
|
async readMessages(input) {
|
|
9324
9836
|
assertLeaseShape(input?.lease);
|
|
9325
9837
|
if (input?.afterSeq !== void 0) assertAfterSeq(input.afterSeq);
|
|
@@ -9710,7 +10222,7 @@ var LongPollRouteError = class extends Error {
|
|
|
9710
10222
|
};
|
|
9711
10223
|
var MAX_TRACKED_VALIDATION_FAILURE_WARNINGS = 1e3;
|
|
9712
10224
|
var MAX_TRACKED_ROUTE_FAILURE_WARNINGS = 1e3;
|
|
9713
|
-
function parseLooseEventsPollResponse(raw) {
|
|
10225
|
+
function parseLooseEventsPollResponse(raw, requestedCursor) {
|
|
9714
10226
|
if (typeof raw !== "object" || raw === null) {
|
|
9715
10227
|
throw new Error("events poll response is not an object");
|
|
9716
10228
|
}
|
|
@@ -9718,19 +10230,49 @@ function parseLooseEventsPollResponse(raw) {
|
|
|
9718
10230
|
if (!Array.isArray(events)) {
|
|
9719
10231
|
throw new Error("events poll response.events is not an array");
|
|
9720
10232
|
}
|
|
9721
|
-
if (
|
|
9722
|
-
throw new Error("events poll response.cursor is not
|
|
10233
|
+
if (!isSafeNonnegativeInteger(cursor)) {
|
|
10234
|
+
throw new Error("events poll response.cursor is not a safe nonnegative integer");
|
|
10235
|
+
}
|
|
10236
|
+
if (cursor < requestedCursor) {
|
|
10237
|
+
throw new Error("events poll response.cursor regressed below the requested cursor");
|
|
9723
10238
|
}
|
|
9724
10239
|
if (capabilities !== void 0 && (!Array.isArray(capabilities) || capabilities.some((flag) => typeof flag !== "string"))) {
|
|
9725
10240
|
throw new Error("events poll response.capabilities is not an array of strings");
|
|
9726
10241
|
}
|
|
9727
10242
|
return { events, cursor, capabilities: capabilities ?? [] };
|
|
9728
10243
|
}
|
|
9729
|
-
function
|
|
10244
|
+
function isSafeNonnegativeInteger(value) {
|
|
10245
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
10246
|
+
}
|
|
10247
|
+
function validateTrustedEventsPage(events, requestedCursor, pageCursor) {
|
|
10248
|
+
let previousTaskSeq;
|
|
10249
|
+
for (const raw of events) {
|
|
10250
|
+
if (typeof raw !== "object" || raw === null) throw new Error("events page contains an unidentifiable message");
|
|
10251
|
+
const { type, seq } = raw;
|
|
10252
|
+
if (typeof type !== "string") throw new Error("events page message has no authoritative type");
|
|
10253
|
+
if (type.startsWith("conn.")) continue;
|
|
10254
|
+
if (!isSafeNonnegativeInteger(seq)) {
|
|
10255
|
+
throw new Error("events poll task seq is not a safe nonnegative integer");
|
|
10256
|
+
}
|
|
10257
|
+
if (seq <= requestedCursor || seq > pageCursor) {
|
|
10258
|
+
throw new Error("events poll task seq lies outside the trusted page cursor range");
|
|
10259
|
+
}
|
|
10260
|
+
if (previousTaskSeq !== void 0 && seq <= previousTaskSeq) {
|
|
10261
|
+
throw new Error("events poll task seq is not strictly page ordered");
|
|
10262
|
+
}
|
|
10263
|
+
const knownTaskType = MESSAGE_TYPES.includes(type);
|
|
10264
|
+
const predecessor = previousTaskSeq ?? requestedCursor;
|
|
10265
|
+
if (!knownTaskType && seq !== predecessor + 1) {
|
|
10266
|
+
throw new Error("events poll unknown task seq would cross an untrusted gap");
|
|
10267
|
+
}
|
|
10268
|
+
previousTaskSeq = seq;
|
|
10269
|
+
}
|
|
10270
|
+
}
|
|
10271
|
+
function extractExecutableSeq(raw) {
|
|
9730
10272
|
if (typeof raw !== "object" || raw === null) return void 0;
|
|
9731
10273
|
const { type, seq } = raw;
|
|
9732
|
-
if (typeof type !== "string" ||
|
|
9733
|
-
return
|
|
10274
|
+
if (typeof type !== "string" || type.startsWith("conn.")) return void 0;
|
|
10275
|
+
return isSafeNonnegativeInteger(seq) ? seq : void 0;
|
|
9734
10276
|
}
|
|
9735
10277
|
var LongPollClient = class {
|
|
9736
10278
|
constructor(opts) {
|
|
@@ -9741,6 +10283,8 @@ var LongPollClient = class {
|
|
|
9741
10283
|
}
|
|
9742
10284
|
opts;
|
|
9743
10285
|
running = false;
|
|
10286
|
+
/** Owns exactly one active loop generation, including its held GET and retry delays. */
|
|
10287
|
+
loopAbortController;
|
|
9744
10288
|
/**
|
|
9745
10289
|
* Finding R1: seqs this loop has already `console.warn`'d about for a
|
|
9746
10290
|
* validation-failed (recognized-type, invalid-payload) entry — a poison
|
|
@@ -9800,16 +10344,22 @@ var LongPollClient = class {
|
|
|
9800
10344
|
noteRevoked(err) {
|
|
9801
10345
|
if (!(err instanceof DeviceRevokedError)) return false;
|
|
9802
10346
|
this.running = false;
|
|
10347
|
+
this.loopAbortController?.abort();
|
|
9803
10348
|
this.opts.onRevoked?.();
|
|
9804
10349
|
return true;
|
|
9805
10350
|
}
|
|
9806
10351
|
start() {
|
|
9807
10352
|
if (this.running) return;
|
|
9808
10353
|
this.running = true;
|
|
9809
|
-
|
|
10354
|
+
const controller = new AbortController();
|
|
10355
|
+
this.loopAbortController = controller;
|
|
10356
|
+
void this.loop(controller.signal).finally(() => {
|
|
10357
|
+
if (this.loopAbortController === controller) this.loopAbortController = void 0;
|
|
10358
|
+
});
|
|
9810
10359
|
}
|
|
9811
10360
|
stop() {
|
|
9812
10361
|
this.running = false;
|
|
10362
|
+
this.loopAbortController?.abort();
|
|
9813
10363
|
}
|
|
9814
10364
|
/**
|
|
9815
10365
|
* POST one batch of envelopes to `/byok/messages` (finding F6/protocol
|
|
@@ -9818,14 +10368,14 @@ var LongPollClient = class {
|
|
|
9818
10368
|
* (`ConnectionHub.handleInbound`), so a resend of the SAME batch (same
|
|
9819
10369
|
* envelope `id`s — the caller must never rebuild them) is deduped
|
|
9820
10370
|
* server-side into a safe no-op rather than reprocessed (§9). Returns
|
|
9821
|
-
*
|
|
10371
|
+
* validated frozen-v1 counts only after a readable response body.
|
|
9822
10372
|
*/
|
|
9823
10373
|
async postBatch(envelopes) {
|
|
9824
10374
|
try {
|
|
9825
10375
|
await this.opts.auth.getValidAccessToken();
|
|
9826
10376
|
} catch (err) {
|
|
9827
10377
|
this.noteRevoked(err);
|
|
9828
|
-
return
|
|
10378
|
+
return void 0;
|
|
9829
10379
|
}
|
|
9830
10380
|
let res;
|
|
9831
10381
|
try {
|
|
@@ -9841,23 +10391,26 @@ var LongPollClient = class {
|
|
|
9841
10391
|
);
|
|
9842
10392
|
} catch (err) {
|
|
9843
10393
|
if (!this.noteRevoked(err)) this.warnRouteFailure(this.messagesEndpoint, void 0, err);
|
|
9844
|
-
return
|
|
10394
|
+
return void 0;
|
|
9845
10395
|
}
|
|
9846
10396
|
if (!res.ok) {
|
|
9847
10397
|
this.warnRouteFailure(this.messagesEndpoint, res.status, void 0);
|
|
9848
|
-
return
|
|
10398
|
+
return void 0;
|
|
9849
10399
|
}
|
|
9850
10400
|
try {
|
|
9851
|
-
MessagesSendResponseSchema.parse(await res.json());
|
|
10401
|
+
const response = MessagesSendResponseSchema.parse(await res.json());
|
|
10402
|
+
if (response.accepted + (response.rejected ?? 0) !== envelopes.length) {
|
|
10403
|
+
throw new Error("messages response counts do not match the posted batch length");
|
|
10404
|
+
}
|
|
10405
|
+
return response;
|
|
9852
10406
|
} catch (err) {
|
|
9853
10407
|
this.warnRouteFailure(this.messagesEndpoint, res.status, err);
|
|
9854
|
-
return
|
|
10408
|
+
return void 0;
|
|
9855
10409
|
}
|
|
9856
|
-
return true;
|
|
9857
10410
|
}
|
|
9858
|
-
async loop() {
|
|
10411
|
+
async loop(signal) {
|
|
9859
10412
|
let retryAttempt = 0;
|
|
9860
|
-
while (this.running) {
|
|
10413
|
+
while (this.running && !signal.aborted) {
|
|
9861
10414
|
try {
|
|
9862
10415
|
await this.opts.auth.getValidAccessToken();
|
|
9863
10416
|
const base = toHttpBase(this.opts.serverUrl);
|
|
@@ -9866,8 +10419,9 @@ var LongPollClient = class {
|
|
|
9866
10419
|
if (cursor !== void 0) url.searchParams.set("cursor", String(cursor));
|
|
9867
10420
|
let res;
|
|
9868
10421
|
try {
|
|
9869
|
-
res = await authedFetch(url, { method: "GET" }, this.opts.auth);
|
|
10422
|
+
res = await authedFetch(url, { method: "GET", signal }, this.opts.auth);
|
|
9870
10423
|
} catch (err) {
|
|
10424
|
+
if (signal.aborted) return;
|
|
9871
10425
|
if (!(err instanceof DeviceRevokedError)) {
|
|
9872
10426
|
this.warnRouteFailure(this.eventsEndpoint, void 0, err);
|
|
9873
10427
|
}
|
|
@@ -9881,69 +10435,63 @@ var LongPollClient = class {
|
|
|
9881
10435
|
return;
|
|
9882
10436
|
}
|
|
9883
10437
|
this.warnRouteFailure(this.eventsEndpoint, res.status, void 0);
|
|
9884
|
-
this.opts.
|
|
10438
|
+
this.opts.onServerCapabilitiesInvalidated?.();
|
|
10439
|
+
this.opts.onPollFailure?.();
|
|
9885
10440
|
this.opts.onOperationalOutcome?.("failure");
|
|
9886
10441
|
const baseMs = this.opts.retryDelayMs ?? 2e3;
|
|
9887
|
-
await sleep(this.opts.retryDelayForAttempt?.(retryAttempt++, baseMs) ?? baseMs);
|
|
10442
|
+
await sleep(this.opts.retryDelayForAttempt?.(retryAttempt++, baseMs) ?? baseMs, signal);
|
|
9888
10443
|
continue;
|
|
9889
10444
|
}
|
|
9890
10445
|
let parsed;
|
|
9891
10446
|
try {
|
|
9892
|
-
parsed = parseLooseEventsPollResponse(await res.json());
|
|
10447
|
+
parsed = parseLooseEventsPollResponse(await res.json(), cursor ?? 0);
|
|
10448
|
+
validateTrustedEventsPage(parsed.events, cursor ?? 0, parsed.cursor);
|
|
9893
10449
|
} catch (err) {
|
|
9894
10450
|
this.warnRouteFailure(this.eventsEndpoint, res.status, err);
|
|
9895
10451
|
throw err;
|
|
9896
10452
|
}
|
|
9897
10453
|
this.opts.onServerCapabilities?.(parsed.capabilities);
|
|
9898
10454
|
let hadValidationFailureThisBatch = false;
|
|
10455
|
+
let acceptedAnyEntry = false;
|
|
9899
10456
|
for (const raw of parsed.events) {
|
|
9900
10457
|
let envelope;
|
|
9901
10458
|
try {
|
|
9902
10459
|
envelope = parseMessage(raw);
|
|
9903
10460
|
} catch (err) {
|
|
9904
|
-
|
|
9905
|
-
|
|
9906
|
-
|
|
9907
|
-
|
|
9908
|
-
|
|
9909
|
-
|
|
9910
|
-
|
|
9911
|
-
|
|
9912
|
-
if (!this.warnedValidationFailureSeqs.has(failedSeq)) {
|
|
9913
|
-
if (this.warnedValidationFailureSeqs.size > MAX_TRACKED_VALIDATION_FAILURE_WARNINGS) {
|
|
9914
|
-
this.warnedValidationFailureSeqs.clear();
|
|
9915
|
-
}
|
|
9916
|
-
this.warnedValidationFailureSeqs.add(failedSeq);
|
|
9917
|
-
console.warn(
|
|
9918
|
-
`[byok/client] long-poll: a recognized message type at seq=${failedSeq} failed payload validation \u2014 skipped for this batch, cursor frozen so the server keeps redelivering it until a corrected version arrives:`,
|
|
9919
|
-
err
|
|
9920
|
-
);
|
|
9921
|
-
}
|
|
10461
|
+
const failedSeq = extractExecutableSeq(raw);
|
|
10462
|
+
if (failedSeq !== void 0) {
|
|
10463
|
+
hadValidationFailureThisBatch = true;
|
|
10464
|
+
this.opts.onValidationFailedSeq?.(failedSeq);
|
|
10465
|
+
if (!this.warnedValidationFailureSeqs.has(failedSeq)) {
|
|
10466
|
+
if (this.warnedValidationFailureSeqs.size > MAX_TRACKED_VALIDATION_FAILURE_WARNINGS) this.warnedValidationFailureSeqs.clear();
|
|
10467
|
+
this.warnedValidationFailureSeqs.add(failedSeq);
|
|
10468
|
+
console.warn(`[byok/client] long-poll: unknown or invalid executable message at seq=${failedSeq}; cursor frozen without durable disposition`, err);
|
|
9922
10469
|
}
|
|
9923
10470
|
}
|
|
9924
10471
|
continue;
|
|
9925
10472
|
}
|
|
9926
|
-
this.opts.onEnvelope(envelope);
|
|
10473
|
+
if (this.opts.onEnvelope(envelope) !== false) acceptedAnyEntry = true;
|
|
9927
10474
|
}
|
|
9928
10475
|
if (parsed.events.length === 0) {
|
|
9929
10476
|
retryAttempt = 0;
|
|
9930
10477
|
this.opts.onOperationalOutcome?.("success");
|
|
9931
|
-
await sleep(this.opts.idleDelayMs ?? 250);
|
|
9932
|
-
} else if (this.opts.isStalled?.() || hadValidationFailureThisBatch) {
|
|
10478
|
+
await sleep(this.opts.idleDelayMs ?? 250, signal);
|
|
10479
|
+
} else if (this.opts.isStalled?.() || hadValidationFailureThisBatch || !acceptedAnyEntry && this.opts.getCursor() === cursor) {
|
|
9933
10480
|
this.opts.onOperationalOutcome?.("failure");
|
|
9934
10481
|
const baseMs = this.opts.retryDelayMs ?? 2e3;
|
|
9935
|
-
await sleep(this.opts.retryDelayForAttempt?.(retryAttempt++, baseMs) ?? baseMs);
|
|
10482
|
+
await sleep(this.opts.retryDelayForAttempt?.(retryAttempt++, baseMs) ?? baseMs, signal);
|
|
9936
10483
|
} else {
|
|
9937
10484
|
retryAttempt = 0;
|
|
9938
10485
|
this.opts.onOperationalOutcome?.("success");
|
|
9939
10486
|
}
|
|
9940
10487
|
} catch (err) {
|
|
9941
|
-
this.opts.
|
|
10488
|
+
this.opts.onServerCapabilitiesInvalidated?.();
|
|
9942
10489
|
if (this.noteRevoked(err)) return;
|
|
9943
|
-
if (!this.running) return;
|
|
10490
|
+
if (!this.running || signal.aborted) return;
|
|
10491
|
+
this.opts.onPollFailure?.();
|
|
9944
10492
|
this.opts.onOperationalOutcome?.("failure");
|
|
9945
10493
|
const baseMs = this.opts.retryDelayMs ?? 2e3;
|
|
9946
|
-
await sleep(this.opts.retryDelayForAttempt?.(retryAttempt++, baseMs) ?? baseMs);
|
|
10494
|
+
await sleep(this.opts.retryDelayForAttempt?.(retryAttempt++, baseMs) ?? baseMs, signal);
|
|
9947
10495
|
}
|
|
9948
10496
|
}
|
|
9949
10497
|
}
|
|
@@ -9963,20 +10511,24 @@ async function parseReplayCursorTooOld(res) {
|
|
|
9963
10511
|
}
|
|
9964
10512
|
return new ReplayCursorTooOldError(recoverableFrom);
|
|
9965
10513
|
}
|
|
9966
|
-
function sleep(ms) {
|
|
9967
|
-
|
|
10514
|
+
function sleep(ms, signal) {
|
|
10515
|
+
if (signal.aborted) return Promise.resolve();
|
|
10516
|
+
return new Promise((resolve) => {
|
|
10517
|
+
const finish = () => {
|
|
10518
|
+
clearTimeout(timer);
|
|
10519
|
+
signal.removeEventListener("abort", finish);
|
|
10520
|
+
resolve();
|
|
10521
|
+
};
|
|
10522
|
+
const timer = setTimeout(finish, ms);
|
|
10523
|
+
signal.addEventListener("abort", finish, { once: true });
|
|
10524
|
+
});
|
|
9968
10525
|
}
|
|
9969
|
-
|
|
9970
|
-
|
|
9971
|
-
|
|
9972
|
-
|
|
9973
|
-
|
|
9974
|
-
|
|
9975
|
-
}
|
|
9976
|
-
status;
|
|
9977
|
-
endpoint;
|
|
9978
|
-
};
|
|
9979
|
-
function createConnectionHelloEnvelope(opts) {
|
|
10526
|
+
|
|
10527
|
+
// src/daemon/connection-manager.ts
|
|
10528
|
+
function isCursorEnvelopeType(type) {
|
|
10529
|
+
return isServerToDaemonType(type) && !type.startsWith("conn.");
|
|
10530
|
+
}
|
|
10531
|
+
function createConnectionHelloEnvelope(opts, getCursor) {
|
|
9980
10532
|
const configuredToolsets = opts.getConfiguredToolsets?.();
|
|
9981
10533
|
return createEnvelope("conn.hello", {
|
|
9982
10534
|
protocolVersions: [PROTOCOL_VERSION],
|
|
@@ -9986,234 +10538,35 @@ function createConnectionHelloEnvelope(opts) {
|
|
|
9986
10538
|
clientVersion: opts.clientVersion,
|
|
9987
10539
|
runtimes: opts.runtimes,
|
|
9988
10540
|
configuredToolsets: configuredToolsets === void 0 ? void 0 : [...configuredToolsets],
|
|
9989
|
-
cursor:
|
|
10541
|
+
cursor: getCursor()
|
|
9990
10542
|
});
|
|
9991
10543
|
}
|
|
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
|
-
}
|
|
10544
|
+
var MAX_REJECTED_OUTBOX_ENTRIES = 1e3;
|
|
10167
10545
|
var ConnectionManager = class {
|
|
10168
10546
|
constructor(opts) {
|
|
10169
10547
|
this.opts = opts;
|
|
10170
10548
|
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
10549
|
this.longPoll = new LongPollClient({
|
|
10192
10550
|
serverUrl: opts.serverUrl,
|
|
10193
10551
|
auth: opts.auth,
|
|
10194
|
-
//
|
|
10195
|
-
//
|
|
10196
|
-
//
|
|
10197
|
-
//
|
|
10198
|
-
//
|
|
10199
|
-
|
|
10200
|
-
getCursor: () => this.dedupWatermark(),
|
|
10552
|
+
// The long-poll query cursor is also the kernel's irreversible ack.
|
|
10553
|
+
// Only report the successfully processed cursor here. `deliveredSeq`
|
|
10554
|
+
// remains a local dedup watermark; using it on the wire would ack an
|
|
10555
|
+
// in-flight envelope before its handler settles and make a later
|
|
10556
|
+
// failure impossible to redeliver.
|
|
10557
|
+
getCursor: () => this.cursor,
|
|
10201
10558
|
onEnvelope: (envelope) => this.deliver(envelope),
|
|
10202
10559
|
onServerCapabilities: (capabilities) => {
|
|
10203
|
-
|
|
10560
|
+
this.serverCapabilities = capabilities;
|
|
10561
|
+
this.noteConnected();
|
|
10562
|
+
},
|
|
10563
|
+
onServerCapabilitiesInvalidated: () => {
|
|
10564
|
+
this.serverCapabilities = [];
|
|
10204
10565
|
},
|
|
10566
|
+
onPollFailure: () => this.noteDisconnected(),
|
|
10205
10567
|
onRevoked: () => this.enterRevoked(),
|
|
10206
10568
|
onReplayCursorTooOld: (error) => this.enterReplayCursorTooOld(error),
|
|
10207
|
-
//
|
|
10208
|
-
// LongPollClient couldn't parse into a known Envelope at all (an
|
|
10209
|
-
// unrecognized message type) still needs its cursor/watermark
|
|
10210
|
-
// advanced past it, exactly like a successfully-processed envelope
|
|
10211
|
-
// would — see `noteSkippedSeq`'s own doc comment.
|
|
10212
|
-
onSkippedSeq: (seq) => this.noteSkippedSeq(seq),
|
|
10213
|
-
// Finding R1: a batch entry LongPollClient recognized the TYPE of but
|
|
10214
|
-
// whose payload failed validation — a genuine delivery failure at
|
|
10215
|
-
// that seq, engaged the same way a thrown handler failure is (see
|
|
10216
|
-
// `noteValidationFailure`'s own doc comment).
|
|
10569
|
+
// Unknown and malformed executable work use the same ordered stall.
|
|
10217
10570
|
onValidationFailedSeq: (seq) => this.noteValidationFailure(seq),
|
|
10218
10571
|
// Finding P2 (Fix 2a): lets the long-poll loop distinguish "this
|
|
10219
10572
|
// non-empty batch was a stalled backlog re-pull, no cursor progress
|
|
@@ -10229,13 +10582,10 @@ var ConnectionManager = class {
|
|
|
10229
10582
|
}
|
|
10230
10583
|
opts;
|
|
10231
10584
|
fleetJitter;
|
|
10232
|
-
ws;
|
|
10233
10585
|
longPoll;
|
|
10234
|
-
mode = "ws";
|
|
10235
|
-
consecutiveFailures = 0;
|
|
10236
|
-
wsRetryTimer;
|
|
10237
|
-
wsProbeSequence = 0;
|
|
10238
10586
|
uploadRetryAttempt = 0;
|
|
10587
|
+
started = false;
|
|
10588
|
+
connected = false;
|
|
10239
10589
|
cursor;
|
|
10240
10590
|
/**
|
|
10241
10591
|
* Finding F3 (at-most-once redelivery): the lowest `task.*` envelope `seq`
|
|
@@ -10257,10 +10607,9 @@ var ConnectionManager = class {
|
|
|
10257
10607
|
* (see `advanceCursor`) — that semantics is unchanged. `deliveredSeq`
|
|
10258
10608
|
* advances eagerly, the instant a `task.*` envelope is admitted past
|
|
10259
10609
|
* 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
|
|
10610
|
+
* handler has even started, let alone succeeded. It exists so a repeated
|
|
10611
|
+
* read at the durable cursor does not re-dispatch an envelope already in
|
|
10612
|
+
* flight — `handleOffer` must not start a second adapter session while a
|
|
10264
10613
|
* first attempt is still running. On WS this same field is written the
|
|
10265
10614
|
* same way, but since a live WS connection only ever pushes a given `seq`
|
|
10266
10615
|
* once, it never has an observable effect there beyond mirroring
|
|
@@ -10271,18 +10620,18 @@ var ConnectionManager = class {
|
|
|
10271
10620
|
/** Finding F3: serializes `onEnvelope` calls into a per-connection FIFO — one envelope's handler always fully settles before the next one starts. */
|
|
10272
10621
|
processingChain = Promise.resolve();
|
|
10273
10622
|
/**
|
|
10274
|
-
* Design B (finding N4): the ONE outbound queue
|
|
10275
|
-
*
|
|
10623
|
+
* Design B (finding N4): the ONE outbound queue holds `Envelope` OBJECTS,
|
|
10624
|
+
* never re-encoded/rebuilt strings, so a
|
|
10276
10625
|
* resend after a failed send attempt is byte-identical to the original
|
|
10277
10626
|
* (same `id`), which is what lets the server's per-(deviceId,id) dedup
|
|
10278
10627
|
* (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.
|
|
10628
|
+
* application (protocol §9).
|
|
10282
10629
|
*/
|
|
10283
10630
|
outbox = [];
|
|
10631
|
+
/** Terminally rejected outbound envelopes, retained as a bounded observable quarantine. */
|
|
10632
|
+
rejectedOutboundEnvelopes = [];
|
|
10284
10633
|
/**
|
|
10285
|
-
* Finding F5(b): how many envelopes `drainOutbox`
|
|
10634
|
+
* Finding F5(b): how many envelopes `drainOutbox` has
|
|
10286
10635
|
* currently spliced OUT of `this.outbox` for an in-flight (not yet
|
|
10287
10636
|
* confirmed delivered) `postBatch` call — 0 the rest of the time. See
|
|
10288
10637
|
* `outboxLength`'s own doc comment for why this needs to be tracked
|
|
@@ -10335,39 +10684,22 @@ var ConnectionManager = class {
|
|
|
10335
10684
|
*/
|
|
10336
10685
|
cancelPendingDrainRetry;
|
|
10337
10686
|
/**
|
|
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.
|
|
10687
|
+
* The capabilities the current server response advertised — untyped
|
|
10688
|
+
* `string[]` for forward compatibility. An advertisement is scoped to the
|
|
10689
|
+
* current long-poll response stream and is cleared after an HTTP failure,
|
|
10690
|
+
* terminal shutdown, or revocation. This keeps capability-gated outbound
|
|
10691
|
+
* messages fail-closed until the current server has explicitly advertised
|
|
10692
|
+
* support.
|
|
10365
10693
|
*/
|
|
10366
10694
|
serverCapabilities = [];
|
|
10367
10695
|
async start() {
|
|
10368
10696
|
if (this.terminalError) throw this.terminalError;
|
|
10369
10697
|
this.cursor = await this.opts.cursorStore.load(this.opts.serverUrl, this.opts.deviceId);
|
|
10370
|
-
this.
|
|
10698
|
+
this.started = true;
|
|
10699
|
+
this.opts.onStateChange?.("connecting");
|
|
10700
|
+
this.longPoll.start();
|
|
10701
|
+
this.outbox.unshift(createConnectionHelloEnvelope(this.opts, () => this.cursor));
|
|
10702
|
+
void this.drainOutbox();
|
|
10371
10703
|
}
|
|
10372
10704
|
/**
|
|
10373
10705
|
* Design B (finding N4): push onto the single shared outbox and try to
|
|
@@ -10376,19 +10708,16 @@ var ConnectionManager = class {
|
|
|
10376
10708
|
*/
|
|
10377
10709
|
send(envelope) {
|
|
10378
10710
|
this.outbox.push(envelope);
|
|
10711
|
+
this.opts.onOutboundQueued?.(envelope);
|
|
10379
10712
|
void this.drainOutbox();
|
|
10380
10713
|
}
|
|
10714
|
+
/** Publish a fresh local configuration snapshot while this daemon is running. */
|
|
10715
|
+
refreshHello() {
|
|
10716
|
+
if (!this.started || this.stopped || this.revoked || this.terminalError) return;
|
|
10717
|
+
this.send(createConnectionHelloEnvelope(this.opts, () => this.cursor));
|
|
10718
|
+
}
|
|
10381
10719
|
/**
|
|
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
|
|
10720
|
+
* POSTs the outbox through long-poll in chunks of at most
|
|
10392
10721
|
* `MAX_MESSAGES_PER_BATCH` (finding P1) — the server hard-caps a single
|
|
10393
10722
|
* `/byok/messages` batch there (`MessagesSendRequestSchema`, protocol
|
|
10394
10723
|
* §8.2) and 400s the WHOLE request if it's exceeded, which — before this
|
|
@@ -10400,43 +10729,58 @@ var ConnectionManager = class {
|
|
|
10400
10729
|
* failure that SAME chunk is unshifted back (order-preserving, same
|
|
10401
10730
|
* Envelope objects/ids — never rebuilt, so a retry is exactly the resend
|
|
10402
10731
|
* 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
10732
|
* Re-entrancy is guarded by `draining`: a call arriving while a drain is
|
|
10408
10733
|
* 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.
|
|
10734
|
+
* `while (this.outbox.length > 0)` check picks up anything newly pushed.
|
|
10411
10735
|
*/
|
|
10412
10736
|
async drainOutbox() {
|
|
10413
|
-
if (this.draining) return;
|
|
10737
|
+
if (!this.started || this.draining) return;
|
|
10414
10738
|
this.draining = true;
|
|
10739
|
+
const isolateRejectedBatch = async (batch) => {
|
|
10740
|
+
const pending = [[...batch]];
|
|
10741
|
+
while (pending.length > 0) {
|
|
10742
|
+
if (this.stopped || this.revoked) return pending.flat();
|
|
10743
|
+
const segment = pending.shift();
|
|
10744
|
+
try {
|
|
10745
|
+
this.opts.beforeOutboundPost?.(segment);
|
|
10746
|
+
const result = await this.longPoll.postBatch(segment);
|
|
10747
|
+
if (result === void 0) return [...segment, ...pending.flat()];
|
|
10748
|
+
if ((result.rejected ?? 0) === 0) {
|
|
10749
|
+
await this.opts.onOutboundAccepted?.(segment);
|
|
10750
|
+
continue;
|
|
10751
|
+
}
|
|
10752
|
+
if (segment.length === 1) {
|
|
10753
|
+
await this.opts.onOutboundRejected?.(segment[0]);
|
|
10754
|
+
this.quarantineRejectedOutbound(segment[0], "inbound_rejected");
|
|
10755
|
+
continue;
|
|
10756
|
+
}
|
|
10757
|
+
} catch (error) {
|
|
10758
|
+
console.error("[byok/client] outbound durable disposition failed; retaining exact batch", error);
|
|
10759
|
+
return [...segment, ...pending.flat()];
|
|
10760
|
+
}
|
|
10761
|
+
const midpoint = Math.floor(segment.length / 2);
|
|
10762
|
+
pending.unshift(segment.slice(0, midpoint), segment.slice(midpoint));
|
|
10763
|
+
}
|
|
10764
|
+
return void 0;
|
|
10765
|
+
};
|
|
10415
10766
|
try {
|
|
10416
10767
|
while (this.outbox.length > 0) {
|
|
10417
10768
|
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
10769
|
const batch = this.outbox.splice(0, MAX_MESSAGES_PER_BATCH);
|
|
10426
10770
|
this.inFlightBatchSize = batch.length;
|
|
10427
|
-
let
|
|
10771
|
+
let retrySegments;
|
|
10428
10772
|
try {
|
|
10429
|
-
|
|
10773
|
+
retrySegments = await isolateRejectedBatch(batch);
|
|
10430
10774
|
} finally {
|
|
10431
10775
|
this.inFlightBatchSize = 0;
|
|
10432
10776
|
}
|
|
10433
|
-
if (
|
|
10777
|
+
if (retrySegments === void 0) {
|
|
10434
10778
|
this.uploadRetryAttempt = 0;
|
|
10435
10779
|
this.opts.onOperationalOutcome?.("success", "upload");
|
|
10436
10780
|
continue;
|
|
10437
10781
|
}
|
|
10438
10782
|
this.opts.onOperationalOutcome?.("failure", "upload");
|
|
10439
|
-
this.outbox.unshift(...
|
|
10783
|
+
this.outbox.unshift(...retrySegments);
|
|
10440
10784
|
if (this.stopped || this.revoked) return;
|
|
10441
10785
|
const baseMs = this.opts.longPollRetryDelayMs ?? 2e3;
|
|
10442
10786
|
await this.drainRetryDelay(this.fleetJitter.delay("upload", this.uploadRetryAttempt++, baseMs));
|
|
@@ -10452,8 +10796,7 @@ var ConnectionManager = class {
|
|
|
10452
10796
|
* in-flight wait immediately instead of leaving `drainOutbox` parked here
|
|
10453
10797
|
* for up to the rest of the delay before it next checks `this.revoked` —
|
|
10454
10798
|
* and (b) unref'd, so the timer never keeps the Node process alive by
|
|
10455
|
-
* itself while nothing else (
|
|
10456
|
-
* legitimately is.
|
|
10799
|
+
* itself while nothing else (such as the live long-poll GET) legitimately is.
|
|
10457
10800
|
*/
|
|
10458
10801
|
drainRetryDelay(ms) {
|
|
10459
10802
|
return new Promise((resolve) => {
|
|
@@ -10469,14 +10812,9 @@ var ConnectionManager = class {
|
|
|
10469
10812
|
};
|
|
10470
10813
|
});
|
|
10471
10814
|
}
|
|
10472
|
-
isTransportDegraded() {
|
|
10473
|
-
return this.mode === "long-poll";
|
|
10474
|
-
}
|
|
10475
10815
|
/**
|
|
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.
|
|
10816
|
+
* The capabilities the latest successful `GET /byok/events` response
|
|
10817
|
+
* advertised. Empty before a successful response and after a failed one.
|
|
10480
10818
|
*/
|
|
10481
10819
|
getServerCapabilities() {
|
|
10482
10820
|
return this.serverCapabilities;
|
|
@@ -10484,29 +10822,23 @@ var ConnectionManager = class {
|
|
|
10484
10822
|
getTerminalError() {
|
|
10485
10823
|
return this.terminalError;
|
|
10486
10824
|
}
|
|
10487
|
-
getMode() {
|
|
10488
|
-
return this.mode;
|
|
10489
|
-
}
|
|
10490
10825
|
isConnected() {
|
|
10491
|
-
return this.
|
|
10826
|
+
return this.connected;
|
|
10492
10827
|
}
|
|
10493
10828
|
isRevoked() {
|
|
10494
10829
|
return this.revoked;
|
|
10495
10830
|
}
|
|
10496
10831
|
/**
|
|
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.
|
|
10832
|
+
* Resolves after the first successful long-poll response establishes the
|
|
10833
|
+
* authenticated connection.
|
|
10502
10834
|
*
|
|
10503
10835
|
* Rejects with {@link DeviceRevokedError} — instead of hanging until
|
|
10504
10836
|
* `timeoutMs` — if the device turns out to be revoked while settling (or
|
|
10505
10837
|
* already was): a cold `daemon.start()` against an already-revoked device
|
|
10506
10838
|
* must fail fast, not surface a generic timeout (protocol §6.3).
|
|
10507
10839
|
*/
|
|
10508
|
-
|
|
10509
|
-
if (this.
|
|
10840
|
+
waitForConnection(timeoutMs = 1e4) {
|
|
10841
|
+
if (this.connected) return Promise.resolve();
|
|
10510
10842
|
if (this.terminalError) return Promise.reject(this.terminalError);
|
|
10511
10843
|
if (this.revoked) return Promise.reject(new DeviceRevokedError());
|
|
10512
10844
|
return new Promise((resolve, reject) => {
|
|
@@ -10514,7 +10846,7 @@ var ConnectionManager = class {
|
|
|
10514
10846
|
};
|
|
10515
10847
|
const timer = setTimeout(() => {
|
|
10516
10848
|
this.settledWaiters = this.settledWaiters.filter((w) => w !== settle);
|
|
10517
|
-
reject(new Error("Timed out waiting for the connection to settle
|
|
10849
|
+
reject(new Error("Timed out waiting for the long-poll connection to settle"));
|
|
10518
10850
|
}, timeoutMs);
|
|
10519
10851
|
settle = (err) => {
|
|
10520
10852
|
clearTimeout(timer);
|
|
@@ -10525,7 +10857,7 @@ var ConnectionManager = class {
|
|
|
10525
10857
|
});
|
|
10526
10858
|
}
|
|
10527
10859
|
/**
|
|
10528
|
-
* Stops
|
|
10860
|
+
* Stops the long-poll transport and waits for every in-flight envelope handler
|
|
10529
10861
|
* (the F3 FIFO chain) and the most recent cursor write to actually land on
|
|
10530
10862
|
* disk — otherwise a `stop()` racing a just-processed envelope's
|
|
10531
10863
|
* persistence could lose that cursor advance, or leave a handler running
|
|
@@ -10534,14 +10866,14 @@ var ConnectionManager = class {
|
|
|
10534
10866
|
* Finding F5(b) (cross-model adversarial review): `drainTimeoutMs`, when
|
|
10535
10867
|
* passed, bounds how long this waits for the shared outbox (`this.outbox`
|
|
10536
10868
|
* — Design B) to actually finish draining BEFORE flipping `this.stopped`
|
|
10537
|
-
* and
|
|
10869
|
+
* and stopping the transport. Before this fix, `stop()` set `stopped`
|
|
10538
10870
|
* synchronously and never waited for `drainOutbox` at all: an envelope
|
|
10539
10871
|
* `send()` had just pushed moments earlier (e.g. `TaskRunner.shutdownTask`'s
|
|
10540
10872
|
* own `task.fail`, sent right before `create-daemon.ts`'s
|
|
10541
10873
|
* `performControlShutdown` calls this) could still be sitting UNSENT in
|
|
10542
10874
|
* `this.outbox` — mid long-poll retry backoff, or simply not yet picked up
|
|
10543
10875
|
* by the fire-and-forget `drainOutbox()` `send()` kicked off — and this
|
|
10544
|
-
* method would happily proceed to `stopped = true`
|
|
10876
|
+
* method would happily proceed to `stopped = true` regardless,
|
|
10545
10877
|
* after which NOTHING ever drains it again: silently lost, even though
|
|
10546
10878
|
* `TaskRunner` believed it had been sent. `drainTimeoutMs` omitted (the
|
|
10547
10879
|
* default) preserves the EXACT prior behavior for every other existing
|
|
@@ -10558,9 +10890,9 @@ var ConnectionManager = class {
|
|
|
10558
10890
|
}
|
|
10559
10891
|
this.stopped = true;
|
|
10560
10892
|
this.serverCapabilities = [];
|
|
10561
|
-
if (this.wsRetryTimer) clearInterval(this.wsRetryTimer);
|
|
10562
10893
|
this.longPoll.stop();
|
|
10563
|
-
this.
|
|
10894
|
+
this.connected = false;
|
|
10895
|
+
this.opts.onStateChange?.("closed");
|
|
10564
10896
|
await this.processingChain;
|
|
10565
10897
|
await this.pendingCursorSave;
|
|
10566
10898
|
}
|
|
@@ -10585,6 +10917,10 @@ var ConnectionManager = class {
|
|
|
10585
10917
|
outboxLength() {
|
|
10586
10918
|
return this.outbox.length + this.inFlightBatchSize;
|
|
10587
10919
|
}
|
|
10920
|
+
/** A bounded terminal quarantine for operator inspection; these entries are never retried. */
|
|
10921
|
+
rejectedOutbox() {
|
|
10922
|
+
return this.rejectedOutboundEnvelopes;
|
|
10923
|
+
}
|
|
10588
10924
|
/**
|
|
10589
10925
|
* Finding F5(b): polls {@link outboxLength} (not `this.outbox.length`
|
|
10590
10926
|
* alone — see that method's own doc comment for why a spliced-out,
|
|
@@ -10592,15 +10928,14 @@ var ConnectionManager = class {
|
|
|
10592
10928
|
* a single `drainOutbox()` promise directly — a drain in progress can
|
|
10593
10929
|
* itself loop through multiple retry/backoff cycles (`drainRetryDelay`)
|
|
10594
10930
|
* while the server is unreachable, and a fresh, INDEPENDENT
|
|
10595
|
-
* `drainOutbox()` call can also be triggered concurrently (`send()
|
|
10596
|
-
*
|
|
10931
|
+
* `drainOutbox()` call can also be triggered concurrently (`send()`) —
|
|
10932
|
+
* polling the one thing
|
|
10597
10933
|
* that actually matters (is anything still undelivered) can never go
|
|
10598
10934
|
* stale the way capturing one specific in-flight promise reference
|
|
10599
10935
|
* could. Kicks off one more `drainOutbox()` attempt itself first
|
|
10600
10936
|
* (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
|
|
10937
|
+
* guard) in case nothing is currently actively retrying, so this bounded
|
|
10938
|
+
* wait isn't just passively hoping something else happens to be making
|
|
10604
10939
|
* progress.
|
|
10605
10940
|
*/
|
|
10606
10941
|
waitForOutboxDrained(timeoutMs) {
|
|
@@ -10641,21 +10976,27 @@ var ConnectionManager = class {
|
|
|
10641
10976
|
* documented idempotent (protocol §9).
|
|
10642
10977
|
*/
|
|
10643
10978
|
deliver(envelope) {
|
|
10979
|
+
if (!isServerToDaemonType(envelope.type)) {
|
|
10980
|
+
if (typeof envelope.seq === "number") this.noteValidationFailure(envelope.seq);
|
|
10981
|
+
return false;
|
|
10982
|
+
}
|
|
10644
10983
|
const tracked = isCursorEnvelopeType(envelope.type) && typeof envelope.seq === "number";
|
|
10645
10984
|
const watermark = this.dedupWatermark();
|
|
10646
|
-
if (tracked && watermark !== void 0 && envelope.seq <= watermark) return;
|
|
10985
|
+
if (tracked && watermark !== void 0 && envelope.seq <= watermark) return false;
|
|
10647
10986
|
if (tracked) {
|
|
10648
10987
|
const seq = envelope.seq;
|
|
10649
|
-
if (this.inFlightSeqs.has(seq) || this.processedSeqs.has(seq)) return;
|
|
10988
|
+
if (this.inFlightSeqs.has(seq) || this.processedSeqs.has(seq)) return false;
|
|
10650
10989
|
this.inFlightSeqs.add(seq);
|
|
10651
10990
|
this.noteDelivered(seq);
|
|
10652
10991
|
}
|
|
10653
10992
|
this.processingChain = this.processingChain.then(() => this.process(envelope, tracked));
|
|
10993
|
+
return true;
|
|
10654
10994
|
}
|
|
10655
10995
|
/**
|
|
10656
|
-
*
|
|
10657
|
-
* against
|
|
10658
|
-
*
|
|
10996
|
+
* The local watermark `deliver()` dedupes inbound `task.*` envelopes
|
|
10997
|
+
* against. It is deliberately NOT the long-poll query cursor: that query
|
|
10998
|
+
* is the kernel acknowledgement and uses only the successfully processed
|
|
10999
|
+
* `cursor` (see the constructor). Normally this local watermark is
|
|
10659
11000
|
* `deliveredSeq` — which is always >= `cursor` (every envelope that
|
|
10660
11001
|
* reaches `advanceCursor` already passed through `noteDelivered` first,
|
|
10661
11002
|
* see `deliver`) — so this is the literal `max(cursor, deliveredSeq)` the
|
|
@@ -10671,11 +11012,9 @@ var ConnectionManager = class {
|
|
|
10671
11012
|
* whose outcome wasn't known yet. No separate "reset deliveredSeq on
|
|
10672
11013
|
* reconnect" step is needed for this to be correct — collapsing to
|
|
10673
11014
|
* `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.
|
|
11015
|
+
* every long-poll retry path. NOT resetting it unconditionally on every
|
|
11016
|
+
* retry lets `deliveredSeq` keep doing its job of not re-dispatching
|
|
11017
|
+
* something already in flight while a handler is still running.
|
|
10679
11018
|
*/
|
|
10680
11019
|
dedupWatermark() {
|
|
10681
11020
|
return this.stalledAtSeq !== void 0 ? this.cursor : this.deliveredSeq ?? this.cursor;
|
|
@@ -10693,11 +11032,13 @@ var ConnectionManager = class {
|
|
|
10693
11032
|
}
|
|
10694
11033
|
await this.opts.onEnvelope(envelope);
|
|
10695
11034
|
if (!tracked) return;
|
|
10696
|
-
this.
|
|
10697
|
-
|
|
11035
|
+
if (this.stalledAtSeq !== void 0 && envelope.seq !== this.stalledAtSeq) {
|
|
11036
|
+
this.processedSeqs.add(seq);
|
|
11037
|
+
return;
|
|
11038
|
+
}
|
|
10698
11039
|
this.stalledAtSeq = void 0;
|
|
11040
|
+
await this.advanceCursor(envelope.seq);
|
|
10699
11041
|
this.processedSeqs.clear();
|
|
10700
|
-
this.advanceCursor(envelope.seq);
|
|
10701
11042
|
} catch (err) {
|
|
10702
11043
|
if (tracked && this.stalledAtSeq === void 0) this.stalledAtSeq = envelope.seq;
|
|
10703
11044
|
console.error(
|
|
@@ -10708,226 +11049,58 @@ var ConnectionManager = class {
|
|
|
10708
11049
|
if (tracked) this.inFlightSeqs.delete(seq);
|
|
10709
11050
|
}
|
|
10710
11051
|
}
|
|
10711
|
-
/**
|
|
10712
|
-
* M4 Phase 4 (version-negotiation drill fix): `LongPollClient` calls this
|
|
10713
|
-
* for a batch entry it could not parse into a known `Envelope` at all (an
|
|
10714
|
-
* unrecognized message type — mirrors `ws-transport.ts`'s identical
|
|
10715
|
-
* per-frame tolerance, see `long-poll-transport.ts`'s own doc comment on
|
|
10716
|
-
* `parseLooseEventsPollResponse`) but which still carried a numeric,
|
|
10717
|
-
* task-class envelope-level `seq` (the caller only invokes this for a
|
|
10718
|
-
* `task.`-prefixed type — see `long-poll-transport.ts`'s own
|
|
10719
|
-
* `extractSkippableSeq`; `conn.*`-shaped or type-less entries never reach
|
|
10720
|
-
* here at all, mirroring F2's "conn.* is never cursor-tracked" rule).
|
|
10721
|
-
* There is no real `Envelope` to hand to a handler — a genuinely
|
|
10722
|
-
* unrecognized type has nothing this build could ever act on.
|
|
10723
|
-
*
|
|
10724
|
-
* GATEKEEPER-CAUGHT REGRESSION (fixed here): this used to call
|
|
10725
|
-
* `advanceCursor(seq)` DIRECTLY, synchronously, the instant a skip was
|
|
10726
|
-
* detected in `LongPollClient.loop()`'s per-entry for-loop. That is NOT
|
|
10727
|
-
* "instantaneous and race-free" the way the previous version of this
|
|
10728
|
-
* comment claimed — the hazard was never the skip racing against itself,
|
|
10729
|
-
* it was the skip racing AHEAD of an EARLIER real envelope in the SAME
|
|
10730
|
-
* batch that is still in flight on `processingChain` (`deliver()`, above,
|
|
10731
|
-
* only ever CHAINS `process()` onto that promise chain — it never awaits
|
|
10732
|
-
* it before returning). Concretely, batch `[real seq1, unknown seq2]`:
|
|
10733
|
-
* `deliver(seq1)` chains `process(seq1)` but returns immediately without
|
|
10734
|
-
* running it; the for-loop then reaches `seq2` and (pre-fix) called
|
|
10735
|
-
* `advanceCursor(2)` synchronously, BEFORE `process(seq1)` had even
|
|
10736
|
-
* started, let alone failed. If `seq1`'s handler then failed,
|
|
10737
|
-
* `stalledAtSeq` became 1 — but the durable cursor was already 2, so
|
|
10738
|
-
* `dedupWatermark()` returned 2, and every future redelivery of seq1 was
|
|
10739
|
-
* dedup-dropped as "already past the cursor" forever: permanent envelope
|
|
10740
|
-
* loss, exactly the F3 bug class the whole `stalledAtSeq`/frozen-watermark
|
|
10741
|
-
* mechanism exists to prevent.
|
|
10742
|
-
*
|
|
10743
|
-
* Fix: the cursor-advancing half is now CHAINED onto `processingChain`
|
|
10744
|
-
* too, exactly like `process()`'s own post-handler bookkeeping — so it
|
|
10745
|
-
* only ever runs once every earlier envelope already queued ahead of it
|
|
10746
|
-
* has fully settled (success or failure), and can observe `stalledAtSeq`'s
|
|
10747
|
-
* REAL, up-to-date value rather than whatever it happened to be at the
|
|
10748
|
-
* instant the skip was first noticed. The guard mirrors `process()`'s own
|
|
10749
|
-
* success-path guard exactly: never advance past a still-unresolved
|
|
10750
|
-
* earlier failure, unless (degenerate, cannot really happen for a skip)
|
|
10751
|
-
* this exact seq IS the stalled one.
|
|
10752
|
-
*
|
|
10753
|
-
* `noteDelivered` (the eager, in-memory watermark) stays UNCHAINED —
|
|
10754
|
-
* called immediately, unconditionally, regardless of `stalledAtSeq` —
|
|
10755
|
-
* matching `deliver()`'s own eager, unconditional call for a real
|
|
10756
|
-
* envelope: its only job is "don't re-pull something already handed off,"
|
|
10757
|
-
* independent of outcome, and that property does not depend on FIFO
|
|
10758
|
-
* ordering the way the DURABLE cursor does.
|
|
10759
|
-
*
|
|
10760
|
-
* Deliberately NO top-level `dedupWatermark() <= seq` early-return before
|
|
10761
|
-
* queuing the chained callback (an earlier draft of this fix had one, and
|
|
10762
|
-
* it was itself subtly wrong): `deliveredSeq` can already reflect a seq
|
|
10763
|
-
* from the FIRST time it was ever seen, while the DURABLE cursor is still
|
|
10764
|
-
* behind it because a stall intervened before that seq's chained
|
|
10765
|
-
* advancement ran — a pre-check keyed on `deliveredSeq` would then
|
|
10766
|
-
* wrongly treat a LATER redelivery of the same seq (arriving once the
|
|
10767
|
-
* stall has since cleared) as "already accounted for" and never queue
|
|
10768
|
-
* another attempt, permanently stranding the cursor one seq short. Always
|
|
10769
|
-
* queuing is safe and cheap: `advanceCursor`'s own `seq <= this.cursor`
|
|
10770
|
-
* guard already makes a genuinely-redundant call a no-op, so there is no
|
|
10771
|
-
* correctness reason to short-circuit earlier, only a (here, unnecessary)
|
|
10772
|
-
* micro-optimization one.
|
|
10773
|
-
*/
|
|
10774
|
-
noteSkippedSeq(seq) {
|
|
10775
|
-
this.noteDelivered(seq);
|
|
10776
|
-
this.processingChain = this.processingChain.then(() => {
|
|
10777
|
-
if (this.stalledAtSeq === void 0 || seq === this.stalledAtSeq) {
|
|
10778
|
-
this.advanceCursor(seq);
|
|
10779
|
-
}
|
|
10780
|
-
});
|
|
10781
|
-
}
|
|
10782
|
-
/**
|
|
10783
|
-
* Finding R1 (cross-model re-review — was NOT-CLOSED against F1):
|
|
10784
|
-
* `LongPollClient` calls this for a batch entry whose `type` it
|
|
10785
|
-
* recognized but whose payload failed schema validation
|
|
10786
|
-
* ({@link EnvelopeValidationError}) — a genuine delivery failure at that
|
|
10787
|
-
* seq, unlike `noteSkippedSeq`'s forward-compat case. Deliberately mirrors
|
|
10788
|
-
* `process()`'s own catch block (`if (tracked && this.stalledAtSeq ===
|
|
10789
|
-
* undefined) this.stalledAtSeq = envelope.seq;`) as closely as possible:
|
|
10790
|
-
* the SAME "only the lowest unresolved failure holds the stall" rule, the
|
|
10791
|
-
* SAME resulting freeze of `dedupWatermark()` at the durable cursor
|
|
10792
|
-
* (protocol §9 keeps this seq alive), and — because it's the SAME
|
|
10793
|
-
* `stalledAtSeq` field `process()`'s own post-success guard already
|
|
10794
|
-
* checks — anything ELSE delivered after this seq (same batch or a later
|
|
10795
|
-
* one) is automatically held back from advancing the cursor too, with
|
|
10796
|
-
* zero changes needed to `process()` itself.
|
|
10797
|
-
*
|
|
10798
|
-
* Chained onto `processingChain` for exactly the reason `noteSkippedSeq`
|
|
10799
|
-
* documents for its own identical chaining (see that method's sibling
|
|
10800
|
-
* doc comment on `LongPollClient`, "GATEKEEPER-CAUGHT REGRESSION"): an
|
|
10801
|
-
* EARLIER real envelope in the SAME batch may still be in flight on that
|
|
10802
|
-
* FIFO chain when this is called (`deliver()` only ever chains
|
|
10803
|
-
* `process()` onto it, never awaits before returning) — mutating
|
|
10804
|
-
* `stalledAtSeq` synchronously here could race ahead of that still-
|
|
10805
|
-
* unresolved earlier envelope. Chaining instead guarantees this only
|
|
10806
|
-
* takes effect once every earlier-queued envelope has already settled,
|
|
10807
|
-
* and reads `stalledAtSeq`'s real, up-to-date value rather than whatever
|
|
10808
|
-
* it happened to be the instant the failure was first noticed.
|
|
10809
|
-
*
|
|
10810
|
-
* No `noteDelivered` call here (contrast `noteSkippedSeq`, which does
|
|
10811
|
-
* call it): a validation-failed entry never becomes a real `Envelope` and
|
|
10812
|
-
* never reaches `deliver()`, so it was never "delivered" in the eager
|
|
10813
|
-
* in-memory-watermark sense that field tracks — there is nothing for it
|
|
10814
|
-
* to eagerly mark. Once a corrected redelivery of this exact seq DOES
|
|
10815
|
-
* arrive as a real envelope, it flows through the ordinary `deliver()`
|
|
10816
|
-
* path (which calls `noteDelivered` itself) and, on success, clears the
|
|
10817
|
-
* stall via `process()`'s own existing logic — no special-casing needed.
|
|
10818
|
-
*/
|
|
11052
|
+
/** Serialized with handler completion, so invalid work cannot be acked by later success. */
|
|
10819
11053
|
noteValidationFailure(seq) {
|
|
10820
11054
|
this.processingChain = this.processingChain.then(() => {
|
|
10821
11055
|
if (this.stalledAtSeq === void 0) this.stalledAtSeq = seq;
|
|
10822
11056
|
});
|
|
10823
11057
|
}
|
|
10824
|
-
advanceCursor(seq) {
|
|
11058
|
+
async advanceCursor(seq) {
|
|
10825
11059
|
if (this.cursor !== void 0 && seq <= this.cursor) return;
|
|
11060
|
+
const save = this.pendingCursorSave.catch(() => void 0).then(() => this.opts.cursorStore.save(this.opts.serverUrl, this.opts.deviceId, seq));
|
|
11061
|
+
this.pendingCursorSave = save;
|
|
11062
|
+
await save;
|
|
10826
11063
|
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
11064
|
}
|
|
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
|
-
}
|
|
11065
|
+
quarantineRejectedOutbound(envelope, reason) {
|
|
11066
|
+
if (this.rejectedOutboundEnvelopes.length === MAX_REJECTED_OUTBOX_ENTRIES) this.rejectedOutboundEnvelopes.shift();
|
|
11067
|
+
this.rejectedOutboundEnvelopes.push({ envelope, reason });
|
|
11068
|
+
console.error(
|
|
11069
|
+
`[byok/client] outbound envelope ${envelope.id} was permanently rejected (${reason}) and moved to the terminal quarantine.`
|
|
11070
|
+
);
|
|
10865
11071
|
}
|
|
10866
11072
|
notifySettled(err) {
|
|
10867
11073
|
for (const waiter of this.settledWaiters.splice(0)) waiter(err);
|
|
10868
11074
|
}
|
|
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
|
-
}));
|
|
11075
|
+
noteConnected() {
|
|
11076
|
+
if (this.connected || this.stopped || this.revoked || this.terminalError) return;
|
|
11077
|
+
this.connected = true;
|
|
11078
|
+
this.opts.onStateChange?.("open");
|
|
10884
11079
|
this.notifySettled();
|
|
10885
|
-
|
|
10886
|
-
|
|
11080
|
+
}
|
|
11081
|
+
noteDisconnected() {
|
|
11082
|
+
if (!this.connected || this.stopped || this.revoked || this.terminalError) return;
|
|
11083
|
+
this.connected = false;
|
|
11084
|
+
this.opts.onStateChange?.("connecting");
|
|
10887
11085
|
}
|
|
10888
11086
|
enterReplayCursorTooOld(error) {
|
|
10889
11087
|
if (this.terminalError) return;
|
|
10890
11088
|
this.terminalError = error;
|
|
10891
11089
|
this.stopped = true;
|
|
10892
11090
|
this.serverCapabilities = [];
|
|
10893
|
-
if (this.wsRetryTimer) clearInterval(this.wsRetryTimer);
|
|
10894
11091
|
this.longPoll.stop();
|
|
10895
|
-
this.
|
|
10896
|
-
this.ws.close();
|
|
11092
|
+
this.connected = false;
|
|
10897
11093
|
this.cancelPendingDrainRetry?.();
|
|
10898
11094
|
this.notifySettled(error);
|
|
10899
11095
|
this.opts.onStateChange?.("closed");
|
|
10900
11096
|
this.opts.onTerminalError?.(error);
|
|
10901
11097
|
}
|
|
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
11098
|
enterRevoked() {
|
|
10926
11099
|
if (this.revoked) return;
|
|
10927
11100
|
this.revoked = true;
|
|
10928
|
-
|
|
11101
|
+
this.serverCapabilities = [];
|
|
10929
11102
|
this.longPoll.stop();
|
|
10930
|
-
this.
|
|
11103
|
+
this.connected = false;
|
|
10931
11104
|
this.cancelPendingDrainRetry?.();
|
|
10932
11105
|
this.opts.onStateChange?.("revoked");
|
|
10933
11106
|
this.notifySettled(new DeviceRevokedError());
|
|
@@ -11263,11 +11436,12 @@ var DaemonObserver = class {
|
|
|
11263
11436
|
* where those are actually reported from.
|
|
11264
11437
|
*/
|
|
11265
11438
|
handleInboundEnvelope(envelope) {
|
|
11266
|
-
if (envelope.type
|
|
11267
|
-
const
|
|
11439
|
+
if (!isTaskOfferType(envelope.type)) return;
|
|
11440
|
+
const offer = envelope;
|
|
11441
|
+
const taskId = offer.task_id;
|
|
11268
11442
|
if (this.taskInfo.has(taskId)) return;
|
|
11269
|
-
this.upsertTask(taskId, { state: "Offered", runtime:
|
|
11270
|
-
this.emit({ kind: "offered", ts: nowIso(), taskId, runtime:
|
|
11443
|
+
this.upsertTask(taskId, { state: "Offered", runtime: offer.payload.runtime });
|
|
11444
|
+
this.emit({ kind: "offered", ts: nowIso(), taskId, runtime: offer.payload.runtime });
|
|
11271
11445
|
}
|
|
11272
11446
|
/**
|
|
11273
11447
|
* Feed a raw OUTBOUND (daemon -> server) envelope — this is where every
|
|
@@ -11796,6 +11970,7 @@ CREATE INDEX IF NOT EXISTS journal_transition_task ON journal_transition (task_i
|
|
|
11796
11970
|
CREATE TABLE IF NOT EXISTS journal_terminal (
|
|
11797
11971
|
task_id TEXT PRIMARY KEY REFERENCES journal_task (task_id),
|
|
11798
11972
|
terminal_type TEXT NOT NULL,
|
|
11973
|
+
bytes TEXT NOT NULL,
|
|
11799
11974
|
payload_hash TEXT NOT NULL,
|
|
11800
11975
|
truth_state TEXT NOT NULL,
|
|
11801
11976
|
attempt INTEGER NOT NULL,
|
|
@@ -11950,6 +12125,11 @@ var SqliteLocalTaskJournal = class {
|
|
|
11950
12125
|
const quarantinePath = quarantineDatabase(options.storeDir, this.#dbPath, reason, this.#clock());
|
|
11951
12126
|
throw new JournalCorruptError(this.#dbPath, quarantinePath, reason, { cause: err });
|
|
11952
12127
|
}
|
|
12128
|
+
const terminalColumns = db.prepare("PRAGMA table_info(journal_terminal)").all();
|
|
12129
|
+
if (!terminalColumns.some((column) => column.name === "bytes")) {
|
|
12130
|
+
db.close();
|
|
12131
|
+
throw new JournalUnavailableError("hash-only journal format cannot replay terminals; preserve/export this journal and resolve it before starting the new execution-recovery format");
|
|
12132
|
+
}
|
|
11953
12133
|
this.#db = db;
|
|
11954
12134
|
try {
|
|
11955
12135
|
secureJournalFilePermissions(this.#dbPath);
|
|
@@ -12039,10 +12219,13 @@ var SqliteLocalTaskJournal = class {
|
|
|
12039
12219
|
}
|
|
12040
12220
|
return this.#enqueue("appendEnvelope", () => {
|
|
12041
12221
|
this.#fault("append:before-begin");
|
|
12042
|
-
|
|
12222
|
+
const committed = this.#transaction(() => {
|
|
12043
12223
|
const existing = this.#db.prepare("SELECT receipt FROM journal_idempotency WHERE scope = ? AND key = ?").get("envelope", record3.envelopeId);
|
|
12044
12224
|
if (existing) {
|
|
12045
12225
|
const prior = JSON.parse(existing.receipt);
|
|
12226
|
+
if (prior.bytesHash !== record3.bytesHash) throw new Error("journal envelope identity collision");
|
|
12227
|
+
const priorIdentity = this.#db.prepare("SELECT tenant_id, product_id, device_id FROM journal_envelope WHERE envelope_id = ?").get(record3.envelopeId);
|
|
12228
|
+
this.#assertIdentity(priorIdentity, record3.identity);
|
|
12046
12229
|
return { ...prior, created: false };
|
|
12047
12230
|
}
|
|
12048
12231
|
const committedAt = this.#clock().toISOString();
|
|
@@ -12064,6 +12247,16 @@ var SqliteLocalTaskJournal = class {
|
|
|
12064
12247
|
);
|
|
12065
12248
|
this.#fault("append:after-envelope");
|
|
12066
12249
|
if (record3.opensTask && record3.taskId !== void 0) {
|
|
12250
|
+
const task = this.#db.prepare("SELECT * FROM journal_task WHERE task_id = ?").get(record3.taskId);
|
|
12251
|
+
if (task) {
|
|
12252
|
+
this.#assertIdentity(task, record3.identity);
|
|
12253
|
+
const original = this.#db.prepare("SELECT bytes FROM journal_envelope WHERE envelope_id = ?").get(task.envelope_id);
|
|
12254
|
+
const oldOffer = JSON.parse(original.bytes);
|
|
12255
|
+
const newOffer = JSON.parse(record3.bytes);
|
|
12256
|
+
if (oldOffer.type !== newOffer.type || JSON.stringify(oldOffer.payload) !== JSON.stringify(newOffer.payload)) {
|
|
12257
|
+
throw new Error("journal task identity reused with a different execution contract");
|
|
12258
|
+
}
|
|
12259
|
+
}
|
|
12067
12260
|
this.#db.prepare(
|
|
12068
12261
|
`INSERT OR IGNORE INTO journal_task
|
|
12069
12262
|
(task_id, envelope_id, tenant_id, product_id, device_id, seq, local_state, created_at, updated_at)
|
|
@@ -12092,6 +12285,8 @@ var SqliteLocalTaskJournal = class {
|
|
|
12092
12285
|
this.#fault("append:before-commit");
|
|
12093
12286
|
return { ...receipt, created: true };
|
|
12094
12287
|
});
|
|
12288
|
+
this.#fault("append:after-commit");
|
|
12289
|
+
return committed;
|
|
12095
12290
|
});
|
|
12096
12291
|
}
|
|
12097
12292
|
recordAdmission(record3) {
|
|
@@ -12162,17 +12357,22 @@ var SqliteLocalTaskJournal = class {
|
|
|
12162
12357
|
*/
|
|
12163
12358
|
recordTerminal(record3) {
|
|
12164
12359
|
return this.#enqueue("recordTerminal", () => {
|
|
12360
|
+
if (typeof record3.bytes !== "string" || journalHash(record3.bytes) !== record3.payloadHash) {
|
|
12361
|
+
throw new Error("terminal bytes do not match their canonical hash");
|
|
12362
|
+
}
|
|
12363
|
+
if (byteLength2(record3.bytes) > this.#maxRecordBytes) {
|
|
12364
|
+
throw new JournalRecordTooLargeError("terminal bytes", byteLength2(record3.bytes), this.#maxRecordBytes);
|
|
12365
|
+
}
|
|
12165
12366
|
this.#transaction(() => {
|
|
12166
12367
|
this.#requireTask(record3.taskId, "recordTerminal");
|
|
12167
|
-
const existing = this.#db.prepare("SELECT
|
|
12168
|
-
if (existing
|
|
12169
|
-
this.#db.prepare(
|
|
12170
|
-
|
|
12171
|
-
|
|
12172
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
|
12173
|
-
).run(
|
|
12368
|
+
const existing = this.#db.prepare("SELECT * FROM journal_terminal WHERE task_id = ?").get(record3.taskId);
|
|
12369
|
+
if (!existing) {
|
|
12370
|
+
this.#db.prepare(`INSERT INTO journal_terminal
|
|
12371
|
+
(task_id, terminal_type, bytes, payload_hash, truth_state, attempt, last_error, recorded_at, updated_at)
|
|
12372
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(
|
|
12174
12373
|
record3.taskId,
|
|
12175
12374
|
record3.terminalType,
|
|
12375
|
+
record3.bytes,
|
|
12176
12376
|
record3.payloadHash,
|
|
12177
12377
|
record3.truthState,
|
|
12178
12378
|
record3.attempt,
|
|
@@ -12181,11 +12381,98 @@ var SqliteLocalTaskJournal = class {
|
|
|
12181
12381
|
record3.recordedAt
|
|
12182
12382
|
);
|
|
12183
12383
|
this.#db.prepare("UPDATE journal_task SET local_state = ?, updated_at = ? WHERE task_id = ?").run(`terminal:${record3.terminalType}`, record3.recordedAt, record3.taskId);
|
|
12184
|
-
} else if (existing.payload_hash === record3.payloadHash) {
|
|
12384
|
+
} else if (existing.payload_hash === record3.payloadHash && existing.bytes === record3.bytes && existing.truth_state !== "confirmed") {
|
|
12185
12385
|
this.#db.prepare("UPDATE journal_terminal SET truth_state = ?, attempt = ?, last_error = ?, updated_at = ? WHERE task_id = ?").run(record3.truthState, record3.attempt, record3.lastError ?? null, record3.recordedAt, record3.taskId);
|
|
12186
12386
|
}
|
|
12387
|
+
if (record3.recovery !== void 0 && !existing) {
|
|
12388
|
+
this.#db.prepare("UPDATE journal_task SET recovery_marker = ?, updated_at = ? WHERE task_id = ?").run(JSON.stringify({ ...record3.recovery, occurredAt: record3.recovery.occurredAt ?? record3.recordedAt }), record3.recordedAt, record3.taskId);
|
|
12389
|
+
this.#fault("recovery:before-commit");
|
|
12390
|
+
}
|
|
12187
12391
|
this.#fault("terminal:before-commit");
|
|
12188
12392
|
});
|
|
12393
|
+
if (record3.recovery !== void 0) this.#fault("recovery:after-commit");
|
|
12394
|
+
this.#fault("terminal:after-commit");
|
|
12395
|
+
});
|
|
12396
|
+
}
|
|
12397
|
+
#assertIdentity(row, identity) {
|
|
12398
|
+
if (row.tenant_id !== identity.tenantId || row.product_id !== identity.productId || row.device_id !== identity.deviceId) {
|
|
12399
|
+
throw new Error("journal enrollment identity mismatch; refusing cross-enrollment execution or terminal replay");
|
|
12400
|
+
}
|
|
12401
|
+
}
|
|
12402
|
+
#taskRow(row) {
|
|
12403
|
+
if (typeof row.envelope_bytes !== "string" || journalHash(row.envelope_bytes) !== row.envelope_hash) {
|
|
12404
|
+
throw new Error("durable offer bytes/hash mismatch");
|
|
12405
|
+
}
|
|
12406
|
+
return {
|
|
12407
|
+
taskId: row.task_id,
|
|
12408
|
+
envelopeId: row.envelope_id,
|
|
12409
|
+
seq: Number(row.seq),
|
|
12410
|
+
identity: { tenantId: row.tenant_id, productId: row.product_id, deviceId: row.device_id },
|
|
12411
|
+
localState: row.recovery_marker !== null && !String(row.local_state).startsWith("terminal:") ? "interrupted" : row.local_state,
|
|
12412
|
+
...row.claimed_runtime == null ? {} : { claimedRuntime: row.claimed_runtime },
|
|
12413
|
+
...row.workspace_ref == null ? {} : { workspaceRef: row.workspace_ref },
|
|
12414
|
+
updatedAt: row.updated_at,
|
|
12415
|
+
envelopeBytes: row.envelope_bytes
|
|
12416
|
+
};
|
|
12417
|
+
}
|
|
12418
|
+
readTask(taskId, identity) {
|
|
12419
|
+
return this.#enqueue("readTask", () => {
|
|
12420
|
+
const row = this.#db.prepare(`SELECT t.*, e.bytes AS envelope_bytes, e.bytes_hash AS envelope_hash FROM journal_task t
|
|
12421
|
+
JOIN journal_envelope e ON e.envelope_id = t.envelope_id WHERE t.task_id = ?`).get(taskId);
|
|
12422
|
+
if (!row) return void 0;
|
|
12423
|
+
this.#assertIdentity(row, identity);
|
|
12424
|
+
return this.#taskRow(row);
|
|
12425
|
+
});
|
|
12426
|
+
}
|
|
12427
|
+
listRecoveryTasks(identity) {
|
|
12428
|
+
return this.#enqueue("listRecoveryTasks", () => {
|
|
12429
|
+
const rows = this.#db.prepare(`SELECT t.*, e.bytes AS envelope_bytes, e.bytes_hash AS envelope_hash FROM journal_task t
|
|
12430
|
+
JOIN journal_envelope e ON e.envelope_id = t.envelope_id
|
|
12431
|
+
LEFT JOIN journal_terminal term ON term.task_id = t.task_id
|
|
12432
|
+
WHERE term.task_id IS NULL ORDER BY t.seq, t.task_id`).all();
|
|
12433
|
+
return rows.map((row) => {
|
|
12434
|
+
this.#assertIdentity(row, identity);
|
|
12435
|
+
return this.#taskRow(row);
|
|
12436
|
+
});
|
|
12437
|
+
});
|
|
12438
|
+
}
|
|
12439
|
+
listPendingTerminals(identity) {
|
|
12440
|
+
return this.#enqueue("listPendingTerminals", () => {
|
|
12441
|
+
const rows = this.#db.prepare(`SELECT term.*, t.tenant_id, t.product_id, t.device_id FROM journal_terminal term
|
|
12442
|
+
JOIN journal_task t ON t.task_id = term.task_id WHERE term.truth_state <> 'confirmed'
|
|
12443
|
+
ORDER BY term.recorded_at, term.task_id`).all();
|
|
12444
|
+
return rows.map((row) => {
|
|
12445
|
+
this.#assertIdentity(row, identity);
|
|
12446
|
+
if (typeof row.bytes !== "string" || journalHash(row.bytes) !== row.payload_hash) throw new Error("durable terminal bytes/hash mismatch");
|
|
12447
|
+
return {
|
|
12448
|
+
taskId: row.task_id,
|
|
12449
|
+
terminalType: row.terminal_type,
|
|
12450
|
+
bytes: row.bytes,
|
|
12451
|
+
payloadHash: row.payload_hash,
|
|
12452
|
+
truthState: row.truth_state,
|
|
12453
|
+
attempt: Number(row.attempt),
|
|
12454
|
+
recordedAt: row.recorded_at,
|
|
12455
|
+
...row.last_error === null ? {} : { lastError: row.last_error }
|
|
12456
|
+
};
|
|
12457
|
+
});
|
|
12458
|
+
});
|
|
12459
|
+
}
|
|
12460
|
+
confirmTerminal(taskId, payloadHash) {
|
|
12461
|
+
return this.#terminalDisposition(taskId, payloadHash, "confirmed");
|
|
12462
|
+
}
|
|
12463
|
+
rejectTerminal(taskId, payloadHash, reason) {
|
|
12464
|
+
return this.#terminalDisposition(taskId, payloadHash, "failed", reason);
|
|
12465
|
+
}
|
|
12466
|
+
#terminalDisposition(taskId, payloadHash, state, reason) {
|
|
12467
|
+
return this.#enqueue("terminalDisposition", () => {
|
|
12468
|
+
this.#transaction(() => {
|
|
12469
|
+
const row = this.#db.prepare("SELECT payload_hash FROM journal_terminal WHERE task_id = ?").get(taskId);
|
|
12470
|
+
if (!row || row.payload_hash !== payloadHash) throw new Error("terminal acknowledgement does not match durable terminal");
|
|
12471
|
+
this.#db.prepare(`UPDATE journal_terminal SET truth_state = ?, last_error = ?, updated_at = ?
|
|
12472
|
+
WHERE task_id = ? AND truth_state <> 'confirmed'`).run(state, reason ?? null, this.#clock().toISOString(), taskId);
|
|
12473
|
+
this.#fault("confirm:before-commit");
|
|
12474
|
+
});
|
|
12475
|
+
this.#fault("confirm:after-commit");
|
|
12189
12476
|
});
|
|
12190
12477
|
}
|
|
12191
12478
|
/**
|
|
@@ -12202,8 +12489,9 @@ var SqliteLocalTaskJournal = class {
|
|
|
12202
12489
|
return this.#enqueue("listRecoverable", () => {
|
|
12203
12490
|
const rows = this.#db.prepare(
|
|
12204
12491
|
`SELECT t.task_id, t.envelope_id, t.seq, t.tenant_id, t.product_id, t.device_id,
|
|
12205
|
-
t.local_state, t.claimed_runtime, t.workspace_ref, t.updated_at
|
|
12492
|
+
t.local_state, t.claimed_runtime, t.workspace_ref, t.updated_at, e.bytes AS envelope_bytes, e.bytes_hash AS envelope_hash
|
|
12206
12493
|
FROM journal_task t
|
|
12494
|
+
JOIN journal_envelope e ON e.envelope_id = t.envelope_id
|
|
12207
12495
|
LEFT JOIN journal_terminal term ON term.task_id = t.task_id
|
|
12208
12496
|
WHERE t.recovery_marker IS NULL
|
|
12209
12497
|
AND term.task_id IS NULL
|
|
@@ -12225,7 +12513,8 @@ var SqliteLocalTaskJournal = class {
|
|
|
12225
12513
|
localState: row.local_state,
|
|
12226
12514
|
...claimedRuntime === null ? {} : { claimedRuntime },
|
|
12227
12515
|
...workspaceRef === null ? {} : { workspaceRef },
|
|
12228
|
-
updatedAt: row.updated_at
|
|
12516
|
+
updatedAt: row.updated_at,
|
|
12517
|
+
envelopeBytes: row.envelope_bytes
|
|
12229
12518
|
};
|
|
12230
12519
|
});
|
|
12231
12520
|
});
|
|
@@ -12882,6 +13171,204 @@ function createStatfsFreeBytesProvider(dir) {
|
|
|
12882
13171
|
return Number(stats.bavail) * Number(stats.bsize);
|
|
12883
13172
|
};
|
|
12884
13173
|
}
|
|
13174
|
+
var DEFAULT_MAX_INLINE_EVENT_BYTES = 64 * 1024;
|
|
13175
|
+
var MIN_MAX_INLINE_EVENT_BYTES = 4096;
|
|
13176
|
+
var MAX_UNSTORED_REASON_BYTES = 512;
|
|
13177
|
+
var MAX_SPILL_DESCRIPTOR_BYTES = 768;
|
|
13178
|
+
function byteLength3(value) {
|
|
13179
|
+
return Buffer.byteLength(value, "utf8");
|
|
13180
|
+
}
|
|
13181
|
+
function safeStringify(value) {
|
|
13182
|
+
try {
|
|
13183
|
+
return JSON.stringify(value);
|
|
13184
|
+
} catch {
|
|
13185
|
+
return void 0;
|
|
13186
|
+
}
|
|
13187
|
+
}
|
|
13188
|
+
function utf8Width(codePoint) {
|
|
13189
|
+
if (codePoint < 128) return 1;
|
|
13190
|
+
if (codePoint < 2048) return 2;
|
|
13191
|
+
if (codePoint < 65536) return 3;
|
|
13192
|
+
return 4;
|
|
13193
|
+
}
|
|
13194
|
+
function utf8Prefix(value, maxBytes) {
|
|
13195
|
+
if (maxBytes <= 0) return "";
|
|
13196
|
+
let bytes = 0;
|
|
13197
|
+
let index = 0;
|
|
13198
|
+
while (index < value.length) {
|
|
13199
|
+
const codePoint = value.codePointAt(index);
|
|
13200
|
+
const width = utf8Width(codePoint);
|
|
13201
|
+
if (bytes + width > maxBytes) break;
|
|
13202
|
+
bytes += width;
|
|
13203
|
+
index += codePoint > 65535 ? 2 : 1;
|
|
13204
|
+
}
|
|
13205
|
+
return value.slice(0, index);
|
|
13206
|
+
}
|
|
13207
|
+
function utf8Suffix(value, maxBytes) {
|
|
13208
|
+
if (maxBytes <= 0) return "";
|
|
13209
|
+
let bytes = 0;
|
|
13210
|
+
let index = value.length;
|
|
13211
|
+
while (index > 0) {
|
|
13212
|
+
let step = 1;
|
|
13213
|
+
const last = value.charCodeAt(index - 1);
|
|
13214
|
+
if (last >= 56320 && last <= 57343 && index >= 2) {
|
|
13215
|
+
const previous = value.charCodeAt(index - 2);
|
|
13216
|
+
if (previous >= 55296 && previous <= 56319) step = 2;
|
|
13217
|
+
}
|
|
13218
|
+
const codePoint = value.codePointAt(index - step);
|
|
13219
|
+
const width = utf8Width(codePoint);
|
|
13220
|
+
if (bytes + width > maxBytes) break;
|
|
13221
|
+
bytes += width;
|
|
13222
|
+
index -= step;
|
|
13223
|
+
}
|
|
13224
|
+
return value.slice(index);
|
|
13225
|
+
}
|
|
13226
|
+
function boundReasonText(raw) {
|
|
13227
|
+
const collapsed = raw.replace(/\s+/g, " ").trim();
|
|
13228
|
+
const fallback = "blob upload failed with a non-descriptive error";
|
|
13229
|
+
if (collapsed.length === 0) return fallback;
|
|
13230
|
+
let budget = MAX_UNSTORED_REASON_BYTES;
|
|
13231
|
+
for (; ; ) {
|
|
13232
|
+
const reason = utf8Prefix(collapsed, budget);
|
|
13233
|
+
if (reason.length === 0) return fallback;
|
|
13234
|
+
const encoded = byteLength3(JSON.stringify(reason)) - 2;
|
|
13235
|
+
if (encoded <= MAX_UNSTORED_REASON_BYTES) return reason;
|
|
13236
|
+
budget = Math.min(Math.floor(budget * MAX_UNSTORED_REASON_BYTES / encoded), budget - 1);
|
|
13237
|
+
}
|
|
13238
|
+
}
|
|
13239
|
+
function boundedUnstoredReason(error) {
|
|
13240
|
+
const raw = error instanceof Error && error.message.length > 0 ? `${error.name}: ${error.message}` : String(error);
|
|
13241
|
+
return boundReasonText(raw);
|
|
13242
|
+
}
|
|
13243
|
+
async function spillOversizedEvent(event, deps) {
|
|
13244
|
+
if (event.type !== "tool_use" && event.type !== "tool_result") return event;
|
|
13245
|
+
const serializedEvent = safeStringify(event);
|
|
13246
|
+
if (serializedEvent === void 0) {
|
|
13247
|
+
deps.log?.(
|
|
13248
|
+
`task ${deps.taskId}: ${event.type} event cannot be JSON-serialized (a BigInt, a cycle, or a throwing toJSON in its payload); forwarding it unchanged and unbounded`
|
|
13249
|
+
);
|
|
13250
|
+
return event;
|
|
13251
|
+
}
|
|
13252
|
+
if (byteLength3(serializedEvent) <= deps.maxInlineBytes) return event;
|
|
13253
|
+
const spillable = event;
|
|
13254
|
+
const field = spillable.type === "tool_use" ? "input" : "output";
|
|
13255
|
+
const value = spillable.type === "tool_use" ? spillable.input : spillable.output;
|
|
13256
|
+
if (value === void 0) {
|
|
13257
|
+
deps.log?.(
|
|
13258
|
+
`task ${deps.taskId}: ${spillable.type} event is ${byteLength3(serializedEvent)} bytes, over the ${deps.maxInlineBytes}-byte inline cap, but carries no ${field} to spill; forwarding unchanged`
|
|
13259
|
+
);
|
|
13260
|
+
return event;
|
|
13261
|
+
}
|
|
13262
|
+
const serialized = safeStringify(value);
|
|
13263
|
+
if (serialized === void 0) {
|
|
13264
|
+
deps.log?.(
|
|
13265
|
+
`task ${deps.taskId}: ${spillable.type}.${field} is not JSON-serializable; forwarding the event unchanged`
|
|
13266
|
+
);
|
|
13267
|
+
return event;
|
|
13268
|
+
}
|
|
13269
|
+
const totalBytes = byteLength3(serialized);
|
|
13270
|
+
const skeleton = safeStringify({ ...spillable, [field]: { preview: { head: "", tail: "" } } });
|
|
13271
|
+
if (skeleton === void 0) {
|
|
13272
|
+
deps.log?.(
|
|
13273
|
+
`task ${deps.taskId}: ${spillable.type} event cannot be JSON-serialized once its ${field} is replaced by a preview; forwarding it unchanged and unbounded`
|
|
13274
|
+
);
|
|
13275
|
+
return event;
|
|
13276
|
+
}
|
|
13277
|
+
if (byteLength3(skeleton) + MAX_SPILL_DESCRIPTOR_BYTES > deps.maxInlineBytes) {
|
|
13278
|
+
deps.log?.(
|
|
13279
|
+
`task ${deps.taskId}: ${spillable.type} event exceeds the ${deps.maxInlineBytes}-byte inline cap independently of its ${field} (${byteLength3(skeleton)} bytes without it, plus up to ${MAX_SPILL_DESCRIPTOR_BYTES} bytes of spill descriptor); forwarding unchanged`
|
|
13280
|
+
);
|
|
13281
|
+
return event;
|
|
13282
|
+
}
|
|
13283
|
+
const digest4 = createHash("sha256").update(serialized, "utf8").digest("hex");
|
|
13284
|
+
let stored;
|
|
13285
|
+
let storedBlobId;
|
|
13286
|
+
try {
|
|
13287
|
+
const blob = await deps.blobClient.uploadArtifact(serialized, "application/json", {
|
|
13288
|
+
idempotencyKey: `spill_${deps.taskId}_${digest4}`,
|
|
13289
|
+
...deps.signal === void 0 ? {} : { signal: deps.signal }
|
|
13290
|
+
});
|
|
13291
|
+
stored = { blob };
|
|
13292
|
+
storedBlobId = blob.blobId;
|
|
13293
|
+
} catch (error) {
|
|
13294
|
+
const unstoredReason = boundedUnstoredReason(error);
|
|
13295
|
+
stored = { unstoredReason };
|
|
13296
|
+
deps.log?.(
|
|
13297
|
+
`task ${deps.taskId}: ${totalBytes}-byte ${spillable.type}.${field} could not be stored, only a preview will reach the wire \u2014 ${unstoredReason}`
|
|
13298
|
+
);
|
|
13299
|
+
}
|
|
13300
|
+
const descriptorFor = (omittedBytes, half) => ({
|
|
13301
|
+
field,
|
|
13302
|
+
totalBytes,
|
|
13303
|
+
omittedBytes,
|
|
13304
|
+
contentType: "application/json",
|
|
13305
|
+
...half
|
|
13306
|
+
});
|
|
13307
|
+
const bound = (half) => {
|
|
13308
|
+
const overheadJson = safeStringify({
|
|
13309
|
+
...spillable,
|
|
13310
|
+
[field]: { preview: { head: "", tail: "" } },
|
|
13311
|
+
spill: descriptorFor(totalBytes, half)
|
|
13312
|
+
});
|
|
13313
|
+
if (overheadJson === void 0) return void 0;
|
|
13314
|
+
const overhead = byteLength3(overheadJson);
|
|
13315
|
+
let headBudget = Math.max(0, Math.ceil((deps.maxInlineBytes - overhead) / 2));
|
|
13316
|
+
let tailBudget = Math.max(0, Math.floor((deps.maxInlineBytes - overhead) / 2));
|
|
13317
|
+
for (; ; ) {
|
|
13318
|
+
const head = utf8Prefix(serialized, headBudget);
|
|
13319
|
+
const tail = utf8Suffix(serialized.slice(head.length), tailBudget);
|
|
13320
|
+
const retained = byteLength3(head) + byteLength3(tail);
|
|
13321
|
+
const result = {
|
|
13322
|
+
...spillable,
|
|
13323
|
+
[field]: { preview: { head, tail } },
|
|
13324
|
+
spill: descriptorFor(totalBytes - retained, half)
|
|
13325
|
+
};
|
|
13326
|
+
const resultJson = safeStringify(result);
|
|
13327
|
+
if (resultJson === void 0) return void 0;
|
|
13328
|
+
const resultBytes = byteLength3(resultJson);
|
|
13329
|
+
if (resultBytes <= deps.maxInlineBytes) return { result, resultBytes, overhead };
|
|
13330
|
+
const headBytes = byteLength3(head);
|
|
13331
|
+
const tailBytes = byteLength3(tail);
|
|
13332
|
+
if (headBytes === 0 && tailBytes === 0) return { result, resultBytes, overhead };
|
|
13333
|
+
const excess = resultBytes - deps.maxInlineBytes;
|
|
13334
|
+
const dropHead = headBytes === 0 ? 0 : Math.min(headBytes, Math.max(1, Math.ceil(excess / 2)));
|
|
13335
|
+
const dropTail = tailBytes === 0 ? 0 : Math.min(tailBytes, Math.max(dropHead === 0 ? 1 : 0, excess - dropHead));
|
|
13336
|
+
if (dropHead === 0 && dropTail === 0) return { result, resultBytes, overhead };
|
|
13337
|
+
headBudget = headBytes - dropHead;
|
|
13338
|
+
tailBudget = tailBytes - dropTail;
|
|
13339
|
+
}
|
|
13340
|
+
};
|
|
13341
|
+
let bounded2 = bound(stored);
|
|
13342
|
+
if (bounded2 === void 0) {
|
|
13343
|
+
deps.log?.(
|
|
13344
|
+
`task ${deps.taskId}: ${spillable.type} event cannot be JSON-serialized once its ${field} is replaced by a preview and a spill descriptor; forwarding it unchanged and unbounded`
|
|
13345
|
+
);
|
|
13346
|
+
return event;
|
|
13347
|
+
}
|
|
13348
|
+
if (bounded2.resultBytes > deps.maxInlineBytes && storedBlobId !== void 0) {
|
|
13349
|
+
const unstoredReason = boundReasonText(
|
|
13350
|
+
`spill locator too large for maxInlineBytes: blobId ${storedBlobId.length} chars`
|
|
13351
|
+
);
|
|
13352
|
+
deps.log?.(
|
|
13353
|
+
`task ${deps.taskId}: ${totalBytes}-byte ${spillable.type}.${field} was stored, but its spill locator does not fit the ${deps.maxInlineBytes}-byte inline cap (descriptor overhead ${bounded2.overhead} bytes); shipping the preview with unstoredReason instead \u2014 ${unstoredReason}`
|
|
13354
|
+
);
|
|
13355
|
+
stored = { unstoredReason };
|
|
13356
|
+
storedBlobId = void 0;
|
|
13357
|
+
bounded2 = bound(stored);
|
|
13358
|
+
if (bounded2 === void 0) {
|
|
13359
|
+
deps.log?.(
|
|
13360
|
+
`task ${deps.taskId}: ${spillable.type} event cannot be JSON-serialized once its ${field} is replaced by a preview and a spill descriptor; forwarding it unchanged and unbounded`
|
|
13361
|
+
);
|
|
13362
|
+
return event;
|
|
13363
|
+
}
|
|
13364
|
+
}
|
|
13365
|
+
if (bounded2.resultBytes > deps.maxInlineBytes) {
|
|
13366
|
+
throw new Error(
|
|
13367
|
+
`event spill failed to bound a ${spillable.type} event: replacement is ${bounded2.resultBytes} bytes against a ${deps.maxInlineBytes}-byte cap (${field} was ${totalBytes} bytes, descriptor overhead ${bounded2.overhead} bytes)`
|
|
13368
|
+
);
|
|
13369
|
+
}
|
|
13370
|
+
return bounded2.result;
|
|
13371
|
+
}
|
|
12885
13372
|
|
|
12886
13373
|
// src/daemon/environment.ts
|
|
12887
13374
|
var BASE_PLATFORM_ALLOWLIST = [
|
|
@@ -14261,6 +14748,7 @@ function resultDocumentRejectionDetail(check) {
|
|
|
14261
14748
|
}
|
|
14262
14749
|
}
|
|
14263
14750
|
var DEFAULT_MAX_TASK_OUTPUT_BYTES = 64 * 1024 * 1024;
|
|
14751
|
+
var DEFAULT_MAX_CONCURRENT_MUTABLE_SESSIONS_PER_AGENT_HOME = 1;
|
|
14264
14752
|
function isKnownRuntimeId(id) {
|
|
14265
14753
|
return RuntimeIdSchema.safeParse(id).success;
|
|
14266
14754
|
}
|
|
@@ -14507,6 +14995,14 @@ var TaskRunner = class {
|
|
|
14507
14995
|
get maxTaskOutputBytes() {
|
|
14508
14996
|
return this.deps.maxTaskOutputBytes ?? DEFAULT_MAX_TASK_OUTPUT_BYTES;
|
|
14509
14997
|
}
|
|
14998
|
+
/** Effective per-event inline ceiling for this daemon — see `DaemonConfig.maxInlineEventBytes`. */
|
|
14999
|
+
get maxInlineEventBytes() {
|
|
15000
|
+
return this.deps.maxInlineEventBytes ?? DEFAULT_MAX_INLINE_EVENT_BYTES;
|
|
15001
|
+
}
|
|
15002
|
+
/** WP0: effective per-canonical-Agent-home Attempt cap — see {@link DEFAULT_MAX_CONCURRENT_MUTABLE_SESSIONS_PER_AGENT_HOME}. */
|
|
15003
|
+
get maxConcurrentMutableSessionsPerAgentHome() {
|
|
15004
|
+
return this.deps.maxConcurrentMutableSessionsPerAgentHome ?? DEFAULT_MAX_CONCURRENT_MUTABLE_SESSIONS_PER_AGENT_HOME;
|
|
15005
|
+
}
|
|
14510
15006
|
/**
|
|
14511
15007
|
* M4 Phase 4 (part B.3, observability): per-active-task queue watermarks
|
|
14512
15008
|
* for the control socket's `status` result — see
|
|
@@ -14896,6 +15392,24 @@ var TaskRunner = class {
|
|
|
14896
15392
|
decline("strict Agent-only daemon refuses legacy task offers", false);
|
|
14897
15393
|
return;
|
|
14898
15394
|
}
|
|
15395
|
+
if (agentRef !== void 0) {
|
|
15396
|
+
const limit = this.maxConcurrentMutableSessionsPerAgentHome;
|
|
15397
|
+
let canonicalHome;
|
|
15398
|
+
try {
|
|
15399
|
+
canonicalHome = await this.deps.agentHome.layout.canonicalHomePath(agentRef);
|
|
15400
|
+
} catch (error) {
|
|
15401
|
+
decline(
|
|
15402
|
+
`Agent home admission failed: ${errorMessage6(error)}`,
|
|
15403
|
+
!(error instanceof AgentHomeResolutionError)
|
|
15404
|
+
);
|
|
15405
|
+
return;
|
|
15406
|
+
}
|
|
15407
|
+
const active2 = this.deps.agentHome.executionLeaseManager.activeAttemptCount(canonicalHome);
|
|
15408
|
+
if (active2 >= limit) {
|
|
15409
|
+
decline(`agent home busy: ${active2} active attempt(s)`, true);
|
|
15410
|
+
return;
|
|
15411
|
+
}
|
|
15412
|
+
}
|
|
14899
15413
|
const guarded = this.deps.admissionGuard?.({ taskId, payload });
|
|
14900
15414
|
if (guarded !== void 0 && !guarded.admit) {
|
|
14901
15415
|
decline(guarded.reason, guarded.retryable);
|
|
@@ -15165,6 +15679,7 @@ var TaskRunner = class {
|
|
|
15165
15679
|
},
|
|
15166
15680
|
forwardedEnvironmentNames: Object.freeze(Object.keys(env).sort())
|
|
15167
15681
|
});
|
|
15682
|
+
await this.deps.beforeClaim?.(taskId, manifest.descriptor.id);
|
|
15168
15683
|
this.deps.send(
|
|
15169
15684
|
createEnvelope(
|
|
15170
15685
|
"task.claim",
|
|
@@ -15651,9 +16166,17 @@ var TaskRunner = class {
|
|
|
15651
16166
|
}
|
|
15652
16167
|
async pump(active) {
|
|
15653
16168
|
try {
|
|
15654
|
-
for await (
|
|
16169
|
+
for await (let event of active.session.events) {
|
|
15655
16170
|
if (this.tasks.get(active.taskId) !== active || active.beingTornDown) return;
|
|
15656
16171
|
if (this.tasks.get(active.taskId) !== active) return;
|
|
16172
|
+
event = await spillOversizedEvent(event, {
|
|
16173
|
+
maxInlineBytes: this.maxInlineEventBytes,
|
|
16174
|
+
blobClient: this.deps.blobClient,
|
|
16175
|
+
taskId: active.taskId,
|
|
16176
|
+
signal: active.blobAbort.signal,
|
|
16177
|
+
log: (message) => console.error(`[byok/client] ${message}`)
|
|
16178
|
+
});
|
|
16179
|
+
if (this.tasks.get(active.taskId) !== active || active.beingTornDown) return;
|
|
15657
16180
|
active.outputBytesSoFar += estimateEventBytes(event);
|
|
15658
16181
|
if (active.outputBytesSoFar > this.maxTaskOutputBytes) {
|
|
15659
16182
|
active.batcher.flush();
|
|
@@ -18573,6 +19096,7 @@ function delay2(ms) {
|
|
|
18573
19096
|
});
|
|
18574
19097
|
}
|
|
18575
19098
|
function terminalKindOf(type) {
|
|
19099
|
+
if (type === "task.decline") return "declined";
|
|
18576
19100
|
if (type === "task.complete") return "complete";
|
|
18577
19101
|
if (type === "task.fail") return "failed";
|
|
18578
19102
|
if (type === "task.cancelled") return "cancelled";
|
|
@@ -18588,7 +19112,7 @@ function toJournalEnvelopeRecord(envelope, identity) {
|
|
|
18588
19112
|
bytes,
|
|
18589
19113
|
bytesHash: journalHash(bytes),
|
|
18590
19114
|
receivedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
18591
|
-
opensTask: envelope.type
|
|
19115
|
+
opensTask: isTaskOfferType(envelope.type)
|
|
18592
19116
|
};
|
|
18593
19117
|
}
|
|
18594
19118
|
function isRuntimeId(id) {
|
|
@@ -18607,7 +19131,7 @@ async function detectRuntimes(adapters) {
|
|
|
18607
19131
|
}
|
|
18608
19132
|
return runtimes;
|
|
18609
19133
|
}
|
|
18610
|
-
function computeCapabilities(adapters, agentHomeConfigured = false, strictAgentOnly = false, agentHomeProjectionConfigured = false, agentEgressConfigured = false, contentReadPolicies) {
|
|
19134
|
+
function computeCapabilities(adapters, agentHomeConfigured = false, strictAgentOnly = false, agentHomeProjectionConfigured = false, agentEgressConfigured = false, contentReadPolicies, providerProfileBindingConfigured = false) {
|
|
18611
19135
|
const flags = [];
|
|
18612
19136
|
if (adapters.some((adapter) => adapter.descriptor.capabilities.steer)) flags.push("steer");
|
|
18613
19137
|
flags.push("blob-upload");
|
|
@@ -18619,6 +19143,9 @@ function computeCapabilities(adapters, agentHomeConfigured = false, strictAgentO
|
|
|
18619
19143
|
if (selectionAdapters.length > 0 && selectionAdapters.every((adapter) => adapter.descriptor.supportsDispatchSelection === true)) {
|
|
18620
19144
|
flags.push("dispatch-selection");
|
|
18621
19145
|
}
|
|
19146
|
+
if (providerProfileBindingConfigured && adapters.some((adapter) => adapter.descriptor.id === "pi" && adapter.descriptor.supportsDispatchSelection)) {
|
|
19147
|
+
flags.push(PROVIDER_PROFILE_BINDING_CAPABILITY);
|
|
19148
|
+
}
|
|
18622
19149
|
if (adapters.some((adapter) => adapter.descriptor.capabilities.mcpToolsets === true)) {
|
|
18623
19150
|
flags.push("toolset-selection");
|
|
18624
19151
|
}
|
|
@@ -18774,6 +19301,11 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
18774
19301
|
`DaemonConfig.maxTaskOutputBytes must be a positive number (or omitted to use the ${DEFAULT_MAX_TASK_OUTPUT_BYTES}-byte default) \u2014 got ${config.maxTaskOutputBytes}. Pass Number.POSITIVE_INFINITY to explicitly disable the cap; 0 or a negative number is rejected rather than silently treated as "disabled".`
|
|
18775
19302
|
);
|
|
18776
19303
|
}
|
|
19304
|
+
if (config.maxInlineEventBytes !== void 0 && !(Number.isSafeInteger(config.maxInlineEventBytes) && config.maxInlineEventBytes >= MIN_MAX_INLINE_EVENT_BYTES)) {
|
|
19305
|
+
throw new Error(
|
|
19306
|
+
`DaemonConfig.maxInlineEventBytes must be a safe integer of at least ${MIN_MAX_INLINE_EVENT_BYTES} (or omitted to use the ${DEFAULT_MAX_INLINE_EVENT_BYTES}-byte default) \u2014 got ${config.maxInlineEventBytes}. A smaller cap cannot hold the spill descriptor that documents the omission, and there is no "disabled" value: an unbounded tool_use.input/tool_result.output is what this cap exists to prevent.`
|
|
19307
|
+
);
|
|
19308
|
+
}
|
|
18777
19309
|
validateProgressBatcherOptions(config.progressBatch);
|
|
18778
19310
|
if (config.agentEgress !== void 0 && config.agentHome === void 0) {
|
|
18779
19311
|
throw new Error("DaemonConfig.agentEgress requires DaemonConfig.agentHome for the per-Agent local spool");
|
|
@@ -18781,6 +19313,12 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
18781
19313
|
if (config.strictAgentOnly === true && config.agentHome === void 0) {
|
|
18782
19314
|
throw new Error("DaemonConfig.strictAgentOnly requires DaemonConfig.agentHome");
|
|
18783
19315
|
}
|
|
19316
|
+
if (config.maxConcurrentMutableSessionsPerAgentHome !== void 0 && !(Number.isSafeInteger(config.maxConcurrentMutableSessionsPerAgentHome) && config.maxConcurrentMutableSessionsPerAgentHome > 0)) {
|
|
19317
|
+
throw new Error(
|
|
19318
|
+
`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".`
|
|
19319
|
+
);
|
|
19320
|
+
}
|
|
19321
|
+
const agentHomeAttemptLimit = config.maxConcurrentMutableSessionsPerAgentHome ?? DEFAULT_MAX_CONCURRENT_MUTABLE_SESSIONS_PER_AGENT_HOME;
|
|
18784
19322
|
const egressPolicy = resolveAgentEgressPolicy(config.agentEgress?.policy);
|
|
18785
19323
|
const egressBatcherOptions = egressPolicy.activity.mode === "contentful-trajectory" ? {
|
|
18786
19324
|
...config.progressBatch,
|
|
@@ -19062,19 +19600,31 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
19062
19600
|
`[byok/client] WARNING: permissionDefaults.workspaceRoot ("${config.permissionDefaults.workspaceRoot}") is configured, but no bundled runtime adapter (pi/claude/codex) enforces PermissionPolicy.workspaceRoot \u2014 every adapter confines a task to ctx.workspaceDir instead. This ceiling value has no enforcement effect; see docs/security.md's "Workspace confinement is a convention, not a sandbox" section.`
|
|
19063
19601
|
);
|
|
19064
19602
|
}
|
|
19065
|
-
|
|
19066
|
-
|
|
19067
|
-
|
|
19068
|
-
|
|
19069
|
-
|
|
19070
|
-
|
|
19603
|
+
const journalIdentity = activeJournal ? { tenantId: record3.tenantId, productId: config.productId, deviceId: record3.deviceId } : void 0;
|
|
19604
|
+
if (activeJournal && journalIdentity) {
|
|
19605
|
+
const durableCursor = await cursorStore.load(config.serverUrl, record3.deviceId) ?? 0;
|
|
19606
|
+
for (const task of await activeJournal.listRecoveryTasks(journalIdentity)) {
|
|
19607
|
+
if (task.localState === "received" && task.seq > durableCursor) continue;
|
|
19608
|
+
const offer = decodeEnvelope(task.envelopeBytes);
|
|
19609
|
+
if (!isTaskOfferType(offer.type) || offer.task_id !== task.taskId) throw new Error("invalid durable recovery offer binding");
|
|
19610
|
+
const payload = offer.payload;
|
|
19611
|
+
const terminal = createEnvelope("task.fail", {
|
|
19612
|
+
reason: "daemon_interrupted",
|
|
19613
|
+
retryable: false,
|
|
19614
|
+
...payload.agentRef === void 0 ? {} : { agentRef: payload.agentRef }
|
|
19615
|
+
}, { taskId: task.taskId });
|
|
19616
|
+
const bytes = encodeEnvelope(terminal);
|
|
19617
|
+
await activeJournal.recordTerminal({
|
|
19618
|
+
taskId: task.taskId,
|
|
19619
|
+
terminalType: "failed",
|
|
19620
|
+
bytes,
|
|
19621
|
+
payloadHash: journalHash(bytes),
|
|
19622
|
+
truthState: "pending",
|
|
19623
|
+
attempt: 1,
|
|
19624
|
+
recordedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19625
|
+
recovery: { disposition: "interrupted", reason: "daemon_interrupted" }
|
|
19071
19626
|
});
|
|
19072
19627
|
}
|
|
19073
|
-
if (recoverable.length > 0) {
|
|
19074
|
-
console.warn(
|
|
19075
|
-
`[byok/client] local journal recovery: ${recoverable.length} task(s) were interrupted by the previous shutdown and have been marked (not resumed, not deleted): ${recoverable.map((task) => task.taskId).join(", ")}`
|
|
19076
|
-
);
|
|
19077
|
-
}
|
|
19078
19628
|
}
|
|
19079
19629
|
if (activeOwnedPressureEngine) {
|
|
19080
19630
|
await activeOwnedPressureEngine.tick();
|
|
@@ -19101,7 +19651,8 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
19101
19651
|
config.strictAgentOnly === true,
|
|
19102
19652
|
agentHomeManager?.supportsTaskFreeProjection() === true,
|
|
19103
19653
|
config.agentEgress !== void 0,
|
|
19104
|
-
agentContentReadPolicies
|
|
19654
|
+
agentContentReadPolicies,
|
|
19655
|
+
config.piByokLauncher !== void 0
|
|
19105
19656
|
);
|
|
19106
19657
|
const agentHomeProjectionCompletion = agentHomeManager?.supportsTaskFreeProjection() === true ? new AgentHomeProjectionCompletionClient({
|
|
19107
19658
|
serverUrl: config.serverUrl,
|
|
@@ -19109,35 +19660,64 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
19109
19660
|
tenantId: record3.tenantId,
|
|
19110
19661
|
deviceId: record3.deviceId
|
|
19111
19662
|
}) : void 0;
|
|
19112
|
-
const
|
|
19113
|
-
const sendSanitizedEnvelope = activeJournal && journalIdentity ? (envelope) => {
|
|
19114
|
-
observer.handleOutboundEnvelope(envelope);
|
|
19663
|
+
const sendSanitizedEnvelope = (envelope) => {
|
|
19115
19664
|
const terminalKind = terminalKindOf(envelope.type);
|
|
19116
|
-
|
|
19117
|
-
|
|
19665
|
+
if (!activeJournal || !journalIdentity || terminalKind === void 0 || envelope.task_id === void 0) {
|
|
19666
|
+
observer.handleOutboundEnvelope(envelope);
|
|
19118
19667
|
connection?.send(envelope);
|
|
19119
19668
|
return;
|
|
19120
19669
|
}
|
|
19121
|
-
const
|
|
19122
|
-
|
|
19123
|
-
|
|
19124
|
-
|
|
19125
|
-
|
|
19126
|
-
|
|
19127
|
-
|
|
19128
|
-
|
|
19129
|
-
|
|
19130
|
-
|
|
19131
|
-
|
|
19132
|
-
|
|
19133
|
-
|
|
19134
|
-
|
|
19135
|
-
|
|
19136
|
-
|
|
19670
|
+
const taskId = envelope.task_id;
|
|
19671
|
+
const bytes = encodeEnvelope(envelope);
|
|
19672
|
+
journalTerminalTail = journalTerminalTail.catch(() => void 0).then(async () => {
|
|
19673
|
+
try {
|
|
19674
|
+
await activeJournal.recordTerminal({
|
|
19675
|
+
taskId,
|
|
19676
|
+
terminalType: terminalKind,
|
|
19677
|
+
bytes,
|
|
19678
|
+
payloadHash: journalHash(bytes),
|
|
19679
|
+
truthState: "pending",
|
|
19680
|
+
attempt: 1,
|
|
19681
|
+
recordedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
19682
|
+
});
|
|
19683
|
+
const terminal = (await activeJournal.listPendingTerminals(journalIdentity)).find((row) => row.taskId === taskId);
|
|
19684
|
+
if (!terminal || terminal.truthState !== "pending") return;
|
|
19685
|
+
const durable = decodeEnvelope(terminal.bytes);
|
|
19686
|
+
overrides.executionRecoveryFault?.("terminal:before-send");
|
|
19687
|
+
observer.handleOutboundEnvelope(durable);
|
|
19688
|
+
connection?.send(durable);
|
|
19689
|
+
} catch (error) {
|
|
19690
|
+
if (!(error instanceof JournalRecordTooLargeError)) throw error;
|
|
19691
|
+
if (!activeJournal || !journalIdentity) throw error;
|
|
19692
|
+
const task = await activeJournal.readTask(taskId, journalIdentity);
|
|
19693
|
+
if (!task) throw new Error(`missing durable task ${taskId}`);
|
|
19694
|
+
const offer = decodeEnvelope(task.envelopeBytes);
|
|
19695
|
+
if (!isTaskOfferType(offer.type) || offer.task_id !== taskId) throw new Error(`invalid durable offer binding for ${taskId}`);
|
|
19696
|
+
const offerPayload = offer.payload;
|
|
19697
|
+
const failure = createEnvelope("task.fail", {
|
|
19698
|
+
reason: "terminal_result_too_large",
|
|
19699
|
+
retryable: false,
|
|
19700
|
+
...offerPayload.agentRef === void 0 ? {} : { agentRef: offerPayload.agentRef }
|
|
19701
|
+
}, { taskId });
|
|
19702
|
+
const failureBytes = encodeEnvelope(failure);
|
|
19703
|
+
await activeJournal.recordTerminal({
|
|
19704
|
+
taskId,
|
|
19705
|
+
terminalType: "failed",
|
|
19706
|
+
bytes: failureBytes,
|
|
19707
|
+
payloadHash: journalHash(failureBytes),
|
|
19708
|
+
truthState: "pending",
|
|
19709
|
+
attempt: 1,
|
|
19710
|
+
recordedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
19711
|
+
});
|
|
19712
|
+
const durableFailure = (await activeJournal.listPendingTerminals(journalIdentity)).find((row) => row.taskId === taskId);
|
|
19713
|
+
if (!durableFailure || durableFailure.truthState !== "pending") return;
|
|
19714
|
+
const decoded = decodeEnvelope(durableFailure.bytes);
|
|
19715
|
+
observer.handleOutboundEnvelope(decoded);
|
|
19716
|
+
connection?.send(decoded);
|
|
19717
|
+
}
|
|
19718
|
+
}).catch((error) => {
|
|
19719
|
+
console.error("[byok/client] terminal durability failed; execution remains unsettled", error);
|
|
19137
19720
|
});
|
|
19138
|
-
} : (envelope) => {
|
|
19139
|
-
observer.handleOutboundEnvelope(envelope);
|
|
19140
|
-
connection?.send(envelope);
|
|
19141
19721
|
};
|
|
19142
19722
|
const sendEnvelope = (candidate) => {
|
|
19143
19723
|
if (candidate.task_id === void 0 || runner?.usesAgentEgress(candidate.task_id) !== true) {
|
|
@@ -19160,6 +19740,8 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
19160
19740
|
workspaceRoot: config.workspaceRoot,
|
|
19161
19741
|
...agentHomeManager === void 0 ? {} : { agentHome: agentHomeManager },
|
|
19162
19742
|
...config.strictAgentOnly === true ? { strictAgentOnly: true } : {},
|
|
19743
|
+
// WP0: already validated up front — see `DaemonConfig.maxConcurrentMutableSessionsPerAgentHome`.
|
|
19744
|
+
maxConcurrentMutableSessionsPerAgentHome: agentHomeAttemptLimit,
|
|
19163
19745
|
...agentSessionHandoffs === void 0 ? {} : { agentSessionHandoffs },
|
|
19164
19746
|
deviceId: record3.deviceId,
|
|
19165
19747
|
// M5: see `DaemonConfig.runtimeEnvironment`'s own doc comment above.
|
|
@@ -19174,6 +19756,11 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
19174
19756
|
// before is the entire integration; `task-runner.ts` itself is
|
|
19175
19757
|
// untouched. See `observer.ts`'s module doc comment.
|
|
19176
19758
|
send: sendEnvelope,
|
|
19759
|
+
...activeJournal === void 0 ? {} : {
|
|
19760
|
+
beforeClaim: async (taskId, runtime) => {
|
|
19761
|
+
await activeJournal.recordAdmission({ taskId, admitted: true, claimedRuntime: runtime, decidedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
19762
|
+
}
|
|
19763
|
+
},
|
|
19177
19764
|
blobClient,
|
|
19178
19765
|
batcherOptions: egressBatcherOptions,
|
|
19179
19766
|
agentEgress,
|
|
@@ -19212,6 +19799,8 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
19212
19799
|
shutdownInterruptTimeoutMs: overrides.shutdown?.taskInterruptTimeoutMs,
|
|
19213
19800
|
// M5 batch-3 (workstream 2): see DaemonConfig.maxTaskOutputBytes's own doc comment — already validated above.
|
|
19214
19801
|
maxTaskOutputBytes: config.maxTaskOutputBytes,
|
|
19802
|
+
// See DaemonConfig.maxInlineEventBytes's own doc comment — already validated above.
|
|
19803
|
+
maxInlineEventBytes: config.maxInlineEventBytes,
|
|
19215
19804
|
// additive-minor (`task.complete.document`): passed through verbatim,
|
|
19216
19805
|
// absent when unconfigured — see `DaemonConfig.resultDocument`'s own
|
|
19217
19806
|
// doc comment. Spread rather than assigned so an unconfigured daemon
|
|
@@ -19431,7 +20020,13 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
19431
20020
|
if (await handleAgentContentReadEnvelope(envelope)) return;
|
|
19432
20021
|
activePressureEngine?.assertAckCriticalAllowed();
|
|
19433
20022
|
await activeJournal.appendEnvelope(toJournalEnvelopeRecord(envelope, journalIdentity));
|
|
19434
|
-
|
|
20023
|
+
if (isTaskOfferType(envelope.type) && envelope.task_id !== void 0) {
|
|
20024
|
+
const task = await activeJournal.readTask(envelope.task_id, journalIdentity);
|
|
20025
|
+
if (!task) throw new Error("durable offer has no execution record");
|
|
20026
|
+
if (task.localState !== "received") return;
|
|
20027
|
+
}
|
|
20028
|
+
await runner?.handleEnvelope(envelope);
|
|
20029
|
+
await journalTerminalTail;
|
|
19435
20030
|
} : (envelope) => {
|
|
19436
20031
|
if (tenantRebinding) {
|
|
19437
20032
|
return Promise.reject(new Error("tenant enrollment is being re-paired; inbound work is blocked until restart"));
|
|
@@ -19443,17 +20038,32 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
19443
20038
|
if (envelope.type === "agent.content.read") return handleAgentContentReadEnvelope(envelope).then(() => void 0);
|
|
19444
20039
|
return runner?.handleEnvelope(envelope) ?? Promise.resolve();
|
|
19445
20040
|
},
|
|
20041
|
+
onOutboundQueued: (envelope) => {
|
|
20042
|
+
if (terminalKindOf(envelope.type) !== void 0) overrides.executionRecoveryFault?.("terminal:queued");
|
|
20043
|
+
},
|
|
20044
|
+
beforeOutboundPost: (envelopes) => {
|
|
20045
|
+
if (envelopes.some((envelope) => terminalKindOf(envelope.type) !== void 0)) overrides.executionRecoveryFault?.("outbound:before-post");
|
|
20046
|
+
},
|
|
20047
|
+
onOutboundAccepted: async (envelopes) => {
|
|
20048
|
+
if (!activeJournal) return;
|
|
20049
|
+
for (const envelope of envelopes) {
|
|
20050
|
+
if (terminalKindOf(envelope.type) === void 0 || envelope.task_id === void 0) continue;
|
|
20051
|
+
overrides.executionRecoveryFault?.("outbound:after-ack");
|
|
20052
|
+
await activeJournal.confirmTerminal(envelope.task_id, journalHash(encodeEnvelope(envelope)));
|
|
20053
|
+
}
|
|
20054
|
+
},
|
|
20055
|
+
onOutboundRejected: async (envelope) => {
|
|
20056
|
+
if (activeJournal && terminalKindOf(envelope.type) !== void 0 && envelope.task_id !== void 0) {
|
|
20057
|
+
await activeJournal.rejectTerminal(envelope.task_id, journalHash(encodeEnvelope(envelope)), "inbound_rejected");
|
|
20058
|
+
}
|
|
20059
|
+
},
|
|
19446
20060
|
onStateChange: (state) => {
|
|
19447
|
-
const wasSettled = connectionState === "open"
|
|
20061
|
+
const wasSettled = connectionState === "open";
|
|
19448
20062
|
connectionState = state;
|
|
19449
20063
|
observer.noteConnectionState(state);
|
|
19450
|
-
if (!wasSettled &&
|
|
19451
|
-
if (!wasSettled &&
|
|
20064
|
+
if (!wasSettled && state === "open") runner?.retryRecoveredAgentMessages();
|
|
20065
|
+
if (!wasSettled && state === "open") runPresenceDiscovery();
|
|
19452
20066
|
},
|
|
19453
|
-
backoff: overrides.backoff,
|
|
19454
|
-
liveness: overrides.liveness,
|
|
19455
|
-
wsFailureThreshold: overrides.longPoll?.wsFailureThreshold,
|
|
19456
|
-
wsRetryIntervalMs: overrides.longPoll?.wsRetryIntervalMs,
|
|
19457
20067
|
longPollRetryDelayMs: overrides.longPoll?.retryDelayMs,
|
|
19458
20068
|
longPollIdleDelayMs: overrides.longPoll?.idleDelayMs,
|
|
19459
20069
|
fleetJitter,
|
|
@@ -19464,8 +20074,20 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
19464
20074
|
});
|
|
19465
20075
|
}
|
|
19466
20076
|
});
|
|
20077
|
+
if (activeJournal && journalIdentity) {
|
|
20078
|
+
for (const terminal of await activeJournal.listPendingTerminals(journalIdentity)) {
|
|
20079
|
+
if (terminal.truthState !== "pending") continue;
|
|
20080
|
+
const envelope = decodeEnvelope(terminal.bytes);
|
|
20081
|
+
if (envelope.task_id !== terminal.taskId || terminalKindOf(envelope.type) !== terminal.terminalType) {
|
|
20082
|
+
throw new Error("durable terminal execution binding mismatch");
|
|
20083
|
+
}
|
|
20084
|
+
overrides.executionRecoveryFault?.("terminal:before-send");
|
|
20085
|
+
observer.handleOutboundEnvelope(envelope);
|
|
20086
|
+
connection.send(envelope);
|
|
20087
|
+
}
|
|
20088
|
+
}
|
|
19467
20089
|
await connection.start();
|
|
19468
|
-
await connection.
|
|
20090
|
+
await connection.waitForConnection();
|
|
19469
20091
|
runner.retryRecoveredAgentMessages();
|
|
19470
20092
|
for (const record4 of agentEgress.retryableReliableRecords(connection.getServerCapabilities())) {
|
|
19471
20093
|
dispatchReliableRecord(record4);
|
|
@@ -19724,7 +20346,9 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
19724
20346
|
// fine.
|
|
19725
20347
|
...storageStatus === void 0 ? {} : { storage: storageStatus },
|
|
19726
20348
|
operationalHealth: operationalHealth.snapshot(),
|
|
19727
|
-
toolsets: toolsetRegistry.status()
|
|
20349
|
+
toolsets: toolsetRegistry.status(),
|
|
20350
|
+
// WP0: same counts `Daemon.status()` reports, from the same reader.
|
|
20351
|
+
agentHomeExecution: agentHomeExecutionStatus()
|
|
19728
20352
|
};
|
|
19729
20353
|
}
|
|
19730
20354
|
async function performControlShutdown(reason) {
|
|
@@ -19849,6 +20473,11 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
19849
20473
|
if (!parsed) throw new ControlError("bad_request", "team_messages.ack requires exactly {context,throughSeq}");
|
|
19850
20474
|
return teamWorkspaces.ackMessages({ lease: decodeTeamMemberContext(parsed.context), throughSeq: parsed.throughSeq });
|
|
19851
20475
|
},
|
|
20476
|
+
"team_notifications.snapshot": (params) => {
|
|
20477
|
+
const parsed = parseTeamNotificationSnapshotParams(params);
|
|
20478
|
+
if (!parsed) throw new ControlError("bad_request", "team_notifications.snapshot requires exactly {context,afterSeq?}");
|
|
20479
|
+
return teamWorkspaces.notificationSnapshot({ lease: decodeTeamMemberContext(parsed.context), ...parsed.afterSeq === void 0 ? {} : { afterSeq: parsed.afterSeq } });
|
|
20480
|
+
},
|
|
19852
20481
|
"team_messages.inspect": (params) => {
|
|
19853
20482
|
const parsed = parseTeamMessageInspectParams(params);
|
|
19854
20483
|
if (!parsed) throw new ControlError("bad_request", "team_messages.inspect requires exactly {workspaceId,afterSeq?}");
|
|
@@ -19959,9 +20588,8 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
19959
20588
|
const minted = mintDeviceAssertion({
|
|
19960
20589
|
record: record3,
|
|
19961
20590
|
// `toHttpBase` is the one place a configured serverUrl is normalized
|
|
19962
|
-
// (
|
|
19963
|
-
//
|
|
19964
|
-
// spelling of the same deployment produce the same issuer.
|
|
20591
|
+
// (path stripped), so the issuer ignores an operator-provided path
|
|
20592
|
+
// or query.
|
|
19965
20593
|
issuer: new URL(toHttpBase(config.serverUrl)).origin,
|
|
19966
20594
|
productId: config.productId,
|
|
19967
20595
|
audience: parsed.audience,
|
|
@@ -20076,23 +20704,33 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
20076
20704
|
await binding.lease.release();
|
|
20077
20705
|
}
|
|
20078
20706
|
}
|
|
20707
|
+
function agentHomeExecutionStatus() {
|
|
20708
|
+
const summary = agentHomeManager?.executionLeaseManager.activeAttemptSummary();
|
|
20709
|
+
return {
|
|
20710
|
+
maxConcurrentMutableSessionsPerAgentHome: agentHomeAttemptLimit,
|
|
20711
|
+
activeHomes: summary?.homes ?? 0,
|
|
20712
|
+
activeAttempts: summary?.attempts ?? 0
|
|
20713
|
+
};
|
|
20714
|
+
}
|
|
20079
20715
|
function status() {
|
|
20080
20716
|
return {
|
|
20081
20717
|
localAgentRelease,
|
|
20082
20718
|
paired: auth.deviceId !== void 0,
|
|
20083
20719
|
connected: connectionState === "open",
|
|
20084
|
-
degraded: connection?.isTransportDegraded() ?? false,
|
|
20085
20720
|
revoked: connection?.isRevoked() ?? auth.isRevoked(),
|
|
20086
20721
|
deviceId: auth.deviceId,
|
|
20087
20722
|
activeTaskCount: runner?.activeTaskCount ?? 0,
|
|
20088
20723
|
branding: config.branding,
|
|
20089
20724
|
operationalHealth: operationalHealth.snapshot(),
|
|
20090
20725
|
toolsets: toolsetRegistry.status(),
|
|
20091
|
-
egress: agentEgress.status()
|
|
20726
|
+
egress: agentEgress.status(),
|
|
20727
|
+
agentHomeExecution: agentHomeExecutionStatus()
|
|
20092
20728
|
};
|
|
20093
20729
|
}
|
|
20094
20730
|
function reloadMcpToolsets(mcpToolsets, expectedRevision) {
|
|
20095
|
-
|
|
20731
|
+
const receipt = toolsetRegistry.reload(mcpToolsets, expectedRevision);
|
|
20732
|
+
if (daemonStarted) connection?.refreshHello();
|
|
20733
|
+
return receipt;
|
|
20096
20734
|
}
|
|
20097
20735
|
function reportMcpToolsetObservation(toolsetId, expectedDefinitionRevision, observation) {
|
|
20098
20736
|
toolsetRegistry.report(toolsetId, expectedDefinitionRevision, observation);
|