@byok-sdk/client 0.3.0 → 0.4.1
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 +14 -1
- package/dist/adapters/claude/claude-adapter.d.ts +4 -20
- package/dist/adapters/claude/events.d.ts +3 -0
- package/dist/adapters/claude/process-client.d.ts +15 -1
- package/dist/adapters/codex/codex-adapter.d.ts +4 -16
- package/dist/adapters/codex/process-runner.d.ts +9 -1
- package/dist/adapters/index.d.ts +3 -1
- package/dist/adapters/index.js +1057 -261
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/pi/pi-adapter.d.ts +3 -16
- package/dist/adapters/pi/resolve-bin.d.ts +1 -1
- package/dist/adapters/pi/rpc-client.d.ts +15 -1
- package/dist/adapters/process-tree.d.ts +60 -0
- package/dist/adapters/taskkill-pid-set.d.ts +34 -0
- package/dist/bin/audit-log.d.ts +12 -0
- package/dist/bin/byok-agent.js +1452 -507
- package/dist/bin/byok-agent.js.map +1 -1
- package/dist/bin/byok-approval-mcp.js.map +1 -1
- package/dist/bin/commands/workspaces.d.ts +11 -0
- package/dist/bin/format.d.ts +13 -0
- package/dist/bin/runtime-probe.d.ts +1 -1
- package/dist/bin/tasks-view.d.ts +13 -0
- package/dist/daemon/approvals.d.ts +2 -2
- package/dist/daemon/connection-manager.d.ts +15 -17
- package/dist/daemon/control-server.d.ts +18 -1
- package/dist/daemon/create-daemon.d.ts +2 -2
- package/dist/daemon/daemon-owner.d.ts +4 -2
- package/dist/daemon/environment.d.ts +9 -9
- package/dist/daemon/git-workspace.d.ts +21 -0
- package/dist/daemon/long-poll-transport.d.ts +6 -0
- package/dist/daemon/observer.d.ts +13 -0
- package/dist/daemon/presence-publisher.d.ts +29 -0
- package/dist/daemon/runtime-capabilities.d.ts +1 -1
- package/dist/daemon/task-runner.d.ts +34 -40
- package/dist/daemon/ws-transport.d.ts +3 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +1413 -455
- package/dist/index.js.map +1 -1
- package/dist/runtime-failure.d.ts +64 -0
- package/dist/types.d.ts +100 -73
- package/package.json +14 -14
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { execFile, spawn
|
|
1
|
+
import { execFile, spawn } from 'child_process';
|
|
2
2
|
import { createHash, randomUUID, sign, createPrivateKey, generateKeyPairSync, randomBytes, timingSafeEqual, createHmac } from 'crypto';
|
|
3
3
|
import { promises, mkdirSync, existsSync, renameSync, writeFileSync, chmodSync, statSync, readdirSync, linkSync, fstatSync, lstatSync, unlinkSync, constants, readFileSync, realpathSync } from 'fs';
|
|
4
4
|
import path17, { join, isAbsolute } from 'path';
|
|
5
5
|
import os5 from 'os';
|
|
6
6
|
import { parseDeviceAssertionEnvelope, tenantId, DeviceProofProtectedClaimsSchema, deviceProofSigningInput, DEVICE_PROOF_SCHEMA_ID, SKILL_PACK_MAX_BYTES, hasCapability, parseSkillPackManifest, checkSkillPackManifest, skillPackContentHashInput, checkSkillPackFileContent, SKILL_PACK_ENTRY_PATH, checkSkillPackEntry, isSkillPackPathSafe, DEVICE_PROOF_HEADER, contentHash, TRUTH_RECORD_KINDS, nonceSigningBytes, DEVICE_ASSERTION_AUDIENCE_MAX_BYTES, DEVICE_ASSERTION_DEFAULT_TTL_MS, DEVICE_ASSERTION_MAX_TTL_MS, CONTENT_HASH_PATTERN, CapabilityDeclarationSchema, DeviceAssertionClaimsSchema, deviceAssertionSigningInput, DEVICE_ASSERTION_SCHEMA_ID } from '@byok-sdk/core';
|
|
7
|
-
import { BYOK_PAIR_PATH, BYOK_CHALLENGE_PATH, BYOK_TOKEN_PATH, byokBlobUrlPath, BYOK_BLOBS_PATH, byokBlobFinalizePath, partitionAgentEvents, BYOK_SKILL_PACKS_PATH, byokSkillPackFilePath, BYOK_RECORDS_PATH, byokRecordPath, TASK_TRANSITIONS, ToolsetIdSchema, encodeEnvelope, createEnvelope, checkResultDocument, MAX_MESSAGES_PER_BATCH, BYOK_CAPABILITIES_PATH, BYOK_PRESENCE_PATH, RuntimeIdSchema, RESULT_DOCUMENT_MAX_BYTES, PROTOCOL_VERSION, decodeEnvelope, BYOK_MESSAGES_PATH, MessagesSendResponseSchema, BYOK_EVENTS_PATH, parseMessage, UnknownMessageTypeError, BYOK_WS_PATH } from '@byok-sdk/protocol';
|
|
7
|
+
import { BYOK_PAIR_PATH, BYOK_CHALLENGE_PATH, BYOK_TOKEN_PATH, byokBlobUrlPath, BYOK_BLOBS_PATH, byokBlobFinalizePath, partitionAgentEvents, BYOK_SKILL_PACKS_PATH, byokSkillPackFilePath, BYOK_RECORDS_PATH, byokRecordPath, TASK_TRANSITIONS, CONFIGURED_TOOLSETS_MAX_ITEMS, ToolsetIdSchema, encodeEnvelope, createEnvelope, checkResultDocument, MAX_MESSAGES_PER_BATCH, BYOK_CAPABILITIES_PATH, BYOK_PRESENCE_PATH, RuntimeIdSchema, RESULT_DOCUMENT_MAX_BYTES, PROTOCOL_VERSION, decodeEnvelope, BYOK_MESSAGES_PATH, MessagesSendResponseSchema, BYOK_EVENTS_PATH, parseMessage, UnknownMessageTypeError, BYOK_WS_PATH } from '@byok-sdk/protocol';
|
|
8
8
|
import { promisify } from 'util';
|
|
9
9
|
import { fileURLToPath } from 'url';
|
|
10
10
|
import 'readline';
|
|
@@ -13,6 +13,52 @@ import { WebSocket } from 'ws';
|
|
|
13
13
|
import { createRequire } from 'module';
|
|
14
14
|
|
|
15
15
|
// src/types.ts
|
|
16
|
+
function frozenStrings(values) {
|
|
17
|
+
return values === void 0 ? void 0 : Object.freeze([...values]);
|
|
18
|
+
}
|
|
19
|
+
function frozenPolicy(policy) {
|
|
20
|
+
const allowTools = policy.allowTools === void 0 ? void 0 : Object.freeze([...policy.allowTools]);
|
|
21
|
+
const denyTools = policy.denyTools === void 0 ? void 0 : Object.freeze([...policy.denyTools]);
|
|
22
|
+
return Object.freeze({
|
|
23
|
+
mode: policy.mode,
|
|
24
|
+
...allowTools === void 0 ? {} : { allowTools },
|
|
25
|
+
...denyTools === void 0 ? {} : { denyTools },
|
|
26
|
+
...policy.workspaceRoot === void 0 ? {} : { workspaceRoot: policy.workspaceRoot },
|
|
27
|
+
...policy.network === void 0 ? {} : { network: policy.network }
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
function freezeRuntimeAdapterDescriptor(descriptor) {
|
|
31
|
+
const baseNames = frozenStrings(descriptor.environmentRequirements.baseNames);
|
|
32
|
+
const credentialNames = frozenStrings(descriptor.environmentRequirements.credentialNames);
|
|
33
|
+
return Object.freeze({
|
|
34
|
+
id: descriptor.id,
|
|
35
|
+
supportsDispatchSelection: descriptor.supportsDispatchSelection === true,
|
|
36
|
+
capabilities: Object.freeze({
|
|
37
|
+
steer: descriptor.capabilities.steer === true,
|
|
38
|
+
resume: descriptor.capabilities.resume === true,
|
|
39
|
+
approvalInteractive: descriptor.capabilities.approvalInteractive === true,
|
|
40
|
+
...descriptor.capabilities.mcpToolsets === void 0 ? {} : { mcpToolsets: descriptor.capabilities.mcpToolsets === true },
|
|
41
|
+
permissionModes: Object.freeze([...descriptor.capabilities.permissionModes])
|
|
42
|
+
}),
|
|
43
|
+
environmentRequirements: Object.freeze({
|
|
44
|
+
...baseNames === void 0 ? {} : { baseNames },
|
|
45
|
+
...credentialNames === void 0 ? {} : { credentialNames }
|
|
46
|
+
})
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
function sealRuntimeOperationManifest(manifest) {
|
|
50
|
+
return Object.freeze({
|
|
51
|
+
taskId: manifest.taskId,
|
|
52
|
+
runtimeId: manifest.runtimeId,
|
|
53
|
+
descriptor: freezeRuntimeAdapterDescriptor(manifest.descriptor),
|
|
54
|
+
policy: frozenPolicy(manifest.policy),
|
|
55
|
+
requiredToolsetIds: Object.freeze([...manifest.requiredToolsetIds]),
|
|
56
|
+
...manifest.dispatchSelection === void 0 ? {} : { dispatchSelection: Object.freeze({ ...manifest.dispatchSelection }) },
|
|
57
|
+
...manifest.sessionRef === void 0 ? {} : { sessionRef: manifest.sessionRef },
|
|
58
|
+
workspace: Object.freeze({ ...manifest.workspace }),
|
|
59
|
+
forwardedEnvironmentNames: Object.freeze([...manifest.forwardedEnvironmentNames])
|
|
60
|
+
});
|
|
61
|
+
}
|
|
16
62
|
var PolicyUnsupportedError = class extends Error {
|
|
17
63
|
constructor(message) {
|
|
18
64
|
super(message);
|
|
@@ -20,7 +66,7 @@ var PolicyUnsupportedError = class extends Error {
|
|
|
20
66
|
}
|
|
21
67
|
};
|
|
22
68
|
var SteerUnsupportedError = class extends Error {
|
|
23
|
-
/** The `RuntimeAdapter.id` that cannot steer (e.g. `claude`, `codex`). */
|
|
69
|
+
/** The `RuntimeAdapter.descriptor.id` that cannot steer (e.g. `claude`, `codex`). */
|
|
24
70
|
runtimeId;
|
|
25
71
|
constructor(runtimeId, message) {
|
|
26
72
|
super(message);
|
|
@@ -28,6 +74,90 @@ var SteerUnsupportedError = class extends Error {
|
|
|
28
74
|
this.runtimeId = runtimeId;
|
|
29
75
|
}
|
|
30
76
|
};
|
|
77
|
+
|
|
78
|
+
// src/runtime-failure.ts
|
|
79
|
+
var RUNTIME_EXECUTION_FAILURE_BRAND = /* @__PURE__ */ Symbol.for("@byok-sdk/client/RuntimeExecutionFailure/v1");
|
|
80
|
+
var RUNTIME_DISPOSAL_FAILURE_BRAND = /* @__PURE__ */ Symbol.for("@byok-sdk/client/RuntimeDisposalFailure/v1");
|
|
81
|
+
var RuntimeDisposalFailure = class extends Error {
|
|
82
|
+
stage;
|
|
83
|
+
constructor(input, options) {
|
|
84
|
+
if (!isRuntimeDisposalStage(input.stage) || typeof input.reason !== "string" || input.reason.length === 0) {
|
|
85
|
+
throw new TypeError("invalid RuntimeDisposalFailure input");
|
|
86
|
+
}
|
|
87
|
+
super(input.reason, options);
|
|
88
|
+
this.name = "RuntimeDisposalFailure";
|
|
89
|
+
this.stage = input.stage;
|
|
90
|
+
Object.defineProperty(this, RUNTIME_DISPOSAL_FAILURE_BRAND, { value: true });
|
|
91
|
+
Object.freeze(this);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
function isRuntimeDisposalStage(value) {
|
|
95
|
+
return value === "signal" || value === "quiescence" || value === "cleanup";
|
|
96
|
+
}
|
|
97
|
+
function isRuntimeDisposalFailure(value) {
|
|
98
|
+
if (typeof value !== "object" || value === null) return false;
|
|
99
|
+
const candidate = value;
|
|
100
|
+
return candidate[RUNTIME_DISPOSAL_FAILURE_BRAND] === true && isRuntimeDisposalStage(candidate.stage) && typeof candidate.message === "string" && candidate.message.length > 0;
|
|
101
|
+
}
|
|
102
|
+
var RuntimeExecutionFailure = class extends Error {
|
|
103
|
+
phase;
|
|
104
|
+
category;
|
|
105
|
+
retry;
|
|
106
|
+
constructor(input, options) {
|
|
107
|
+
if (!isRuntimeFailurePhase(input.phase) || !isRuntimeFailureCategory(input.category) || !isRuntimeRetryDisposition(input.retry) || typeof input.reason !== "string" || input.reason.length === 0) {
|
|
108
|
+
throw new TypeError("invalid RuntimeExecutionFailure input");
|
|
109
|
+
}
|
|
110
|
+
super(input.reason, options);
|
|
111
|
+
this.name = "RuntimeExecutionFailure";
|
|
112
|
+
this.phase = input.phase;
|
|
113
|
+
this.category = input.category;
|
|
114
|
+
this.retry = input.retry;
|
|
115
|
+
Object.defineProperty(this, RUNTIME_EXECUTION_FAILURE_BRAND, { value: true });
|
|
116
|
+
Object.freeze(this);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
function isRuntimeFailurePhase(value) {
|
|
120
|
+
return value === "start" || value === "run";
|
|
121
|
+
}
|
|
122
|
+
function isRuntimeFailureCategory(value) {
|
|
123
|
+
return value === "semantic" || value === "infrastructure" || value === "authority";
|
|
124
|
+
}
|
|
125
|
+
function isRuntimeRetryDisposition(value) {
|
|
126
|
+
return value === "retryable" || value === "non-retryable";
|
|
127
|
+
}
|
|
128
|
+
function isRuntimeExecutionFailure(value) {
|
|
129
|
+
if (typeof value !== "object" || value === null) return false;
|
|
130
|
+
const candidate = value;
|
|
131
|
+
return candidate[RUNTIME_EXECUTION_FAILURE_BRAND] === true && isRuntimeFailurePhase(candidate.phase) && isRuntimeFailureCategory(candidate.category) && isRuntimeRetryDisposition(candidate.retry) && typeof candidate.message === "string" && candidate.message.length > 0;
|
|
132
|
+
}
|
|
133
|
+
function retryableFromDisposition(disposition) {
|
|
134
|
+
switch (disposition) {
|
|
135
|
+
case "retryable":
|
|
136
|
+
return true;
|
|
137
|
+
case "non-retryable":
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function projectRuntimeExecutionFailure(failure) {
|
|
142
|
+
return {
|
|
143
|
+
reason: failure.message,
|
|
144
|
+
retryable: retryableFromDisposition(failure.retry)
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
var RUNTIME_ADAPTER_CONTRACT_VIOLATION_REASON = Object.freeze({
|
|
148
|
+
start: "runtime adapter contract violation during start",
|
|
149
|
+
run: "runtime adapter contract violation during run"
|
|
150
|
+
});
|
|
151
|
+
function projectRuntimeBoundaryFailure(value, expectedPhase) {
|
|
152
|
+
if (isRuntimeExecutionFailure(value) && value.phase === expectedPhase) {
|
|
153
|
+
return { ...projectRuntimeExecutionFailure(value), contractViolation: false };
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
reason: RUNTIME_ADAPTER_CONTRACT_VIOLATION_REASON[expectedPhase],
|
|
157
|
+
retryable: false,
|
|
158
|
+
contractViolation: true
|
|
159
|
+
};
|
|
160
|
+
}
|
|
31
161
|
var GitWorkspaceError = class extends Error {
|
|
32
162
|
constructor(category, message = category) {
|
|
33
163
|
super(message);
|
|
@@ -704,12 +834,12 @@ function resolvePiBin() {
|
|
|
704
834
|
}
|
|
705
835
|
} catch (cause) {
|
|
706
836
|
throw new Error(
|
|
707
|
-
`Required ${PI_PACKAGE_NAME} could not be resolved; install @byok-sdk/client dependencies or set BYOK_PI_BIN to a Node 22.
|
|
837
|
+
`Required ${PI_PACKAGE_NAME} could not be resolved; install @byok-sdk/client dependencies or set BYOK_PI_BIN to a Node 22.22+ pi sidecar`,
|
|
708
838
|
{ cause }
|
|
709
839
|
);
|
|
710
840
|
}
|
|
711
841
|
throw new Error(
|
|
712
|
-
`Required ${PI_PACKAGE_NAME} does not expose the pi CLI; reinstall the pinned dependency or set BYOK_PI_BIN to a Node 22.
|
|
842
|
+
`Required ${PI_PACKAGE_NAME} does not expose the pi CLI; reinstall the pinned dependency or set BYOK_PI_BIN to a Node 22.22+ pi sidecar`
|
|
713
843
|
);
|
|
714
844
|
}
|
|
715
845
|
|
|
@@ -915,6 +1045,266 @@ var AsyncQueue = class {
|
|
|
915
1045
|
}
|
|
916
1046
|
};
|
|
917
1047
|
|
|
1048
|
+
// src/adapters/taskkill-pid-set.ts
|
|
1049
|
+
var INTEGER_PATTERN = /\d+/g;
|
|
1050
|
+
function isCandidatePid(value) {
|
|
1051
|
+
return Number.isSafeInteger(value) && value > 0;
|
|
1052
|
+
}
|
|
1053
|
+
function walkTaskkillPidSet(text, rootPid, excludedPids = []) {
|
|
1054
|
+
const excluded = new Set(excludedPids);
|
|
1055
|
+
const accepted = /* @__PURE__ */ new Set();
|
|
1056
|
+
if (isCandidatePid(rootPid)) accepted.add(rootPid);
|
|
1057
|
+
const lines = text.split(/\r?\n/).map((line) => {
|
|
1058
|
+
const pids = [];
|
|
1059
|
+
for (const match of line.matchAll(INTEGER_PATTERN)) {
|
|
1060
|
+
const pid = Number(match[0]);
|
|
1061
|
+
if (isCandidatePid(pid) && !excluded.has(pid)) pids.push(pid);
|
|
1062
|
+
}
|
|
1063
|
+
return pids;
|
|
1064
|
+
});
|
|
1065
|
+
let changed = true;
|
|
1066
|
+
while (changed) {
|
|
1067
|
+
changed = false;
|
|
1068
|
+
for (const pids of lines) {
|
|
1069
|
+
if (!pids.some((pid) => accepted.has(pid))) continue;
|
|
1070
|
+
for (const pid of pids) {
|
|
1071
|
+
if (accepted.has(pid)) continue;
|
|
1072
|
+
accepted.add(pid);
|
|
1073
|
+
changed = true;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
return accepted;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// src/adapters/process-tree.ts
|
|
1081
|
+
var DEFAULT_TERM_GRACE_MS = 750;
|
|
1082
|
+
var DEFAULT_KILL_GRACE_MS = 2e3;
|
|
1083
|
+
var POLL_MS = 20;
|
|
1084
|
+
var terminationState = /* @__PURE__ */ new WeakMap();
|
|
1085
|
+
function stateFor(child) {
|
|
1086
|
+
const existing = terminationState.get(child);
|
|
1087
|
+
if (existing) return existing;
|
|
1088
|
+
const created = { requested: false, acceptedPids: /* @__PURE__ */ new Set() };
|
|
1089
|
+
terminationState.set(child, created);
|
|
1090
|
+
return created;
|
|
1091
|
+
}
|
|
1092
|
+
function defaultKill(pid, signal) {
|
|
1093
|
+
process.kill(pid, signal);
|
|
1094
|
+
}
|
|
1095
|
+
function withOwnedProcessTree(options) {
|
|
1096
|
+
return {
|
|
1097
|
+
...options,
|
|
1098
|
+
...process.platform === "win32" ? { windowsHide: true } : { detached: true }
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
function positivePid(child, label) {
|
|
1102
|
+
const pid = child.pid;
|
|
1103
|
+
if (pid === void 0) return void 0;
|
|
1104
|
+
if (!Number.isSafeInteger(pid) || pid <= 0 || pid === process.pid) {
|
|
1105
|
+
throw new RuntimeDisposalFailure({
|
|
1106
|
+
stage: "signal",
|
|
1107
|
+
reason: `${label} runtime process has an unsafe owned pid`
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
return pid;
|
|
1111
|
+
}
|
|
1112
|
+
function groupExists(pid, label, kill) {
|
|
1113
|
+
try {
|
|
1114
|
+
kill(-pid, 0);
|
|
1115
|
+
return true;
|
|
1116
|
+
} catch (cause) {
|
|
1117
|
+
const code = cause.code;
|
|
1118
|
+
if (code === "ESRCH") return false;
|
|
1119
|
+
if (code === "EPERM") return true;
|
|
1120
|
+
throw new RuntimeDisposalFailure({
|
|
1121
|
+
stage: "quiescence",
|
|
1122
|
+
reason: `${label} runtime process-group state could not be verified`
|
|
1123
|
+
}, { cause });
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
function processExists(pid, label, kill) {
|
|
1127
|
+
try {
|
|
1128
|
+
kill(pid, 0);
|
|
1129
|
+
return true;
|
|
1130
|
+
} catch (cause) {
|
|
1131
|
+
const code = cause.code;
|
|
1132
|
+
if (code === "ESRCH") return false;
|
|
1133
|
+
if (code === "EPERM") return true;
|
|
1134
|
+
throw new RuntimeDisposalFailure({
|
|
1135
|
+
stage: "quiescence",
|
|
1136
|
+
reason: `${label} runtime process ${pid} state could not be verified`
|
|
1137
|
+
}, { cause });
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
function signalGroup(pid, signal, label, kill) {
|
|
1141
|
+
try {
|
|
1142
|
+
kill(-pid, signal);
|
|
1143
|
+
} catch (cause) {
|
|
1144
|
+
const code = cause.code;
|
|
1145
|
+
if (code === "ESRCH" || code === "EPERM") return;
|
|
1146
|
+
throw new RuntimeDisposalFailure({
|
|
1147
|
+
stage: "signal",
|
|
1148
|
+
reason: `${label} runtime process group ${pid} could not receive ${signal} (${code ?? "unknown"})`
|
|
1149
|
+
}, { cause });
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
async function runTaskkill(pid, options) {
|
|
1153
|
+
const spawnFn = options.spawnFn ?? spawn;
|
|
1154
|
+
return new Promise((resolve, reject) => {
|
|
1155
|
+
const signalFailure = (cause) => {
|
|
1156
|
+
reject(new RuntimeDisposalFailure({
|
|
1157
|
+
stage: "signal",
|
|
1158
|
+
reason: `${options.label} runtime process tree termination could not be requested`
|
|
1159
|
+
}, { cause }));
|
|
1160
|
+
};
|
|
1161
|
+
let taskkill;
|
|
1162
|
+
try {
|
|
1163
|
+
taskkill = spawnFn("taskkill", ["/PID", String(pid), "/T", "/F"], {
|
|
1164
|
+
windowsHide: true,
|
|
1165
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1166
|
+
});
|
|
1167
|
+
} catch (cause) {
|
|
1168
|
+
signalFailure(cause);
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
const chunks = [];
|
|
1172
|
+
taskkill.stdout?.on("data", (chunk) => chunks.push(chunk));
|
|
1173
|
+
taskkill.stderr?.on("data", (chunk) => chunks.push(chunk));
|
|
1174
|
+
taskkill.once("error", signalFailure);
|
|
1175
|
+
taskkill.once("close", () => resolve(Buffer.concat(chunks).toString("latin1")));
|
|
1176
|
+
});
|
|
1177
|
+
}
|
|
1178
|
+
function liveAcceptedPids(accepted, label, kill) {
|
|
1179
|
+
const live = [];
|
|
1180
|
+
for (const pid of accepted) {
|
|
1181
|
+
if (processExists(pid, label, kill)) live.push(pid);
|
|
1182
|
+
}
|
|
1183
|
+
return live;
|
|
1184
|
+
}
|
|
1185
|
+
async function waitUntil(predicate, timeoutMs) {
|
|
1186
|
+
const deadline = Date.now() + timeoutMs;
|
|
1187
|
+
while (predicate()) {
|
|
1188
|
+
if (Date.now() >= deadline) return false;
|
|
1189
|
+
await new Promise((resolve) => {
|
|
1190
|
+
setTimeout(resolve, POLL_MS);
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
return true;
|
|
1194
|
+
}
|
|
1195
|
+
async function waitWithDeadline(promise, timeoutMs) {
|
|
1196
|
+
return new Promise((resolve) => {
|
|
1197
|
+
let settled = false;
|
|
1198
|
+
const timer = setTimeout(() => {
|
|
1199
|
+
if (!settled) {
|
|
1200
|
+
settled = true;
|
|
1201
|
+
resolve(false);
|
|
1202
|
+
}
|
|
1203
|
+
}, timeoutMs);
|
|
1204
|
+
void promise.then(
|
|
1205
|
+
() => {
|
|
1206
|
+
if (!settled) {
|
|
1207
|
+
settled = true;
|
|
1208
|
+
clearTimeout(timer);
|
|
1209
|
+
resolve(true);
|
|
1210
|
+
}
|
|
1211
|
+
},
|
|
1212
|
+
() => {
|
|
1213
|
+
if (!settled) {
|
|
1214
|
+
settled = true;
|
|
1215
|
+
clearTimeout(timer);
|
|
1216
|
+
resolve(false);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
);
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
async function requestOwnedProcessTreeTermination(options) {
|
|
1223
|
+
const platform = options.platform ?? process.platform;
|
|
1224
|
+
if (platform !== "win32" && options.isClosed()) return;
|
|
1225
|
+
const pid = positivePid(options.child, options.label);
|
|
1226
|
+
if (pid === void 0) return;
|
|
1227
|
+
if (platform === "win32") {
|
|
1228
|
+
const output = await runTaskkill(pid, options);
|
|
1229
|
+
const state = stateFor(options.child);
|
|
1230
|
+
for (const walked of walkTaskkillPidSet(output, pid, [process.pid])) state.acceptedPids.add(walked);
|
|
1231
|
+
state.requested = true;
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
signalGroup(pid, "SIGTERM", options.label, options.killFn ?? defaultKill);
|
|
1235
|
+
stateFor(options.child).requested = true;
|
|
1236
|
+
}
|
|
1237
|
+
async function disposeOwnedProcessTree(options) {
|
|
1238
|
+
const pid = positivePid(options.child, options.label);
|
|
1239
|
+
const termGraceMs = options.termGraceMs ?? DEFAULT_TERM_GRACE_MS;
|
|
1240
|
+
const killGraceMs = options.killGraceMs ?? DEFAULT_KILL_GRACE_MS;
|
|
1241
|
+
const platform = options.platform ?? process.platform;
|
|
1242
|
+
const kill = options.killFn ?? defaultKill;
|
|
1243
|
+
if (pid === void 0) {
|
|
1244
|
+
if (await waitWithDeadline(options.waitClosed(), killGraceMs)) return;
|
|
1245
|
+
throw new RuntimeDisposalFailure({
|
|
1246
|
+
stage: "quiescence",
|
|
1247
|
+
reason: `${options.label} runtime process did not settle after spawn failure`
|
|
1248
|
+
});
|
|
1249
|
+
}
|
|
1250
|
+
if (platform === "win32") {
|
|
1251
|
+
if (!terminationState.get(options.child)?.requested) {
|
|
1252
|
+
await requestOwnedProcessTreeTermination(options);
|
|
1253
|
+
}
|
|
1254
|
+
const accepted = terminationState.get(options.child)?.acceptedPids ?? /* @__PURE__ */ new Set();
|
|
1255
|
+
const deadline = Date.now() + killGraceMs;
|
|
1256
|
+
const resweepAt = Date.now() + Math.floor(killGraceMs / 2);
|
|
1257
|
+
let reswept = false;
|
|
1258
|
+
let live = liveAcceptedPids(accepted, options.label, kill);
|
|
1259
|
+
while (live.length > 0) {
|
|
1260
|
+
if (Date.now() >= deadline) {
|
|
1261
|
+
throw new RuntimeDisposalFailure({
|
|
1262
|
+
stage: "quiescence",
|
|
1263
|
+
reason: `${options.label} runtime process tree did not quiesce: ${live.length} of ${accepted.size} walked process ids were still alive at the disposal deadline`
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
if (!reswept && Date.now() >= resweepAt) {
|
|
1267
|
+
reswept = true;
|
|
1268
|
+
for (const livePid of live) {
|
|
1269
|
+
for (const walked of walkTaskkillPidSet(await runTaskkill(livePid, options), livePid, [process.pid])) {
|
|
1270
|
+
accepted.add(walked);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
await new Promise((resolve) => {
|
|
1275
|
+
setTimeout(resolve, POLL_MS);
|
|
1276
|
+
});
|
|
1277
|
+
live = liveAcceptedPids(accepted, options.label, kill);
|
|
1278
|
+
}
|
|
1279
|
+
if (!await waitWithDeadline(options.waitClosed(), killGraceMs)) {
|
|
1280
|
+
throw new RuntimeDisposalFailure({
|
|
1281
|
+
stage: "quiescence",
|
|
1282
|
+
reason: `${options.label} runtime root did not emit close after its process tree quiesced`
|
|
1283
|
+
});
|
|
1284
|
+
}
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
if (groupExists(pid, options.label, kill) && !terminationState.get(options.child)?.requested) {
|
|
1288
|
+
signalGroup(pid, "SIGTERM", options.label, kill);
|
|
1289
|
+
stateFor(options.child).requested = true;
|
|
1290
|
+
}
|
|
1291
|
+
if (!await waitUntil(() => groupExists(pid, options.label, kill), termGraceMs)) {
|
|
1292
|
+
signalGroup(pid, "SIGKILL", options.label, kill);
|
|
1293
|
+
if (!await waitUntil(() => groupExists(pid, options.label, kill), killGraceMs)) {
|
|
1294
|
+
throw new RuntimeDisposalFailure({
|
|
1295
|
+
stage: "quiescence",
|
|
1296
|
+
reason: `${options.label} runtime process group remained live after SIGKILL`
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
if (!await waitWithDeadline(options.waitClosed(), killGraceMs)) {
|
|
1301
|
+
throw new RuntimeDisposalFailure({
|
|
1302
|
+
stage: "quiescence",
|
|
1303
|
+
reason: `${options.label} runtime root did not emit close after its process group exited`
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
|
|
918
1308
|
// src/adapters/pi/rpc-client.ts
|
|
919
1309
|
var STDERR_RING_CAPACITY = 20;
|
|
920
1310
|
var DIALOG_UI_METHODS = /* @__PURE__ */ new Set(["select", "confirm", "input", "editor"]);
|
|
@@ -926,16 +1316,22 @@ var PiRpcClient = class {
|
|
|
926
1316
|
eventQueue = new AsyncQueue();
|
|
927
1317
|
closed = false;
|
|
928
1318
|
exitError;
|
|
1319
|
+
closedPromise;
|
|
1320
|
+
resolveClosed;
|
|
1321
|
+
disposalAttempt;
|
|
929
1322
|
/** 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`. */
|
|
930
1323
|
stderrRing = [];
|
|
931
1324
|
/** Count of pi RPC message types `PiSession` (pi-adapter.ts) has told us have no `AgentEvent` mapping and aren't routine bookkeeping — see `recordUnmappedFrame`. */
|
|
932
1325
|
unmappedFrameCounts = /* @__PURE__ */ new Map();
|
|
933
1326
|
constructor(options) {
|
|
934
1327
|
const spawnFn = options.spawnFn ?? spawn;
|
|
935
|
-
this.child = spawnFn(options.command, options.args, {
|
|
1328
|
+
this.child = spawnFn(options.command, options.args, withOwnedProcessTree({
|
|
936
1329
|
cwd: options.cwd,
|
|
937
1330
|
env: options.env,
|
|
938
1331
|
stdio: ["pipe", "pipe", "pipe"]
|
|
1332
|
+
}));
|
|
1333
|
+
this.closedPromise = new Promise((resolve) => {
|
|
1334
|
+
this.resolveClosed = resolve;
|
|
939
1335
|
});
|
|
940
1336
|
this.child.stdout.setEncoding("utf8");
|
|
941
1337
|
this.child.stdout.on("data", (chunk) => this.onData(chunk));
|
|
@@ -970,6 +1366,10 @@ var PiRpcClient = class {
|
|
|
970
1366
|
get events() {
|
|
971
1367
|
return this.eventQueue;
|
|
972
1368
|
}
|
|
1369
|
+
/** Local transport diagnostic retained when the process closes; consumers must classify it explicitly. */
|
|
1370
|
+
get terminalError() {
|
|
1371
|
+
return this.exitError;
|
|
1372
|
+
}
|
|
973
1373
|
/**
|
|
974
1374
|
* Record a pi RPC message `type` that `PiSession` (pi-adapter.ts) decided
|
|
975
1375
|
* has no `AgentEvent` mapping and isn't routine bookkeeping (see
|
|
@@ -988,15 +1388,37 @@ var PiRpcClient = class {
|
|
|
988
1388
|
);
|
|
989
1389
|
}
|
|
990
1390
|
}
|
|
991
|
-
/**
|
|
1391
|
+
/**
|
|
1392
|
+
* Immediate process-tree termination request. `dispose()` is the settlement
|
|
1393
|
+
* receipt, so this stays fire-and-forget: an interrupt must not block on a
|
|
1394
|
+
* terminator. A request that could not be spawned is left unrecorded, so
|
|
1395
|
+
* `dispose()` re-issues it and raises the typed `stage:'signal'` failure —
|
|
1396
|
+
* swallowing it here loses nothing.
|
|
1397
|
+
*/
|
|
992
1398
|
kill() {
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
1399
|
+
void requestOwnedProcessTreeTermination(this.processTreeOptions()).catch(() => {
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1402
|
+
waitClosed() {
|
|
1403
|
+
return this.closedPromise;
|
|
1404
|
+
}
|
|
1405
|
+
dispose() {
|
|
1406
|
+
if (!this.disposalAttempt) {
|
|
1407
|
+
const attempt = disposeOwnedProcessTree(this.processTreeOptions());
|
|
1408
|
+
this.disposalAttempt = attempt.catch((error) => {
|
|
1409
|
+
this.disposalAttempt = void 0;
|
|
1410
|
+
throw error;
|
|
1411
|
+
});
|
|
999
1412
|
}
|
|
1413
|
+
return this.disposalAttempt;
|
|
1414
|
+
}
|
|
1415
|
+
processTreeOptions() {
|
|
1416
|
+
return {
|
|
1417
|
+
child: this.child,
|
|
1418
|
+
waitClosed: () => this.closedPromise,
|
|
1419
|
+
isClosed: () => this.closed,
|
|
1420
|
+
label: "pi"
|
|
1421
|
+
};
|
|
1000
1422
|
}
|
|
1001
1423
|
onData(chunk) {
|
|
1002
1424
|
this.buffer += chunk;
|
|
@@ -1082,6 +1504,7 @@ var PiRpcClient = class {
|
|
|
1082
1504
|
if (this.closed) return;
|
|
1083
1505
|
this.closed = true;
|
|
1084
1506
|
this.exitError = err;
|
|
1507
|
+
this.resolveClosed();
|
|
1085
1508
|
for (const [, waiter] of this.pending) waiter.reject(err);
|
|
1086
1509
|
this.pending.clear();
|
|
1087
1510
|
this.eventQueue.end();
|
|
@@ -1152,8 +1575,17 @@ var PiAdapter = class {
|
|
|
1152
1575
|
this.options = options;
|
|
1153
1576
|
}
|
|
1154
1577
|
options;
|
|
1155
|
-
|
|
1156
|
-
|
|
1578
|
+
descriptor = freezeRuntimeAdapterDescriptor({
|
|
1579
|
+
id: "pi",
|
|
1580
|
+
supportsDispatchSelection: true,
|
|
1581
|
+
capabilities: {
|
|
1582
|
+
steer: true,
|
|
1583
|
+
resume: true,
|
|
1584
|
+
approvalInteractive: false,
|
|
1585
|
+
permissionModes: ["auto", "readonly"]
|
|
1586
|
+
},
|
|
1587
|
+
environmentRequirements: { credentialNames: PROVIDER_CREDENTIAL_ENV_NAMES }
|
|
1588
|
+
});
|
|
1157
1589
|
async detect() {
|
|
1158
1590
|
try {
|
|
1159
1591
|
const bin = this.resolveBin();
|
|
@@ -1165,49 +1597,26 @@ var PiAdapter = class {
|
|
|
1165
1597
|
return { present: false };
|
|
1166
1598
|
}
|
|
1167
1599
|
}
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
}
|
|
1171
|
-
/**
|
|
1172
|
-
* M5: pi authenticates to its ~30 supported providers via env-var API
|
|
1173
|
-
* keys — `detect()`'s own `authPresent` probe above checks this identical
|
|
1174
|
-
* list — so these MUST keep flowing into pi's spawned process or pi auth
|
|
1175
|
-
* breaks entirely. `KNOWN_PROVIDER_ENV_VARS` above is the single source
|
|
1176
|
-
* of truth, reused here rather than duplicated. No `baseNames`: nothing
|
|
1177
|
-
* in this adapter or `rpc-client.ts` reads a pi-specific config-discovery
|
|
1178
|
-
* variable beyond the platform baseline (`daemon/environment.ts`).
|
|
1179
|
-
*/
|
|
1180
|
-
environmentRequirements() {
|
|
1181
|
-
return { credentialNames: PROVIDER_CREDENTIAL_ENV_NAMES };
|
|
1182
|
-
}
|
|
1183
|
-
async start(task, ctx) {
|
|
1184
|
-
if (typeof task.instruction !== "string") {
|
|
1185
|
-
throw new PolicyUnsupportedError("pi adapter only supports string instructions in M0 (no blob-ref fetch yet)");
|
|
1186
|
-
}
|
|
1187
|
-
const mapping = mapPermissionPolicyToPiArgs(ctx.policy);
|
|
1600
|
+
async prepare(input) {
|
|
1601
|
+
const mapping = mapPermissionPolicyToPiArgs(input.policy);
|
|
1188
1602
|
if (!mapping.ok) {
|
|
1189
|
-
|
|
1603
|
+
return { kind: "reject", reason: mapping.reason ?? "policy rejected by pi adapter", retryable: false };
|
|
1190
1604
|
}
|
|
1191
1605
|
const bin = this.resolveBin();
|
|
1192
|
-
const
|
|
1193
|
-
const
|
|
1194
|
-
const selection = task.dispatchSelection;
|
|
1606
|
+
const selection = input.offer.dispatchSelection;
|
|
1607
|
+
const pinnedSelection = selection === void 0 ? void 0 : Object.freeze({ ...selection });
|
|
1195
1608
|
let command = bin.command;
|
|
1196
|
-
let
|
|
1197
|
-
if (
|
|
1198
|
-
if (
|
|
1199
|
-
|
|
1200
|
-
`pi adapter cannot execute ${selection.lane} selection for runtime ${selection.runtimeId}`
|
|
1201
|
-
);
|
|
1609
|
+
let launcherArgs;
|
|
1610
|
+
if (pinnedSelection !== void 0) {
|
|
1611
|
+
if (pinnedSelection.lane !== "byok" || pinnedSelection.runtimeId !== "pi") {
|
|
1612
|
+
return { kind: "reject", reason: `pi adapter cannot execute ${pinnedSelection.lane} selection for runtime ${pinnedSelection.runtimeId}`, retryable: false };
|
|
1202
1613
|
}
|
|
1203
1614
|
const launcher = this.options.byokLauncher;
|
|
1204
1615
|
if (launcher === void 0) {
|
|
1205
|
-
|
|
1206
|
-
"pi BYOK selection requires a configured credential-custody launcher"
|
|
1207
|
-
);
|
|
1616
|
+
return { kind: "reject", reason: "pi BYOK selection requires a configured credential-custody launcher", retryable: false };
|
|
1208
1617
|
}
|
|
1209
1618
|
command = launcher.command;
|
|
1210
|
-
|
|
1619
|
+
launcherArgs = [
|
|
1211
1620
|
...launcher.args ?? [],
|
|
1212
1621
|
"--pi-bin",
|
|
1213
1622
|
bin.command,
|
|
@@ -1217,62 +1626,136 @@ var PiAdapter = class {
|
|
|
1217
1626
|
launcher.sessionDir,
|
|
1218
1627
|
...launcher.secretServicePrefix ? ["--secret-service-prefix", launcher.secretServicePrefix] : [],
|
|
1219
1628
|
"--provider",
|
|
1220
|
-
|
|
1629
|
+
pinnedSelection.providerId,
|
|
1221
1630
|
"--model",
|
|
1222
|
-
|
|
1223
|
-
"--",
|
|
1224
|
-
...piArgs
|
|
1631
|
+
pinnedSelection.modelId
|
|
1225
1632
|
];
|
|
1226
1633
|
}
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1634
|
+
return {
|
|
1635
|
+
kind: "prepared",
|
|
1636
|
+
operation: {
|
|
1637
|
+
start: async (startInput) => {
|
|
1638
|
+
const manifestSelection = startInput.manifest.dispatchSelection;
|
|
1639
|
+
if (!sameDispatchSelection(manifestSelection, pinnedSelection)) {
|
|
1640
|
+
throw new RuntimeExecutionFailure({
|
|
1641
|
+
phase: "start",
|
|
1642
|
+
category: "authority",
|
|
1643
|
+
retry: "non-retryable",
|
|
1644
|
+
reason: "prepared pi operation received a manifest with different runtime selection"
|
|
1645
|
+
});
|
|
1646
|
+
}
|
|
1647
|
+
if (typeof startInput.instruction !== "string") {
|
|
1648
|
+
throw new RuntimeExecutionFailure({
|
|
1649
|
+
phase: "start",
|
|
1650
|
+
category: "authority",
|
|
1651
|
+
retry: "non-retryable",
|
|
1652
|
+
reason: "prepared pi operation requires a resolved string instruction"
|
|
1653
|
+
});
|
|
1654
|
+
}
|
|
1655
|
+
const resumeSessionId = startInput.manifest.sessionRef;
|
|
1656
|
+
const piArgs = ["--mode", "rpc", ...resumeSessionId ? ["--session", resumeSessionId] : [], ...mapping.args];
|
|
1657
|
+
const args = launcherArgs === void 0 ? piArgs : [...launcherArgs, "--", ...piArgs];
|
|
1658
|
+
let rpc;
|
|
1659
|
+
try {
|
|
1660
|
+
rpc = new PiRpcClient({
|
|
1661
|
+
command,
|
|
1662
|
+
args,
|
|
1663
|
+
cwd: startInput.manifest.workspace.workspaceDir,
|
|
1664
|
+
env: manifestSelection === void 0 ? startInput.env : withoutProviderCredentials(startInput.env),
|
|
1665
|
+
spawnFn: this.options.spawnFn
|
|
1666
|
+
});
|
|
1667
|
+
} catch (cause) {
|
|
1668
|
+
throw new RuntimeExecutionFailure({
|
|
1669
|
+
phase: "start",
|
|
1670
|
+
category: "infrastructure",
|
|
1671
|
+
retry: "retryable",
|
|
1672
|
+
reason: "pi runtime process could not be spawned"
|
|
1673
|
+
}, { cause });
|
|
1674
|
+
}
|
|
1675
|
+
let response;
|
|
1676
|
+
try {
|
|
1677
|
+
response = await rpc.send({ type: "prompt", message: startInput.instruction });
|
|
1678
|
+
} catch (cause) {
|
|
1679
|
+
rpc.kill();
|
|
1680
|
+
throw new RuntimeExecutionFailure({
|
|
1681
|
+
phase: "start",
|
|
1682
|
+
category: "infrastructure",
|
|
1683
|
+
retry: "retryable",
|
|
1684
|
+
reason: `pi initial prompt transport failed: ${errorMessage(cause)}`
|
|
1685
|
+
}, { cause });
|
|
1686
|
+
}
|
|
1687
|
+
if (response.success === false) {
|
|
1688
|
+
rpc.kill();
|
|
1689
|
+
throw new RuntimeExecutionFailure({
|
|
1690
|
+
phase: "start",
|
|
1691
|
+
category: "semantic",
|
|
1692
|
+
retry: "non-retryable",
|
|
1693
|
+
reason: typeof response.error === "string" ? response.error : "pi rejected the initial prompt"
|
|
1694
|
+
});
|
|
1695
|
+
}
|
|
1696
|
+
let sessionRef;
|
|
1697
|
+
try {
|
|
1698
|
+
sessionRef = await resolveAuthoritativeSessionId(rpc);
|
|
1699
|
+
} catch (err) {
|
|
1700
|
+
rpc.kill();
|
|
1701
|
+
throw err;
|
|
1702
|
+
}
|
|
1703
|
+
if (resumeSessionId !== void 0 && sessionRef !== resumeSessionId) {
|
|
1704
|
+
rpc.kill();
|
|
1705
|
+
throw new RuntimeExecutionFailure({
|
|
1706
|
+
phase: "start",
|
|
1707
|
+
category: "authority",
|
|
1708
|
+
retry: "non-retryable",
|
|
1709
|
+
reason: "pi resumed a different authoritative session than requested"
|
|
1710
|
+
});
|
|
1711
|
+
}
|
|
1712
|
+
return new PiSession(sessionRef, rpc, manifestSelection);
|
|
1713
|
+
}
|
|
1248
1714
|
}
|
|
1249
|
-
}
|
|
1250
|
-
return new PiSession(sessionRef, rpc, selection);
|
|
1715
|
+
};
|
|
1251
1716
|
}
|
|
1252
1717
|
resolveBin() {
|
|
1253
1718
|
return (this.options.resolveBin ?? resolvePiBin)();
|
|
1254
1719
|
}
|
|
1255
1720
|
};
|
|
1256
|
-
|
|
1721
|
+
function sameDispatchSelection(left, right) {
|
|
1722
|
+
if (left === void 0 || right === void 0) return left === right;
|
|
1723
|
+
return left.lane === right.lane && left.runtimeId === right.runtimeId && left.providerId === right.providerId && left.modelId === right.modelId;
|
|
1724
|
+
}
|
|
1725
|
+
async function resolveAuthoritativeSessionId(rpc) {
|
|
1257
1726
|
let state;
|
|
1258
1727
|
try {
|
|
1259
1728
|
state = await rpc.send({ type: "get_state" });
|
|
1260
1729
|
} catch (err) {
|
|
1261
|
-
|
|
1730
|
+
if (isRuntimeExecutionFailure(err)) throw err;
|
|
1731
|
+
throw new RuntimeExecutionFailure({
|
|
1732
|
+
phase: "start",
|
|
1733
|
+
category: "infrastructure",
|
|
1734
|
+
retry: "retryable",
|
|
1735
|
+
reason: `pi transport ended before yielding an authoritative session id: ${errorMessage(err)}`
|
|
1736
|
+
}, {
|
|
1262
1737
|
cause: err
|
|
1263
1738
|
});
|
|
1264
1739
|
}
|
|
1265
1740
|
if (state.success === false) {
|
|
1266
1741
|
const reason = typeof state.error === "string" ? state.error : "get_state reported failure";
|
|
1267
|
-
throw new
|
|
1742
|
+
throw new RuntimeExecutionFailure({
|
|
1743
|
+
phase: "start",
|
|
1744
|
+
category: "authority",
|
|
1745
|
+
retry: "non-retryable",
|
|
1746
|
+
reason: `pi did not yield an authoritative session id: ${reason}`
|
|
1747
|
+
});
|
|
1268
1748
|
}
|
|
1269
1749
|
const data = state.data;
|
|
1270
1750
|
if (typeof data?.sessionId === "string" && data.sessionId.length > 0) {
|
|
1271
1751
|
return data.sessionId;
|
|
1272
1752
|
}
|
|
1273
|
-
throw new
|
|
1274
|
-
|
|
1275
|
-
|
|
1753
|
+
throw new RuntimeExecutionFailure({
|
|
1754
|
+
phase: "start",
|
|
1755
|
+
category: "authority",
|
|
1756
|
+
retry: "non-retryable",
|
|
1757
|
+
reason: "pi get_state reported no authoritative session id"
|
|
1758
|
+
});
|
|
1276
1759
|
}
|
|
1277
1760
|
var PiSession = class {
|
|
1278
1761
|
constructor(sessionRef, rpc, selection) {
|
|
@@ -1288,12 +1771,40 @@ var PiSession = class {
|
|
|
1288
1771
|
return {
|
|
1289
1772
|
[Symbol.asyncIterator]() {
|
|
1290
1773
|
const inner = rpc.events[Symbol.asyncIterator]();
|
|
1774
|
+
let terminalFailure;
|
|
1291
1775
|
return {
|
|
1292
1776
|
async next() {
|
|
1293
1777
|
for (; ; ) {
|
|
1294
|
-
|
|
1295
|
-
|
|
1778
|
+
if (terminalFailure) throw terminalFailure;
|
|
1779
|
+
let result;
|
|
1780
|
+
try {
|
|
1781
|
+
result = await inner.next();
|
|
1782
|
+
} catch (cause) {
|
|
1783
|
+
throw new RuntimeExecutionFailure({
|
|
1784
|
+
phase: "run",
|
|
1785
|
+
category: "infrastructure",
|
|
1786
|
+
retry: "retryable",
|
|
1787
|
+
reason: "pi runtime event transport failed"
|
|
1788
|
+
}, { cause });
|
|
1789
|
+
}
|
|
1790
|
+
const { value, done } = result;
|
|
1791
|
+
if (done) {
|
|
1792
|
+
throw new RuntimeExecutionFailure({
|
|
1793
|
+
phase: "run",
|
|
1794
|
+
category: "infrastructure",
|
|
1795
|
+
retry: "retryable",
|
|
1796
|
+
reason: "pi runtime process ended before agent_settled"
|
|
1797
|
+
}, { cause: rpc.terminalError });
|
|
1798
|
+
}
|
|
1296
1799
|
const mapped = mapPiMessageToAgentEvent(value);
|
|
1800
|
+
if (value.type === "auto_retry_end" && value.success === false) {
|
|
1801
|
+
terminalFailure = new RuntimeExecutionFailure({
|
|
1802
|
+
phase: "run",
|
|
1803
|
+
category: "semantic",
|
|
1804
|
+
retry: "non-retryable",
|
|
1805
|
+
reason: "pi exhausted its native retry policy"
|
|
1806
|
+
});
|
|
1807
|
+
}
|
|
1297
1808
|
if (mapped) return { value: mapped, done: false };
|
|
1298
1809
|
if (!ROUTINE_PI_EVENT_TYPES.has(value.type)) {
|
|
1299
1810
|
rpc.recordUnmappedFrame(value.type);
|
|
@@ -1323,7 +1834,7 @@ var PiSession = class {
|
|
|
1323
1834
|
await this.rpc.send({ type: "abort" });
|
|
1324
1835
|
}
|
|
1325
1836
|
async close() {
|
|
1326
|
-
this.rpc.
|
|
1837
|
+
await this.rpc.dispose();
|
|
1327
1838
|
}
|
|
1328
1839
|
async resolveApproval() {
|
|
1329
1840
|
throw new Error("pi adapter does not support approval resume: pi never emits needs_approval in M0/M1");
|
|
@@ -1535,7 +2046,15 @@ function mapResult(msg) {
|
|
|
1535
2046
|
diagnostic ? `claude result frame had a missing/invalid is_error flag (got ${JSON.stringify(msg.is_error)}) \u2014 treating as failure, fail-closed; diagnostic content on the frame: ${truncateResultDiagnostic(diagnostic)}` : `claude result frame had a missing/invalid is_error flag (got ${JSON.stringify(msg.is_error)}) \u2014 treating as failure, fail-closed`
|
|
1536
2047
|
);
|
|
1537
2048
|
const events = usageEvent ? [usageEvent, { type: "error", message }] : [{ type: "error", message }];
|
|
1538
|
-
return {
|
|
2049
|
+
return {
|
|
2050
|
+
events,
|
|
2051
|
+
terminalFailure: new RuntimeExecutionFailure({
|
|
2052
|
+
phase: "run",
|
|
2053
|
+
category: msg.is_error === true ? "semantic" : "authority",
|
|
2054
|
+
retry: "non-retryable",
|
|
2055
|
+
reason: msg.is_error === true ? "claude reported terminal task failure" : "claude emitted a malformed terminal result frame"
|
|
2056
|
+
})
|
|
2057
|
+
};
|
|
1539
2058
|
}
|
|
1540
2059
|
var RESULT_DIAGNOSTIC_MAX_CHARS = 2e3;
|
|
1541
2060
|
function truncateResultDiagnostic(text) {
|
|
@@ -1587,16 +2106,22 @@ var ClaudeProcessClient = class {
|
|
|
1587
2106
|
eventQueue = new AsyncQueue();
|
|
1588
2107
|
closed = false;
|
|
1589
2108
|
exitError;
|
|
2109
|
+
closedPromise;
|
|
2110
|
+
resolveClosed;
|
|
2111
|
+
disposalAttempt;
|
|
1590
2112
|
stderrRing = [];
|
|
1591
2113
|
unmappedFrameCounts = /* @__PURE__ */ new Map();
|
|
1592
2114
|
sessionId;
|
|
1593
2115
|
initWaiter;
|
|
1594
2116
|
constructor(options) {
|
|
1595
2117
|
const spawnFn = options.spawnFn ?? spawn;
|
|
1596
|
-
this.child = spawnFn(options.command, options.args, {
|
|
2118
|
+
this.child = spawnFn(options.command, options.args, withOwnedProcessTree({
|
|
1597
2119
|
cwd: options.cwd,
|
|
1598
2120
|
env: options.env,
|
|
1599
2121
|
stdio: ["pipe", "pipe", "pipe"]
|
|
2122
|
+
}));
|
|
2123
|
+
this.closedPromise = new Promise((resolve) => {
|
|
2124
|
+
this.resolveClosed = resolve;
|
|
1600
2125
|
});
|
|
1601
2126
|
this.child.stdout.setEncoding("utf8");
|
|
1602
2127
|
this.child.stdout.on("data", (chunk) => this.onData(chunk));
|
|
@@ -1652,6 +2177,10 @@ var ClaudeProcessClient = class {
|
|
|
1652
2177
|
get events() {
|
|
1653
2178
|
return this.eventQueue;
|
|
1654
2179
|
}
|
|
2180
|
+
/** Local transport diagnostic retained when the process closes; consumers classify it at the session boundary. */
|
|
2181
|
+
get terminalError() {
|
|
2182
|
+
return this.exitError;
|
|
2183
|
+
}
|
|
1655
2184
|
/**
|
|
1656
2185
|
* Record a claude stream-json frame/subtype/content-block label that
|
|
1657
2186
|
* `ClaudeSession`'s event iterator (`../claude-adapter.ts`) decided has
|
|
@@ -1671,15 +2200,37 @@ var ClaudeProcessClient = class {
|
|
|
1671
2200
|
);
|
|
1672
2201
|
}
|
|
1673
2202
|
}
|
|
1674
|
-
/**
|
|
2203
|
+
/**
|
|
2204
|
+
* Immediate process-tree termination request. `dispose()` is the settlement
|
|
2205
|
+
* receipt, so this stays fire-and-forget: an interrupt must not block on a
|
|
2206
|
+
* terminator. A request that could not be spawned is left unrecorded, so
|
|
2207
|
+
* `dispose()` re-issues it and raises the typed `stage:'signal'` failure —
|
|
2208
|
+
* swallowing it here loses nothing.
|
|
2209
|
+
*/
|
|
1675
2210
|
kill() {
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
2211
|
+
void requestOwnedProcessTreeTermination(this.processTreeOptions()).catch(() => {
|
|
2212
|
+
});
|
|
2213
|
+
}
|
|
2214
|
+
waitClosed() {
|
|
2215
|
+
return this.closedPromise;
|
|
2216
|
+
}
|
|
2217
|
+
dispose() {
|
|
2218
|
+
if (!this.disposalAttempt) {
|
|
2219
|
+
const attempt = disposeOwnedProcessTree(this.processTreeOptions());
|
|
2220
|
+
this.disposalAttempt = attempt.catch((error) => {
|
|
2221
|
+
this.disposalAttempt = void 0;
|
|
2222
|
+
throw error;
|
|
2223
|
+
});
|
|
1682
2224
|
}
|
|
2225
|
+
return this.disposalAttempt;
|
|
2226
|
+
}
|
|
2227
|
+
processTreeOptions() {
|
|
2228
|
+
return {
|
|
2229
|
+
child: this.child,
|
|
2230
|
+
waitClosed: () => this.closedPromise,
|
|
2231
|
+
isClosed: () => this.closed,
|
|
2232
|
+
label: "claude"
|
|
2233
|
+
};
|
|
1683
2234
|
}
|
|
1684
2235
|
onData(chunk) {
|
|
1685
2236
|
this.buffer += chunk;
|
|
@@ -1730,6 +2281,7 @@ var ClaudeProcessClient = class {
|
|
|
1730
2281
|
if (this.closed) return;
|
|
1731
2282
|
this.closed = true;
|
|
1732
2283
|
this.exitError = err;
|
|
2284
|
+
this.resolveClosed();
|
|
1733
2285
|
this.initWaiter?.reject(err);
|
|
1734
2286
|
this.initWaiter = void 0;
|
|
1735
2287
|
this.eventQueue.end();
|
|
@@ -1741,18 +2293,37 @@ var APPROVAL_TOOL_NAME = "approval_prompt";
|
|
|
1741
2293
|
var APPROVAL_MCP_SERVER_NAME = "byokapproval";
|
|
1742
2294
|
var execFileAsync2 = promisify(execFile);
|
|
1743
2295
|
var DETECT_TIMEOUT_MS2 = 5e3;
|
|
2296
|
+
function errorMessage2(err) {
|
|
2297
|
+
return err instanceof Error ? err.message : String(err);
|
|
2298
|
+
}
|
|
1744
2299
|
async function cleanupMcpConfigDir(dir) {
|
|
1745
2300
|
if (!dir) return;
|
|
1746
|
-
|
|
1747
|
-
|
|
2301
|
+
try {
|
|
2302
|
+
await promises.rm(dir, { recursive: true, force: true });
|
|
2303
|
+
} catch (cause) {
|
|
2304
|
+
throw new RuntimeDisposalFailure({
|
|
2305
|
+
stage: "cleanup",
|
|
2306
|
+
reason: "claude task-scoped MCP configuration could not be removed"
|
|
2307
|
+
}, { cause });
|
|
2308
|
+
}
|
|
1748
2309
|
}
|
|
1749
2310
|
var ClaudeAdapter = class {
|
|
1750
2311
|
constructor(options = {}) {
|
|
1751
2312
|
this.options = options;
|
|
1752
2313
|
}
|
|
1753
2314
|
options;
|
|
1754
|
-
|
|
1755
|
-
|
|
2315
|
+
descriptor = freezeRuntimeAdapterDescriptor({
|
|
2316
|
+
id: "claude",
|
|
2317
|
+
supportsDispatchSelection: true,
|
|
2318
|
+
capabilities: {
|
|
2319
|
+
steer: false,
|
|
2320
|
+
resume: true,
|
|
2321
|
+
approvalInteractive: true,
|
|
2322
|
+
mcpToolsets: true,
|
|
2323
|
+
permissionModes: ["auto", "readonly", "plan", "confirm"]
|
|
2324
|
+
},
|
|
2325
|
+
environmentRequirements: { credentialNames: [] }
|
|
2326
|
+
});
|
|
1756
2327
|
async detect() {
|
|
1757
2328
|
const bin = this.resolveBin();
|
|
1758
2329
|
try {
|
|
@@ -1764,54 +2335,73 @@ var ClaudeAdapter = class {
|
|
|
1764
2335
|
return { present: false };
|
|
1765
2336
|
}
|
|
1766
2337
|
}
|
|
1767
|
-
|
|
2338
|
+
async prepare(input) {
|
|
2339
|
+
const mapping = mapPermissionPolicyToClaudeArgs(input.policy);
|
|
2340
|
+
if (!mapping.ok) return { kind: "reject", reason: mapping.reason ?? "policy rejected by claude adapter", retryable: false };
|
|
2341
|
+
let modelId;
|
|
2342
|
+
try {
|
|
2343
|
+
modelId = subscriptionModel(input.offer.dispatchSelection, "claude");
|
|
2344
|
+
} catch (error) {
|
|
2345
|
+
return { kind: "reject", reason: error instanceof Error ? error.message : String(error), retryable: false };
|
|
2346
|
+
}
|
|
2347
|
+
if (mapping.needsApprovalMcp && Object.prototype.hasOwnProperty.call(input.mcpServers ?? {}, APPROVAL_MCP_SERVER_NAME)) {
|
|
2348
|
+
return { kind: "reject", reason: `MCP server name "${APPROVAL_MCP_SERVER_NAME}" is reserved by the claude approval channel`, retryable: false };
|
|
2349
|
+
}
|
|
2350
|
+
let bin;
|
|
2351
|
+
try {
|
|
2352
|
+
bin = this.resolveBin();
|
|
2353
|
+
} catch (error) {
|
|
2354
|
+
return { kind: "reject", reason: error instanceof Error ? error.message : String(error), retryable: true };
|
|
2355
|
+
}
|
|
2356
|
+
let approvalMcpBin;
|
|
2357
|
+
if (mapping.needsApprovalMcp) {
|
|
2358
|
+
try {
|
|
2359
|
+
approvalMcpBin = (this.options.resolveApprovalMcpBin ?? resolveApprovalMcpBin)();
|
|
2360
|
+
} catch (error) {
|
|
2361
|
+
return { kind: "reject", reason: error instanceof Error ? error.message : String(error), retryable: true };
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
1768
2364
|
return {
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
permissionModes: ["auto", "readonly", "plan", "confirm"]
|
|
2365
|
+
kind: "prepared",
|
|
2366
|
+
operation: {
|
|
2367
|
+
start: (startInput) => this.startPrepared(startInput, mapping, modelId, bin, approvalMcpBin)
|
|
2368
|
+
}
|
|
1774
2369
|
};
|
|
1775
2370
|
}
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
return { credentialNames: [] };
|
|
1791
|
-
}
|
|
1792
|
-
async start(task, ctx) {
|
|
1793
|
-
if (typeof task.instruction !== "string") {
|
|
1794
|
-
throw new PolicyUnsupportedError("claude adapter only supports string instructions in M2 (no blob-ref fetch yet)");
|
|
1795
|
-
}
|
|
1796
|
-
const mapping = mapPermissionPolicyToClaudeArgs(ctx.policy);
|
|
1797
|
-
if (!mapping.ok) {
|
|
1798
|
-
throw new PolicyUnsupportedError(mapping.reason ?? "policy rejected by claude adapter");
|
|
2371
|
+
async startPrepared(startInput, initialMapping, modelId, bin, approvalMcpBin) {
|
|
2372
|
+
if (!initialMapping.ok) throw new RuntimeExecutionFailure({
|
|
2373
|
+
phase: "start",
|
|
2374
|
+
category: "authority",
|
|
2375
|
+
retry: "non-retryable",
|
|
2376
|
+
reason: "prepared claude permission mapping was invalid"
|
|
2377
|
+
});
|
|
2378
|
+
if (typeof startInput.instruction !== "string") {
|
|
2379
|
+
throw new RuntimeExecutionFailure({
|
|
2380
|
+
phase: "start",
|
|
2381
|
+
category: "authority",
|
|
2382
|
+
retry: "non-retryable",
|
|
2383
|
+
reason: "prepared claude operation requires a resolved string instruction"
|
|
2384
|
+
});
|
|
1799
2385
|
}
|
|
1800
|
-
const
|
|
2386
|
+
const mapping = { ...initialMapping, args: [...initialMapping.args] };
|
|
1801
2387
|
let mcpConfigDir;
|
|
1802
|
-
const taskMcpServers =
|
|
2388
|
+
const taskMcpServers = startInput.mcpServers ?? {};
|
|
1803
2389
|
const needsMcpConfig = mapping.needsApprovalMcp || Object.keys(taskMcpServers).length > 0;
|
|
1804
2390
|
if (mapping.needsApprovalMcp) {
|
|
1805
|
-
if (!
|
|
1806
|
-
throw new
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
`MCP server name "${APPROVAL_MCP_SERVER_NAME}" is reserved by the claude approval channel`
|
|
1813
|
-
);
|
|
2391
|
+
if (!startInput.approvalChannel) {
|
|
2392
|
+
throw new RuntimeExecutionFailure({
|
|
2393
|
+
phase: "start",
|
|
2394
|
+
category: "authority",
|
|
2395
|
+
retry: "non-retryable",
|
|
2396
|
+
reason: 'claude adapter requires policy.mode "confirm" to be started with an approval channel'
|
|
2397
|
+
});
|
|
1814
2398
|
}
|
|
2399
|
+
if (!approvalMcpBin) throw new RuntimeExecutionFailure({
|
|
2400
|
+
phase: "start",
|
|
2401
|
+
category: "authority",
|
|
2402
|
+
retry: "non-retryable",
|
|
2403
|
+
reason: "prepared claude approval MCP binary was not resolved"
|
|
2404
|
+
});
|
|
1815
2405
|
}
|
|
1816
2406
|
if (needsMcpConfig) {
|
|
1817
2407
|
mcpConfigDir = await promises.mkdtemp(path17.join(os5.tmpdir(), "byok-mcp-"));
|
|
@@ -1820,12 +2410,23 @@ var ClaudeAdapter = class {
|
|
|
1820
2410
|
const mcpConfigPath = path17.join(mcpConfigDir, "mcp-config.json");
|
|
1821
2411
|
const mcpServers = { ...taskMcpServers };
|
|
1822
2412
|
if (mapping.needsApprovalMcp) {
|
|
1823
|
-
const approvalChannel =
|
|
1824
|
-
if (!approvalChannel) throw new
|
|
1825
|
-
|
|
2413
|
+
const approvalChannel = startInput.approvalChannel;
|
|
2414
|
+
if (!approvalChannel) throw new RuntimeExecutionFailure({
|
|
2415
|
+
phase: "start",
|
|
2416
|
+
category: "authority",
|
|
2417
|
+
retry: "non-retryable",
|
|
2418
|
+
reason: "prepared claude approval channel was not available"
|
|
2419
|
+
});
|
|
2420
|
+
const preparedApprovalMcpBin = approvalMcpBin;
|
|
2421
|
+
if (!preparedApprovalMcpBin) throw new RuntimeExecutionFailure({
|
|
2422
|
+
phase: "start",
|
|
2423
|
+
category: "authority",
|
|
2424
|
+
retry: "non-retryable",
|
|
2425
|
+
reason: "prepared claude approval MCP binary was not resolved"
|
|
2426
|
+
});
|
|
1826
2427
|
mcpServers[APPROVAL_MCP_SERVER_NAME] = {
|
|
1827
|
-
command:
|
|
1828
|
-
args:
|
|
2428
|
+
command: preparedApprovalMcpBin.command,
|
|
2429
|
+
args: preparedApprovalMcpBin.args,
|
|
1829
2430
|
env: {
|
|
1830
2431
|
BYOK_STORE_DIR: approvalChannel.storeDir,
|
|
1831
2432
|
BYOK_PRODUCT_ID: approvalChannel.productId,
|
|
@@ -1845,8 +2446,26 @@ var ClaudeAdapter = class {
|
|
|
1845
2446
|
"--strict-mcp-config"
|
|
1846
2447
|
];
|
|
1847
2448
|
}
|
|
1848
|
-
const
|
|
1849
|
-
|
|
2449
|
+
const resumeSessionId = startInput.manifest.sessionRef;
|
|
2450
|
+
let manifestModelId;
|
|
2451
|
+
try {
|
|
2452
|
+
manifestModelId = subscriptionModel(startInput.manifest.dispatchSelection, "claude");
|
|
2453
|
+
} catch (cause) {
|
|
2454
|
+
throw new RuntimeExecutionFailure({
|
|
2455
|
+
phase: "start",
|
|
2456
|
+
category: "authority",
|
|
2457
|
+
retry: "non-retryable",
|
|
2458
|
+
reason: "prepared claude operation received an invalid runtime selection manifest"
|
|
2459
|
+
}, { cause });
|
|
2460
|
+
}
|
|
2461
|
+
if (manifestModelId !== modelId) {
|
|
2462
|
+
throw new RuntimeExecutionFailure({
|
|
2463
|
+
phase: "start",
|
|
2464
|
+
category: "authority",
|
|
2465
|
+
retry: "non-retryable",
|
|
2466
|
+
reason: "prepared claude operation received a manifest with different runtime selection"
|
|
2467
|
+
});
|
|
2468
|
+
}
|
|
1850
2469
|
const args = [
|
|
1851
2470
|
"-p",
|
|
1852
2471
|
"--input-format",
|
|
@@ -1858,40 +2477,71 @@ var ClaudeAdapter = class {
|
|
|
1858
2477
|
// "Error: When using --print, --output-format=stream-json requires
|
|
1859
2478
|
// --verbose", before spawning any model call.
|
|
1860
2479
|
"--verbose",
|
|
1861
|
-
...
|
|
2480
|
+
...manifestModelId ? ["--model", manifestModelId] : [],
|
|
1862
2481
|
...resumeSessionId ? ["--resume", resumeSessionId] : [],
|
|
1863
2482
|
...mapping.args
|
|
1864
2483
|
];
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
2484
|
+
let client;
|
|
2485
|
+
try {
|
|
2486
|
+
client = new ClaudeProcessClient({
|
|
2487
|
+
command: bin.command,
|
|
2488
|
+
args,
|
|
2489
|
+
cwd: startInput.manifest.workspace.workspaceDir,
|
|
2490
|
+
env: withoutProviderCredentials(startInput.env),
|
|
2491
|
+
spawnFn: this.options.spawnFn
|
|
2492
|
+
});
|
|
2493
|
+
} catch (cause) {
|
|
2494
|
+
await cleanupMcpConfigDir(mcpConfigDir);
|
|
2495
|
+
throw new RuntimeExecutionFailure({
|
|
2496
|
+
phase: "start",
|
|
2497
|
+
category: "infrastructure",
|
|
2498
|
+
retry: "retryable",
|
|
2499
|
+
reason: "claude runtime process could not be spawned"
|
|
2500
|
+
}, { cause });
|
|
2501
|
+
}
|
|
2502
|
+
try {
|
|
2503
|
+
client.writeUserMessage(startInput.instruction);
|
|
2504
|
+
} catch (cause) {
|
|
2505
|
+
client.kill();
|
|
2506
|
+
await cleanupMcpConfigDir(mcpConfigDir);
|
|
2507
|
+
throw new RuntimeExecutionFailure({
|
|
2508
|
+
phase: "start",
|
|
2509
|
+
category: "infrastructure",
|
|
2510
|
+
retry: "retryable",
|
|
2511
|
+
reason: "claude initial instruction transport failed"
|
|
2512
|
+
}, { cause });
|
|
2513
|
+
}
|
|
1873
2514
|
let sessionRef;
|
|
1874
2515
|
try {
|
|
1875
2516
|
sessionRef = await client.waitForInit();
|
|
1876
2517
|
} catch (err) {
|
|
1877
2518
|
client.kill();
|
|
1878
2519
|
await cleanupMcpConfigDir(mcpConfigDir);
|
|
1879
|
-
throw err;
|
|
2520
|
+
if (isRuntimeExecutionFailure(err)) throw err;
|
|
2521
|
+
throw new RuntimeExecutionFailure({
|
|
2522
|
+
phase: "start",
|
|
2523
|
+
category: "infrastructure",
|
|
2524
|
+
retry: "retryable",
|
|
2525
|
+
reason: `claude exited before yielding an authoritative session id: ${errorMessage2(err)}`
|
|
2526
|
+
}, { cause: err });
|
|
1880
2527
|
}
|
|
1881
2528
|
if (resumeSessionId !== void 0 && sessionRef !== resumeSessionId) {
|
|
1882
2529
|
client.kill();
|
|
1883
2530
|
await cleanupMcpConfigDir(mcpConfigDir);
|
|
1884
|
-
throw new
|
|
1885
|
-
|
|
1886
|
-
|
|
2531
|
+
throw new RuntimeExecutionFailure({
|
|
2532
|
+
phase: "start",
|
|
2533
|
+
category: "authority",
|
|
2534
|
+
retry: "non-retryable",
|
|
2535
|
+
reason: `claude --resume echoed a different session id than requested (requested ${resumeSessionId}, got ${sessionRef})`
|
|
2536
|
+
});
|
|
1887
2537
|
}
|
|
1888
2538
|
return new ClaudeSession(
|
|
1889
2539
|
sessionRef,
|
|
1890
2540
|
client,
|
|
1891
|
-
|
|
1892
|
-
|
|
2541
|
+
startInput.manifest.workspace.workspaceDir,
|
|
2542
|
+
startInput.approvalChannel,
|
|
1893
2543
|
mcpConfigDir,
|
|
1894
|
-
|
|
2544
|
+
manifestModelId
|
|
1895
2545
|
);
|
|
1896
2546
|
}
|
|
1897
2547
|
/**
|
|
@@ -1926,8 +2576,7 @@ var ClaudeAdapter = class {
|
|
|
1926
2576
|
return (this.options.resolveBin ?? resolveClaudeBin)();
|
|
1927
2577
|
}
|
|
1928
2578
|
};
|
|
1929
|
-
function subscriptionModel(
|
|
1930
|
-
const selection = task.dispatchSelection;
|
|
2579
|
+
function subscriptionModel(selection, runtimeId) {
|
|
1931
2580
|
if (selection === void 0) return void 0;
|
|
1932
2581
|
if (selection.lane !== "subscription" || selection.runtimeId !== runtimeId) {
|
|
1933
2582
|
throw new PolicyUnsupportedError(
|
|
@@ -1952,6 +2601,7 @@ var ClaudeSession = class {
|
|
|
1952
2601
|
mcpConfigDir;
|
|
1953
2602
|
modelId;
|
|
1954
2603
|
correlation = createToolUseCorrelation();
|
|
2604
|
+
closeAttempt;
|
|
1955
2605
|
get events() {
|
|
1956
2606
|
const client = this.client;
|
|
1957
2607
|
const correlation = this.correlation;
|
|
@@ -1960,17 +2610,40 @@ var ClaudeSession = class {
|
|
|
1960
2610
|
[Symbol.asyncIterator]() {
|
|
1961
2611
|
const inner = client.events[Symbol.asyncIterator]();
|
|
1962
2612
|
let pending = [];
|
|
2613
|
+
let terminalFailure;
|
|
1963
2614
|
let turnSettled = false;
|
|
1964
2615
|
return {
|
|
1965
2616
|
async next() {
|
|
1966
2617
|
for (; ; ) {
|
|
1967
2618
|
const buffered = pending.shift();
|
|
1968
2619
|
if (buffered) return { value: buffered, done: false };
|
|
1969
|
-
if (turnSettled)
|
|
1970
|
-
|
|
1971
|
-
|
|
2620
|
+
if (turnSettled) {
|
|
2621
|
+
if (terminalFailure) throw terminalFailure;
|
|
2622
|
+
return { value: void 0, done: true };
|
|
2623
|
+
}
|
|
2624
|
+
let raw;
|
|
2625
|
+
try {
|
|
2626
|
+
raw = await inner.next();
|
|
2627
|
+
} catch (cause) {
|
|
2628
|
+
throw new RuntimeExecutionFailure({
|
|
2629
|
+
phase: "run",
|
|
2630
|
+
category: "infrastructure",
|
|
2631
|
+
retry: "retryable",
|
|
2632
|
+
reason: "claude runtime event transport failed"
|
|
2633
|
+
}, { cause });
|
|
2634
|
+
}
|
|
2635
|
+
const { value, done } = raw;
|
|
2636
|
+
if (done) {
|
|
2637
|
+
throw new RuntimeExecutionFailure({
|
|
2638
|
+
phase: "run",
|
|
2639
|
+
category: "infrastructure",
|
|
2640
|
+
retry: "retryable",
|
|
2641
|
+
reason: "claude runtime process ended before a terminal result frame"
|
|
2642
|
+
}, { cause: client.terminalError });
|
|
2643
|
+
}
|
|
1972
2644
|
if (value.type === "result") turnSettled = true;
|
|
1973
2645
|
const mapped = mapClaudeMessageToAgentEvents(value, correlation, { workspaceDir });
|
|
2646
|
+
terminalFailure = mapped.terminalFailure ?? terminalFailure;
|
|
1974
2647
|
if (mapped.unmappedLabel) {
|
|
1975
2648
|
client.recordUnmappedFrame(mapped.unmappedLabel);
|
|
1976
2649
|
}
|
|
@@ -2001,7 +2674,7 @@ var ClaudeSession = class {
|
|
|
2001
2674
|
if (typeof task.instruction !== "string") {
|
|
2002
2675
|
throw new PolicyUnsupportedError("claude adapter only supports string instructions in M2 (no blob-ref fetch yet)");
|
|
2003
2676
|
}
|
|
2004
|
-
const requestedModel = subscriptionModel(task, "claude");
|
|
2677
|
+
const requestedModel = subscriptionModel(task.dispatchSelection, "claude");
|
|
2005
2678
|
if (requestedModel !== void 0 && requestedModel !== this.modelId) {
|
|
2006
2679
|
throw new PolicyUnsupportedError(
|
|
2007
2680
|
`claude persistent session cannot change model from ${this.modelId ?? "(legacy default)"} to ${requestedModel}`
|
|
@@ -2025,8 +2698,17 @@ var ClaudeSession = class {
|
|
|
2025
2698
|
this.client.kill();
|
|
2026
2699
|
}
|
|
2027
2700
|
async close() {
|
|
2028
|
-
this.
|
|
2029
|
-
|
|
2701
|
+
if (!this.closeAttempt) {
|
|
2702
|
+
const attempt = (async () => {
|
|
2703
|
+
await this.client.dispose();
|
|
2704
|
+
await cleanupMcpConfigDir(this.mcpConfigDir);
|
|
2705
|
+
})();
|
|
2706
|
+
this.closeAttempt = attempt.catch((error) => {
|
|
2707
|
+
this.closeAttempt = void 0;
|
|
2708
|
+
throw error;
|
|
2709
|
+
});
|
|
2710
|
+
}
|
|
2711
|
+
await this.closeAttempt;
|
|
2030
2712
|
}
|
|
2031
2713
|
/**
|
|
2032
2714
|
* M4 Phase 3: routes into the out-of-band approval channel `start()`
|
|
@@ -2243,14 +2925,15 @@ var CodexProcessRunner = class {
|
|
|
2243
2925
|
exitSignal = null;
|
|
2244
2926
|
closedPromise;
|
|
2245
2927
|
resolveClosed;
|
|
2928
|
+
disposalAttempt;
|
|
2246
2929
|
constructor(options) {
|
|
2247
2930
|
this.onEvent = options.onEvent;
|
|
2248
2931
|
const spawnFn = options.spawnFn ?? spawn;
|
|
2249
|
-
this.child = spawnFn(options.command, options.args, {
|
|
2932
|
+
this.child = spawnFn(options.command, options.args, withOwnedProcessTree({
|
|
2250
2933
|
cwd: options.cwd,
|
|
2251
2934
|
env: options.env,
|
|
2252
2935
|
stdio: ["ignore", "pipe", "pipe"]
|
|
2253
|
-
});
|
|
2936
|
+
}));
|
|
2254
2937
|
this.closedPromise = new Promise((resolve) => {
|
|
2255
2938
|
this.resolveClosed = resolve;
|
|
2256
2939
|
});
|
|
@@ -2280,7 +2963,7 @@ var CodexProcessRunner = class {
|
|
|
2280
2963
|
return this.closed;
|
|
2281
2964
|
}
|
|
2282
2965
|
/**
|
|
2283
|
-
*
|
|
2966
|
+
* Immediate tree termination request. SIGTERM on POSIX: SIGINT was empirically confirmed
|
|
2284
2967
|
* to be silently ignored by `codex exec` (a real, direct test — a 60s
|
|
2285
2968
|
* shell `sleep` ran to full, unaffected completion despite SIGINT sent at
|
|
2286
2969
|
* t=4s) — a genuine, evidence-based correction to this task's own initial
|
|
@@ -2291,15 +2974,33 @@ var CodexProcessRunner = class {
|
|
|
2291
2974
|
* cleanly resumable afterward via `codex exec resume` (no corruption from
|
|
2292
2975
|
* killing mid-turn). `taskkill /T /F` on Windows, mirroring
|
|
2293
2976
|
* `../pi/rpc-client.ts`'s own cross-platform convention.
|
|
2977
|
+
*
|
|
2978
|
+
* Fire-and-forget by design: an interrupt must not block on a terminator,
|
|
2979
|
+
* and `dispose()` is the settlement receipt. A request that could not be
|
|
2980
|
+
* spawned is left unrecorded, so `dispose()` re-issues it and raises the
|
|
2981
|
+
* typed `stage:'signal'` failure — swallowing it here loses nothing.
|
|
2294
2982
|
*/
|
|
2295
2983
|
kill() {
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
this.
|
|
2984
|
+
void requestOwnedProcessTreeTermination(this.processTreeOptions()).catch(() => {
|
|
2985
|
+
});
|
|
2986
|
+
}
|
|
2987
|
+
dispose() {
|
|
2988
|
+
if (!this.disposalAttempt) {
|
|
2989
|
+
const attempt = disposeOwnedProcessTree(this.processTreeOptions());
|
|
2990
|
+
this.disposalAttempt = attempt.catch((error) => {
|
|
2991
|
+
this.disposalAttempt = void 0;
|
|
2992
|
+
throw error;
|
|
2993
|
+
});
|
|
2302
2994
|
}
|
|
2995
|
+
return this.disposalAttempt;
|
|
2996
|
+
}
|
|
2997
|
+
processTreeOptions() {
|
|
2998
|
+
return {
|
|
2999
|
+
child: this.child,
|
|
3000
|
+
waitClosed: () => this.closedPromise,
|
|
3001
|
+
isClosed: () => this.closed,
|
|
3002
|
+
label: "codex"
|
|
3003
|
+
};
|
|
2303
3004
|
}
|
|
2304
3005
|
/** Builds a descriptive error folding in the exit code/signal and the stderr tail — mirrors `PiRpcClient.buildExitError`'s reasoning: a post-mortem on a failed start/resume should never need separately re-running codex by hand with a raw JSONL logger to learn why. */
|
|
2305
3006
|
buildExitError(context) {
|
|
@@ -2350,8 +3051,17 @@ var CodexAdapter = class {
|
|
|
2350
3051
|
this.options = options;
|
|
2351
3052
|
}
|
|
2352
3053
|
options;
|
|
2353
|
-
|
|
2354
|
-
|
|
3054
|
+
descriptor = freezeRuntimeAdapterDescriptor({
|
|
3055
|
+
id: "codex",
|
|
3056
|
+
supportsDispatchSelection: true,
|
|
3057
|
+
capabilities: {
|
|
3058
|
+
steer: false,
|
|
3059
|
+
resume: true,
|
|
3060
|
+
approvalInteractive: false,
|
|
3061
|
+
permissionModes: ["auto", "readonly"]
|
|
3062
|
+
},
|
|
3063
|
+
environmentRequirements: { credentialNames: [] }
|
|
3064
|
+
});
|
|
2355
3065
|
async detect() {
|
|
2356
3066
|
const bin = this.resolveBin();
|
|
2357
3067
|
try {
|
|
@@ -2400,61 +3110,100 @@ ${result.stderr}`);
|
|
|
2400
3110
|
${withStreams.stderr ?? ""}`);
|
|
2401
3111
|
}
|
|
2402
3112
|
}
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
* API-key passthrough remains a separate, pending product decision. No
|
|
2412
|
-
* `baseNames` either: nothing in this adapter reads a codex-specific
|
|
2413
|
-
* config-discovery variable (e.g. `CODEX_HOME`) today.
|
|
2414
|
-
*/
|
|
2415
|
-
environmentRequirements() {
|
|
2416
|
-
return { credentialNames: [] };
|
|
2417
|
-
}
|
|
2418
|
-
async start(task, ctx) {
|
|
2419
|
-
if (typeof task.instruction !== "string") {
|
|
2420
|
-
throw new PolicyUnsupportedError("codex adapter only supports string instructions in M2 (no blob-ref fetch yet)");
|
|
3113
|
+
async prepare(input) {
|
|
3114
|
+
const mapping = mapPermissionPolicyToCodexArgs(input.policy);
|
|
3115
|
+
if (!mapping.ok) return { kind: "reject", reason: mapping.reason ?? "policy rejected by codex adapter", retryable: false };
|
|
3116
|
+
let modelId;
|
|
3117
|
+
try {
|
|
3118
|
+
modelId = subscriptionModel2(input.offer.dispatchSelection);
|
|
3119
|
+
} catch (error) {
|
|
3120
|
+
return { kind: "reject", reason: error instanceof Error ? error.message : String(error), retryable: false };
|
|
2421
3121
|
}
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
3122
|
+
let command;
|
|
3123
|
+
try {
|
|
3124
|
+
command = this.resolveBin().command;
|
|
3125
|
+
} catch (error) {
|
|
3126
|
+
return { kind: "reject", reason: error instanceof Error ? error.message : String(error), retryable: true };
|
|
3127
|
+
}
|
|
3128
|
+
return {
|
|
3129
|
+
kind: "prepared",
|
|
3130
|
+
operation: {
|
|
3131
|
+
start: (startInput) => this.startPrepared(startInput, mapping.args, modelId, command)
|
|
3132
|
+
}
|
|
3133
|
+
};
|
|
3134
|
+
}
|
|
3135
|
+
async startPrepared(startInput, policyArgs, modelId, command) {
|
|
3136
|
+
if (typeof startInput.instruction !== "string") {
|
|
3137
|
+
throw new RuntimeExecutionFailure({
|
|
3138
|
+
phase: "start",
|
|
3139
|
+
category: "authority",
|
|
3140
|
+
retry: "non-retryable",
|
|
3141
|
+
reason: "prepared codex operation requires a resolved string instruction"
|
|
3142
|
+
});
|
|
2425
3143
|
}
|
|
2426
|
-
const modelId = subscriptionModel2(task);
|
|
2427
|
-
const bin = this.resolveBin();
|
|
2428
3144
|
const queue = new AsyncQueue();
|
|
3145
|
+
const terminal = {};
|
|
2429
3146
|
const recordUnmapped = makeUnmappedFrameRecorder(/* @__PURE__ */ new Map());
|
|
2430
|
-
|
|
2431
|
-
|
|
3147
|
+
let workspaceDir;
|
|
3148
|
+
try {
|
|
3149
|
+
workspaceDir = await resolveRealWorkspaceDir(startInput.manifest.workspace.workspaceDir);
|
|
3150
|
+
} catch (cause) {
|
|
3151
|
+
throw new RuntimeExecutionFailure({
|
|
3152
|
+
phase: "start",
|
|
3153
|
+
category: "infrastructure",
|
|
3154
|
+
retry: "retryable",
|
|
3155
|
+
reason: "codex runtime workspace could not be resolved"
|
|
3156
|
+
}, { cause });
|
|
3157
|
+
}
|
|
3158
|
+
const runtimeEnv = withoutProviderCredentials(startInput.env);
|
|
3159
|
+
let manifestModelId;
|
|
3160
|
+
try {
|
|
3161
|
+
manifestModelId = subscriptionModel2(startInput.manifest.dispatchSelection);
|
|
3162
|
+
} catch (cause) {
|
|
3163
|
+
throw new RuntimeExecutionFailure({
|
|
3164
|
+
phase: "start",
|
|
3165
|
+
category: "authority",
|
|
3166
|
+
retry: "non-retryable",
|
|
3167
|
+
reason: "prepared codex operation received an invalid runtime selection manifest"
|
|
3168
|
+
}, { cause });
|
|
3169
|
+
}
|
|
3170
|
+
if (manifestModelId !== modelId) {
|
|
3171
|
+
throw new RuntimeExecutionFailure({
|
|
3172
|
+
phase: "start",
|
|
3173
|
+
category: "authority",
|
|
3174
|
+
retry: "non-retryable",
|
|
3175
|
+
reason: "prepared codex operation received a manifest with different runtime selection"
|
|
3176
|
+
});
|
|
3177
|
+
}
|
|
2432
3178
|
const { sessionRef, runner } = await runCodexTurn({
|
|
2433
|
-
command
|
|
2434
|
-
resumeRef:
|
|
2435
|
-
instruction:
|
|
2436
|
-
modelId,
|
|
2437
|
-
policyArgs:
|
|
2438
|
-
cwd:
|
|
3179
|
+
command,
|
|
3180
|
+
resumeRef: startInput.manifest.sessionRef,
|
|
3181
|
+
instruction: startInput.instruction,
|
|
3182
|
+
modelId: manifestModelId,
|
|
3183
|
+
policyArgs: [...policyArgs],
|
|
3184
|
+
cwd: startInput.manifest.workspace.workspaceDir,
|
|
2439
3185
|
env: runtimeEnv,
|
|
2440
3186
|
spawnFn: this.options.spawnFn,
|
|
2441
3187
|
workspaceDir,
|
|
2442
3188
|
queue,
|
|
2443
3189
|
recordUnmapped,
|
|
2444
|
-
expectedSessionRef:
|
|
2445
|
-
preparedGit:
|
|
3190
|
+
expectedSessionRef: startInput.manifest.sessionRef,
|
|
3191
|
+
preparedGit: startInput.manifest.workspace.workspaceId !== void 0,
|
|
3192
|
+
failurePhase: "start",
|
|
3193
|
+
terminal
|
|
2446
3194
|
});
|
|
2447
3195
|
return new CodexSession({
|
|
2448
3196
|
sessionRef,
|
|
2449
|
-
command
|
|
3197
|
+
command,
|
|
2450
3198
|
workspaceDir,
|
|
2451
|
-
env:
|
|
3199
|
+
env: startInput.env,
|
|
2452
3200
|
spawnFn: this.options.spawnFn,
|
|
2453
3201
|
queue,
|
|
2454
3202
|
recordUnmapped,
|
|
2455
3203
|
initialRunner: runner,
|
|
2456
|
-
preparedGit:
|
|
2457
|
-
modelId
|
|
3204
|
+
preparedGit: startInput.manifest.workspace.workspaceId !== void 0,
|
|
3205
|
+
modelId: manifestModelId,
|
|
3206
|
+
terminal
|
|
2458
3207
|
});
|
|
2459
3208
|
}
|
|
2460
3209
|
resolveBin() {
|
|
@@ -2502,37 +3251,69 @@ async function runCodexTurn(params) {
|
|
|
2502
3251
|
rejectFirstLine = reject;
|
|
2503
3252
|
});
|
|
2504
3253
|
let turnEnded = false;
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
if (
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
3254
|
+
let runner;
|
|
3255
|
+
try {
|
|
3256
|
+
runner = new CodexProcessRunner({
|
|
3257
|
+
command: params.command,
|
|
3258
|
+
args: argv,
|
|
3259
|
+
cwd: params.cwd,
|
|
3260
|
+
env: params.env,
|
|
3261
|
+
spawnFn: params.spawnFn,
|
|
3262
|
+
onEvent: (evt) => {
|
|
3263
|
+
if (!firstLineSettled) {
|
|
3264
|
+
firstLineSettled = true;
|
|
3265
|
+
if (evt.type === "thread.started" && typeof evt.thread_id === "string" && evt.thread_id.length > 0) {
|
|
3266
|
+
resolveFirstLine(evt.thread_id);
|
|
3267
|
+
} else {
|
|
3268
|
+
rejectFirstLine(
|
|
3269
|
+
new RuntimeExecutionFailure({
|
|
3270
|
+
phase: params.failurePhase,
|
|
3271
|
+
category: "authority",
|
|
3272
|
+
retry: "non-retryable",
|
|
3273
|
+
reason: `codex did not yield thread.started as its first event (got ${JSON.stringify(evt).slice(0, 200)})`
|
|
3274
|
+
})
|
|
3275
|
+
);
|
|
3276
|
+
}
|
|
3277
|
+
return;
|
|
3278
|
+
}
|
|
3279
|
+
const mapped = mapCodexEventToAgentEvents(evt, params.workspaceDir);
|
|
3280
|
+
for (const agentEvent of mapped) {
|
|
3281
|
+
if (agentEvent.type === "turn_end") turnEnded = true;
|
|
3282
|
+
params.queue.push(agentEvent);
|
|
3283
|
+
}
|
|
3284
|
+
if (evt.type === "turn.failed") {
|
|
3285
|
+
params.terminal.failure = new RuntimeExecutionFailure({
|
|
3286
|
+
phase: "run",
|
|
3287
|
+
category: "semantic",
|
|
3288
|
+
retry: "non-retryable",
|
|
3289
|
+
reason: "codex reported terminal task failure"
|
|
3290
|
+
});
|
|
3291
|
+
params.queue.end();
|
|
3292
|
+
}
|
|
3293
|
+
if (mapped.length === 0 && !isRoutineCodexEvent(evt)) {
|
|
3294
|
+
params.recordUnmapped(unmappedFrameKey(evt));
|
|
2520
3295
|
}
|
|
2521
|
-
return;
|
|
2522
|
-
}
|
|
2523
|
-
const mapped = mapCodexEventToAgentEvents(evt, params.workspaceDir);
|
|
2524
|
-
for (const agentEvent of mapped) {
|
|
2525
|
-
if (agentEvent.type === "turn_end") turnEnded = true;
|
|
2526
|
-
params.queue.push(agentEvent);
|
|
2527
|
-
}
|
|
2528
|
-
if (mapped.length === 0 && !isRoutineCodexEvent(evt)) {
|
|
2529
|
-
params.recordUnmapped(unmappedFrameKey(evt));
|
|
2530
3296
|
}
|
|
2531
|
-
}
|
|
2532
|
-
})
|
|
3297
|
+
});
|
|
3298
|
+
} catch (cause) {
|
|
3299
|
+
throw new RuntimeExecutionFailure({
|
|
3300
|
+
phase: params.failurePhase,
|
|
3301
|
+
category: "infrastructure",
|
|
3302
|
+
retry: "retryable",
|
|
3303
|
+
reason: "codex runtime process could not be spawned"
|
|
3304
|
+
}, { cause });
|
|
3305
|
+
}
|
|
3306
|
+
params.onRunnerCreated?.(runner);
|
|
2533
3307
|
void runner.waitClosed().then(() => {
|
|
2534
|
-
if (turnEnded) return;
|
|
2535
|
-
|
|
3308
|
+
if (turnEnded || params.terminal.failure) return;
|
|
3309
|
+
const cause = runner.buildExitError("codex exited without completing the turn");
|
|
3310
|
+
params.terminal.failure = new RuntimeExecutionFailure({
|
|
3311
|
+
phase: "run",
|
|
3312
|
+
category: "infrastructure",
|
|
3313
|
+
retry: "retryable",
|
|
3314
|
+
reason: cause.message
|
|
3315
|
+
}, { cause });
|
|
3316
|
+
params.queue.push({ type: "error", message: cause.message });
|
|
2536
3317
|
params.queue.end();
|
|
2537
3318
|
});
|
|
2538
3319
|
let sessionRef;
|
|
@@ -2556,7 +3337,13 @@ async function runCodexTurn(params) {
|
|
|
2556
3337
|
void runner.waitClosed().then(() => {
|
|
2557
3338
|
if (!settled) {
|
|
2558
3339
|
settled = true;
|
|
2559
|
-
|
|
3340
|
+
const cause = runner.buildExitError("codex exited before yielding an authoritative thread id");
|
|
3341
|
+
reject(new RuntimeExecutionFailure({
|
|
3342
|
+
phase: params.failurePhase,
|
|
3343
|
+
category: "infrastructure",
|
|
3344
|
+
retry: "retryable",
|
|
3345
|
+
reason: cause.message
|
|
3346
|
+
}, { cause }));
|
|
2560
3347
|
}
|
|
2561
3348
|
});
|
|
2562
3349
|
});
|
|
@@ -2566,9 +3353,12 @@ async function runCodexTurn(params) {
|
|
|
2566
3353
|
}
|
|
2567
3354
|
if (params.expectedSessionRef !== void 0 && sessionRef !== params.expectedSessionRef) {
|
|
2568
3355
|
runner.kill();
|
|
2569
|
-
throw new
|
|
2570
|
-
|
|
2571
|
-
|
|
3356
|
+
throw new RuntimeExecutionFailure({
|
|
3357
|
+
phase: params.failurePhase,
|
|
3358
|
+
category: "authority",
|
|
3359
|
+
retry: "non-retryable",
|
|
3360
|
+
reason: `codex exec resume echoed a different thread id than requested (requested ${params.expectedSessionRef}, got ${sessionRef})`
|
|
3361
|
+
});
|
|
2572
3362
|
}
|
|
2573
3363
|
return { sessionRef, runner };
|
|
2574
3364
|
}
|
|
@@ -2595,8 +3385,12 @@ var CodexSession = class {
|
|
|
2595
3385
|
recordUnmapped;
|
|
2596
3386
|
preparedGit;
|
|
2597
3387
|
modelId;
|
|
3388
|
+
terminal;
|
|
2598
3389
|
currentRunner;
|
|
3390
|
+
ownedRunners = /* @__PURE__ */ new Set();
|
|
3391
|
+
followUpAttempts = /* @__PURE__ */ new Set();
|
|
2599
3392
|
closed = false;
|
|
3393
|
+
closeAttempt;
|
|
2600
3394
|
constructor(options) {
|
|
2601
3395
|
this.sessionRef = options.sessionRef;
|
|
2602
3396
|
this.command = options.command;
|
|
@@ -2607,11 +3401,36 @@ var CodexSession = class {
|
|
|
2607
3401
|
this.recordUnmapped = options.recordUnmapped;
|
|
2608
3402
|
this.preparedGit = options.preparedGit;
|
|
2609
3403
|
this.modelId = options.modelId;
|
|
3404
|
+
this.terminal = options.terminal;
|
|
2610
3405
|
this.currentRunner = options.initialRunner;
|
|
3406
|
+
this.ownedRunners.add(options.initialRunner);
|
|
2611
3407
|
void this.forgetRunnerOnceClosed(options.initialRunner);
|
|
2612
3408
|
}
|
|
2613
3409
|
get events() {
|
|
2614
|
-
|
|
3410
|
+
const queue = this.queue;
|
|
3411
|
+
const session = this;
|
|
3412
|
+
return {
|
|
3413
|
+
[Symbol.asyncIterator]() {
|
|
3414
|
+
const inner = queue[Symbol.asyncIterator]();
|
|
3415
|
+
return {
|
|
3416
|
+
async next() {
|
|
3417
|
+
let result;
|
|
3418
|
+
try {
|
|
3419
|
+
result = await inner.next();
|
|
3420
|
+
} catch (cause) {
|
|
3421
|
+
throw new RuntimeExecutionFailure({
|
|
3422
|
+
phase: "run",
|
|
3423
|
+
category: "infrastructure",
|
|
3424
|
+
retry: "retryable",
|
|
3425
|
+
reason: "codex runtime event transport failed"
|
|
3426
|
+
}, { cause });
|
|
3427
|
+
}
|
|
3428
|
+
if (result.done && session.terminal.failure) throw session.terminal.failure;
|
|
3429
|
+
return result;
|
|
3430
|
+
}
|
|
3431
|
+
};
|
|
3432
|
+
}
|
|
3433
|
+
};
|
|
2615
3434
|
}
|
|
2616
3435
|
async forgetRunnerOnceClosed(runner) {
|
|
2617
3436
|
await runner.waitClosed();
|
|
@@ -2656,7 +3475,14 @@ var CodexSession = class {
|
|
|
2656
3475
|
* stale id even after codex had moved on) — it just can now only ever be
|
|
2657
3476
|
* the SAME id this call asked to resume, never a silently-different one.
|
|
2658
3477
|
*/
|
|
2659
|
-
|
|
3478
|
+
followUp(task) {
|
|
3479
|
+
const attempt = this.runFollowUp(task);
|
|
3480
|
+
this.followUpAttempts.add(attempt);
|
|
3481
|
+
void attempt.finally(() => this.followUpAttempts.delete(attempt)).catch(() => {
|
|
3482
|
+
});
|
|
3483
|
+
return attempt;
|
|
3484
|
+
}
|
|
3485
|
+
async runFollowUp(task) {
|
|
2660
3486
|
if (typeof task.instruction !== "string") {
|
|
2661
3487
|
throw new PolicyUnsupportedError("codex adapter only supports string instructions in M2 (no blob-ref fetch yet)");
|
|
2662
3488
|
}
|
|
@@ -2667,7 +3493,7 @@ var CodexSession = class {
|
|
|
2667
3493
|
if (!mapping.ok) {
|
|
2668
3494
|
throw new PolicyUnsupportedError(mapping.reason ?? "policy rejected by codex adapter");
|
|
2669
3495
|
}
|
|
2670
|
-
const requestedModel = subscriptionModel2(task);
|
|
3496
|
+
const requestedModel = subscriptionModel2(task.dispatchSelection);
|
|
2671
3497
|
if (requestedModel !== void 0 && requestedModel !== this.modelId) {
|
|
2672
3498
|
throw new PolicyUnsupportedError(
|
|
2673
3499
|
`codex persistent session cannot change model from ${this.modelId ?? "(legacy default)"} to ${requestedModel}`
|
|
@@ -2677,6 +3503,7 @@ var CodexSession = class {
|
|
|
2677
3503
|
const resumeRef = this.sessionRef;
|
|
2678
3504
|
let sessionRef;
|
|
2679
3505
|
let runner;
|
|
3506
|
+
const terminal = {};
|
|
2680
3507
|
try {
|
|
2681
3508
|
({ sessionRef, runner } = await runCodexTurn({
|
|
2682
3509
|
command: this.command,
|
|
@@ -2691,25 +3518,54 @@ var CodexSession = class {
|
|
|
2691
3518
|
queue: this.queue,
|
|
2692
3519
|
recordUnmapped: this.recordUnmapped,
|
|
2693
3520
|
expectedSessionRef: resumeRef,
|
|
2694
|
-
preparedGit: this.preparedGit
|
|
3521
|
+
preparedGit: this.preparedGit,
|
|
3522
|
+
failurePhase: "run",
|
|
3523
|
+
terminal,
|
|
3524
|
+
onRunnerCreated: (created) => {
|
|
3525
|
+
this.ownedRunners.add(created);
|
|
3526
|
+
void this.forgetRunnerOnceClosed(created);
|
|
3527
|
+
}
|
|
2695
3528
|
}));
|
|
2696
3529
|
} catch (err) {
|
|
2697
3530
|
this.queue.end();
|
|
3531
|
+
this.terminal = {
|
|
3532
|
+
failure: isRuntimeExecutionFailure(err) ? err : new RuntimeExecutionFailure({
|
|
3533
|
+
phase: "run",
|
|
3534
|
+
category: "authority",
|
|
3535
|
+
retry: "non-retryable",
|
|
3536
|
+
reason: "codex follow-up violated the runtime adapter contract"
|
|
3537
|
+
}, { cause: err })
|
|
3538
|
+
};
|
|
2698
3539
|
throw err;
|
|
2699
3540
|
}
|
|
3541
|
+
if (this.closed) {
|
|
3542
|
+
await runner.dispose();
|
|
3543
|
+
throw new Error("codex session closed while follow-up was starting");
|
|
3544
|
+
}
|
|
3545
|
+
this.terminal = terminal;
|
|
2700
3546
|
this.sessionRef = sessionRef;
|
|
2701
3547
|
this.currentRunner = runner;
|
|
2702
|
-
void this.forgetRunnerOnceClosed(runner);
|
|
2703
3548
|
}
|
|
2704
3549
|
/** Best-effort abort of the current turn. SIGTERM's the currently-running child, if any — see `process-runner.ts`'s `kill()` doc comment for why SIGTERM (not SIGINT) and why this is safe: the underlying codex thread survives and stays resumable, confirmed empirically. A no-op when no turn is currently in flight. */
|
|
2705
3550
|
async interrupt() {
|
|
2706
3551
|
this.currentRunner?.kill();
|
|
2707
3552
|
}
|
|
2708
3553
|
async close() {
|
|
2709
|
-
if (this.
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
3554
|
+
if (!this.closeAttempt) {
|
|
3555
|
+
this.closed = true;
|
|
3556
|
+
this.queue.end();
|
|
3557
|
+
const attempt = (async () => {
|
|
3558
|
+
const runners = [...this.ownedRunners];
|
|
3559
|
+
await Promise.all(runners.map((runner) => runner.dispose()));
|
|
3560
|
+
for (const runner of runners) this.ownedRunners.delete(runner);
|
|
3561
|
+
await Promise.allSettled([...this.followUpAttempts]);
|
|
3562
|
+
})();
|
|
3563
|
+
this.closeAttempt = attempt.catch((error) => {
|
|
3564
|
+
this.closeAttempt = void 0;
|
|
3565
|
+
throw error;
|
|
3566
|
+
});
|
|
3567
|
+
}
|
|
3568
|
+
await this.closeAttempt;
|
|
2713
3569
|
}
|
|
2714
3570
|
/**
|
|
2715
3571
|
* `codex exec` has no in-band channel to inject text into an already-
|
|
@@ -2745,8 +3601,7 @@ var CodexSession = class {
|
|
|
2745
3601
|
);
|
|
2746
3602
|
}
|
|
2747
3603
|
};
|
|
2748
|
-
function subscriptionModel2(
|
|
2749
|
-
const selection = task.dispatchSelection;
|
|
3604
|
+
function subscriptionModel2(selection) {
|
|
2750
3605
|
if (selection === void 0) return void 0;
|
|
2751
3606
|
if (selection.lane !== "subscription" || selection.runtimeId !== "codex") {
|
|
2752
3607
|
throw new PolicyUnsupportedError(
|
|
@@ -2811,7 +3666,7 @@ var ApprovalRegistry = class {
|
|
|
2811
3666
|
* `requestApproval` timeout and `finish()` fail-closed cleanup
|
|
2812
3667
|
* (`task-runner.ts`) — resolves a decision this device made on its own.
|
|
2813
3668
|
* The one exception, a server-sent wire `task.approve`/`task.reject`
|
|
2814
|
-
* relayed through `
|
|
3669
|
+
* relayed through `RuntimeOperationStartInput.approvalChannel.resolve`
|
|
2815
3670
|
* (`task-runner.ts`'s `handleOffer`), passes `'wire'` explicitly.
|
|
2816
3671
|
*/
|
|
2817
3672
|
resolve(approvalId, decision, reason, origin = "local") {
|
|
@@ -3442,7 +4297,10 @@ var PresencePublisher = class {
|
|
|
3442
4297
|
{
|
|
3443
4298
|
method: "PUT",
|
|
3444
4299
|
headers: { "content-type": "application/json" },
|
|
3445
|
-
body: JSON.stringify({
|
|
4300
|
+
body: JSON.stringify({
|
|
4301
|
+
level: "online",
|
|
4302
|
+
...this.opts.configuredToolsets === void 0 ? {} : { configuredToolsets: this.opts.configuredToolsets }
|
|
4303
|
+
})
|
|
3446
4304
|
},
|
|
3447
4305
|
this.opts.auth
|
|
3448
4306
|
);
|
|
@@ -3636,12 +4494,12 @@ var AnotherControlServerRunningError = class extends Error {
|
|
|
3636
4494
|
}
|
|
3637
4495
|
};
|
|
3638
4496
|
var MAX_HALF_OPEN_CONNECTIONS = 8;
|
|
3639
|
-
function
|
|
4497
|
+
function errorMessage3(err) {
|
|
3640
4498
|
return err instanceof Error ? err.message : String(err);
|
|
3641
4499
|
}
|
|
3642
4500
|
function toControlErrorShape(err) {
|
|
3643
4501
|
if (err instanceof ControlError) return { code: err.code, message: err.message };
|
|
3644
|
-
return { code: "internal_error", message:
|
|
4502
|
+
return { code: "internal_error", message: errorMessage3(err) };
|
|
3645
4503
|
}
|
|
3646
4504
|
function probeUnixSocketAlive(socketPath) {
|
|
3647
4505
|
return new Promise((resolve) => {
|
|
@@ -3848,12 +4706,19 @@ async function startControlServer(opts) {
|
|
|
3848
4706
|
}
|
|
3849
4707
|
throw err;
|
|
3850
4708
|
}
|
|
4709
|
+
let stopServingPromise;
|
|
4710
|
+
async function stopServing() {
|
|
4711
|
+
stopServingPromise ??= (async () => {
|
|
4712
|
+
for (const socket of sockets) socket.destroy();
|
|
4713
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
4714
|
+
})();
|
|
4715
|
+
await stopServingPromise;
|
|
4716
|
+
}
|
|
3851
4717
|
let closed = false;
|
|
3852
4718
|
async function close() {
|
|
3853
4719
|
if (closed) return;
|
|
3854
4720
|
closed = true;
|
|
3855
|
-
|
|
3856
|
-
await new Promise((resolve) => server.close(() => resolve()));
|
|
4721
|
+
await stopServing();
|
|
3857
4722
|
if (process.platform !== "win32") {
|
|
3858
4723
|
await promises.rm(endpoint, { force: true }).catch(() => {
|
|
3859
4724
|
});
|
|
@@ -3861,7 +4726,7 @@ async function startControlServer(opts) {
|
|
|
3861
4726
|
await promises.rm(tokenPath, { force: true }).catch(() => {
|
|
3862
4727
|
});
|
|
3863
4728
|
}
|
|
3864
|
-
return { endpoint, close };
|
|
4729
|
+
return { endpoint, stopServing, close };
|
|
3865
4730
|
}
|
|
3866
4731
|
function deterministicJitterMs(input) {
|
|
3867
4732
|
const ratio = input.ratio ?? 0.2;
|
|
@@ -3897,14 +4762,17 @@ function parseLooseEventsPollResponse(raw) {
|
|
|
3897
4762
|
if (typeof raw !== "object" || raw === null) {
|
|
3898
4763
|
throw new Error("events poll response is not an object");
|
|
3899
4764
|
}
|
|
3900
|
-
const { events, cursor } = raw;
|
|
4765
|
+
const { events, cursor, capabilities } = raw;
|
|
3901
4766
|
if (!Array.isArray(events)) {
|
|
3902
4767
|
throw new Error("events poll response.events is not an array");
|
|
3903
4768
|
}
|
|
3904
4769
|
if (typeof cursor !== "number" || !Number.isInteger(cursor)) {
|
|
3905
4770
|
throw new Error("events poll response.cursor is not an integer");
|
|
3906
4771
|
}
|
|
3907
|
-
|
|
4772
|
+
if (capabilities !== void 0 && (!Array.isArray(capabilities) || capabilities.some((flag) => typeof flag !== "string"))) {
|
|
4773
|
+
throw new Error("events poll response.capabilities is not an array of strings");
|
|
4774
|
+
}
|
|
4775
|
+
return { events, cursor, capabilities: capabilities ?? [] };
|
|
3908
4776
|
}
|
|
3909
4777
|
function extractSkippableSeq(raw) {
|
|
3910
4778
|
if (typeof raw !== "object" || raw === null) return void 0;
|
|
@@ -3984,12 +4852,14 @@ var LongPollClient = class {
|
|
|
3984
4852
|
if (cursor !== void 0) url.searchParams.set("cursor", String(cursor));
|
|
3985
4853
|
const res = await authedFetch(url, { method: "GET" }, this.opts.auth);
|
|
3986
4854
|
if (!res.ok) {
|
|
4855
|
+
this.opts.onServerCapabilities?.([]);
|
|
3987
4856
|
this.opts.onOperationalOutcome?.("failure");
|
|
3988
4857
|
const baseMs = this.opts.retryDelayMs ?? 2e3;
|
|
3989
4858
|
await sleep(this.opts.retryDelayForAttempt?.(retryAttempt++, baseMs) ?? baseMs);
|
|
3990
4859
|
continue;
|
|
3991
4860
|
}
|
|
3992
4861
|
const parsed = parseLooseEventsPollResponse(await res.json());
|
|
4862
|
+
this.opts.onServerCapabilities?.(parsed.capabilities);
|
|
3993
4863
|
let hadValidationFailureThisBatch = false;
|
|
3994
4864
|
for (const raw of parsed.events) {
|
|
3995
4865
|
let envelope;
|
|
@@ -4033,6 +4903,7 @@ var LongPollClient = class {
|
|
|
4033
4903
|
this.opts.onOperationalOutcome?.("success");
|
|
4034
4904
|
}
|
|
4035
4905
|
} catch (err) {
|
|
4906
|
+
this.opts.onServerCapabilities?.([]);
|
|
4036
4907
|
if (err instanceof DeviceRevokedError) {
|
|
4037
4908
|
this.running = false;
|
|
4038
4909
|
this.opts.onRevoked?.();
|
|
@@ -4155,6 +5026,7 @@ var WsTransport = class {
|
|
|
4155
5026
|
deviceId: this.opts.deviceId,
|
|
4156
5027
|
productId: this.opts.productId,
|
|
4157
5028
|
runtimes: this.opts.runtimes,
|
|
5029
|
+
configuredToolsets: this.opts.configuredToolsets === void 0 ? void 0 : [...this.opts.configuredToolsets],
|
|
4158
5030
|
cursor: this.opts.getCursor?.()
|
|
4159
5031
|
});
|
|
4160
5032
|
socket.send(encodeEnvelope(hello));
|
|
@@ -4244,6 +5116,7 @@ var ConnectionManager = class {
|
|
|
4244
5116
|
productId: opts.productId,
|
|
4245
5117
|
capabilities: opts.capabilities,
|
|
4246
5118
|
runtimes: opts.runtimes,
|
|
5119
|
+
configuredToolsets: opts.configuredToolsets,
|
|
4247
5120
|
getCursor: () => this.cursor,
|
|
4248
5121
|
onEnvelope: (envelope) => this.deliver(envelope),
|
|
4249
5122
|
onStateChange: (state) => {
|
|
@@ -4266,6 +5139,9 @@ var ConnectionManager = class {
|
|
|
4266
5139
|
// re-attempted.
|
|
4267
5140
|
getCursor: () => this.dedupWatermark(),
|
|
4268
5141
|
onEnvelope: (envelope) => this.deliver(envelope),
|
|
5142
|
+
onServerCapabilities: (capabilities) => {
|
|
5143
|
+
if (this.mode === "long-poll") this.serverCapabilities = capabilities;
|
|
5144
|
+
},
|
|
4269
5145
|
onRevoked: () => this.enterRevoked(),
|
|
4270
5146
|
// M4 Phase 4 (version-negotiation drill fix): a batch entry
|
|
4271
5147
|
// LongPollClient couldn't parse into a known Envelope at all (an
|
|
@@ -4366,7 +5242,7 @@ var ConnectionManager = class {
|
|
|
4366
5242
|
* already-delivered seqs (see its own doc comment) so the failed seq's own
|
|
4367
5243
|
* redelivery can get through — but that same frozen watermark also means
|
|
4368
5244
|
* every OTHER seq above it rides along on every re-poll too. Without this,
|
|
4369
|
-
* a seq already mid-flight (e.g. a `task.offer` whose
|
|
5245
|
+
* a seq already mid-flight (e.g. a `task.offer` whose prepared operation start()
|
|
4370
5246
|
* hasn't resolved yet) would be re-enqueued into `processingChain` on
|
|
4371
5247
|
* every such re-poll, piling up duplicate copies that — once the first
|
|
4372
5248
|
* finally resolves and the chain unwinds through them — run its handler
|
|
@@ -4397,25 +5273,24 @@ var ConnectionManager = class {
|
|
|
4397
5273
|
*/
|
|
4398
5274
|
cancelPendingDrainRetry;
|
|
4399
5275
|
/**
|
|
4400
|
-
* The capabilities the
|
|
4401
|
-
* `
|
|
4402
|
-
*
|
|
4403
|
-
*
|
|
4404
|
-
* successful handshake.
|
|
5276
|
+
* The capabilities the CURRENT transport's server advertised — untyped
|
|
5277
|
+
* `string[]` for forward compatibility. WS populates it from `conn.ack`;
|
|
5278
|
+
* long-poll populates it from each successful events response. Empty until
|
|
5279
|
+
* the active transport supplies an advertisement.
|
|
4405
5280
|
*
|
|
4406
5281
|
* Finding R2 (cross-model re-review — was P1): strictly PER-CONNECTION,
|
|
4407
5282
|
* not per-daemon-lifetime. Cleared to `[]` the instant the acked WS
|
|
4408
5283
|
* connection ends for ANY reason — an ordinary disconnect (`onWsOutcome`'s
|
|
4409
5284
|
* `acked` branch), `stop()`, or a transport switch to long-poll
|
|
4410
|
-
* (`enterLongPoll`) — and only
|
|
5285
|
+
* (`enterLongPoll`) — and only repopulated by a fresh advertisement from
|
|
5286
|
+
* the transport that is still current.
|
|
4411
5287
|
* The previous version of this doc comment claimed long-poll mode simply
|
|
4412
5288
|
* "stays at whatever the last real WS `conn.ack` said" — that was the bug:
|
|
4413
5289
|
* a daemon that once learned e.g. `approval_resolved` from an earlier WS
|
|
4414
5290
|
* session kept believing it applied to whatever it's connected to NOW,
|
|
4415
5291
|
* even after a disconnect/degrade where nothing has actually confirmed
|
|
4416
5292
|
* that's still true (a reconnect could land on a DIFFERENT server behind a
|
|
4417
|
-
* load balancer
|
|
4418
|
-
* handshake at all). Concretely, `TaskRunner.sendApprovalResolved` gates
|
|
5293
|
+
* load balancer). Concretely, `TaskRunner.sendApprovalResolved` gates
|
|
4419
5294
|
* `task.approval_resolved` on this list — sending it to a server that
|
|
4420
5295
|
* doesn't actually understand it over the long-poll path would get a
|
|
4421
5296
|
* batch-level 400 from `MessagesSendRequestSchema` (protocol §8.2), which
|
|
@@ -4535,13 +5410,10 @@ var ConnectionManager = class {
|
|
|
4535
5410
|
return this.mode === "long-poll";
|
|
4536
5411
|
}
|
|
4537
5412
|
/**
|
|
4538
|
-
* The capabilities the
|
|
4539
|
-
* `conn.ack`
|
|
4540
|
-
*
|
|
4541
|
-
*
|
|
4542
|
-
* first handshake completes, AND (finding R2) once again empty after any
|
|
4543
|
-
* disconnect/degrade — see `serverCapabilities`'s own doc comment for why
|
|
4544
|
-
* this is strictly per-connection rather than "sticky" across one.
|
|
5413
|
+
* The capabilities the CURRENT transport's server advertised: from
|
|
5414
|
+
* `conn.ack` on WS, or the latest successful `GET /byok/events` response
|
|
5415
|
+
* on long-poll. Empty before either transport has supplied its current
|
|
5416
|
+
* advertisement, and cleared across disconnect/switch boundaries.
|
|
4545
5417
|
*/
|
|
4546
5418
|
getServerCapabilities() {
|
|
4547
5419
|
return this.serverCapabilities;
|
|
@@ -5205,6 +6077,12 @@ var RECLAIM_FILENAME = `${DAEMON_OWNER_FILENAME}.reclaim`;
|
|
|
5205
6077
|
var MAX_OWNER_BYTES = 4096;
|
|
5206
6078
|
var RECLAIM_MALFORMED_GRACE_MS = 3e4;
|
|
5207
6079
|
var SELF_PROCESS_STARTED_AT = new Date(Date.now() - process.uptime() * 1e3).toISOString();
|
|
6080
|
+
function endOwnershipProbe(socket, response) {
|
|
6081
|
+
socket.on("error", () => {
|
|
6082
|
+
});
|
|
6083
|
+
if (response === void 0) socket.end();
|
|
6084
|
+
else socket.end(response);
|
|
6085
|
+
}
|
|
5208
6086
|
var STORE_MUTEX_ID_PREFIX = "byok-store-mutex-v1:";
|
|
5209
6087
|
var STORE_MUTEX_PROBE_TIMEOUT_MS = 1e3;
|
|
5210
6088
|
var STORE_MUTEX_SOCKET_FILENAME = "mutex.sock";
|
|
@@ -5311,7 +6189,7 @@ async function portIsBound(port) {
|
|
|
5311
6189
|
});
|
|
5312
6190
|
}
|
|
5313
6191
|
async function createLivenessListener() {
|
|
5314
|
-
const server = createServer((socket) => socket
|
|
6192
|
+
const server = createServer((socket) => endOwnershipProbe(socket));
|
|
5315
6193
|
const port = await new Promise((resolve, reject) => {
|
|
5316
6194
|
server.once("error", reject);
|
|
5317
6195
|
server.listen({ host: "127.0.0.1", port: 0, exclusive: true }, () => {
|
|
@@ -5396,7 +6274,7 @@ async function acquireStoreMutex(canonicalStoreDir) {
|
|
|
5396
6274
|
}
|
|
5397
6275
|
await clearStaleStoreMutexSocket(endpoint, identity);
|
|
5398
6276
|
}
|
|
5399
|
-
const server = createServer((socket) => socket
|
|
6277
|
+
const server = createServer((socket) => endOwnershipProbe(socket, `${STORE_MUTEX_ID_PREFIX}${identity}
|
|
5400
6278
|
`));
|
|
5401
6279
|
try {
|
|
5402
6280
|
await new Promise((resolve, reject) => {
|
|
@@ -5538,7 +6416,7 @@ function toRuntimeInfoCapabilities(caps) {
|
|
|
5538
6416
|
resume: caps.resume,
|
|
5539
6417
|
approvalInteractive: caps.approvalInteractive,
|
|
5540
6418
|
...caps.mcpToolsets === void 0 ? {} : { mcpToolsets: caps.mcpToolsets },
|
|
5541
|
-
permissionModes: caps.permissionModes
|
|
6419
|
+
permissionModes: [...caps.permissionModes]
|
|
5542
6420
|
};
|
|
5543
6421
|
}
|
|
5544
6422
|
var CursorStore = class {
|
|
@@ -5778,6 +6656,9 @@ var DaemonObserver = class {
|
|
|
5778
6656
|
noteGitWorkspace(event) {
|
|
5779
6657
|
this.emit({ kind: "git-workspace", ts: nowIso(), ...event });
|
|
5780
6658
|
}
|
|
6659
|
+
noteRuntimeDisposalFailure(event) {
|
|
6660
|
+
this.emit({ kind: "runtime-disposal-failed", ts: nowIso(), ...event });
|
|
6661
|
+
}
|
|
5781
6662
|
/**
|
|
5782
6663
|
* Finding F4: wired from `TaskRunnerDeps.onApprovalDispatched`, called
|
|
5783
6664
|
* synchronously by `TaskRunner.dispatchApproval` BEFORE its own
|
|
@@ -7434,20 +8315,20 @@ function isKnownRuntimeId(id) {
|
|
|
7434
8315
|
var DEFAULT_RUNTIME_PREFERENCE = ["claude", "codex", "pi"];
|
|
7435
8316
|
function orderByPreference(candidates, preference) {
|
|
7436
8317
|
const rank = new Map(preference.map((id, index) => [id, index]));
|
|
7437
|
-
return [...candidates].sort((a, b) => (rank.get(a.id) ?? preference.length) - (rank.get(b.id) ?? preference.length));
|
|
8318
|
+
return [...candidates].sort((a, b) => (rank.get(a.descriptor.id) ?? preference.length) - (rank.get(b.descriptor.id) ?? preference.length));
|
|
7438
8319
|
}
|
|
7439
|
-
function adapterSupportsMode(
|
|
7440
|
-
return
|
|
8320
|
+
function adapterSupportsMode(descriptor, mode) {
|
|
8321
|
+
return descriptor.capabilities.permissionModes.includes(mode);
|
|
7441
8322
|
}
|
|
7442
|
-
function adapterSupportsMcpToolsets(
|
|
7443
|
-
return
|
|
8323
|
+
function adapterSupportsMcpToolsets(descriptor) {
|
|
8324
|
+
return descriptor.capabilities.mcpToolsets === true;
|
|
7444
8325
|
}
|
|
7445
8326
|
function withoutRequiredToolsets(payload) {
|
|
7446
8327
|
if (!("requiredToolsets" in payload)) return payload;
|
|
7447
8328
|
const { requiredToolsets, ...offer } = payload;
|
|
7448
8329
|
return offer;
|
|
7449
8330
|
}
|
|
7450
|
-
function
|
|
8331
|
+
function errorMessage4(err) {
|
|
7451
8332
|
return err instanceof Error ? err.message : String(err);
|
|
7452
8333
|
}
|
|
7453
8334
|
function raceSettleFirst(fn, timeoutMs) {
|
|
@@ -7500,7 +8381,7 @@ async function openArtifact(workspaceDir, name) {
|
|
|
7500
8381
|
try {
|
|
7501
8382
|
handle = await promises.open(candidate, constants.O_RDONLY | O_NOFOLLOW);
|
|
7502
8383
|
} catch (err) {
|
|
7503
|
-
return { ok: false, reason: `artifact "${name}" could not be opened: ${
|
|
8384
|
+
return { ok: false, reason: `artifact "${name}" could not be opened: ${errorMessage4(err)}` };
|
|
7504
8385
|
}
|
|
7505
8386
|
try {
|
|
7506
8387
|
const st = await handle.stat();
|
|
@@ -7512,7 +8393,7 @@ async function openArtifact(workspaceDir, name) {
|
|
|
7512
8393
|
} catch (err) {
|
|
7513
8394
|
await handle.close().catch(() => {
|
|
7514
8395
|
});
|
|
7515
|
-
return { ok: false, reason: `artifact "${name}" could not be verified: ${
|
|
8396
|
+
return { ok: false, reason: `artifact "${name}" could not be verified: ${errorMessage4(err)}` };
|
|
7516
8397
|
}
|
|
7517
8398
|
return { ok: true, handle };
|
|
7518
8399
|
}
|
|
@@ -7526,13 +8407,13 @@ var TaskRunner = class {
|
|
|
7526
8407
|
* Finding F4 (cancel lost during the offer-processing window): a
|
|
7527
8408
|
* `task.cancel` for a taskId that hasn't finished `handleOffer` yet (still
|
|
7528
8409
|
* awaiting adapter detection / instruction resolution / workspace setup /
|
|
7529
|
-
* `
|
|
8410
|
+
* prepared operation `start()`) has no `this.tasks` entry to land on — it used to be
|
|
7530
8411
|
* silently dropped, and the runtime session `handleOffer` was about to
|
|
7531
8412
|
* register would then run an unsupervised ("zombie") turn nobody asked
|
|
7532
8413
|
* for anymore. Recording the taskId here lets `handleOffer` consult it at
|
|
7533
8414
|
* the two points where it can still safely react (see its body): before
|
|
7534
8415
|
* claiming at all (decline instead of ever starting a session), and right
|
|
7535
|
-
* after
|
|
8416
|
+
* after the prepared operation resolves but before this task is registered as
|
|
7536
8417
|
* active (tear the just-started session down immediately, before its
|
|
7537
8418
|
* event loop ever pumps a single event). Consumed (deleted) at whichever
|
|
7538
8419
|
* checkpoint handles it; a cancel for a taskId that's already active,
|
|
@@ -7558,12 +8439,12 @@ var TaskRunner = class {
|
|
|
7558
8439
|
* checkpoint-2 cancel-teardown, or successful registration into
|
|
7559
8440
|
* `this.tasks`). Bounded eviction on `pendingCancelled` (below) must never
|
|
7560
8441
|
* remove an entry for a taskId in this set: doing so is exactly the bug —
|
|
7561
|
-
* block task A in `
|
|
8442
|
+
* block task A in prepared-operation `start()`, deliver A's own `task.cancel` (so
|
|
7562
8443
|
* `pendingCancelled` gets an entry for A while A is still in-flight),
|
|
7563
8444
|
* then deliver `MAX_TRACKED_TASK_IDS` more cancels for unrelated taskIds
|
|
7564
8445
|
* nobody ever offered — under naive oldest-wins eviction, A's entry (the
|
|
7565
8446
|
* single oldest) gets evicted purely because of unrelated churn, so when
|
|
7566
|
-
*
|
|
8447
|
+
* the prepared operation finally resolves, checkpoint 2 finds no cancel marker
|
|
7567
8448
|
* and the already-cancelled task starts a real session. See
|
|
7568
8449
|
* `evictPendingCancelled` below for the fix, and
|
|
7569
8450
|
* `task-runner-bounded-collections.test.ts` for a test mirroring this
|
|
@@ -7583,7 +8464,7 @@ var TaskRunner = class {
|
|
|
7583
8464
|
* explicitly relies on redelivered handlers being idempotent for exactly
|
|
7584
8465
|
* this reason). `handleOffer` must treat a redelivered offer for a taskId
|
|
7585
8466
|
* that's already active (`this.tasks`) or already finished (this set) as
|
|
7586
|
-
* a no-op — never a second `
|
|
8467
|
+
* a no-op — never a second prepared-operation `start()` call, which would orphan the
|
|
7587
8468
|
* first session.
|
|
7588
8469
|
*
|
|
7589
8470
|
* M3-B: unbounded otherwise — a long-lived daemon that's finished many
|
|
@@ -7652,10 +8533,9 @@ var TaskRunner = class {
|
|
|
7652
8533
|
this.stoppingOffers = true;
|
|
7653
8534
|
}
|
|
7654
8535
|
/**
|
|
7655
|
-
*
|
|
7656
|
-
*
|
|
7657
|
-
*
|
|
7658
|
-
* terminal message is sent either way) but reports `task.fail` rather than
|
|
8536
|
+
* Shutdown of every currently ACTIVE task for the control socket's
|
|
8537
|
+
* `shutdown` RPC. Soft interrupt remains bounded, but each task's
|
|
8538
|
+
* authoritative close receipt must settle successfully. Reports `task.fail` rather than
|
|
7659
8539
|
* `task.cancelled` — these tasks aren't ending because the SERVER
|
|
7660
8540
|
* cancelled them, they're ending because this device is shutting down.
|
|
7661
8541
|
* `retryable: true` throughout: nothing about the task/policy itself was
|
|
@@ -7709,28 +8589,13 @@ var TaskRunner = class {
|
|
|
7709
8589
|
* unconditionally, so a hung `interrupt()` (a misbehaving adapter) can
|
|
7710
8590
|
* never block `task.fail` from being sent at all.
|
|
7711
8591
|
*
|
|
7712
|
-
*
|
|
7713
|
-
*
|
|
7714
|
-
*
|
|
7715
|
-
*
|
|
7716
|
-
* far more here than it used to for the pre-existing graceful-shutdown-only
|
|
7717
|
-
* caller, since THAT path is additionally bounded by an outer deadline
|
|
7718
|
-
* (`SHUTDOWN_TASK_TEARDOWN_DEADLINE_MS`/`DaemonConfig.shutdownGraceMs`,
|
|
7719
|
-
* `create-daemon.ts`), while resource-limit enforcement fires during
|
|
7720
|
-
* ordinary operation with no such outer bound watching it). `close()` is
|
|
7721
|
-
* every adapter's harder teardown primitive — an actual process-level kill
|
|
7722
|
-
* (SIGTERM, or `taskkill /F` on Windows — see e.g.
|
|
7723
|
-
* `ClaudeProcessClient.kill()`/`PiRpcClient.kill()`) as opposed to pi's own
|
|
7724
|
-
* soft in-band `interrupt()` (an RPC `abort` message that leaves the
|
|
7725
|
-
* process alive and resumable) — so escalating to it is the closest thing
|
|
7726
|
-
* to a "hard kill" the `Session` interface exposes. `finish()` below calls
|
|
7727
|
-
* `session.close()` again regardless (documented idempotent) — this isn't
|
|
7728
|
-
* a substitute for that, only an earlier, bounded attempt at actually
|
|
7729
|
-
* stopping a stuck runtime before this method gives up and reports failure
|
|
7730
|
-
* anyway.
|
|
8592
|
+
* After the bounded soft interrupt, `finish()` always awaits the authoritative
|
|
8593
|
+
* `Session.close()` receipt. A failed receipt retains active/Git ownership;
|
|
8594
|
+
* shutdown surfaces the rejection while resource enforcement leaves local
|
|
8595
|
+
* evidence for a later retry.
|
|
7731
8596
|
*
|
|
7732
8597
|
* Re-checks task identity (`this.tasks.get(...) === active`) immediately
|
|
7733
|
-
* before sending `task.fail`: the interrupt
|
|
8598
|
+
* before sending `task.fail`: the interrupt race above has await
|
|
7734
8599
|
* points during which a DIFFERENT path (a racing `task.cancel`/
|
|
7735
8600
|
* `task.reject`, or the session completing normally on its own) may have
|
|
7736
8601
|
* already finished this exact task and sent its own terminal message.
|
|
@@ -7739,20 +8604,25 @@ var TaskRunner = class {
|
|
|
7739
8604
|
* identity-check guard for the same class of race.
|
|
7740
8605
|
*/
|
|
7741
8606
|
async teardownActiveTask(active, reason, retryable) {
|
|
8607
|
+
if (active.finalizationStarted) return this.finish(active.taskId);
|
|
8608
|
+
if (!this.reserveSemanticTerminal(active)) return active.semanticTerminalSettled ?? false;
|
|
7742
8609
|
active.beingTornDown = true;
|
|
7743
8610
|
await this.observeGit(active, "salvage");
|
|
7744
8611
|
const timeoutMs = this.deps.shutdownInterruptTimeoutMs ?? DEFAULT_SHUTDOWN_INTERRUPT_TIMEOUT_MS;
|
|
7745
|
-
|
|
7746
|
-
if (
|
|
7747
|
-
await raceSettleFirst(() => active.session.close(), timeoutMs);
|
|
7748
|
-
}
|
|
7749
|
-
if (this.tasks.get(active.taskId) !== active) return;
|
|
8612
|
+
await raceSettleFirst(() => active.session.interrupt(), timeoutMs);
|
|
8613
|
+
if (this.tasks.get(active.taskId) !== active) return true;
|
|
7750
8614
|
this.deps.send(createEnvelope("task.fail", { reason, retryable }, { taskId: active.taskId }));
|
|
7751
|
-
|
|
8615
|
+
return this.finish(active.taskId);
|
|
7752
8616
|
}
|
|
7753
8617
|
/** Graceful-shutdown caller of {@link teardownActiveTask} — see `shutdownActiveTasks`'s own doc comment. `retryable: true`: nothing about the task/policy itself was ever at fault, only this device's own availability right now. */
|
|
7754
8618
|
async shutdownTask(active, reason) {
|
|
7755
|
-
await this.teardownActiveTask(active, `daemon shutting down: ${reason}`, true);
|
|
8619
|
+
const disposed = await this.teardownActiveTask(active, `daemon shutting down: ${reason}`, true);
|
|
8620
|
+
if (!disposed) {
|
|
8621
|
+
throw new RuntimeDisposalFailure({
|
|
8622
|
+
stage: "quiescence",
|
|
8623
|
+
reason: `${active.adapter.descriptor.id} runtime ownership remains active after shutdown disposal failed`
|
|
8624
|
+
});
|
|
8625
|
+
}
|
|
7756
8626
|
}
|
|
7757
8627
|
/**
|
|
7758
8628
|
* M5 batch-3 (workstream 2): shared entry point for both resource-limit
|
|
@@ -7872,15 +8742,33 @@ var TaskRunner = class {
|
|
|
7872
8742
|
this.decline(taskId, resolvedMcp.reason, true);
|
|
7873
8743
|
return;
|
|
7874
8744
|
}
|
|
8745
|
+
const decision = computeEffectivePolicy(payload.policy, this.deps.permissionDefaults);
|
|
8746
|
+
if (!decision.ok) {
|
|
8747
|
+
this.decline(taskId, decision.reason ?? "policy rejected", false);
|
|
8748
|
+
return;
|
|
8749
|
+
}
|
|
8750
|
+
const offered = withoutRequiredToolsets(payload);
|
|
7875
8751
|
const requestedRuntime = payload.dispatchSelection?.runtimeId ?? payload.runtime;
|
|
7876
8752
|
const pick = await this.pickAdapter(requestedRuntime, payload.policy.mode, requiredToolsets !== void 0);
|
|
7877
8753
|
if (!pick.ok) {
|
|
7878
8754
|
this.decline(taskId, pick.reason, pick.retryable);
|
|
7879
8755
|
return;
|
|
7880
8756
|
}
|
|
7881
|
-
|
|
7882
|
-
|
|
7883
|
-
|
|
8757
|
+
let prepared;
|
|
8758
|
+
try {
|
|
8759
|
+
prepared = await pick.adapter.prepare({
|
|
8760
|
+
offer: offered,
|
|
8761
|
+
policy: decision.policy,
|
|
8762
|
+
descriptor: pick.descriptor,
|
|
8763
|
+
requiredToolsetIds: requiredToolsets ?? [],
|
|
8764
|
+
...resolvedMcp?.ok ? { mcpServers: resolvedMcp.servers } : {}
|
|
8765
|
+
});
|
|
8766
|
+
} catch (error) {
|
|
8767
|
+
this.decline(taskId, `runtime preparation failed: ${errorMessage4(error)}`, true);
|
|
8768
|
+
return;
|
|
8769
|
+
}
|
|
8770
|
+
if (prepared.kind === "reject") {
|
|
8771
|
+
this.decline(taskId, prepared.reason, prepared.retryable);
|
|
7884
8772
|
return;
|
|
7885
8773
|
}
|
|
7886
8774
|
let known = void 0;
|
|
@@ -7915,6 +8803,7 @@ var TaskRunner = class {
|
|
|
7915
8803
|
}
|
|
7916
8804
|
} else {
|
|
7917
8805
|
workspaceDir = path17.join(this.deps.workspaceRoot, taskId);
|
|
8806
|
+
gitWorkspaceId = randomUUID();
|
|
7918
8807
|
}
|
|
7919
8808
|
try {
|
|
7920
8809
|
gitLease = await gitManager.acquireLease(workspaceDir, payload.sessionRef);
|
|
@@ -7930,6 +8819,26 @@ var TaskRunner = class {
|
|
|
7930
8819
|
this.decline(taskId, "workspace mode is unavailable", true);
|
|
7931
8820
|
return;
|
|
7932
8821
|
}
|
|
8822
|
+
const env = buildRuntimeEnv({
|
|
8823
|
+
ambient: process.env,
|
|
8824
|
+
requirements: pick.descriptor.environmentRequirements,
|
|
8825
|
+
locallyAllowedNames: this.deps.runtimeEnvironment?.[pick.descriptor.id]?.allow
|
|
8826
|
+
});
|
|
8827
|
+
const manifest = sealRuntimeOperationManifest({
|
|
8828
|
+
taskId,
|
|
8829
|
+
runtimeId: pick.descriptor.id,
|
|
8830
|
+
descriptor: pick.descriptor,
|
|
8831
|
+
policy: decision.policy,
|
|
8832
|
+
requiredToolsetIds: requiredToolsets ?? [],
|
|
8833
|
+
...offered.dispatchSelection === void 0 ? {} : { dispatchSelection: offered.dispatchSelection },
|
|
8834
|
+
...known === void 0 || payload.sessionRef === void 0 ? {} : { sessionRef: payload.sessionRef },
|
|
8835
|
+
workspace: {
|
|
8836
|
+
workspaceDir,
|
|
8837
|
+
...gitWorkspaceId === void 0 ? {} : { workspaceId: gitWorkspaceId },
|
|
8838
|
+
...gitBaseline === void 0 ? {} : { baseline: gitBaseline }
|
|
8839
|
+
},
|
|
8840
|
+
forwardedEnvironmentNames: Object.freeze(Object.keys(env).sort())
|
|
8841
|
+
});
|
|
7933
8842
|
this.deps.send(
|
|
7934
8843
|
createEnvelope(
|
|
7935
8844
|
"task.claim",
|
|
@@ -7945,7 +8854,7 @@ var TaskRunner = class {
|
|
|
7945
8854
|
// (the merely REQUESTED runtime): this is what closes the gap
|
|
7946
8855
|
// where an auto-selected task left the server never learning
|
|
7947
8856
|
// which runtime actually ran.
|
|
7948
|
-
runtime: isKnownRuntimeId(
|
|
8857
|
+
runtime: isKnownRuntimeId(manifest.descriptor.id) ? manifest.descriptor.id : void 0,
|
|
7949
8858
|
// S0/D-4 (`task.claim.capabilities`, docs/protocol.md §2.4): the
|
|
7950
8859
|
// selected adapter's own capability self-report, carried on the
|
|
7951
8860
|
// same message that establishes the task↔runtime binding. The
|
|
@@ -7962,7 +8871,7 @@ var TaskRunner = class {
|
|
|
7962
8871
|
// Gating them would silently strip a custom steer-capable
|
|
7963
8872
|
// adapter's own truth and leave the server fail-closing on it
|
|
7964
8873
|
// forever.
|
|
7965
|
-
capabilities: toRuntimeInfoCapabilities(
|
|
8874
|
+
capabilities: toRuntimeInfoCapabilities(manifest.descriptor.capabilities)
|
|
7966
8875
|
},
|
|
7967
8876
|
{ taskId }
|
|
7968
8877
|
)
|
|
@@ -7973,7 +8882,7 @@ var TaskRunner = class {
|
|
|
7973
8882
|
if (plainWorkspaceNeedsResolve) workspaceDir = await this.resolveWorkspaceDir(taskId, known?.workspaceDir);
|
|
7974
8883
|
} catch (err) {
|
|
7975
8884
|
gitLease?.release();
|
|
7976
|
-
await this.fail(taskId, `failed to resolve instruction blob: ${
|
|
8885
|
+
await this.fail(taskId, `failed to resolve instruction blob: ${errorMessage4(err)}`, true);
|
|
7977
8886
|
return;
|
|
7978
8887
|
}
|
|
7979
8888
|
if (this.deps.gitWorkspaceManager && gitLease) {
|
|
@@ -7982,7 +8891,7 @@ var TaskRunner = class {
|
|
|
7982
8891
|
if (gitExisting) {
|
|
7983
8892
|
observation = await this.deps.gitWorkspaceManager.validateExisting(workspaceDir);
|
|
7984
8893
|
} else {
|
|
7985
|
-
const workspaceId2 = randomUUID();
|
|
8894
|
+
const workspaceId2 = gitWorkspaceId ?? randomUUID();
|
|
7986
8895
|
const now2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
7987
8896
|
gitWorkspaceId = workspaceId2;
|
|
7988
8897
|
await this.deps.gitWorkspaceStore?.upsert({
|
|
@@ -8033,30 +8942,11 @@ var TaskRunner = class {
|
|
|
8033
8942
|
return;
|
|
8034
8943
|
}
|
|
8035
8944
|
}
|
|
8036
|
-
const
|
|
8037
|
-
|
|
8038
|
-
|
|
8945
|
+
const startInput = {
|
|
8946
|
+
manifest,
|
|
8947
|
+
instruction: gitWorkspaceId ? prependGitWorkspaceGuidance(resolvedInstruction) : resolvedInstruction,
|
|
8948
|
+
env,
|
|
8039
8949
|
...resolvedMcp?.ok ? { mcpServers: resolvedMcp.servers } : {},
|
|
8040
|
-
...gitWorkspaceId ? { gitWorkspace: { workspaceId: gitWorkspaceId, baseline: gitBaseline } } : {},
|
|
8041
|
-
// M5: no longer `process.env` verbatim (see `environment.ts`'s own
|
|
8042
|
-
// module doc comment for the credential-leak gap that closed) —
|
|
8043
|
-
// built fresh per task from the SPECIFIC adapter `pickAdapter`
|
|
8044
|
-
// above already selected, so this always runs after adapter
|
|
8045
|
-
// selection: `pick.adapter.environmentRequirements?.()` (undefined
|
|
8046
|
-
// ⇒ platform baseline only, fail-closed) plus this device's own
|
|
8047
|
-
// `runtimeEnvironment` override, keyed by that same adapter's `id`.
|
|
8048
|
-
env: buildRuntimeEnv({
|
|
8049
|
-
ambient: process.env,
|
|
8050
|
-
requirements: pick.adapter.environmentRequirements?.(),
|
|
8051
|
-
locallyAllowedNames: this.deps.runtimeEnvironment?.[pick.adapter.id]?.allow
|
|
8052
|
-
}),
|
|
8053
|
-
// M4 Phase 3: adapter-agnostic and cheap to always populate — only an
|
|
8054
|
-
// adapter whose runtime genuinely supports an out-of-band approval
|
|
8055
|
-
// pause (claude, today) ever reads this. `resolve` is a closure over
|
|
8056
|
-
// `taskId` (not a pre-bound approvalId): it looks up whichever
|
|
8057
|
-
// approval is CURRENTLY pending for this task at call time, since one
|
|
8058
|
-
// task/session can face several approval requests, one at a time,
|
|
8059
|
-
// over its life. See `types.ts`'s `ApprovalChannel` doc comment.
|
|
8060
8950
|
approvalChannel: {
|
|
8061
8951
|
taskId,
|
|
8062
8952
|
storeDir: this.deps.storeDir,
|
|
@@ -8072,45 +8962,19 @@ var TaskRunner = class {
|
|
|
8072
8962
|
}
|
|
8073
8963
|
}
|
|
8074
8964
|
};
|
|
8075
|
-
const effectiveOffer = {
|
|
8076
|
-
...withoutRequiredToolsets(payload),
|
|
8077
|
-
instruction: gitWorkspaceId ? prependGitWorkspaceGuidance(resolvedInstruction) : resolvedInstruction,
|
|
8078
|
-
// Never forward a sessionRef this device has no recorded workspace
|
|
8079
|
-
// for (stale, from another device, or simply made up) — an adapter
|
|
8080
|
-
// that tries to resume an id it never minted fails outright (pi:
|
|
8081
|
-
// "No session found matching '<id>'", exit 1, empirically confirmed)
|
|
8082
|
-
// instead of silently starting fresh, so an unresolvable sessionRef
|
|
8083
|
-
// must look identical to "none supplied" by the time it reaches the
|
|
8084
|
-
// adapter, not get forwarded as a resume attempt doomed to fail.
|
|
8085
|
-
sessionRef: known ? payload.sessionRef : void 0
|
|
8086
|
-
};
|
|
8087
8965
|
let session;
|
|
8088
8966
|
try {
|
|
8089
|
-
session = await
|
|
8967
|
+
session = await prepared.operation.start(startInput);
|
|
8090
8968
|
} catch (err) {
|
|
8091
|
-
const
|
|
8969
|
+
const failure = projectRuntimeBoundaryFailure(err, "start");
|
|
8970
|
+
if (failure.contractViolation) {
|
|
8971
|
+
console.error("[byok/client] runtime adapter start() returned an untyped failure", err);
|
|
8972
|
+
}
|
|
8092
8973
|
await this.updateGitPhaseBestEffort(gitWorkspaceId, "failed", "repository-invalid");
|
|
8093
8974
|
gitLease?.release();
|
|
8094
|
-
await this.fail(taskId,
|
|
8975
|
+
await this.fail(taskId, failure.reason, failure.retryable);
|
|
8095
8976
|
return;
|
|
8096
8977
|
}
|
|
8097
|
-
if (this.pendingCancelled.has(taskId)) {
|
|
8098
|
-
const reason = this.pendingCancelled.get(taskId);
|
|
8099
|
-
this.pendingCancelled.delete(taskId);
|
|
8100
|
-
try {
|
|
8101
|
-
await session.interrupt();
|
|
8102
|
-
} catch {
|
|
8103
|
-
}
|
|
8104
|
-
try {
|
|
8105
|
-
await session.close();
|
|
8106
|
-
} catch {
|
|
8107
|
-
}
|
|
8108
|
-
gitLease?.release();
|
|
8109
|
-
await this.updateGitPhaseBestEffort(gitWorkspaceId, "cancelled");
|
|
8110
|
-
this.deps.send(createEnvelope("task.cancelled", { reason }, { taskId }));
|
|
8111
|
-
return;
|
|
8112
|
-
}
|
|
8113
|
-
this.deps.send(createEnvelope("task.started", {}, { taskId }));
|
|
8114
8978
|
const active = {
|
|
8115
8979
|
taskId,
|
|
8116
8980
|
adapter: pick.adapter,
|
|
@@ -8127,6 +8991,21 @@ var TaskRunner = class {
|
|
|
8127
8991
|
approvalQueue: [],
|
|
8128
8992
|
outputBytesSoFar: 0
|
|
8129
8993
|
};
|
|
8994
|
+
if (this.pendingCancelled.has(taskId)) {
|
|
8995
|
+
const reason = this.pendingCancelled.get(taskId);
|
|
8996
|
+
this.pendingCancelled.delete(taskId);
|
|
8997
|
+
this.tasks.set(taskId, active);
|
|
8998
|
+
this.reserveSemanticTerminal(active);
|
|
8999
|
+
try {
|
|
9000
|
+
await session.interrupt();
|
|
9001
|
+
} catch {
|
|
9002
|
+
}
|
|
9003
|
+
await this.updateGitPhaseBestEffort(gitWorkspaceId, "cancelled");
|
|
9004
|
+
this.deps.send(createEnvelope("task.cancelled", { reason }, { taskId }));
|
|
9005
|
+
await this.finish(taskId);
|
|
9006
|
+
return;
|
|
9007
|
+
}
|
|
9008
|
+
this.deps.send(createEnvelope("task.started", {}, { taskId }));
|
|
8130
9009
|
this.tasks.set(taskId, active);
|
|
8131
9010
|
if (payload.limits?.maxDurationMs !== void 0) {
|
|
8132
9011
|
this.armMaxDurationTimer(active, payload.limits.maxDurationMs);
|
|
@@ -8184,6 +9063,7 @@ var TaskRunner = class {
|
|
|
8184
9063
|
async pump(active) {
|
|
8185
9064
|
try {
|
|
8186
9065
|
for await (const event of active.session.events) {
|
|
9066
|
+
if (this.tasks.get(active.taskId) !== active || active.beingTornDown) return;
|
|
8187
9067
|
if (this.tasks.get(active.taskId) !== active) return;
|
|
8188
9068
|
active.outputBytesSoFar += estimateEventBytes(event);
|
|
8189
9069
|
if (active.outputBytesSoFar > this.maxTaskOutputBytes) {
|
|
@@ -8206,7 +9086,7 @@ var TaskRunner = class {
|
|
|
8206
9086
|
try {
|
|
8207
9087
|
await active.session.resolveApproval(approved, reason);
|
|
8208
9088
|
} catch (err) {
|
|
8209
|
-
await this.fail(taskId, `failed to resume session after approval decision: ${
|
|
9089
|
+
await this.fail(taskId, `failed to resume session after approval decision: ${errorMessage4(err)}`, false);
|
|
8210
9090
|
}
|
|
8211
9091
|
});
|
|
8212
9092
|
continue;
|
|
@@ -8218,6 +9098,7 @@ var TaskRunner = class {
|
|
|
8218
9098
|
const outcome = await this.resolveResultDocument(active, finalOutput);
|
|
8219
9099
|
if (!outcome.deliver) return;
|
|
8220
9100
|
await this.observeGit(active, "completed");
|
|
9101
|
+
if (this.tasks.get(active.taskId) !== active || active.beingTornDown) return;
|
|
8221
9102
|
if (outcome.document !== void 0 && !this.hasResultDocumentCapability()) {
|
|
8222
9103
|
await this.fail(
|
|
8223
9104
|
active.taskId,
|
|
@@ -8226,6 +9107,7 @@ var TaskRunner = class {
|
|
|
8226
9107
|
);
|
|
8227
9108
|
return;
|
|
8228
9109
|
}
|
|
9110
|
+
if (!this.reserveSemanticTerminal(active)) return;
|
|
8229
9111
|
this.deps.send(
|
|
8230
9112
|
createEnvelope(
|
|
8231
9113
|
"task.complete",
|
|
@@ -8261,11 +9143,17 @@ var TaskRunner = class {
|
|
|
8261
9143
|
active.batcher.push(event);
|
|
8262
9144
|
}
|
|
8263
9145
|
if (this.tasks.get(active.taskId) !== active || active.beingTornDown) return;
|
|
8264
|
-
|
|
9146
|
+
const failure = projectRuntimeBoundaryFailure(void 0, "run");
|
|
9147
|
+
console.error("[byok/client] runtime adapter events iterable ended without terminal authority");
|
|
9148
|
+
await this.fail(active.taskId, failure.reason, failure.retryable);
|
|
8265
9149
|
} catch (err) {
|
|
8266
9150
|
if (this.tasks.get(active.taskId) !== active || active.beingTornDown) return;
|
|
8267
9151
|
active.batcher.flush();
|
|
8268
|
-
|
|
9152
|
+
const failure = projectRuntimeBoundaryFailure(err, "run");
|
|
9153
|
+
if (failure.contractViolation) {
|
|
9154
|
+
console.error("[byok/client] runtime adapter events iterable returned an untyped failure", err);
|
|
9155
|
+
}
|
|
9156
|
+
await this.fail(active.taskId, failure.reason, failure.retryable);
|
|
8269
9157
|
}
|
|
8270
9158
|
}
|
|
8271
9159
|
/**
|
|
@@ -8304,7 +9192,7 @@ var TaskRunner = class {
|
|
|
8304
9192
|
try {
|
|
8305
9193
|
bytes = await opened.handle.readFile();
|
|
8306
9194
|
} catch (err) {
|
|
8307
|
-
this.reportArtifactError(active, name, `failed to read artifact "${name}": ${
|
|
9195
|
+
this.reportArtifactError(active, name, `failed to read artifact "${name}": ${errorMessage4(err)}`);
|
|
8308
9196
|
return;
|
|
8309
9197
|
} finally {
|
|
8310
9198
|
await opened.handle.close().catch(() => {
|
|
@@ -8321,7 +9209,7 @@ var TaskRunner = class {
|
|
|
8321
9209
|
const blobRef = await this.deps.blobClient.uploadArtifact(bytes, contentType);
|
|
8322
9210
|
this.deps.send(createEnvelope("task.artifact", { name, contentType, blobRef }, { taskId: active.taskId }));
|
|
8323
9211
|
} catch (err) {
|
|
8324
|
-
this.reportArtifactError(active, name, `failed to upload artifact "${name}": ${
|
|
9212
|
+
this.reportArtifactError(active, name, `failed to upload artifact "${name}": ${errorMessage4(err)}`);
|
|
8325
9213
|
}
|
|
8326
9214
|
}
|
|
8327
9215
|
/** Loud, non-silent artifact failure (finding F7): logged, and folded into this task's own progress stream as an `error` AgentEvent rather than swallowed — the task itself can still complete normally, but the omission is now visible. */
|
|
@@ -8335,6 +9223,14 @@ var TaskRunner = class {
|
|
|
8335
9223
|
this.setPendingCancelled(taskId, reason);
|
|
8336
9224
|
return;
|
|
8337
9225
|
}
|
|
9226
|
+
if (active.finalizationStarted) {
|
|
9227
|
+
await this.finish(taskId);
|
|
9228
|
+
return;
|
|
9229
|
+
}
|
|
9230
|
+
if (!this.reserveSemanticTerminal(active)) {
|
|
9231
|
+
await active.semanticTerminalSettled;
|
|
9232
|
+
return;
|
|
9233
|
+
}
|
|
8338
9234
|
try {
|
|
8339
9235
|
await active.session.interrupt();
|
|
8340
9236
|
} catch {
|
|
@@ -8363,7 +9259,7 @@ var TaskRunner = class {
|
|
|
8363
9259
|
*
|
|
8364
9260
|
* `inFlightOffers` is naturally tiny (bounded by this device's real
|
|
8365
9261
|
* concurrent-offer-processing count — normally single digits, driven by
|
|
8366
|
-
* how many `task.offer`s are simultaneously mid
|
|
9262
|
+
* how many `task.offer`s are simultaneously mid-prepared-operation start() — nowhere
|
|
8367
9263
|
* near `MAX_TRACKED_TASK_IDS`), so this scan is cheap in practice: it
|
|
8368
9264
|
* finds a safe entry at or near the front almost always. The only case
|
|
8369
9265
|
* where NO entry is safe to evict is every single tracked cancel
|
|
@@ -8716,7 +9612,7 @@ var TaskRunner = class {
|
|
|
8716
9612
|
this.deps.onStaleApprovalDecision?.(taskId, "approve");
|
|
8717
9613
|
return;
|
|
8718
9614
|
}
|
|
8719
|
-
await this.fail(taskId, `failed to resume session after approval: ${
|
|
9615
|
+
await this.fail(taskId, `failed to resume session after approval: ${errorMessage4(err)}`, false);
|
|
8720
9616
|
return;
|
|
8721
9617
|
}
|
|
8722
9618
|
this.clearPendingApproval(resolvedId, "approve", void 0);
|
|
@@ -8747,6 +9643,10 @@ var TaskRunner = class {
|
|
|
8747
9643
|
async handleReject(taskId, reason, approvalId) {
|
|
8748
9644
|
const active = this.tasks.get(taskId);
|
|
8749
9645
|
if (!active) return;
|
|
9646
|
+
if (active.finalizationStarted) {
|
|
9647
|
+
await this.finish(taskId);
|
|
9648
|
+
return;
|
|
9649
|
+
}
|
|
8750
9650
|
if (approvalId !== void 0 && approvalId !== active.pendingApprovalId) {
|
|
8751
9651
|
this.deps.onStaleApprovalDecision?.(
|
|
8752
9652
|
taskId,
|
|
@@ -8765,6 +9665,10 @@ var TaskRunner = class {
|
|
|
8765
9665
|
}
|
|
8766
9666
|
}
|
|
8767
9667
|
this.clearPendingApproval(resolvedId, "reject", reason);
|
|
9668
|
+
if (!this.reserveSemanticTerminal(active)) {
|
|
9669
|
+
await active.semanticTerminalSettled;
|
|
9670
|
+
return;
|
|
9671
|
+
}
|
|
8768
9672
|
try {
|
|
8769
9673
|
await active.session.interrupt();
|
|
8770
9674
|
} catch {
|
|
@@ -8779,6 +9683,14 @@ var TaskRunner = class {
|
|
|
8779
9683
|
}
|
|
8780
9684
|
async fail(taskId, reason, retryable) {
|
|
8781
9685
|
const active = this.tasks.get(taskId);
|
|
9686
|
+
if (active?.finalizationStarted) {
|
|
9687
|
+
await this.finish(taskId);
|
|
9688
|
+
return;
|
|
9689
|
+
}
|
|
9690
|
+
if (active && !this.reserveSemanticTerminal(active)) {
|
|
9691
|
+
await active.semanticTerminalSettled;
|
|
9692
|
+
return;
|
|
9693
|
+
}
|
|
8782
9694
|
if (active) await this.observeGit(active, "salvage");
|
|
8783
9695
|
this.deps.send(createEnvelope("task.fail", { reason, retryable }, { taskId }));
|
|
8784
9696
|
await this.finish(taskId);
|
|
@@ -8838,7 +9750,7 @@ var TaskRunner = class {
|
|
|
8838
9750
|
} catch (err) {
|
|
8839
9751
|
await this.fail(
|
|
8840
9752
|
active.taskId,
|
|
8841
|
-
`${RESULT_DOCUMENT_UNDELIVERABLE_REASON_PREFIX}: the configured resultDocument.extract threw: ${
|
|
9753
|
+
`${RESULT_DOCUMENT_UNDELIVERABLE_REASON_PREFIX}: the configured resultDocument.extract threw: ${errorMessage4(err)}`,
|
|
8842
9754
|
false
|
|
8843
9755
|
);
|
|
8844
9756
|
return { deliver: false };
|
|
@@ -8871,9 +9783,9 @@ var TaskRunner = class {
|
|
|
8871
9783
|
/**
|
|
8872
9784
|
* Whether the CURRENTLY connected server advertised `result-document` —
|
|
8873
9785
|
* read fresh on every call, never captured, because the answer changes
|
|
8874
|
-
* across a reconnect (`ConnectionManager
|
|
8875
|
-
*
|
|
8876
|
-
*
|
|
9786
|
+
* across a reconnect or transport switch (`ConnectionManager` clears the
|
|
9787
|
+
* old advertisement at the boundary, then repopulates it from a fresh WS
|
|
9788
|
+
* ack or successful poll response). An absent `getServerCapabilities` seam is
|
|
8877
9789
|
* "no capabilities", the fail-closed reading.
|
|
8878
9790
|
*/
|
|
8879
9791
|
hasResultDocumentCapability() {
|
|
@@ -8913,32 +9825,64 @@ var TaskRunner = class {
|
|
|
8913
9825
|
}
|
|
8914
9826
|
async finish(taskId) {
|
|
8915
9827
|
const active = this.tasks.get(taskId);
|
|
8916
|
-
if (!active) return;
|
|
8917
|
-
if (active.
|
|
8918
|
-
|
|
8919
|
-
active.
|
|
8920
|
-
|
|
8921
|
-
|
|
8922
|
-
|
|
8923
|
-
|
|
8924
|
-
|
|
8925
|
-
|
|
8926
|
-
|
|
8927
|
-
|
|
8928
|
-
|
|
8929
|
-
|
|
8930
|
-
|
|
8931
|
-
|
|
8932
|
-
|
|
8933
|
-
|
|
8934
|
-
|
|
9828
|
+
if (!active) return true;
|
|
9829
|
+
if (!active.finalizationStarted) {
|
|
9830
|
+
active.finalizationStarted = true;
|
|
9831
|
+
active.beingTornDown = true;
|
|
9832
|
+
if (active.maxDurationTimer) {
|
|
9833
|
+
clearTimeout(active.maxDurationTimer);
|
|
9834
|
+
active.maxDurationTimer = void 0;
|
|
9835
|
+
}
|
|
9836
|
+
active.batcher.stop();
|
|
9837
|
+
this.addFinishedTaskId(taskId);
|
|
9838
|
+
const queued = active.approvalQueue.splice(0);
|
|
9839
|
+
for (const request of queued) {
|
|
9840
|
+
request.resolve({
|
|
9841
|
+
approved: false,
|
|
9842
|
+
reason: `task ${taskId} finished before this queued approval request could be dispatched`
|
|
9843
|
+
});
|
|
9844
|
+
}
|
|
9845
|
+
if (active.pendingApprovalId !== void 0) {
|
|
9846
|
+
try {
|
|
9847
|
+
this.deps.approvalRegistry.resolve(active.pendingApprovalId, "reject", `task ${taskId} finished`);
|
|
9848
|
+
} catch {
|
|
9849
|
+
}
|
|
8935
9850
|
}
|
|
8936
9851
|
}
|
|
8937
|
-
|
|
9852
|
+
const attempt = active.disposalAttempt ?? active.session.close();
|
|
9853
|
+
active.disposalAttempt = attempt;
|
|
8938
9854
|
try {
|
|
8939
|
-
await
|
|
8940
|
-
} catch {
|
|
9855
|
+
await attempt;
|
|
9856
|
+
} catch (caught) {
|
|
9857
|
+
if (active.disposalAttempt === attempt) active.disposalAttempt = void 0;
|
|
9858
|
+
const failure = isRuntimeDisposalFailure(caught) ? caught : new RuntimeDisposalFailure({
|
|
9859
|
+
stage: "quiescence",
|
|
9860
|
+
reason: `${active.adapter.descriptor.id} session.close() returned an untyped disposal failure`
|
|
9861
|
+
}, { cause: caught });
|
|
9862
|
+
console.error(`[byok/client] runtime disposal failed for task ${taskId}: ${failure.message}`);
|
|
9863
|
+
this.deps.onRuntimeDisposalFailure?.({
|
|
9864
|
+
taskId,
|
|
9865
|
+
runtimeId: active.adapter.descriptor.id,
|
|
9866
|
+
stage: failure.stage,
|
|
9867
|
+
reason: failure.message
|
|
9868
|
+
});
|
|
9869
|
+
active.resolveSemanticTerminalSettled?.(false);
|
|
9870
|
+
return false;
|
|
8941
9871
|
}
|
|
9872
|
+
if (this.tasks.get(taskId) !== active) return true;
|
|
9873
|
+
active.gitLease?.release();
|
|
9874
|
+
this.tasks.delete(taskId);
|
|
9875
|
+
active.resolveSemanticTerminalSettled?.(true);
|
|
9876
|
+
return true;
|
|
9877
|
+
}
|
|
9878
|
+
reserveSemanticTerminal(active) {
|
|
9879
|
+
if (active.semanticTerminalReserved || active.finalizationStarted) return false;
|
|
9880
|
+
active.semanticTerminalReserved = true;
|
|
9881
|
+
active.beingTornDown = true;
|
|
9882
|
+
active.semanticTerminalSettled = new Promise((resolve) => {
|
|
9883
|
+
active.resolveSemanticTerminalSettled = resolve;
|
|
9884
|
+
});
|
|
9885
|
+
return true;
|
|
8942
9886
|
}
|
|
8943
9887
|
/** M3-B: bounded insert for `finishedTaskIds` — see its class-level doc comment and `MAX_TRACKED_TASK_IDS`. Evicts the oldest (first-inserted) entry once over cap, same idiom as `ConnectionHub.checkAndRecordDuplicate` (packages/server/src/hub.ts). */
|
|
8944
9888
|
addFinishedTaskId(taskId) {
|
|
@@ -8991,18 +9935,19 @@ var TaskRunner = class {
|
|
|
8991
9935
|
retryable: false
|
|
8992
9936
|
};
|
|
8993
9937
|
}
|
|
8994
|
-
const adapter = this.deps.adapters.find((a) => a.id === requestedRuntime);
|
|
9938
|
+
const adapter = this.deps.adapters.find((a) => a.descriptor.id === requestedRuntime);
|
|
8995
9939
|
if (!adapter) {
|
|
8996
9940
|
return { ok: false, reason: `unknown runtime "${requestedRuntime}"`, retryable: false };
|
|
8997
9941
|
}
|
|
8998
|
-
|
|
9942
|
+
const descriptor = freezeRuntimeAdapterDescriptor(adapter.descriptor);
|
|
9943
|
+
if (!adapterSupportsMode(descriptor, policyMode)) {
|
|
8999
9944
|
return {
|
|
9000
9945
|
ok: false,
|
|
9001
9946
|
reason: `runtime "${requestedRuntime}" cannot express permission mode "${policyMode}"`,
|
|
9002
9947
|
retryable: false
|
|
9003
9948
|
};
|
|
9004
9949
|
}
|
|
9005
|
-
if (requiresMcpToolsets && !adapterSupportsMcpToolsets(
|
|
9950
|
+
if (requiresMcpToolsets && !adapterSupportsMcpToolsets(descriptor)) {
|
|
9006
9951
|
return {
|
|
9007
9952
|
ok: false,
|
|
9008
9953
|
reason: `runtime "${requestedRuntime}" cannot project required MCP toolsets`,
|
|
@@ -9017,15 +9962,16 @@ var TaskRunner = class {
|
|
|
9017
9962
|
retryable: true
|
|
9018
9963
|
};
|
|
9019
9964
|
}
|
|
9020
|
-
return { ok: true, adapter };
|
|
9965
|
+
return { ok: true, adapter, descriptor };
|
|
9021
9966
|
}
|
|
9022
|
-
const eligible = allowlist ? this.deps.adapters.filter((a) => allowlist.includes(a.id)) : this.deps.adapters;
|
|
9967
|
+
const eligible = allowlist ? this.deps.adapters.filter((a) => allowlist.includes(a.descriptor.id)) : this.deps.adapters;
|
|
9023
9968
|
const candidates = orderByPreference(eligible, this.deps.runtimePreference ?? DEFAULT_RUNTIME_PREFERENCE);
|
|
9024
9969
|
for (const adapter of candidates) {
|
|
9025
|
-
|
|
9026
|
-
if (
|
|
9970
|
+
const descriptor = freezeRuntimeAdapterDescriptor(adapter.descriptor);
|
|
9971
|
+
if (!adapterSupportsMode(descriptor, policyMode)) continue;
|
|
9972
|
+
if (requiresMcpToolsets && !adapterSupportsMcpToolsets(descriptor)) continue;
|
|
9027
9973
|
const detected = await adapter.detect();
|
|
9028
|
-
if (detected.present) return { ok: true, adapter };
|
|
9974
|
+
if (detected.present) return { ok: true, adapter, descriptor };
|
|
9029
9975
|
}
|
|
9030
9976
|
return {
|
|
9031
9977
|
ok: false,
|
|
@@ -9070,27 +10016,27 @@ async function detectRuntimes(adapters) {
|
|
|
9070
10016
|
const detections = await Promise.all(adapters.map(async (adapter) => ({ adapter, detected: await adapter.detect() })));
|
|
9071
10017
|
const runtimes = [];
|
|
9072
10018
|
for (const { adapter, detected } of detections) {
|
|
9073
|
-
if (!detected.present || !isRuntimeId(adapter.id)) continue;
|
|
9074
|
-
const info = { id: adapter.id };
|
|
10019
|
+
if (!detected.present || !isRuntimeId(adapter.descriptor.id)) continue;
|
|
10020
|
+
const info = { id: adapter.descriptor.id };
|
|
9075
10021
|
if (detected.version !== void 0) info.version = detected.version;
|
|
9076
10022
|
if (detected.authPresent !== void 0) info.authPresent = detected.authPresent;
|
|
9077
|
-
info.capabilities = toRuntimeInfoCapabilities(adapter.capabilities
|
|
10023
|
+
info.capabilities = toRuntimeInfoCapabilities(adapter.descriptor.capabilities);
|
|
9078
10024
|
runtimes.push(info);
|
|
9079
10025
|
}
|
|
9080
10026
|
return runtimes;
|
|
9081
10027
|
}
|
|
9082
10028
|
function computeCapabilities(adapters) {
|
|
9083
10029
|
const flags = [];
|
|
9084
|
-
if (adapters.some((adapter) => adapter.capabilities
|
|
10030
|
+
if (adapters.some((adapter) => adapter.descriptor.capabilities.steer)) flags.push("steer");
|
|
9085
10031
|
flags.push("blob-upload");
|
|
9086
10032
|
flags.push("approval-targeting");
|
|
9087
10033
|
const selectionAdapters = adapters.filter(
|
|
9088
|
-
(adapter) => ALL_RUNTIME_IDS.includes(adapter.id)
|
|
10034
|
+
(adapter) => ALL_RUNTIME_IDS.includes(adapter.descriptor.id)
|
|
9089
10035
|
);
|
|
9090
|
-
if (selectionAdapters.length > 0 && selectionAdapters.every((adapter) => adapter.supportsDispatchSelection === true)) {
|
|
10036
|
+
if (selectionAdapters.length > 0 && selectionAdapters.every((adapter) => adapter.descriptor.supportsDispatchSelection === true)) {
|
|
9091
10037
|
flags.push("dispatch-selection");
|
|
9092
10038
|
}
|
|
9093
|
-
if (adapters.some((adapter) => adapter.capabilities
|
|
10039
|
+
if (adapters.some((adapter) => adapter.descriptor.capabilities.mcpToolsets === true)) {
|
|
9094
10040
|
flags.push("toolset-selection");
|
|
9095
10041
|
}
|
|
9096
10042
|
return flags;
|
|
@@ -9150,7 +10096,6 @@ function validatePiByokLauncherConfig(launcher) {
|
|
|
9150
10096
|
throw new Error("DaemonConfig.piByokLauncher.args must contain only non-empty single-line strings");
|
|
9151
10097
|
}
|
|
9152
10098
|
}
|
|
9153
|
-
var MAX_LOCAL_MCP_TOOLSETS = 64;
|
|
9154
10099
|
var MAX_LOCAL_MCP_SERVERS_PER_TOOLSET = 16;
|
|
9155
10100
|
var MAX_LOCAL_MCP_ARGS = 64;
|
|
9156
10101
|
var MAX_LOCAL_MCP_TOKEN_CHARS = 4096;
|
|
@@ -9163,8 +10108,10 @@ function resolveMcpToolsets(configured) {
|
|
|
9163
10108
|
throw new Error("DaemonConfig.mcpToolsets must be an object keyed by logical toolset id");
|
|
9164
10109
|
}
|
|
9165
10110
|
const toolsetEntries = Object.entries(configured);
|
|
9166
|
-
if (toolsetEntries.length >
|
|
9167
|
-
throw new Error(
|
|
10111
|
+
if (toolsetEntries.length > CONFIGURED_TOOLSETS_MAX_ITEMS) {
|
|
10112
|
+
throw new Error(
|
|
10113
|
+
`DaemonConfig.mcpToolsets may contain at most ${CONFIGURED_TOOLSETS_MAX_ITEMS} toolsets`
|
|
10114
|
+
);
|
|
9168
10115
|
}
|
|
9169
10116
|
const resolved = /* @__PURE__ */ new Map();
|
|
9170
10117
|
for (const [toolsetId, rawToolset] of toolsetEntries) {
|
|
@@ -9277,6 +10224,9 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
9277
10224
|
}
|
|
9278
10225
|
function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProbe) {
|
|
9279
10226
|
const mcpToolsets = resolveMcpToolsets(config.mcpToolsets);
|
|
10227
|
+
const configuredToolsets = Object.freeze(
|
|
10228
|
+
[...mcpToolsets?.keys() ?? []].sort()
|
|
10229
|
+
);
|
|
9280
10230
|
if (config.piByokLauncher !== void 0) {
|
|
9281
10231
|
validatePiByokLauncherConfig(config.piByokLauncher);
|
|
9282
10232
|
}
|
|
@@ -9583,12 +10533,13 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
9583
10533
|
dirty: event.observation ? { staged: event.observation.staged, unstaged: event.observation.unstaged, untracked: event.observation.untracked, conflicted: event.observation.conflicted } : void 0,
|
|
9584
10534
|
errorCategory: event.errorCategory
|
|
9585
10535
|
}),
|
|
10536
|
+
onRuntimeDisposalFailure: (event) => observer.noteRuntimeDisposalFailure(event),
|
|
9586
10537
|
// M4 Phase 3: the SAME `ApprovalRegistry` instance the control
|
|
9587
10538
|
// socket's own `approvals.list`/`approvals.resolve` methods already
|
|
9588
10539
|
// share (see that field's own construction above) — `TaskRunner
|
|
9589
10540
|
// .requestApproval` registers into it directly, so a decision arriving
|
|
9590
10541
|
// via either the server wire or the local CLI resolves the identical
|
|
9591
|
-
// entry. `storeDir`/`productId` let
|
|
10542
|
+
// entry. `storeDir`/`productId` let the prepared operation approval channel
|
|
9592
10543
|
// (populated per-task by `TaskRunner`) tell an out-of-process helper
|
|
9593
10544
|
// (`bin/byok-approval-mcp.ts`) exactly which control socket to dial.
|
|
9594
10545
|
approvalRegistry,
|
|
@@ -9640,6 +10591,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
9640
10591
|
productId: config.productId,
|
|
9641
10592
|
capabilities,
|
|
9642
10593
|
runtimes,
|
|
10594
|
+
configuredToolsets,
|
|
9643
10595
|
auth,
|
|
9644
10596
|
cursorStore,
|
|
9645
10597
|
// Finding F3: return (not void-and-forget) so ConnectionManager can
|
|
@@ -9726,6 +10678,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
9726
10678
|
presencePublisher ??= new PresencePublisher({
|
|
9727
10679
|
serverUrl: config.serverUrl,
|
|
9728
10680
|
auth,
|
|
10681
|
+
configuredToolsets,
|
|
9729
10682
|
...presenceCadence,
|
|
9730
10683
|
onDegraded: (reason) => console.warn(`[byok/client] ${reason}`)
|
|
9731
10684
|
});
|
|
@@ -9819,8 +10772,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
9819
10772
|
await attempt(() => auth.stop(), true);
|
|
9820
10773
|
connectionState = "closed";
|
|
9821
10774
|
await attempt(async () => {
|
|
9822
|
-
await controlServerHandle?.
|
|
9823
|
-
controlServerHandle = void 0;
|
|
10775
|
+
await controlServerHandle?.stopServing();
|
|
9824
10776
|
}, true);
|
|
9825
10777
|
if (mutationBarrierComplete && daemonOwnerLease) {
|
|
9826
10778
|
await attempt(() => operationalHealth.markCleanStop(), false);
|
|
@@ -9829,6 +10781,12 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
9829
10781
|
daemonOwnerLease = void 0;
|
|
9830
10782
|
}, false);
|
|
9831
10783
|
}
|
|
10784
|
+
if (daemonOwnerLease === void 0) {
|
|
10785
|
+
await attempt(async () => {
|
|
10786
|
+
await controlServerHandle?.close();
|
|
10787
|
+
controlServerHandle = void 0;
|
|
10788
|
+
}, false);
|
|
10789
|
+
}
|
|
9832
10790
|
if (errors.length === 1) throw errors[0];
|
|
9833
10791
|
if (errors.length > 1) {
|
|
9834
10792
|
throw new AggregateError(errors, "daemon shutdown completed with errors");
|
|
@@ -9908,7 +10866,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
9908
10866
|
deviceId: auth.deviceId,
|
|
9909
10867
|
transport: connectionState,
|
|
9910
10868
|
activeTasks,
|
|
9911
|
-
runtimeIds: adapters.map((adapter) => adapter.id),
|
|
10869
|
+
runtimeIds: adapters.map((adapter) => adapter.descriptor.id),
|
|
9912
10870
|
// M4 Phase 4 (part B.3): queue watermarks come from TaskRunner's own
|
|
9913
10871
|
// active-task map (distinct from `observer.tasks()` above, which is
|
|
9914
10872
|
// derived from the envelope feed) — see `TaskRunner.getQueueWatermarks`'s
|
|
@@ -10116,7 +11074,7 @@ function createDaemon(config) {
|
|
|
10116
11074
|
return createDaemonWithAdapters(config, buildDefaultAdapters(config));
|
|
10117
11075
|
}
|
|
10118
11076
|
var MAX_CONTROL_TOKEN_BYTES = 256;
|
|
10119
|
-
function
|
|
11077
|
+
function errorMessage5(err) {
|
|
10120
11078
|
return err instanceof Error ? err.message : String(err);
|
|
10121
11079
|
}
|
|
10122
11080
|
function sameFileState3(left, right) {
|
|
@@ -10169,7 +11127,7 @@ async function connectControlClient(opts) {
|
|
|
10169
11127
|
}
|
|
10170
11128
|
token = read;
|
|
10171
11129
|
} catch (err) {
|
|
10172
|
-
return { ok: false, reason: `could not read the control token: ${
|
|
11130
|
+
return { ok: false, reason: `could not read the control token: ${errorMessage5(err)}` };
|
|
10173
11131
|
}
|
|
10174
11132
|
if (!token) {
|
|
10175
11133
|
return { ok: false, reason: "control token file is empty" };
|
|
@@ -10179,7 +11137,7 @@ async function connectControlClient(opts) {
|
|
|
10179
11137
|
const client = await connectAndHandshake(endpoint, token, opts);
|
|
10180
11138
|
return { ok: true, client };
|
|
10181
11139
|
} catch (err) {
|
|
10182
|
-
return { ok: false, reason: `daemon control socket not reachable: ${
|
|
11140
|
+
return { ok: false, reason: `daemon control socket not reachable: ${errorMessage5(err)}` };
|
|
10183
11141
|
}
|
|
10184
11142
|
}
|
|
10185
11143
|
function connectAndHandshake(endpoint, token, opts) {
|
|
@@ -10356,7 +11314,7 @@ function createControlClient(socket, reader, opts) {
|
|
|
10356
11314
|
}
|
|
10357
11315
|
|
|
10358
11316
|
// src/daemon/assertion-client.ts
|
|
10359
|
-
function
|
|
11317
|
+
function errorMessage6(err) {
|
|
10360
11318
|
return err instanceof Error ? err.message : String(err);
|
|
10361
11319
|
}
|
|
10362
11320
|
async function requestDeviceAssertion(options) {
|
|
@@ -10378,7 +11336,7 @@ async function requestDeviceAssertion(options) {
|
|
|
10378
11336
|
return { ok: true, assertion, expiresAt: result.expiresAt };
|
|
10379
11337
|
} catch (err) {
|
|
10380
11338
|
if (err instanceof ControlError) return { ok: false, code: err.code, reason: err.message };
|
|
10381
|
-
return { ok: false, code: "bad_response", reason:
|
|
11339
|
+
return { ok: false, code: "bad_response", reason: errorMessage6(err) };
|
|
10382
11340
|
} finally {
|
|
10383
11341
|
connected.client.close();
|
|
10384
11342
|
}
|
|
@@ -11614,6 +12572,6 @@ function createServiceLifecycle(def, opts = {}) {
|
|
|
11614
12572
|
}
|
|
11615
12573
|
}
|
|
11616
12574
|
|
|
11617
|
-
export { AuthManager, BlobClient, ClaudeAdapter, CodexAdapter, DEFAULT_ACK_CRITICAL_RESERVE_BYTES, DEFAULT_CLEANUP_BATCH_LIMIT, DEFAULT_HARD_BUDGET_RATIO, DEFAULT_INCREMENTAL_VACUUM_PAGES, DEFAULT_LOG_ROTATION, DEFAULT_NORMAL_COMPACTION_INTERVAL_MS, DEFAULT_PRESSURE_COMPACTION_INTERVAL_MS, DEFAULT_RETENTION_MS, DEFAULT_SOFT_BUDGET_RATIO, DaemonObserver, DeviceRevokedError, GitWorkspaceError, GitWorkspaceManager, GitWorkspaceStore, JOURNAL_DB_FILENAME, JOURNAL_QUARANTINE_DIRNAME, JOURNAL_TASK_REF_PREFIX, JournalClosedError, JournalCorruptError, JournalRecordTooLargeError, JournalUnavailableError, JournalUnknownTaskError, LocalStorageEmergencyError, LocalStoragePolicyError, LocalStoragePressureEngine, PI_PACKAGE_NAME, PiAdapter, PolicyUnsupportedError, SKILL_PACKS_CAPABILITY, SKILL_PACKS_DIRNAME, SKILL_PACK_AUDIT_FILENAME, SKILL_PACK_INSTALL_ERROR_CODES, SKILL_PACK_LOCK_FILENAME, SKILL_PACK_LOCK_SCHEMA, SKILL_PACK_RESPONSE_MAX_BYTES, SecureDirHardeningError, SkillPackInstallError, SqliteLocalTaskJournal, SteerUnsupportedError, StoredDeviceProofSigner, TruthMemoryClient, TruthMemoryClientError, UnsupportedServicePlatformError, buildIcaclsArgs, cleanupEligibleAt, cleanupOrderFor, computePressureState, createDaemon, createDaemonWithAdapters, createFilesystemCleanupExecutor, createServiceLifecycle, createStatfsFreeBytesProvider, ensureSecureDir, generateLaunchdPlist, generateSystemdUnit, generateWinswXml, installSkillPacks, isGitWorkspaceConfig, journalHash, listInstalledSkillPacks, nodeAgentProgram, prependGitWorkspaceGuidance, projectSkillPack, requestDeviceAssertion, resolveLocalStoragePolicy, sanitizeServiceName, skillPacksRoot };
|
|
12575
|
+
export { AuthManager, BlobClient, ClaudeAdapter, CodexAdapter, DEFAULT_ACK_CRITICAL_RESERVE_BYTES, DEFAULT_CLEANUP_BATCH_LIMIT, DEFAULT_HARD_BUDGET_RATIO, DEFAULT_INCREMENTAL_VACUUM_PAGES, DEFAULT_LOG_ROTATION, DEFAULT_NORMAL_COMPACTION_INTERVAL_MS, DEFAULT_PRESSURE_COMPACTION_INTERVAL_MS, DEFAULT_RETENTION_MS, DEFAULT_SOFT_BUDGET_RATIO, DaemonObserver, DeviceRevokedError, GitWorkspaceError, GitWorkspaceManager, GitWorkspaceStore, JOURNAL_DB_FILENAME, JOURNAL_QUARANTINE_DIRNAME, JOURNAL_TASK_REF_PREFIX, JournalClosedError, JournalCorruptError, JournalRecordTooLargeError, JournalUnavailableError, JournalUnknownTaskError, LocalStorageEmergencyError, LocalStoragePolicyError, LocalStoragePressureEngine, PI_PACKAGE_NAME, PiAdapter, PolicyUnsupportedError, RUNTIME_ADAPTER_CONTRACT_VIOLATION_REASON, RuntimeDisposalFailure, RuntimeExecutionFailure, SKILL_PACKS_CAPABILITY, SKILL_PACKS_DIRNAME, SKILL_PACK_AUDIT_FILENAME, SKILL_PACK_INSTALL_ERROR_CODES, SKILL_PACK_LOCK_FILENAME, SKILL_PACK_LOCK_SCHEMA, SKILL_PACK_RESPONSE_MAX_BYTES, SecureDirHardeningError, SkillPackInstallError, SqliteLocalTaskJournal, SteerUnsupportedError, StoredDeviceProofSigner, TruthMemoryClient, TruthMemoryClientError, UnsupportedServicePlatformError, buildIcaclsArgs, cleanupEligibleAt, cleanupOrderFor, computePressureState, createDaemon, createDaemonWithAdapters, createFilesystemCleanupExecutor, createServiceLifecycle, createStatfsFreeBytesProvider, ensureSecureDir, freezeRuntimeAdapterDescriptor, generateLaunchdPlist, generateSystemdUnit, generateWinswXml, installSkillPacks, isGitWorkspaceConfig, isRuntimeDisposalFailure, isRuntimeExecutionFailure, journalHash, listInstalledSkillPacks, nodeAgentProgram, prependGitWorkspaceGuidance, projectRuntimeBoundaryFailure, projectRuntimeExecutionFailure, projectSkillPack, requestDeviceAssertion, resolveLocalStoragePolicy, sanitizeServiceName, sealRuntimeOperationManifest, skillPacksRoot };
|
|
11618
12576
|
//# sourceMappingURL=index.js.map
|
|
11619
12577
|
//# sourceMappingURL=index.js.map
|