@byok-sdk/client 0.13.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 +80 -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 +359 -28
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/pi/events.d.ts +1 -1
- 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/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 +9305 -7988
- 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 +6 -100
- package/dist/daemon/control-protocol.d.ts +2 -0
- package/dist/daemon/create-daemon.d.ts +32 -0
- 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 +2 -50
- package/dist/daemon/task-runner.d.ts +13 -0
- package/dist/daemon/team-workspace.d.ts +9 -0
- package/dist/index.js +897 -241
- package/dist/index.js.map +1 -1
- package/package.json +8 -5
package/dist/index.js
CHANGED
|
@@ -2,11 +2,11 @@ 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';
|
|
@@ -14,7 +14,130 @@ import { promisify } from 'util';
|
|
|
14
14
|
import * as fs14 from 'fs/promises';
|
|
15
15
|
import { createRequire } from 'module';
|
|
16
16
|
|
|
17
|
-
|
|
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
|
+
});
|
|
18
141
|
var tmpSeq = 0;
|
|
19
142
|
async function atomicWriteFile(filePath, data, options = {}) {
|
|
20
143
|
const tmpPath = `${filePath}.${process.pid}-${tmpSeq++}.tmp`;
|
|
@@ -2378,6 +2501,9 @@ function parseTeamMessageReadParams(value) {
|
|
|
2378
2501
|
if (!isRecord2(value) || !exactKeys(value, ["context", "afterSeq"]) || !contextString(value.context) || value.afterSeq !== void 0 && !safeInteger(value.afterSeq)) return void 0;
|
|
2379
2502
|
return { context: value.context, ...value.afterSeq === void 0 ? {} : { afterSeq: value.afterSeq } };
|
|
2380
2503
|
}
|
|
2504
|
+
function parseTeamNotificationSnapshotParams(value) {
|
|
2505
|
+
return parseTeamMessageReadParams(value);
|
|
2506
|
+
}
|
|
2381
2507
|
function parseTeamMessageAckParams(value) {
|
|
2382
2508
|
if (!isRecord2(value) || !exactKeys(value, ["context", "throughSeq"]) || !contextString(value.context) || !safeInteger(value.throughSeq)) return void 0;
|
|
2383
2509
|
return { context: value.context, throughSeq: value.throughSeq };
|
|
@@ -4038,6 +4164,55 @@ function withOwnedProcessTree(options) {
|
|
|
4038
4164
|
...process.platform === "win32" ? { windowsHide: true } : { detached: true }
|
|
4039
4165
|
};
|
|
4040
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
|
+
}
|
|
4041
4216
|
function positivePid(child, label) {
|
|
4042
4217
|
const pid = child.pid;
|
|
4043
4218
|
if (pid === void 0) return void 0;
|
|
@@ -4249,21 +4424,8 @@ async function disposeOwnedProcessTree(options) {
|
|
|
4249
4424
|
var STDERR_RING_CAPACITY = 20;
|
|
4250
4425
|
var DIALOG_UI_METHODS = /* @__PURE__ */ new Set(["select", "confirm", "input", "editor"]);
|
|
4251
4426
|
var PiRpcClient = class {
|
|
4252
|
-
child;
|
|
4253
|
-
buffer = "";
|
|
4254
|
-
nextId = 1;
|
|
4255
|
-
pending = /* @__PURE__ */ new Map();
|
|
4256
|
-
eventQueue = new AsyncQueue();
|
|
4257
|
-
closed = false;
|
|
4258
|
-
exitError;
|
|
4259
|
-
closedPromise;
|
|
4260
|
-
resolveClosed;
|
|
4261
|
-
disposalAttempt;
|
|
4262
|
-
/** 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`. */
|
|
4263
|
-
stderrRing = [];
|
|
4264
|
-
/** Count of pi RPC message types `PiSession` (pi-adapter.ts) has told us have no `AgentEvent` mapping and aren't routine bookkeeping — see `recordUnmappedFrame`. */
|
|
4265
|
-
unmappedFrameCounts = /* @__PURE__ */ new Map();
|
|
4266
4427
|
constructor(options) {
|
|
4428
|
+
this.options = options;
|
|
4267
4429
|
const spawnFn = options.spawnFn ?? spawn;
|
|
4268
4430
|
this.child = spawnFn(options.command, options.args, withOwnedProcessTree({
|
|
4269
4431
|
cwd: options.cwd,
|
|
@@ -4273,6 +4435,9 @@ var PiRpcClient = class {
|
|
|
4273
4435
|
this.closedPromise = new Promise((resolve) => {
|
|
4274
4436
|
this.resolveClosed = resolve;
|
|
4275
4437
|
});
|
|
4438
|
+
this.adopted = this.adoptOwnedTree(options);
|
|
4439
|
+
this.adopted.catch(() => {
|
|
4440
|
+
});
|
|
4276
4441
|
this.child.stdout.setEncoding("utf8");
|
|
4277
4442
|
this.child.stdout.on("data", (chunk) => this.onData(chunk));
|
|
4278
4443
|
this.child.stderr.setEncoding("utf8");
|
|
@@ -4284,10 +4449,37 @@ var PiRpcClient = class {
|
|
|
4284
4449
|
this.onClosed(err);
|
|
4285
4450
|
});
|
|
4286
4451
|
}
|
|
4287
|
-
|
|
4288
|
-
|
|
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;
|
|
4289
4481
|
if (this.closed) {
|
|
4290
|
-
|
|
4482
|
+
throw this.exitError ?? new Error("pi process is closed");
|
|
4291
4483
|
}
|
|
4292
4484
|
const id = command.id ?? `req-${this.nextId++}`;
|
|
4293
4485
|
const full = { ...command, id };
|
|
@@ -4360,6 +4552,32 @@ var PiRpcClient = class {
|
|
|
4360
4552
|
label: "pi"
|
|
4361
4553
|
};
|
|
4362
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
|
+
}
|
|
4363
4581
|
onData(chunk) {
|
|
4364
4582
|
this.buffer += chunk;
|
|
4365
4583
|
let newlineIndex = this.buffer.indexOf("\n");
|
|
@@ -4378,6 +4596,7 @@ var PiRpcClient = class {
|
|
|
4378
4596
|
} catch {
|
|
4379
4597
|
return;
|
|
4380
4598
|
}
|
|
4599
|
+
this.options.onFrame?.(msg);
|
|
4381
4600
|
if (msg.type === "response" && typeof msg.id === "string" && this.pending.has(msg.id)) {
|
|
4382
4601
|
const waiter = this.pending.get(msg.id);
|
|
4383
4602
|
this.pending.delete(msg.id);
|
|
@@ -4385,11 +4604,20 @@ var PiRpcClient = class {
|
|
|
4385
4604
|
return;
|
|
4386
4605
|
}
|
|
4387
4606
|
if (msg.type === "extension_ui_request" && typeof msg.id === "string" && typeof msg.method === "string") {
|
|
4388
|
-
this.
|
|
4607
|
+
if (this.options.extensionUi?.mode === "hold") this.options.extensionUi.onRequest(msg);
|
|
4608
|
+
else this.respondToExtensionUiRequest(msg.id, msg.method);
|
|
4389
4609
|
return;
|
|
4390
4610
|
}
|
|
4391
4611
|
this.eventQueue.push(msg);
|
|
4392
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
|
+
}
|
|
4393
4621
|
/**
|
|
4394
4622
|
* Answer pi's extension-UI blocking protocol headlessly (rpc.md's
|
|
4395
4623
|
* "Extension UI Protocol"). Fail-closed policy, stated explicitly because
|
|
@@ -4443,9 +4671,10 @@ var PiRpcClient = class {
|
|
|
4443
4671
|
onClosed(err) {
|
|
4444
4672
|
if (this.closed) return;
|
|
4445
4673
|
this.closed = true;
|
|
4446
|
-
this.
|
|
4674
|
+
const terminal = this.adoptionFailure ?? err;
|
|
4675
|
+
this.exitError = terminal;
|
|
4447
4676
|
this.resolveClosed();
|
|
4448
|
-
for (const [, waiter] of this.pending) waiter.reject(
|
|
4677
|
+
for (const [, waiter] of this.pending) waiter.reject(terminal);
|
|
4449
4678
|
this.pending.clear();
|
|
4450
4679
|
this.eventQueue.end();
|
|
4451
4680
|
}
|
|
@@ -4479,6 +4708,8 @@ var PROVIDER_CREDENTIAL_ENV_DENY_NAMES = [
|
|
|
4479
4708
|
"TOGETHER_API_KEY",
|
|
4480
4709
|
"BASETEN_API_KEY",
|
|
4481
4710
|
"KIMI_API_KEY",
|
|
4711
|
+
"HF_TOKEN",
|
|
4712
|
+
"MOONSHOT_API_KEY",
|
|
4482
4713
|
"MINIMAX_API_KEY",
|
|
4483
4714
|
"MINIMAX_CN_API_KEY",
|
|
4484
4715
|
"QWEN_TOKEN_PLAN_API_KEY",
|
|
@@ -5300,6 +5531,10 @@ var ClaudeProcessClient = class {
|
|
|
5300
5531
|
closedPromise;
|
|
5301
5532
|
resolveClosed;
|
|
5302
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;
|
|
5303
5538
|
stderrRing = [];
|
|
5304
5539
|
unmappedFrameCounts = /* @__PURE__ */ new Map();
|
|
5305
5540
|
sessionId;
|
|
@@ -5314,6 +5549,9 @@ var ClaudeProcessClient = class {
|
|
|
5314
5549
|
this.closedPromise = new Promise((resolve) => {
|
|
5315
5550
|
this.resolveClosed = resolve;
|
|
5316
5551
|
});
|
|
5552
|
+
this.adopted = this.adoptOwnedTree(options);
|
|
5553
|
+
this.adopted.catch(() => {
|
|
5554
|
+
});
|
|
5317
5555
|
this.child.stdout.setEncoding("utf8");
|
|
5318
5556
|
this.child.stdout.on("data", (chunk) => this.onData(chunk));
|
|
5319
5557
|
this.child.stderr.setEncoding("utf8");
|
|
@@ -5357,9 +5595,10 @@ var ClaudeProcessClient = class {
|
|
|
5357
5595
|
* same id; if the process already closed before init ever arrived,
|
|
5358
5596
|
* every call rejects with that same exit error.
|
|
5359
5597
|
*/
|
|
5360
|
-
waitForInit() {
|
|
5361
|
-
|
|
5362
|
-
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");
|
|
5363
5602
|
return new Promise((resolve, reject) => {
|
|
5364
5603
|
this.initWaiter = { resolve, reject };
|
|
5365
5604
|
});
|
|
@@ -5423,6 +5662,32 @@ var ClaudeProcessClient = class {
|
|
|
5423
5662
|
label: "claude"
|
|
5424
5663
|
};
|
|
5425
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
|
+
}
|
|
5426
5691
|
onData(chunk) {
|
|
5427
5692
|
this.buffer += chunk;
|
|
5428
5693
|
let newlineIndex = this.buffer.indexOf("\n");
|
|
@@ -5471,9 +5736,10 @@ var ClaudeProcessClient = class {
|
|
|
5471
5736
|
onClosed(err) {
|
|
5472
5737
|
if (this.closed) return;
|
|
5473
5738
|
this.closed = true;
|
|
5474
|
-
this.
|
|
5739
|
+
const terminal = this.adoptionFailure ?? err;
|
|
5740
|
+
this.exitError = terminal;
|
|
5475
5741
|
this.resolveClosed();
|
|
5476
|
-
this.initWaiter?.reject(
|
|
5742
|
+
this.initWaiter?.reject(terminal);
|
|
5477
5743
|
this.initWaiter = void 0;
|
|
5478
5744
|
this.eventQueue.end();
|
|
5479
5745
|
}
|
|
@@ -6340,6 +6606,19 @@ var CodexProcessRunner = class {
|
|
|
6340
6606
|
closedPromise;
|
|
6341
6607
|
resolveClosed;
|
|
6342
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 = [];
|
|
6343
6622
|
constructor(options) {
|
|
6344
6623
|
this.onEvent = options.onEvent;
|
|
6345
6624
|
const spawnFn = options.spawnFn ?? spawn;
|
|
@@ -6351,6 +6630,9 @@ var CodexProcessRunner = class {
|
|
|
6351
6630
|
this.closedPromise = new Promise((resolve) => {
|
|
6352
6631
|
this.resolveClosed = resolve;
|
|
6353
6632
|
});
|
|
6633
|
+
this.adopted = this.adoptOwnedTree(options);
|
|
6634
|
+
this.adopted.catch(() => {
|
|
6635
|
+
});
|
|
6354
6636
|
this.child.stdout.setEncoding("utf8");
|
|
6355
6637
|
this.child.stdout.on("data", (chunk) => this.onData(chunk));
|
|
6356
6638
|
this.child.stderr.setEncoding("utf8");
|
|
@@ -6416,8 +6698,58 @@ var CodexProcessRunner = class {
|
|
|
6416
6698
|
label: "codex"
|
|
6417
6699
|
};
|
|
6418
6700
|
}
|
|
6419
|
-
/**
|
|
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
|
+
*/
|
|
6420
6751
|
buildExitError(context) {
|
|
6752
|
+
if (this.adoptionFailure !== void 0) return this.adoptionFailure;
|
|
6421
6753
|
const parts = [`${context} (exit code=${this.exitCode}, signal=${this.exitSignal})`];
|
|
6422
6754
|
if (this.stderrRing.length > 0) {
|
|
6423
6755
|
parts.push(`stderr: ${this.stderrRing.join(" | ")}`);
|
|
@@ -6445,7 +6777,7 @@ var CodexProcessRunner = class {
|
|
|
6445
6777
|
if (!parsed || typeof parsed !== "object" || typeof parsed.type !== "string") {
|
|
6446
6778
|
return;
|
|
6447
6779
|
}
|
|
6448
|
-
this.
|
|
6780
|
+
this.deliver(parsed);
|
|
6449
6781
|
}
|
|
6450
6782
|
onStderr(chunk) {
|
|
6451
6783
|
for (const rawLine of chunk.split("\n")) {
|
|
@@ -9478,6 +9810,28 @@ var LocalTeamWorkspace = class {
|
|
|
9478
9810
|
return Object.freeze({ accepted: true, durable: true, workspaceId: state.definition.workspaceId, memberId: input.lease.memberId, seq, messageId: message.messageId, message: publicValue });
|
|
9479
9811
|
});
|
|
9480
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
|
+
}
|
|
9481
9835
|
async readMessages(input) {
|
|
9482
9836
|
assertLeaseShape(input?.lease);
|
|
9483
9837
|
if (input?.afterSeq !== void 0) assertAfterSeq(input.afterSeq);
|
|
@@ -9893,9 +10247,10 @@ function isSafeNonnegativeInteger(value) {
|
|
|
9893
10247
|
function validateTrustedEventsPage(events, requestedCursor, pageCursor) {
|
|
9894
10248
|
let previousTaskSeq;
|
|
9895
10249
|
for (const raw of events) {
|
|
9896
|
-
if (typeof raw !== "object" || raw === null)
|
|
10250
|
+
if (typeof raw !== "object" || raw === null) throw new Error("events page contains an unidentifiable message");
|
|
9897
10251
|
const { type, seq } = raw;
|
|
9898
|
-
if (typeof type !== "string"
|
|
10252
|
+
if (typeof type !== "string") throw new Error("events page message has no authoritative type");
|
|
10253
|
+
if (type.startsWith("conn.")) continue;
|
|
9899
10254
|
if (!isSafeNonnegativeInteger(seq)) {
|
|
9900
10255
|
throw new Error("events poll task seq is not a safe nonnegative integer");
|
|
9901
10256
|
}
|
|
@@ -9913,10 +10268,10 @@ function validateTrustedEventsPage(events, requestedCursor, pageCursor) {
|
|
|
9913
10268
|
previousTaskSeq = seq;
|
|
9914
10269
|
}
|
|
9915
10270
|
}
|
|
9916
|
-
function
|
|
10271
|
+
function extractExecutableSeq(raw) {
|
|
9917
10272
|
if (typeof raw !== "object" || raw === null) return void 0;
|
|
9918
10273
|
const { type, seq } = raw;
|
|
9919
|
-
if (typeof type !== "string" ||
|
|
10274
|
+
if (typeof type !== "string" || type.startsWith("conn.")) return void 0;
|
|
9920
10275
|
return isSafeNonnegativeInteger(seq) ? seq : void 0;
|
|
9921
10276
|
}
|
|
9922
10277
|
var LongPollClient = class {
|
|
@@ -10103,27 +10458,14 @@ var LongPollClient = class {
|
|
|
10103
10458
|
try {
|
|
10104
10459
|
envelope = parseMessage(raw);
|
|
10105
10460
|
} catch (err) {
|
|
10106
|
-
|
|
10107
|
-
|
|
10108
|
-
|
|
10109
|
-
|
|
10110
|
-
|
|
10111
|
-
|
|
10112
|
-
|
|
10113
|
-
|
|
10114
|
-
if (failedSeq !== void 0) {
|
|
10115
|
-
hadValidationFailureThisBatch = true;
|
|
10116
|
-
this.opts.onValidationFailedSeq?.(failedSeq);
|
|
10117
|
-
if (!this.warnedValidationFailureSeqs.has(failedSeq)) {
|
|
10118
|
-
if (this.warnedValidationFailureSeqs.size > MAX_TRACKED_VALIDATION_FAILURE_WARNINGS) {
|
|
10119
|
-
this.warnedValidationFailureSeqs.clear();
|
|
10120
|
-
}
|
|
10121
|
-
this.warnedValidationFailureSeqs.add(failedSeq);
|
|
10122
|
-
console.warn(
|
|
10123
|
-
`[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:`,
|
|
10124
|
-
err
|
|
10125
|
-
);
|
|
10126
|
-
}
|
|
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);
|
|
10127
10469
|
}
|
|
10128
10470
|
}
|
|
10129
10471
|
continue;
|
|
@@ -10184,7 +10526,7 @@ function sleep(ms, signal) {
|
|
|
10184
10526
|
|
|
10185
10527
|
// src/daemon/connection-manager.ts
|
|
10186
10528
|
function isCursorEnvelopeType(type) {
|
|
10187
|
-
return type
|
|
10529
|
+
return isServerToDaemonType(type) && !type.startsWith("conn.");
|
|
10188
10530
|
}
|
|
10189
10531
|
function createConnectionHelloEnvelope(opts, getCursor) {
|
|
10190
10532
|
const configuredToolsets = opts.getConfiguredToolsets?.();
|
|
@@ -10224,16 +10566,7 @@ var ConnectionManager = class {
|
|
|
10224
10566
|
onPollFailure: () => this.noteDisconnected(),
|
|
10225
10567
|
onRevoked: () => this.enterRevoked(),
|
|
10226
10568
|
onReplayCursorTooOld: (error) => this.enterReplayCursorTooOld(error),
|
|
10227
|
-
//
|
|
10228
|
-
// LongPollClient couldn't parse into a known Envelope at all (an
|
|
10229
|
-
// unrecognized message type) still needs its cursor/watermark
|
|
10230
|
-
// advanced past it, exactly like a successfully-processed envelope
|
|
10231
|
-
// would — see `noteSkippedSeq`'s own doc comment.
|
|
10232
|
-
onSkippedSeq: (seq) => this.noteSkippedSeq(seq),
|
|
10233
|
-
// Finding R1: a batch entry LongPollClient recognized the TYPE of but
|
|
10234
|
-
// whose payload failed validation — a genuine delivery failure at
|
|
10235
|
-
// that seq, engaged the same way a thrown handler failure is (see
|
|
10236
|
-
// `noteValidationFailure`'s own doc comment).
|
|
10569
|
+
// Unknown and malformed executable work use the same ordered stall.
|
|
10237
10570
|
onValidationFailedSeq: (seq) => this.noteValidationFailure(seq),
|
|
10238
10571
|
// Finding P2 (Fix 2a): lets the long-poll loop distinguish "this
|
|
10239
10572
|
// non-empty batch was a stalled backlog re-pull, no cursor progress
|
|
@@ -10375,6 +10708,7 @@ var ConnectionManager = class {
|
|
|
10375
10708
|
*/
|
|
10376
10709
|
send(envelope) {
|
|
10377
10710
|
this.outbox.push(envelope);
|
|
10711
|
+
this.opts.onOutboundQueued?.(envelope);
|
|
10378
10712
|
void this.drainOutbox();
|
|
10379
10713
|
}
|
|
10380
10714
|
/** Publish a fresh local configuration snapshot while this daemon is running. */
|
|
@@ -10407,12 +10741,22 @@ var ConnectionManager = class {
|
|
|
10407
10741
|
while (pending.length > 0) {
|
|
10408
10742
|
if (this.stopped || this.revoked) return pending.flat();
|
|
10409
10743
|
const segment = pending.shift();
|
|
10410
|
-
|
|
10411
|
-
|
|
10412
|
-
|
|
10413
|
-
|
|
10414
|
-
|
|
10415
|
-
|
|
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()];
|
|
10416
10760
|
}
|
|
10417
10761
|
const midpoint = Math.floor(segment.length / 2);
|
|
10418
10762
|
pending.unshift(segment.slice(0, midpoint), segment.slice(midpoint));
|
|
@@ -10632,6 +10976,10 @@ var ConnectionManager = class {
|
|
|
10632
10976
|
* documented idempotent (protocol §9).
|
|
10633
10977
|
*/
|
|
10634
10978
|
deliver(envelope) {
|
|
10979
|
+
if (!isServerToDaemonType(envelope.type)) {
|
|
10980
|
+
if (typeof envelope.seq === "number") this.noteValidationFailure(envelope.seq);
|
|
10981
|
+
return false;
|
|
10982
|
+
}
|
|
10635
10983
|
const tracked = isCursorEnvelopeType(envelope.type) && typeof envelope.seq === "number";
|
|
10636
10984
|
const watermark = this.dedupWatermark();
|
|
10637
10985
|
if (tracked && watermark !== void 0 && envelope.seq <= watermark) return false;
|
|
@@ -10701,122 +11049,7 @@ var ConnectionManager = class {
|
|
|
10701
11049
|
if (tracked) this.inFlightSeqs.delete(seq);
|
|
10702
11050
|
}
|
|
10703
11051
|
}
|
|
10704
|
-
/**
|
|
10705
|
-
* M4 Phase 4 (version-negotiation drill fix): `LongPollClient` calls this
|
|
10706
|
-
* for a batch entry it could not parse into a known `Envelope` at all (an
|
|
10707
|
-
* unrecognized message type (see `long-poll-transport.ts`'s own doc
|
|
10708
|
-
* comment on `parseLooseEventsPollResponse`) but which still carried a numeric,
|
|
10709
|
-
* task-class envelope-level `seq` (the caller only invokes this for a
|
|
10710
|
-
* `task.`-prefixed type — see `long-poll-transport.ts`'s own
|
|
10711
|
-
* `extractSkippableSeq`; `conn.*`-shaped or type-less entries never reach
|
|
10712
|
-
* here at all, mirroring F2's "conn.* is never cursor-tracked" rule).
|
|
10713
|
-
* There is no real `Envelope` to hand to a handler — a genuinely
|
|
10714
|
-
* unrecognized type has nothing this build could ever act on.
|
|
10715
|
-
*
|
|
10716
|
-
* GATEKEEPER-CAUGHT REGRESSION (fixed here): this used to call
|
|
10717
|
-
* `advanceCursor(seq)` DIRECTLY, synchronously, the instant a skip was
|
|
10718
|
-
* detected in `LongPollClient.loop()`'s per-entry for-loop. That is NOT
|
|
10719
|
-
* "instantaneous and race-free" the way the previous version of this
|
|
10720
|
-
* comment claimed — the hazard was never the skip racing against itself,
|
|
10721
|
-
* it was the skip racing AHEAD of an EARLIER real envelope in the SAME
|
|
10722
|
-
* batch that is still in flight on `processingChain` (`deliver()`, above,
|
|
10723
|
-
* only ever CHAINS `process()` onto that promise chain — it never awaits
|
|
10724
|
-
* it before returning). Concretely, batch `[real seq1, unknown seq2]`:
|
|
10725
|
-
* `deliver(seq1)` chains `process(seq1)` but returns immediately without
|
|
10726
|
-
* running it; the for-loop then reaches `seq2` and (pre-fix) called
|
|
10727
|
-
* `advanceCursor(2)` synchronously, BEFORE `process(seq1)` had even
|
|
10728
|
-
* started, let alone failed. If `seq1`'s handler then failed,
|
|
10729
|
-
* `stalledAtSeq` became 1 — but the durable cursor was already 2, so
|
|
10730
|
-
* `dedupWatermark()` returned 2, and every future redelivery of seq1 was
|
|
10731
|
-
* dedup-dropped as "already past the cursor" forever: permanent envelope
|
|
10732
|
-
* loss, exactly the F3 bug class the whole `stalledAtSeq`/frozen-watermark
|
|
10733
|
-
* mechanism exists to prevent.
|
|
10734
|
-
*
|
|
10735
|
-
* Fix: the cursor-advancing half is now CHAINED onto `processingChain`
|
|
10736
|
-
* too, exactly like `process()`'s own post-handler bookkeeping — so it
|
|
10737
|
-
* only ever runs once every earlier envelope already queued ahead of it
|
|
10738
|
-
* has fully settled (success or failure), and can observe `stalledAtSeq`'s
|
|
10739
|
-
* REAL, up-to-date value rather than whatever it happened to be at the
|
|
10740
|
-
* instant the skip was first noticed. The guard mirrors `process()`'s own
|
|
10741
|
-
* success-path guard exactly: never advance past a still-unresolved
|
|
10742
|
-
* earlier failure, unless (degenerate, cannot really happen for a skip)
|
|
10743
|
-
* this exact seq IS the stalled one.
|
|
10744
|
-
*
|
|
10745
|
-
* `noteDelivered` (the eager, in-memory watermark) stays UNCHAINED —
|
|
10746
|
-
* called immediately, unconditionally, regardless of `stalledAtSeq` —
|
|
10747
|
-
* matching `deliver()`'s own eager, unconditional call for a real
|
|
10748
|
-
* envelope: its only job is "don't re-dispatch something already handed off,"
|
|
10749
|
-
* independent of outcome, and that property does not depend on FIFO
|
|
10750
|
-
* ordering the way the DURABLE cursor does.
|
|
10751
|
-
*
|
|
10752
|
-
* Deliberately NO top-level `dedupWatermark() <= seq` early-return before
|
|
10753
|
-
* queuing the chained callback (an earlier draft of this fix had one, and
|
|
10754
|
-
* it was itself subtly wrong): `deliveredSeq` can already reflect a seq
|
|
10755
|
-
* from the FIRST time it was ever seen, while the DURABLE cursor is still
|
|
10756
|
-
* behind it because a stall intervened before that seq's chained
|
|
10757
|
-
* advancement ran — a pre-check keyed on `deliveredSeq` would then
|
|
10758
|
-
* wrongly treat a LATER redelivery of the same seq (arriving once the
|
|
10759
|
-
* stall has since cleared) as "already accounted for" and never queue
|
|
10760
|
-
* another attempt, permanently stranding the cursor one seq short. Always
|
|
10761
|
-
* queuing is safe and cheap: `advanceCursor`'s own `seq <= this.cursor`
|
|
10762
|
-
* guard already makes a genuinely-redundant call a no-op, so there is no
|
|
10763
|
-
* correctness reason to short-circuit earlier, only a (here, unnecessary)
|
|
10764
|
-
* micro-optimization one.
|
|
10765
|
-
*/
|
|
10766
|
-
noteSkippedSeq(seq) {
|
|
10767
|
-
this.noteDelivered(seq);
|
|
10768
|
-
this.processingChain = this.processingChain.then(async () => {
|
|
10769
|
-
if (this.stalledAtSeq === void 0 || seq === this.stalledAtSeq) {
|
|
10770
|
-
try {
|
|
10771
|
-
await this.advanceCursor(seq);
|
|
10772
|
-
if (this.stalledAtSeq === seq) this.stalledAtSeq = void 0;
|
|
10773
|
-
} catch (err) {
|
|
10774
|
-
if (this.stalledAtSeq === void 0) this.stalledAtSeq = seq;
|
|
10775
|
-
console.error(
|
|
10776
|
-
`[byok/client] unknown task seq=${seq} could not durably persist its cursor; cursor left unadvanced for redelivery:`,
|
|
10777
|
-
err
|
|
10778
|
-
);
|
|
10779
|
-
}
|
|
10780
|
-
}
|
|
10781
|
-
});
|
|
10782
|
-
}
|
|
10783
|
-
/**
|
|
10784
|
-
* Finding R1 (cross-model re-review — was NOT-CLOSED against F1):
|
|
10785
|
-
* `LongPollClient` calls this for a batch entry whose `type` it
|
|
10786
|
-
* recognized but whose payload failed schema validation
|
|
10787
|
-
* ({@link EnvelopeValidationError}) — a genuine delivery failure at that
|
|
10788
|
-
* seq, unlike `noteSkippedSeq`'s forward-compat case. Deliberately mirrors
|
|
10789
|
-
* `process()`'s own catch block (`if (tracked && this.stalledAtSeq ===
|
|
10790
|
-
* undefined) this.stalledAtSeq = envelope.seq;`) as closely as possible:
|
|
10791
|
-
* the SAME "only the lowest unresolved failure holds the stall" rule, the
|
|
10792
|
-
* SAME resulting freeze of `dedupWatermark()` at the durable cursor
|
|
10793
|
-
* (protocol §9 keeps this seq alive), and — because it's the SAME
|
|
10794
|
-
* `stalledAtSeq` field `process()`'s own post-success guard already
|
|
10795
|
-
* checks — anything ELSE delivered after this seq (same batch or a later
|
|
10796
|
-
* one) is automatically held back from advancing the cursor too, with
|
|
10797
|
-
* zero changes needed to `process()` itself.
|
|
10798
|
-
*
|
|
10799
|
-
* Chained onto `processingChain` for exactly the reason `noteSkippedSeq`
|
|
10800
|
-
* documents for its own identical chaining (see that method's sibling
|
|
10801
|
-
* doc comment on `LongPollClient`, "GATEKEEPER-CAUGHT REGRESSION"): an
|
|
10802
|
-
* EARLIER real envelope in the SAME batch may still be in flight on that
|
|
10803
|
-
* FIFO chain when this is called (`deliver()` only ever chains
|
|
10804
|
-
* `process()` onto it, never awaits before returning) — mutating
|
|
10805
|
-
* `stalledAtSeq` synchronously here could race ahead of that still-
|
|
10806
|
-
* unresolved earlier envelope. Chaining instead guarantees this only
|
|
10807
|
-
* takes effect once every earlier-queued envelope has already settled,
|
|
10808
|
-
* and reads `stalledAtSeq`'s real, up-to-date value rather than whatever
|
|
10809
|
-
* it happened to be the instant the failure was first noticed.
|
|
10810
|
-
*
|
|
10811
|
-
* No `noteDelivered` call here (contrast `noteSkippedSeq`, which does
|
|
10812
|
-
* call it): a validation-failed entry never becomes a real `Envelope` and
|
|
10813
|
-
* never reaches `deliver()`, so it was never "delivered" in the eager
|
|
10814
|
-
* in-memory-watermark sense that field tracks — there is nothing for it
|
|
10815
|
-
* to eagerly mark. Once a corrected redelivery of this exact seq DOES
|
|
10816
|
-
* arrive as a real envelope, it flows through the ordinary `deliver()`
|
|
10817
|
-
* path (which calls `noteDelivered` itself) and, on success, clears the
|
|
10818
|
-
* stall via `process()`'s own existing logic — no special-casing needed.
|
|
10819
|
-
*/
|
|
11052
|
+
/** Serialized with handler completion, so invalid work cannot be acked by later success. */
|
|
10820
11053
|
noteValidationFailure(seq) {
|
|
10821
11054
|
this.processingChain = this.processingChain.then(() => {
|
|
10822
11055
|
if (this.stalledAtSeq === void 0) this.stalledAtSeq = seq;
|
|
@@ -11203,11 +11436,12 @@ var DaemonObserver = class {
|
|
|
11203
11436
|
* where those are actually reported from.
|
|
11204
11437
|
*/
|
|
11205
11438
|
handleInboundEnvelope(envelope) {
|
|
11206
|
-
if (envelope.type
|
|
11207
|
-
const
|
|
11439
|
+
if (!isTaskOfferType(envelope.type)) return;
|
|
11440
|
+
const offer = envelope;
|
|
11441
|
+
const taskId = offer.task_id;
|
|
11208
11442
|
if (this.taskInfo.has(taskId)) return;
|
|
11209
|
-
this.upsertTask(taskId, { state: "Offered", runtime:
|
|
11210
|
-
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 });
|
|
11211
11445
|
}
|
|
11212
11446
|
/**
|
|
11213
11447
|
* Feed a raw OUTBOUND (daemon -> server) envelope — this is where every
|
|
@@ -11736,6 +11970,7 @@ CREATE INDEX IF NOT EXISTS journal_transition_task ON journal_transition (task_i
|
|
|
11736
11970
|
CREATE TABLE IF NOT EXISTS journal_terminal (
|
|
11737
11971
|
task_id TEXT PRIMARY KEY REFERENCES journal_task (task_id),
|
|
11738
11972
|
terminal_type TEXT NOT NULL,
|
|
11973
|
+
bytes TEXT NOT NULL,
|
|
11739
11974
|
payload_hash TEXT NOT NULL,
|
|
11740
11975
|
truth_state TEXT NOT NULL,
|
|
11741
11976
|
attempt INTEGER NOT NULL,
|
|
@@ -11890,6 +12125,11 @@ var SqliteLocalTaskJournal = class {
|
|
|
11890
12125
|
const quarantinePath = quarantineDatabase(options.storeDir, this.#dbPath, reason, this.#clock());
|
|
11891
12126
|
throw new JournalCorruptError(this.#dbPath, quarantinePath, reason, { cause: err });
|
|
11892
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
|
+
}
|
|
11893
12133
|
this.#db = db;
|
|
11894
12134
|
try {
|
|
11895
12135
|
secureJournalFilePermissions(this.#dbPath);
|
|
@@ -11979,10 +12219,13 @@ var SqliteLocalTaskJournal = class {
|
|
|
11979
12219
|
}
|
|
11980
12220
|
return this.#enqueue("appendEnvelope", () => {
|
|
11981
12221
|
this.#fault("append:before-begin");
|
|
11982
|
-
|
|
12222
|
+
const committed = this.#transaction(() => {
|
|
11983
12223
|
const existing = this.#db.prepare("SELECT receipt FROM journal_idempotency WHERE scope = ? AND key = ?").get("envelope", record3.envelopeId);
|
|
11984
12224
|
if (existing) {
|
|
11985
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);
|
|
11986
12229
|
return { ...prior, created: false };
|
|
11987
12230
|
}
|
|
11988
12231
|
const committedAt = this.#clock().toISOString();
|
|
@@ -12004,6 +12247,16 @@ var SqliteLocalTaskJournal = class {
|
|
|
12004
12247
|
);
|
|
12005
12248
|
this.#fault("append:after-envelope");
|
|
12006
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
|
+
}
|
|
12007
12260
|
this.#db.prepare(
|
|
12008
12261
|
`INSERT OR IGNORE INTO journal_task
|
|
12009
12262
|
(task_id, envelope_id, tenant_id, product_id, device_id, seq, local_state, created_at, updated_at)
|
|
@@ -12032,6 +12285,8 @@ var SqliteLocalTaskJournal = class {
|
|
|
12032
12285
|
this.#fault("append:before-commit");
|
|
12033
12286
|
return { ...receipt, created: true };
|
|
12034
12287
|
});
|
|
12288
|
+
this.#fault("append:after-commit");
|
|
12289
|
+
return committed;
|
|
12035
12290
|
});
|
|
12036
12291
|
}
|
|
12037
12292
|
recordAdmission(record3) {
|
|
@@ -12102,17 +12357,22 @@ var SqliteLocalTaskJournal = class {
|
|
|
12102
12357
|
*/
|
|
12103
12358
|
recordTerminal(record3) {
|
|
12104
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
|
+
}
|
|
12105
12366
|
this.#transaction(() => {
|
|
12106
12367
|
this.#requireTask(record3.taskId, "recordTerminal");
|
|
12107
|
-
const existing = this.#db.prepare("SELECT
|
|
12108
|
-
if (existing
|
|
12109
|
-
this.#db.prepare(
|
|
12110
|
-
|
|
12111
|
-
|
|
12112
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
|
12113
|
-
).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(
|
|
12114
12373
|
record3.taskId,
|
|
12115
12374
|
record3.terminalType,
|
|
12375
|
+
record3.bytes,
|
|
12116
12376
|
record3.payloadHash,
|
|
12117
12377
|
record3.truthState,
|
|
12118
12378
|
record3.attempt,
|
|
@@ -12121,11 +12381,98 @@ var SqliteLocalTaskJournal = class {
|
|
|
12121
12381
|
record3.recordedAt
|
|
12122
12382
|
);
|
|
12123
12383
|
this.#db.prepare("UPDATE journal_task SET local_state = ?, updated_at = ? WHERE task_id = ?").run(`terminal:${record3.terminalType}`, record3.recordedAt, record3.taskId);
|
|
12124
|
-
} else if (existing.payload_hash === record3.payloadHash) {
|
|
12384
|
+
} else if (existing.payload_hash === record3.payloadHash && existing.bytes === record3.bytes && existing.truth_state !== "confirmed") {
|
|
12125
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);
|
|
12126
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
|
+
}
|
|
12127
12391
|
this.#fault("terminal:before-commit");
|
|
12128
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");
|
|
12129
12476
|
});
|
|
12130
12477
|
}
|
|
12131
12478
|
/**
|
|
@@ -12142,8 +12489,9 @@ var SqliteLocalTaskJournal = class {
|
|
|
12142
12489
|
return this.#enqueue("listRecoverable", () => {
|
|
12143
12490
|
const rows = this.#db.prepare(
|
|
12144
12491
|
`SELECT t.task_id, t.envelope_id, t.seq, t.tenant_id, t.product_id, t.device_id,
|
|
12145
|
-
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
|
|
12146
12493
|
FROM journal_task t
|
|
12494
|
+
JOIN journal_envelope e ON e.envelope_id = t.envelope_id
|
|
12147
12495
|
LEFT JOIN journal_terminal term ON term.task_id = t.task_id
|
|
12148
12496
|
WHERE t.recovery_marker IS NULL
|
|
12149
12497
|
AND term.task_id IS NULL
|
|
@@ -12165,7 +12513,8 @@ var SqliteLocalTaskJournal = class {
|
|
|
12165
12513
|
localState: row.local_state,
|
|
12166
12514
|
...claimedRuntime === null ? {} : { claimedRuntime },
|
|
12167
12515
|
...workspaceRef === null ? {} : { workspaceRef },
|
|
12168
|
-
updatedAt: row.updated_at
|
|
12516
|
+
updatedAt: row.updated_at,
|
|
12517
|
+
envelopeBytes: row.envelope_bytes
|
|
12169
12518
|
};
|
|
12170
12519
|
});
|
|
12171
12520
|
});
|
|
@@ -12822,6 +13171,204 @@ function createStatfsFreeBytesProvider(dir) {
|
|
|
12822
13171
|
return Number(stats.bavail) * Number(stats.bsize);
|
|
12823
13172
|
};
|
|
12824
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
|
+
}
|
|
12825
13372
|
|
|
12826
13373
|
// src/daemon/environment.ts
|
|
12827
13374
|
var BASE_PLATFORM_ALLOWLIST = [
|
|
@@ -14448,6 +14995,10 @@ var TaskRunner = class {
|
|
|
14448
14995
|
get maxTaskOutputBytes() {
|
|
14449
14996
|
return this.deps.maxTaskOutputBytes ?? DEFAULT_MAX_TASK_OUTPUT_BYTES;
|
|
14450
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
|
+
}
|
|
14451
15002
|
/** WP0: effective per-canonical-Agent-home Attempt cap — see {@link DEFAULT_MAX_CONCURRENT_MUTABLE_SESSIONS_PER_AGENT_HOME}. */
|
|
14452
15003
|
get maxConcurrentMutableSessionsPerAgentHome() {
|
|
14453
15004
|
return this.deps.maxConcurrentMutableSessionsPerAgentHome ?? DEFAULT_MAX_CONCURRENT_MUTABLE_SESSIONS_PER_AGENT_HOME;
|
|
@@ -15128,6 +15679,7 @@ var TaskRunner = class {
|
|
|
15128
15679
|
},
|
|
15129
15680
|
forwardedEnvironmentNames: Object.freeze(Object.keys(env).sort())
|
|
15130
15681
|
});
|
|
15682
|
+
await this.deps.beforeClaim?.(taskId, manifest.descriptor.id);
|
|
15131
15683
|
this.deps.send(
|
|
15132
15684
|
createEnvelope(
|
|
15133
15685
|
"task.claim",
|
|
@@ -15614,9 +16166,17 @@ var TaskRunner = class {
|
|
|
15614
16166
|
}
|
|
15615
16167
|
async pump(active) {
|
|
15616
16168
|
try {
|
|
15617
|
-
for await (
|
|
16169
|
+
for await (let event of active.session.events) {
|
|
15618
16170
|
if (this.tasks.get(active.taskId) !== active || active.beingTornDown) return;
|
|
15619
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;
|
|
15620
16180
|
active.outputBytesSoFar += estimateEventBytes(event);
|
|
15621
16181
|
if (active.outputBytesSoFar > this.maxTaskOutputBytes) {
|
|
15622
16182
|
active.batcher.flush();
|
|
@@ -18536,6 +19096,7 @@ function delay2(ms) {
|
|
|
18536
19096
|
});
|
|
18537
19097
|
}
|
|
18538
19098
|
function terminalKindOf(type) {
|
|
19099
|
+
if (type === "task.decline") return "declined";
|
|
18539
19100
|
if (type === "task.complete") return "complete";
|
|
18540
19101
|
if (type === "task.fail") return "failed";
|
|
18541
19102
|
if (type === "task.cancelled") return "cancelled";
|
|
@@ -18551,7 +19112,7 @@ function toJournalEnvelopeRecord(envelope, identity) {
|
|
|
18551
19112
|
bytes,
|
|
18552
19113
|
bytesHash: journalHash(bytes),
|
|
18553
19114
|
receivedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
18554
|
-
opensTask: envelope.type
|
|
19115
|
+
opensTask: isTaskOfferType(envelope.type)
|
|
18555
19116
|
};
|
|
18556
19117
|
}
|
|
18557
19118
|
function isRuntimeId(id) {
|
|
@@ -18740,6 +19301,11 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
18740
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".`
|
|
18741
19302
|
);
|
|
18742
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
|
+
}
|
|
18743
19309
|
validateProgressBatcherOptions(config.progressBatch);
|
|
18744
19310
|
if (config.agentEgress !== void 0 && config.agentHome === void 0) {
|
|
18745
19311
|
throw new Error("DaemonConfig.agentEgress requires DaemonConfig.agentHome for the per-Agent local spool");
|
|
@@ -19034,19 +19600,31 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
19034
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.`
|
|
19035
19601
|
);
|
|
19036
19602
|
}
|
|
19037
|
-
|
|
19038
|
-
|
|
19039
|
-
|
|
19040
|
-
|
|
19041
|
-
|
|
19042
|
-
|
|
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" }
|
|
19043
19626
|
});
|
|
19044
19627
|
}
|
|
19045
|
-
if (recoverable.length > 0) {
|
|
19046
|
-
console.warn(
|
|
19047
|
-
`[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(", ")}`
|
|
19048
|
-
);
|
|
19049
|
-
}
|
|
19050
19628
|
}
|
|
19051
19629
|
if (activeOwnedPressureEngine) {
|
|
19052
19630
|
await activeOwnedPressureEngine.tick();
|
|
@@ -19082,35 +19660,64 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
19082
19660
|
tenantId: record3.tenantId,
|
|
19083
19661
|
deviceId: record3.deviceId
|
|
19084
19662
|
}) : void 0;
|
|
19085
|
-
const
|
|
19086
|
-
const sendSanitizedEnvelope = activeJournal && journalIdentity ? (envelope) => {
|
|
19087
|
-
observer.handleOutboundEnvelope(envelope);
|
|
19663
|
+
const sendSanitizedEnvelope = (envelope) => {
|
|
19088
19664
|
const terminalKind = terminalKindOf(envelope.type);
|
|
19089
|
-
|
|
19090
|
-
|
|
19665
|
+
if (!activeJournal || !journalIdentity || terminalKind === void 0 || envelope.task_id === void 0) {
|
|
19666
|
+
observer.handleOutboundEnvelope(envelope);
|
|
19091
19667
|
connection?.send(envelope);
|
|
19092
19668
|
return;
|
|
19093
19669
|
}
|
|
19094
|
-
const
|
|
19095
|
-
|
|
19096
|
-
|
|
19097
|
-
|
|
19098
|
-
|
|
19099
|
-
|
|
19100
|
-
|
|
19101
|
-
|
|
19102
|
-
|
|
19103
|
-
|
|
19104
|
-
|
|
19105
|
-
|
|
19106
|
-
|
|
19107
|
-
|
|
19108
|
-
|
|
19109
|
-
|
|
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);
|
|
19110
19720
|
});
|
|
19111
|
-
} : (envelope) => {
|
|
19112
|
-
observer.handleOutboundEnvelope(envelope);
|
|
19113
|
-
connection?.send(envelope);
|
|
19114
19721
|
};
|
|
19115
19722
|
const sendEnvelope = (candidate) => {
|
|
19116
19723
|
if (candidate.task_id === void 0 || runner?.usesAgentEgress(candidate.task_id) !== true) {
|
|
@@ -19149,6 +19756,11 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
19149
19756
|
// before is the entire integration; `task-runner.ts` itself is
|
|
19150
19757
|
// untouched. See `observer.ts`'s module doc comment.
|
|
19151
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
|
+
},
|
|
19152
19764
|
blobClient,
|
|
19153
19765
|
batcherOptions: egressBatcherOptions,
|
|
19154
19766
|
agentEgress,
|
|
@@ -19187,6 +19799,8 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
19187
19799
|
shutdownInterruptTimeoutMs: overrides.shutdown?.taskInterruptTimeoutMs,
|
|
19188
19800
|
// M5 batch-3 (workstream 2): see DaemonConfig.maxTaskOutputBytes's own doc comment — already validated above.
|
|
19189
19801
|
maxTaskOutputBytes: config.maxTaskOutputBytes,
|
|
19802
|
+
// See DaemonConfig.maxInlineEventBytes's own doc comment — already validated above.
|
|
19803
|
+
maxInlineEventBytes: config.maxInlineEventBytes,
|
|
19190
19804
|
// additive-minor (`task.complete.document`): passed through verbatim,
|
|
19191
19805
|
// absent when unconfigured — see `DaemonConfig.resultDocument`'s own
|
|
19192
19806
|
// doc comment. Spread rather than assigned so an unconfigured daemon
|
|
@@ -19406,7 +20020,13 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
19406
20020
|
if (await handleAgentContentReadEnvelope(envelope)) return;
|
|
19407
20021
|
activePressureEngine?.assertAckCriticalAllowed();
|
|
19408
20022
|
await activeJournal.appendEnvelope(toJournalEnvelopeRecord(envelope, journalIdentity));
|
|
19409
|
-
|
|
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;
|
|
19410
20030
|
} : (envelope) => {
|
|
19411
20031
|
if (tenantRebinding) {
|
|
19412
20032
|
return Promise.reject(new Error("tenant enrollment is being re-paired; inbound work is blocked until restart"));
|
|
@@ -19418,6 +20038,25 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
19418
20038
|
if (envelope.type === "agent.content.read") return handleAgentContentReadEnvelope(envelope).then(() => void 0);
|
|
19419
20039
|
return runner?.handleEnvelope(envelope) ?? Promise.resolve();
|
|
19420
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
|
+
},
|
|
19421
20060
|
onStateChange: (state) => {
|
|
19422
20061
|
const wasSettled = connectionState === "open";
|
|
19423
20062
|
connectionState = state;
|
|
@@ -19435,6 +20074,18 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
19435
20074
|
});
|
|
19436
20075
|
}
|
|
19437
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
|
+
}
|
|
19438
20089
|
await connection.start();
|
|
19439
20090
|
await connection.waitForConnection();
|
|
19440
20091
|
runner.retryRecoveredAgentMessages();
|
|
@@ -19822,6 +20473,11 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
19822
20473
|
if (!parsed) throw new ControlError("bad_request", "team_messages.ack requires exactly {context,throughSeq}");
|
|
19823
20474
|
return teamWorkspaces.ackMessages({ lease: decodeTeamMemberContext(parsed.context), throughSeq: parsed.throughSeq });
|
|
19824
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
|
+
},
|
|
19825
20481
|
"team_messages.inspect": (params) => {
|
|
19826
20482
|
const parsed = parseTeamMessageInspectParams(params);
|
|
19827
20483
|
if (!parsed) throw new ControlError("bad_request", "team_messages.inspect requires exactly {workspaceId,afterSeq?}");
|