@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/bin/byok-agent.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { execFile, spawn
|
|
2
|
+
import { execFile, spawn } from 'child_process';
|
|
3
3
|
import { randomUUID, createHash, randomBytes, timingSafeEqual, createHmac, createPrivateKey, generateKeyPairSync, sign } from 'crypto';
|
|
4
4
|
import { readFileSync, promises, linkSync, fstatSync, lstatSync, unlinkSync, constants, readSync, openSync, writeFileSync, fchmodSync, fsyncSync, closeSync, opendirSync, existsSync, realpathSync, mkdirSync, renameSync, chmodSync, statSync, readdirSync } from 'fs';
|
|
5
5
|
import path20, { isAbsolute, join } from 'path';
|
|
6
6
|
import os from 'os';
|
|
7
7
|
import { DEVICE_ASSERTION_AUDIENCE_MAX_BYTES, DEVICE_ASSERTION_DEFAULT_TTL_MS, DEVICE_ASSERTION_MAX_TTL_MS, nonceSigningBytes, CapabilityDeclarationSchema, hasCapability, DeviceAssertionClaimsSchema, deviceAssertionSigningInput, DEVICE_ASSERTION_SCHEMA_ID } from '@byok-sdk/core';
|
|
8
|
-
import { TASK_STATES, ToolsetIdSchema, BYOK_PAIR_PATH, BYOK_CHALLENGE_PATH, BYOK_TOKEN_PATH, partitionAgentEvents, TASK_TRANSITIONS, encodeEnvelope, createEnvelope, byokBlobUrlPath, BYOK_BLOBS_PATH, byokBlobFinalizePath, 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
|
+
import { TASK_STATES, CONFIGURED_TOOLSETS_MAX_ITEMS, ToolsetIdSchema, BYOK_PAIR_PATH, BYOK_CHALLENGE_PATH, BYOK_TOKEN_PATH, partitionAgentEvents, TASK_TRANSITIONS, encodeEnvelope, createEnvelope, byokBlobUrlPath, BYOK_BLOBS_PATH, byokBlobFinalizePath, 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';
|
|
9
9
|
import { promisify } from 'util';
|
|
10
10
|
import { fileURLToPath } from 'url';
|
|
11
11
|
import 'readline';
|
|
@@ -15,6 +15,52 @@ import { createRequire } from 'module';
|
|
|
15
15
|
import { createInterface } from 'readline/promises';
|
|
16
16
|
|
|
17
17
|
// src/types.ts
|
|
18
|
+
function frozenStrings(values) {
|
|
19
|
+
return values === void 0 ? void 0 : Object.freeze([...values]);
|
|
20
|
+
}
|
|
21
|
+
function frozenPolicy(policy) {
|
|
22
|
+
const allowTools = policy.allowTools === void 0 ? void 0 : Object.freeze([...policy.allowTools]);
|
|
23
|
+
const denyTools = policy.denyTools === void 0 ? void 0 : Object.freeze([...policy.denyTools]);
|
|
24
|
+
return Object.freeze({
|
|
25
|
+
mode: policy.mode,
|
|
26
|
+
...allowTools === void 0 ? {} : { allowTools },
|
|
27
|
+
...denyTools === void 0 ? {} : { denyTools },
|
|
28
|
+
...policy.workspaceRoot === void 0 ? {} : { workspaceRoot: policy.workspaceRoot },
|
|
29
|
+
...policy.network === void 0 ? {} : { network: policy.network }
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function freezeRuntimeAdapterDescriptor(descriptor) {
|
|
33
|
+
const baseNames = frozenStrings(descriptor.environmentRequirements.baseNames);
|
|
34
|
+
const credentialNames = frozenStrings(descriptor.environmentRequirements.credentialNames);
|
|
35
|
+
return Object.freeze({
|
|
36
|
+
id: descriptor.id,
|
|
37
|
+
supportsDispatchSelection: descriptor.supportsDispatchSelection === true,
|
|
38
|
+
capabilities: Object.freeze({
|
|
39
|
+
steer: descriptor.capabilities.steer === true,
|
|
40
|
+
resume: descriptor.capabilities.resume === true,
|
|
41
|
+
approvalInteractive: descriptor.capabilities.approvalInteractive === true,
|
|
42
|
+
...descriptor.capabilities.mcpToolsets === void 0 ? {} : { mcpToolsets: descriptor.capabilities.mcpToolsets === true },
|
|
43
|
+
permissionModes: Object.freeze([...descriptor.capabilities.permissionModes])
|
|
44
|
+
}),
|
|
45
|
+
environmentRequirements: Object.freeze({
|
|
46
|
+
...baseNames === void 0 ? {} : { baseNames },
|
|
47
|
+
...credentialNames === void 0 ? {} : { credentialNames }
|
|
48
|
+
})
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
function sealRuntimeOperationManifest(manifest) {
|
|
52
|
+
return Object.freeze({
|
|
53
|
+
taskId: manifest.taskId,
|
|
54
|
+
runtimeId: manifest.runtimeId,
|
|
55
|
+
descriptor: freezeRuntimeAdapterDescriptor(manifest.descriptor),
|
|
56
|
+
policy: frozenPolicy(manifest.policy),
|
|
57
|
+
requiredToolsetIds: Object.freeze([...manifest.requiredToolsetIds]),
|
|
58
|
+
...manifest.dispatchSelection === void 0 ? {} : { dispatchSelection: Object.freeze({ ...manifest.dispatchSelection }) },
|
|
59
|
+
...manifest.sessionRef === void 0 ? {} : { sessionRef: manifest.sessionRef },
|
|
60
|
+
workspace: Object.freeze({ ...manifest.workspace }),
|
|
61
|
+
forwardedEnvironmentNames: Object.freeze([...manifest.forwardedEnvironmentNames])
|
|
62
|
+
});
|
|
63
|
+
}
|
|
18
64
|
var PolicyUnsupportedError = class extends Error {
|
|
19
65
|
constructor(message) {
|
|
20
66
|
super(message);
|
|
@@ -22,7 +68,7 @@ var PolicyUnsupportedError = class extends Error {
|
|
|
22
68
|
}
|
|
23
69
|
};
|
|
24
70
|
var SteerUnsupportedError = class extends Error {
|
|
25
|
-
/** The `RuntimeAdapter.id` that cannot steer (e.g. `claude`, `codex`). */
|
|
71
|
+
/** The `RuntimeAdapter.descriptor.id` that cannot steer (e.g. `claude`, `codex`). */
|
|
26
72
|
runtimeId;
|
|
27
73
|
constructor(runtimeId, message) {
|
|
28
74
|
super(message);
|
|
@@ -30,6 +76,112 @@ var SteerUnsupportedError = class extends Error {
|
|
|
30
76
|
this.runtimeId = runtimeId;
|
|
31
77
|
}
|
|
32
78
|
};
|
|
79
|
+
|
|
80
|
+
// src/runtime-failure.ts
|
|
81
|
+
var RUNTIME_EXECUTION_FAILURE_BRAND = /* @__PURE__ */ Symbol.for("@byok-sdk/client/RuntimeExecutionFailure/v1");
|
|
82
|
+
var RUNTIME_DISPOSAL_FAILURE_BRAND = /* @__PURE__ */ Symbol.for("@byok-sdk/client/RuntimeDisposalFailure/v1");
|
|
83
|
+
var RuntimeDisposalFailure = class extends Error {
|
|
84
|
+
stage;
|
|
85
|
+
constructor(input, options) {
|
|
86
|
+
if (!isRuntimeDisposalStage(input.stage) || typeof input.reason !== "string" || input.reason.length === 0) {
|
|
87
|
+
throw new TypeError("invalid RuntimeDisposalFailure input");
|
|
88
|
+
}
|
|
89
|
+
super(input.reason, options);
|
|
90
|
+
this.name = "RuntimeDisposalFailure";
|
|
91
|
+
this.stage = input.stage;
|
|
92
|
+
Object.defineProperty(this, RUNTIME_DISPOSAL_FAILURE_BRAND, { value: true });
|
|
93
|
+
Object.freeze(this);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
function isRuntimeDisposalStage(value) {
|
|
97
|
+
return value === "signal" || value === "quiescence" || value === "cleanup";
|
|
98
|
+
}
|
|
99
|
+
function isRuntimeDisposalFailure(value) {
|
|
100
|
+
if (typeof value !== "object" || value === null) return false;
|
|
101
|
+
const candidate = value;
|
|
102
|
+
return candidate[RUNTIME_DISPOSAL_FAILURE_BRAND] === true && isRuntimeDisposalStage(candidate.stage) && typeof candidate.message === "string" && candidate.message.length > 0;
|
|
103
|
+
}
|
|
104
|
+
var RuntimeExecutionFailure = class extends Error {
|
|
105
|
+
phase;
|
|
106
|
+
category;
|
|
107
|
+
retry;
|
|
108
|
+
constructor(input, options) {
|
|
109
|
+
if (!isRuntimeFailurePhase(input.phase) || !isRuntimeFailureCategory(input.category) || !isRuntimeRetryDisposition(input.retry) || typeof input.reason !== "string" || input.reason.length === 0) {
|
|
110
|
+
throw new TypeError("invalid RuntimeExecutionFailure input");
|
|
111
|
+
}
|
|
112
|
+
super(input.reason, options);
|
|
113
|
+
this.name = "RuntimeExecutionFailure";
|
|
114
|
+
this.phase = input.phase;
|
|
115
|
+
this.category = input.category;
|
|
116
|
+
this.retry = input.retry;
|
|
117
|
+
Object.defineProperty(this, RUNTIME_EXECUTION_FAILURE_BRAND, { value: true });
|
|
118
|
+
Object.freeze(this);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
function isRuntimeFailurePhase(value) {
|
|
122
|
+
return value === "start" || value === "run";
|
|
123
|
+
}
|
|
124
|
+
function isRuntimeFailureCategory(value) {
|
|
125
|
+
return value === "semantic" || value === "infrastructure" || value === "authority";
|
|
126
|
+
}
|
|
127
|
+
function isRuntimeRetryDisposition(value) {
|
|
128
|
+
return value === "retryable" || value === "non-retryable";
|
|
129
|
+
}
|
|
130
|
+
function isRuntimeExecutionFailure(value) {
|
|
131
|
+
if (typeof value !== "object" || value === null) return false;
|
|
132
|
+
const candidate = value;
|
|
133
|
+
return candidate[RUNTIME_EXECUTION_FAILURE_BRAND] === true && isRuntimeFailurePhase(candidate.phase) && isRuntimeFailureCategory(candidate.category) && isRuntimeRetryDisposition(candidate.retry) && typeof candidate.message === "string" && candidate.message.length > 0;
|
|
134
|
+
}
|
|
135
|
+
function retryableFromDisposition(disposition) {
|
|
136
|
+
switch (disposition) {
|
|
137
|
+
case "retryable":
|
|
138
|
+
return true;
|
|
139
|
+
case "non-retryable":
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function projectRuntimeExecutionFailure(failure) {
|
|
144
|
+
return {
|
|
145
|
+
reason: failure.message,
|
|
146
|
+
retryable: retryableFromDisposition(failure.retry)
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
var RUNTIME_ADAPTER_CONTRACT_VIOLATION_REASON = Object.freeze({
|
|
150
|
+
start: "runtime adapter contract violation during start",
|
|
151
|
+
run: "runtime adapter contract violation during run"
|
|
152
|
+
});
|
|
153
|
+
function projectRuntimeBoundaryFailure(value, expectedPhase) {
|
|
154
|
+
if (isRuntimeExecutionFailure(value) && value.phase === expectedPhase) {
|
|
155
|
+
return { ...projectRuntimeExecutionFailure(value), contractViolation: false };
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
reason: RUNTIME_ADAPTER_CONTRACT_VIOLATION_REASON[expectedPhase],
|
|
159
|
+
retryable: false,
|
|
160
|
+
contractViolation: true
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
var GIT_ERROR_CATEGORIES = [
|
|
164
|
+
"git-unavailable",
|
|
165
|
+
"git-timeout",
|
|
166
|
+
"git-output-limit",
|
|
167
|
+
"git-command-failed",
|
|
168
|
+
"workspace-root-invalid",
|
|
169
|
+
"workspace-root-conflict",
|
|
170
|
+
"workspace-not-owned",
|
|
171
|
+
"repository-root-mismatch",
|
|
172
|
+
"repository-invalid",
|
|
173
|
+
"lease-busy",
|
|
174
|
+
"ledger-invalid"
|
|
175
|
+
];
|
|
176
|
+
var GIT_WORKSPACE_PHASES = [
|
|
177
|
+
"preparing",
|
|
178
|
+
"active",
|
|
179
|
+
"completed",
|
|
180
|
+
"failed",
|
|
181
|
+
"cancelled",
|
|
182
|
+
"interrupted",
|
|
183
|
+
"salvage"
|
|
184
|
+
];
|
|
33
185
|
var GitWorkspaceError = class extends Error {
|
|
34
186
|
constructor(category, message = category) {
|
|
35
187
|
super(message);
|
|
@@ -735,12 +887,12 @@ function resolvePiBin() {
|
|
|
735
887
|
}
|
|
736
888
|
} catch (cause) {
|
|
737
889
|
throw new Error(
|
|
738
|
-
`Required ${PI_PACKAGE_NAME} could not be resolved; install @byok-sdk/client dependencies or set BYOK_PI_BIN to a Node 22.
|
|
890
|
+
`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`,
|
|
739
891
|
{ cause }
|
|
740
892
|
);
|
|
741
893
|
}
|
|
742
894
|
throw new Error(
|
|
743
|
-
`Required ${PI_PACKAGE_NAME} does not expose the pi CLI; reinstall the pinned dependency or set BYOK_PI_BIN to a Node 22.
|
|
895
|
+
`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`
|
|
744
896
|
);
|
|
745
897
|
}
|
|
746
898
|
|
|
@@ -946,6 +1098,266 @@ var AsyncQueue = class {
|
|
|
946
1098
|
}
|
|
947
1099
|
};
|
|
948
1100
|
|
|
1101
|
+
// src/adapters/taskkill-pid-set.ts
|
|
1102
|
+
var INTEGER_PATTERN = /\d+/g;
|
|
1103
|
+
function isCandidatePid(value) {
|
|
1104
|
+
return Number.isSafeInteger(value) && value > 0;
|
|
1105
|
+
}
|
|
1106
|
+
function walkTaskkillPidSet(text, rootPid, excludedPids = []) {
|
|
1107
|
+
const excluded = new Set(excludedPids);
|
|
1108
|
+
const accepted = /* @__PURE__ */ new Set();
|
|
1109
|
+
if (isCandidatePid(rootPid)) accepted.add(rootPid);
|
|
1110
|
+
const lines = text.split(/\r?\n/).map((line) => {
|
|
1111
|
+
const pids = [];
|
|
1112
|
+
for (const match of line.matchAll(INTEGER_PATTERN)) {
|
|
1113
|
+
const pid = Number(match[0]);
|
|
1114
|
+
if (isCandidatePid(pid) && !excluded.has(pid)) pids.push(pid);
|
|
1115
|
+
}
|
|
1116
|
+
return pids;
|
|
1117
|
+
});
|
|
1118
|
+
let changed = true;
|
|
1119
|
+
while (changed) {
|
|
1120
|
+
changed = false;
|
|
1121
|
+
for (const pids of lines) {
|
|
1122
|
+
if (!pids.some((pid) => accepted.has(pid))) continue;
|
|
1123
|
+
for (const pid of pids) {
|
|
1124
|
+
if (accepted.has(pid)) continue;
|
|
1125
|
+
accepted.add(pid);
|
|
1126
|
+
changed = true;
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
return accepted;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
// src/adapters/process-tree.ts
|
|
1134
|
+
var DEFAULT_TERM_GRACE_MS = 750;
|
|
1135
|
+
var DEFAULT_KILL_GRACE_MS = 2e3;
|
|
1136
|
+
var POLL_MS = 20;
|
|
1137
|
+
var terminationState = /* @__PURE__ */ new WeakMap();
|
|
1138
|
+
function stateFor(child) {
|
|
1139
|
+
const existing = terminationState.get(child);
|
|
1140
|
+
if (existing) return existing;
|
|
1141
|
+
const created = { requested: false, acceptedPids: /* @__PURE__ */ new Set() };
|
|
1142
|
+
terminationState.set(child, created);
|
|
1143
|
+
return created;
|
|
1144
|
+
}
|
|
1145
|
+
function defaultKill(pid, signal) {
|
|
1146
|
+
process.kill(pid, signal);
|
|
1147
|
+
}
|
|
1148
|
+
function withOwnedProcessTree(options) {
|
|
1149
|
+
return {
|
|
1150
|
+
...options,
|
|
1151
|
+
...process.platform === "win32" ? { windowsHide: true } : { detached: true }
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
function positivePid(child, label) {
|
|
1155
|
+
const pid = child.pid;
|
|
1156
|
+
if (pid === void 0) return void 0;
|
|
1157
|
+
if (!Number.isSafeInteger(pid) || pid <= 0 || pid === process.pid) {
|
|
1158
|
+
throw new RuntimeDisposalFailure({
|
|
1159
|
+
stage: "signal",
|
|
1160
|
+
reason: `${label} runtime process has an unsafe owned pid`
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
return pid;
|
|
1164
|
+
}
|
|
1165
|
+
function groupExists(pid, label, kill) {
|
|
1166
|
+
try {
|
|
1167
|
+
kill(-pid, 0);
|
|
1168
|
+
return true;
|
|
1169
|
+
} catch (cause) {
|
|
1170
|
+
const code = cause.code;
|
|
1171
|
+
if (code === "ESRCH") return false;
|
|
1172
|
+
if (code === "EPERM") return true;
|
|
1173
|
+
throw new RuntimeDisposalFailure({
|
|
1174
|
+
stage: "quiescence",
|
|
1175
|
+
reason: `${label} runtime process-group state could not be verified`
|
|
1176
|
+
}, { cause });
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
function processExists(pid, label, kill) {
|
|
1180
|
+
try {
|
|
1181
|
+
kill(pid, 0);
|
|
1182
|
+
return true;
|
|
1183
|
+
} catch (cause) {
|
|
1184
|
+
const code = cause.code;
|
|
1185
|
+
if (code === "ESRCH") return false;
|
|
1186
|
+
if (code === "EPERM") return true;
|
|
1187
|
+
throw new RuntimeDisposalFailure({
|
|
1188
|
+
stage: "quiescence",
|
|
1189
|
+
reason: `${label} runtime process ${pid} state could not be verified`
|
|
1190
|
+
}, { cause });
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
function signalGroup(pid, signal, label, kill) {
|
|
1194
|
+
try {
|
|
1195
|
+
kill(-pid, signal);
|
|
1196
|
+
} catch (cause) {
|
|
1197
|
+
const code = cause.code;
|
|
1198
|
+
if (code === "ESRCH" || code === "EPERM") return;
|
|
1199
|
+
throw new RuntimeDisposalFailure({
|
|
1200
|
+
stage: "signal",
|
|
1201
|
+
reason: `${label} runtime process group ${pid} could not receive ${signal} (${code ?? "unknown"})`
|
|
1202
|
+
}, { cause });
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
async function runTaskkill(pid, options) {
|
|
1206
|
+
const spawnFn = options.spawnFn ?? spawn;
|
|
1207
|
+
return new Promise((resolve, reject) => {
|
|
1208
|
+
const signalFailure = (cause) => {
|
|
1209
|
+
reject(new RuntimeDisposalFailure({
|
|
1210
|
+
stage: "signal",
|
|
1211
|
+
reason: `${options.label} runtime process tree termination could not be requested`
|
|
1212
|
+
}, { cause }));
|
|
1213
|
+
};
|
|
1214
|
+
let taskkill;
|
|
1215
|
+
try {
|
|
1216
|
+
taskkill = spawnFn("taskkill", ["/PID", String(pid), "/T", "/F"], {
|
|
1217
|
+
windowsHide: true,
|
|
1218
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1219
|
+
});
|
|
1220
|
+
} catch (cause) {
|
|
1221
|
+
signalFailure(cause);
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
const chunks = [];
|
|
1225
|
+
taskkill.stdout?.on("data", (chunk) => chunks.push(chunk));
|
|
1226
|
+
taskkill.stderr?.on("data", (chunk) => chunks.push(chunk));
|
|
1227
|
+
taskkill.once("error", signalFailure);
|
|
1228
|
+
taskkill.once("close", () => resolve(Buffer.concat(chunks).toString("latin1")));
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
function liveAcceptedPids(accepted, label, kill) {
|
|
1232
|
+
const live = [];
|
|
1233
|
+
for (const pid of accepted) {
|
|
1234
|
+
if (processExists(pid, label, kill)) live.push(pid);
|
|
1235
|
+
}
|
|
1236
|
+
return live;
|
|
1237
|
+
}
|
|
1238
|
+
async function waitUntil(predicate, timeoutMs) {
|
|
1239
|
+
const deadline = Date.now() + timeoutMs;
|
|
1240
|
+
while (predicate()) {
|
|
1241
|
+
if (Date.now() >= deadline) return false;
|
|
1242
|
+
await new Promise((resolve) => {
|
|
1243
|
+
setTimeout(resolve, POLL_MS);
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
return true;
|
|
1247
|
+
}
|
|
1248
|
+
async function waitWithDeadline(promise, timeoutMs) {
|
|
1249
|
+
return new Promise((resolve) => {
|
|
1250
|
+
let settled = false;
|
|
1251
|
+
const timer = setTimeout(() => {
|
|
1252
|
+
if (!settled) {
|
|
1253
|
+
settled = true;
|
|
1254
|
+
resolve(false);
|
|
1255
|
+
}
|
|
1256
|
+
}, timeoutMs);
|
|
1257
|
+
void promise.then(
|
|
1258
|
+
() => {
|
|
1259
|
+
if (!settled) {
|
|
1260
|
+
settled = true;
|
|
1261
|
+
clearTimeout(timer);
|
|
1262
|
+
resolve(true);
|
|
1263
|
+
}
|
|
1264
|
+
},
|
|
1265
|
+
() => {
|
|
1266
|
+
if (!settled) {
|
|
1267
|
+
settled = true;
|
|
1268
|
+
clearTimeout(timer);
|
|
1269
|
+
resolve(false);
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
);
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
async function requestOwnedProcessTreeTermination(options) {
|
|
1276
|
+
const platform = options.platform ?? process.platform;
|
|
1277
|
+
if (platform !== "win32" && options.isClosed()) return;
|
|
1278
|
+
const pid = positivePid(options.child, options.label);
|
|
1279
|
+
if (pid === void 0) return;
|
|
1280
|
+
if (platform === "win32") {
|
|
1281
|
+
const output = await runTaskkill(pid, options);
|
|
1282
|
+
const state = stateFor(options.child);
|
|
1283
|
+
for (const walked of walkTaskkillPidSet(output, pid, [process.pid])) state.acceptedPids.add(walked);
|
|
1284
|
+
state.requested = true;
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
signalGroup(pid, "SIGTERM", options.label, options.killFn ?? defaultKill);
|
|
1288
|
+
stateFor(options.child).requested = true;
|
|
1289
|
+
}
|
|
1290
|
+
async function disposeOwnedProcessTree(options) {
|
|
1291
|
+
const pid = positivePid(options.child, options.label);
|
|
1292
|
+
const termGraceMs = options.termGraceMs ?? DEFAULT_TERM_GRACE_MS;
|
|
1293
|
+
const killGraceMs = options.killGraceMs ?? DEFAULT_KILL_GRACE_MS;
|
|
1294
|
+
const platform = options.platform ?? process.platform;
|
|
1295
|
+
const kill = options.killFn ?? defaultKill;
|
|
1296
|
+
if (pid === void 0) {
|
|
1297
|
+
if (await waitWithDeadline(options.waitClosed(), killGraceMs)) return;
|
|
1298
|
+
throw new RuntimeDisposalFailure({
|
|
1299
|
+
stage: "quiescence",
|
|
1300
|
+
reason: `${options.label} runtime process did not settle after spawn failure`
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1303
|
+
if (platform === "win32") {
|
|
1304
|
+
if (!terminationState.get(options.child)?.requested) {
|
|
1305
|
+
await requestOwnedProcessTreeTermination(options);
|
|
1306
|
+
}
|
|
1307
|
+
const accepted = terminationState.get(options.child)?.acceptedPids ?? /* @__PURE__ */ new Set();
|
|
1308
|
+
const deadline = Date.now() + killGraceMs;
|
|
1309
|
+
const resweepAt = Date.now() + Math.floor(killGraceMs / 2);
|
|
1310
|
+
let reswept = false;
|
|
1311
|
+
let live = liveAcceptedPids(accepted, options.label, kill);
|
|
1312
|
+
while (live.length > 0) {
|
|
1313
|
+
if (Date.now() >= deadline) {
|
|
1314
|
+
throw new RuntimeDisposalFailure({
|
|
1315
|
+
stage: "quiescence",
|
|
1316
|
+
reason: `${options.label} runtime process tree did not quiesce: ${live.length} of ${accepted.size} walked process ids were still alive at the disposal deadline`
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
if (!reswept && Date.now() >= resweepAt) {
|
|
1320
|
+
reswept = true;
|
|
1321
|
+
for (const livePid of live) {
|
|
1322
|
+
for (const walked of walkTaskkillPidSet(await runTaskkill(livePid, options), livePid, [process.pid])) {
|
|
1323
|
+
accepted.add(walked);
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
await new Promise((resolve) => {
|
|
1328
|
+
setTimeout(resolve, POLL_MS);
|
|
1329
|
+
});
|
|
1330
|
+
live = liveAcceptedPids(accepted, options.label, kill);
|
|
1331
|
+
}
|
|
1332
|
+
if (!await waitWithDeadline(options.waitClosed(), killGraceMs)) {
|
|
1333
|
+
throw new RuntimeDisposalFailure({
|
|
1334
|
+
stage: "quiescence",
|
|
1335
|
+
reason: `${options.label} runtime root did not emit close after its process tree quiesced`
|
|
1336
|
+
});
|
|
1337
|
+
}
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
if (groupExists(pid, options.label, kill) && !terminationState.get(options.child)?.requested) {
|
|
1341
|
+
signalGroup(pid, "SIGTERM", options.label, kill);
|
|
1342
|
+
stateFor(options.child).requested = true;
|
|
1343
|
+
}
|
|
1344
|
+
if (!await waitUntil(() => groupExists(pid, options.label, kill), termGraceMs)) {
|
|
1345
|
+
signalGroup(pid, "SIGKILL", options.label, kill);
|
|
1346
|
+
if (!await waitUntil(() => groupExists(pid, options.label, kill), killGraceMs)) {
|
|
1347
|
+
throw new RuntimeDisposalFailure({
|
|
1348
|
+
stage: "quiescence",
|
|
1349
|
+
reason: `${options.label} runtime process group remained live after SIGKILL`
|
|
1350
|
+
});
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
if (!await waitWithDeadline(options.waitClosed(), killGraceMs)) {
|
|
1354
|
+
throw new RuntimeDisposalFailure({
|
|
1355
|
+
stage: "quiescence",
|
|
1356
|
+
reason: `${options.label} runtime root did not emit close after its process group exited`
|
|
1357
|
+
});
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
|
|
949
1361
|
// src/adapters/pi/rpc-client.ts
|
|
950
1362
|
var STDERR_RING_CAPACITY = 20;
|
|
951
1363
|
var DIALOG_UI_METHODS = /* @__PURE__ */ new Set(["select", "confirm", "input", "editor"]);
|
|
@@ -957,16 +1369,22 @@ var PiRpcClient = class {
|
|
|
957
1369
|
eventQueue = new AsyncQueue();
|
|
958
1370
|
closed = false;
|
|
959
1371
|
exitError;
|
|
1372
|
+
closedPromise;
|
|
1373
|
+
resolveClosed;
|
|
1374
|
+
disposalAttempt;
|
|
960
1375
|
/** 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`. */
|
|
961
1376
|
stderrRing = [];
|
|
962
1377
|
/** Count of pi RPC message types `PiSession` (pi-adapter.ts) has told us have no `AgentEvent` mapping and aren't routine bookkeeping — see `recordUnmappedFrame`. */
|
|
963
1378
|
unmappedFrameCounts = /* @__PURE__ */ new Map();
|
|
964
1379
|
constructor(options) {
|
|
965
1380
|
const spawnFn = options.spawnFn ?? spawn;
|
|
966
|
-
this.child = spawnFn(options.command, options.args, {
|
|
1381
|
+
this.child = spawnFn(options.command, options.args, withOwnedProcessTree({
|
|
967
1382
|
cwd: options.cwd,
|
|
968
1383
|
env: options.env,
|
|
969
1384
|
stdio: ["pipe", "pipe", "pipe"]
|
|
1385
|
+
}));
|
|
1386
|
+
this.closedPromise = new Promise((resolve) => {
|
|
1387
|
+
this.resolveClosed = resolve;
|
|
970
1388
|
});
|
|
971
1389
|
this.child.stdout.setEncoding("utf8");
|
|
972
1390
|
this.child.stdout.on("data", (chunk) => this.onData(chunk));
|
|
@@ -1001,6 +1419,10 @@ var PiRpcClient = class {
|
|
|
1001
1419
|
get events() {
|
|
1002
1420
|
return this.eventQueue;
|
|
1003
1421
|
}
|
|
1422
|
+
/** Local transport diagnostic retained when the process closes; consumers must classify it explicitly. */
|
|
1423
|
+
get terminalError() {
|
|
1424
|
+
return this.exitError;
|
|
1425
|
+
}
|
|
1004
1426
|
/**
|
|
1005
1427
|
* Record a pi RPC message `type` that `PiSession` (pi-adapter.ts) decided
|
|
1006
1428
|
* has no `AgentEvent` mapping and isn't routine bookkeeping (see
|
|
@@ -1019,15 +1441,37 @@ var PiRpcClient = class {
|
|
|
1019
1441
|
);
|
|
1020
1442
|
}
|
|
1021
1443
|
}
|
|
1022
|
-
/**
|
|
1444
|
+
/**
|
|
1445
|
+
* Immediate process-tree termination request. `dispose()` is the settlement
|
|
1446
|
+
* receipt, so this stays fire-and-forget: an interrupt must not block on a
|
|
1447
|
+
* terminator. A request that could not be spawned is left unrecorded, so
|
|
1448
|
+
* `dispose()` re-issues it and raises the typed `stage:'signal'` failure —
|
|
1449
|
+
* swallowing it here loses nothing.
|
|
1450
|
+
*/
|
|
1023
1451
|
kill() {
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1452
|
+
void requestOwnedProcessTreeTermination(this.processTreeOptions()).catch(() => {
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1455
|
+
waitClosed() {
|
|
1456
|
+
return this.closedPromise;
|
|
1457
|
+
}
|
|
1458
|
+
dispose() {
|
|
1459
|
+
if (!this.disposalAttempt) {
|
|
1460
|
+
const attempt = disposeOwnedProcessTree(this.processTreeOptions());
|
|
1461
|
+
this.disposalAttempt = attempt.catch((error) => {
|
|
1462
|
+
this.disposalAttempt = void 0;
|
|
1463
|
+
throw error;
|
|
1464
|
+
});
|
|
1030
1465
|
}
|
|
1466
|
+
return this.disposalAttempt;
|
|
1467
|
+
}
|
|
1468
|
+
processTreeOptions() {
|
|
1469
|
+
return {
|
|
1470
|
+
child: this.child,
|
|
1471
|
+
waitClosed: () => this.closedPromise,
|
|
1472
|
+
isClosed: () => this.closed,
|
|
1473
|
+
label: "pi"
|
|
1474
|
+
};
|
|
1031
1475
|
}
|
|
1032
1476
|
onData(chunk) {
|
|
1033
1477
|
this.buffer += chunk;
|
|
@@ -1113,6 +1557,7 @@ var PiRpcClient = class {
|
|
|
1113
1557
|
if (this.closed) return;
|
|
1114
1558
|
this.closed = true;
|
|
1115
1559
|
this.exitError = err;
|
|
1560
|
+
this.resolveClosed();
|
|
1116
1561
|
for (const [, waiter] of this.pending) waiter.reject(err);
|
|
1117
1562
|
this.pending.clear();
|
|
1118
1563
|
this.eventQueue.end();
|
|
@@ -1183,8 +1628,17 @@ var PiAdapter = class {
|
|
|
1183
1628
|
this.options = options;
|
|
1184
1629
|
}
|
|
1185
1630
|
options;
|
|
1186
|
-
|
|
1187
|
-
|
|
1631
|
+
descriptor = freezeRuntimeAdapterDescriptor({
|
|
1632
|
+
id: "pi",
|
|
1633
|
+
supportsDispatchSelection: true,
|
|
1634
|
+
capabilities: {
|
|
1635
|
+
steer: true,
|
|
1636
|
+
resume: true,
|
|
1637
|
+
approvalInteractive: false,
|
|
1638
|
+
permissionModes: ["auto", "readonly"]
|
|
1639
|
+
},
|
|
1640
|
+
environmentRequirements: { credentialNames: PROVIDER_CREDENTIAL_ENV_NAMES }
|
|
1641
|
+
});
|
|
1188
1642
|
async detect() {
|
|
1189
1643
|
try {
|
|
1190
1644
|
const bin = this.resolveBin();
|
|
@@ -1196,49 +1650,26 @@ var PiAdapter = class {
|
|
|
1196
1650
|
return { present: false };
|
|
1197
1651
|
}
|
|
1198
1652
|
}
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
}
|
|
1202
|
-
/**
|
|
1203
|
-
* M5: pi authenticates to its ~30 supported providers via env-var API
|
|
1204
|
-
* keys — `detect()`'s own `authPresent` probe above checks this identical
|
|
1205
|
-
* list — so these MUST keep flowing into pi's spawned process or pi auth
|
|
1206
|
-
* breaks entirely. `KNOWN_PROVIDER_ENV_VARS` above is the single source
|
|
1207
|
-
* of truth, reused here rather than duplicated. No `baseNames`: nothing
|
|
1208
|
-
* in this adapter or `rpc-client.ts` reads a pi-specific config-discovery
|
|
1209
|
-
* variable beyond the platform baseline (`daemon/environment.ts`).
|
|
1210
|
-
*/
|
|
1211
|
-
environmentRequirements() {
|
|
1212
|
-
return { credentialNames: PROVIDER_CREDENTIAL_ENV_NAMES };
|
|
1213
|
-
}
|
|
1214
|
-
async start(task, ctx) {
|
|
1215
|
-
if (typeof task.instruction !== "string") {
|
|
1216
|
-
throw new PolicyUnsupportedError("pi adapter only supports string instructions in M0 (no blob-ref fetch yet)");
|
|
1217
|
-
}
|
|
1218
|
-
const mapping = mapPermissionPolicyToPiArgs(ctx.policy);
|
|
1653
|
+
async prepare(input) {
|
|
1654
|
+
const mapping = mapPermissionPolicyToPiArgs(input.policy);
|
|
1219
1655
|
if (!mapping.ok) {
|
|
1220
|
-
|
|
1656
|
+
return { kind: "reject", reason: mapping.reason ?? "policy rejected by pi adapter", retryable: false };
|
|
1221
1657
|
}
|
|
1222
1658
|
const bin = this.resolveBin();
|
|
1223
|
-
const
|
|
1224
|
-
const
|
|
1225
|
-
const selection = task.dispatchSelection;
|
|
1659
|
+
const selection = input.offer.dispatchSelection;
|
|
1660
|
+
const pinnedSelection = selection === void 0 ? void 0 : Object.freeze({ ...selection });
|
|
1226
1661
|
let command = bin.command;
|
|
1227
|
-
let
|
|
1228
|
-
if (
|
|
1229
|
-
if (
|
|
1230
|
-
|
|
1231
|
-
`pi adapter cannot execute ${selection.lane} selection for runtime ${selection.runtimeId}`
|
|
1232
|
-
);
|
|
1662
|
+
let launcherArgs;
|
|
1663
|
+
if (pinnedSelection !== void 0) {
|
|
1664
|
+
if (pinnedSelection.lane !== "byok" || pinnedSelection.runtimeId !== "pi") {
|
|
1665
|
+
return { kind: "reject", reason: `pi adapter cannot execute ${pinnedSelection.lane} selection for runtime ${pinnedSelection.runtimeId}`, retryable: false };
|
|
1233
1666
|
}
|
|
1234
1667
|
const launcher = this.options.byokLauncher;
|
|
1235
1668
|
if (launcher === void 0) {
|
|
1236
|
-
|
|
1237
|
-
"pi BYOK selection requires a configured credential-custody launcher"
|
|
1238
|
-
);
|
|
1669
|
+
return { kind: "reject", reason: "pi BYOK selection requires a configured credential-custody launcher", retryable: false };
|
|
1239
1670
|
}
|
|
1240
1671
|
command = launcher.command;
|
|
1241
|
-
|
|
1672
|
+
launcherArgs = [
|
|
1242
1673
|
...launcher.args ?? [],
|
|
1243
1674
|
"--pi-bin",
|
|
1244
1675
|
bin.command,
|
|
@@ -1248,62 +1679,136 @@ var PiAdapter = class {
|
|
|
1248
1679
|
launcher.sessionDir,
|
|
1249
1680
|
...launcher.secretServicePrefix ? ["--secret-service-prefix", launcher.secretServicePrefix] : [],
|
|
1250
1681
|
"--provider",
|
|
1251
|
-
|
|
1682
|
+
pinnedSelection.providerId,
|
|
1252
1683
|
"--model",
|
|
1253
|
-
|
|
1254
|
-
"--",
|
|
1255
|
-
...piArgs
|
|
1684
|
+
pinnedSelection.modelId
|
|
1256
1685
|
];
|
|
1257
1686
|
}
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1687
|
+
return {
|
|
1688
|
+
kind: "prepared",
|
|
1689
|
+
operation: {
|
|
1690
|
+
start: async (startInput) => {
|
|
1691
|
+
const manifestSelection = startInput.manifest.dispatchSelection;
|
|
1692
|
+
if (!sameDispatchSelection(manifestSelection, pinnedSelection)) {
|
|
1693
|
+
throw new RuntimeExecutionFailure({
|
|
1694
|
+
phase: "start",
|
|
1695
|
+
category: "authority",
|
|
1696
|
+
retry: "non-retryable",
|
|
1697
|
+
reason: "prepared pi operation received a manifest with different runtime selection"
|
|
1698
|
+
});
|
|
1699
|
+
}
|
|
1700
|
+
if (typeof startInput.instruction !== "string") {
|
|
1701
|
+
throw new RuntimeExecutionFailure({
|
|
1702
|
+
phase: "start",
|
|
1703
|
+
category: "authority",
|
|
1704
|
+
retry: "non-retryable",
|
|
1705
|
+
reason: "prepared pi operation requires a resolved string instruction"
|
|
1706
|
+
});
|
|
1707
|
+
}
|
|
1708
|
+
const resumeSessionId = startInput.manifest.sessionRef;
|
|
1709
|
+
const piArgs = ["--mode", "rpc", ...resumeSessionId ? ["--session", resumeSessionId] : [], ...mapping.args];
|
|
1710
|
+
const args = launcherArgs === void 0 ? piArgs : [...launcherArgs, "--", ...piArgs];
|
|
1711
|
+
let rpc;
|
|
1712
|
+
try {
|
|
1713
|
+
rpc = new PiRpcClient({
|
|
1714
|
+
command,
|
|
1715
|
+
args,
|
|
1716
|
+
cwd: startInput.manifest.workspace.workspaceDir,
|
|
1717
|
+
env: manifestSelection === void 0 ? startInput.env : withoutProviderCredentials(startInput.env),
|
|
1718
|
+
spawnFn: this.options.spawnFn
|
|
1719
|
+
});
|
|
1720
|
+
} catch (cause) {
|
|
1721
|
+
throw new RuntimeExecutionFailure({
|
|
1722
|
+
phase: "start",
|
|
1723
|
+
category: "infrastructure",
|
|
1724
|
+
retry: "retryable",
|
|
1725
|
+
reason: "pi runtime process could not be spawned"
|
|
1726
|
+
}, { cause });
|
|
1727
|
+
}
|
|
1728
|
+
let response;
|
|
1729
|
+
try {
|
|
1730
|
+
response = await rpc.send({ type: "prompt", message: startInput.instruction });
|
|
1731
|
+
} catch (cause) {
|
|
1732
|
+
rpc.kill();
|
|
1733
|
+
throw new RuntimeExecutionFailure({
|
|
1734
|
+
phase: "start",
|
|
1735
|
+
category: "infrastructure",
|
|
1736
|
+
retry: "retryable",
|
|
1737
|
+
reason: `pi initial prompt transport failed: ${errorMessage(cause)}`
|
|
1738
|
+
}, { cause });
|
|
1739
|
+
}
|
|
1740
|
+
if (response.success === false) {
|
|
1741
|
+
rpc.kill();
|
|
1742
|
+
throw new RuntimeExecutionFailure({
|
|
1743
|
+
phase: "start",
|
|
1744
|
+
category: "semantic",
|
|
1745
|
+
retry: "non-retryable",
|
|
1746
|
+
reason: typeof response.error === "string" ? response.error : "pi rejected the initial prompt"
|
|
1747
|
+
});
|
|
1748
|
+
}
|
|
1749
|
+
let sessionRef;
|
|
1750
|
+
try {
|
|
1751
|
+
sessionRef = await resolveAuthoritativeSessionId(rpc);
|
|
1752
|
+
} catch (err) {
|
|
1753
|
+
rpc.kill();
|
|
1754
|
+
throw err;
|
|
1755
|
+
}
|
|
1756
|
+
if (resumeSessionId !== void 0 && sessionRef !== resumeSessionId) {
|
|
1757
|
+
rpc.kill();
|
|
1758
|
+
throw new RuntimeExecutionFailure({
|
|
1759
|
+
phase: "start",
|
|
1760
|
+
category: "authority",
|
|
1761
|
+
retry: "non-retryable",
|
|
1762
|
+
reason: "pi resumed a different authoritative session than requested"
|
|
1763
|
+
});
|
|
1764
|
+
}
|
|
1765
|
+
return new PiSession(sessionRef, rpc, manifestSelection);
|
|
1766
|
+
}
|
|
1279
1767
|
}
|
|
1280
|
-
}
|
|
1281
|
-
return new PiSession(sessionRef, rpc, selection);
|
|
1768
|
+
};
|
|
1282
1769
|
}
|
|
1283
1770
|
resolveBin() {
|
|
1284
1771
|
return (this.options.resolveBin ?? resolvePiBin)();
|
|
1285
1772
|
}
|
|
1286
1773
|
};
|
|
1287
|
-
|
|
1774
|
+
function sameDispatchSelection(left, right) {
|
|
1775
|
+
if (left === void 0 || right === void 0) return left === right;
|
|
1776
|
+
return left.lane === right.lane && left.runtimeId === right.runtimeId && left.providerId === right.providerId && left.modelId === right.modelId;
|
|
1777
|
+
}
|
|
1778
|
+
async function resolveAuthoritativeSessionId(rpc) {
|
|
1288
1779
|
let state;
|
|
1289
1780
|
try {
|
|
1290
1781
|
state = await rpc.send({ type: "get_state" });
|
|
1291
1782
|
} catch (err) {
|
|
1292
|
-
|
|
1783
|
+
if (isRuntimeExecutionFailure(err)) throw err;
|
|
1784
|
+
throw new RuntimeExecutionFailure({
|
|
1785
|
+
phase: "start",
|
|
1786
|
+
category: "infrastructure",
|
|
1787
|
+
retry: "retryable",
|
|
1788
|
+
reason: `pi transport ended before yielding an authoritative session id: ${errorMessage(err)}`
|
|
1789
|
+
}, {
|
|
1293
1790
|
cause: err
|
|
1294
1791
|
});
|
|
1295
1792
|
}
|
|
1296
1793
|
if (state.success === false) {
|
|
1297
1794
|
const reason = typeof state.error === "string" ? state.error : "get_state reported failure";
|
|
1298
|
-
throw new
|
|
1795
|
+
throw new RuntimeExecutionFailure({
|
|
1796
|
+
phase: "start",
|
|
1797
|
+
category: "authority",
|
|
1798
|
+
retry: "non-retryable",
|
|
1799
|
+
reason: `pi did not yield an authoritative session id: ${reason}`
|
|
1800
|
+
});
|
|
1299
1801
|
}
|
|
1300
1802
|
const data = state.data;
|
|
1301
1803
|
if (typeof data?.sessionId === "string" && data.sessionId.length > 0) {
|
|
1302
1804
|
return data.sessionId;
|
|
1303
1805
|
}
|
|
1304
|
-
throw new
|
|
1305
|
-
|
|
1306
|
-
|
|
1806
|
+
throw new RuntimeExecutionFailure({
|
|
1807
|
+
phase: "start",
|
|
1808
|
+
category: "authority",
|
|
1809
|
+
retry: "non-retryable",
|
|
1810
|
+
reason: "pi get_state reported no authoritative session id"
|
|
1811
|
+
});
|
|
1307
1812
|
}
|
|
1308
1813
|
var PiSession = class {
|
|
1309
1814
|
constructor(sessionRef, rpc, selection) {
|
|
@@ -1319,12 +1824,40 @@ var PiSession = class {
|
|
|
1319
1824
|
return {
|
|
1320
1825
|
[Symbol.asyncIterator]() {
|
|
1321
1826
|
const inner = rpc.events[Symbol.asyncIterator]();
|
|
1827
|
+
let terminalFailure;
|
|
1322
1828
|
return {
|
|
1323
1829
|
async next() {
|
|
1324
1830
|
for (; ; ) {
|
|
1325
|
-
|
|
1326
|
-
|
|
1831
|
+
if (terminalFailure) throw terminalFailure;
|
|
1832
|
+
let result;
|
|
1833
|
+
try {
|
|
1834
|
+
result = await inner.next();
|
|
1835
|
+
} catch (cause) {
|
|
1836
|
+
throw new RuntimeExecutionFailure({
|
|
1837
|
+
phase: "run",
|
|
1838
|
+
category: "infrastructure",
|
|
1839
|
+
retry: "retryable",
|
|
1840
|
+
reason: "pi runtime event transport failed"
|
|
1841
|
+
}, { cause });
|
|
1842
|
+
}
|
|
1843
|
+
const { value, done } = result;
|
|
1844
|
+
if (done) {
|
|
1845
|
+
throw new RuntimeExecutionFailure({
|
|
1846
|
+
phase: "run",
|
|
1847
|
+
category: "infrastructure",
|
|
1848
|
+
retry: "retryable",
|
|
1849
|
+
reason: "pi runtime process ended before agent_settled"
|
|
1850
|
+
}, { cause: rpc.terminalError });
|
|
1851
|
+
}
|
|
1327
1852
|
const mapped = mapPiMessageToAgentEvent(value);
|
|
1853
|
+
if (value.type === "auto_retry_end" && value.success === false) {
|
|
1854
|
+
terminalFailure = new RuntimeExecutionFailure({
|
|
1855
|
+
phase: "run",
|
|
1856
|
+
category: "semantic",
|
|
1857
|
+
retry: "non-retryable",
|
|
1858
|
+
reason: "pi exhausted its native retry policy"
|
|
1859
|
+
});
|
|
1860
|
+
}
|
|
1328
1861
|
if (mapped) return { value: mapped, done: false };
|
|
1329
1862
|
if (!ROUTINE_PI_EVENT_TYPES.has(value.type)) {
|
|
1330
1863
|
rpc.recordUnmappedFrame(value.type);
|
|
@@ -1354,7 +1887,7 @@ var PiSession = class {
|
|
|
1354
1887
|
await this.rpc.send({ type: "abort" });
|
|
1355
1888
|
}
|
|
1356
1889
|
async close() {
|
|
1357
|
-
this.rpc.
|
|
1890
|
+
await this.rpc.dispose();
|
|
1358
1891
|
}
|
|
1359
1892
|
async resolveApproval() {
|
|
1360
1893
|
throw new Error("pi adapter does not support approval resume: pi never emits needs_approval in M0/M1");
|
|
@@ -1566,7 +2099,15 @@ function mapResult(msg) {
|
|
|
1566
2099
|
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`
|
|
1567
2100
|
);
|
|
1568
2101
|
const events = usageEvent ? [usageEvent, { type: "error", message }] : [{ type: "error", message }];
|
|
1569
|
-
return {
|
|
2102
|
+
return {
|
|
2103
|
+
events,
|
|
2104
|
+
terminalFailure: new RuntimeExecutionFailure({
|
|
2105
|
+
phase: "run",
|
|
2106
|
+
category: msg.is_error === true ? "semantic" : "authority",
|
|
2107
|
+
retry: "non-retryable",
|
|
2108
|
+
reason: msg.is_error === true ? "claude reported terminal task failure" : "claude emitted a malformed terminal result frame"
|
|
2109
|
+
})
|
|
2110
|
+
};
|
|
1570
2111
|
}
|
|
1571
2112
|
var RESULT_DIAGNOSTIC_MAX_CHARS = 2e3;
|
|
1572
2113
|
function truncateResultDiagnostic(text) {
|
|
@@ -1618,16 +2159,22 @@ var ClaudeProcessClient = class {
|
|
|
1618
2159
|
eventQueue = new AsyncQueue();
|
|
1619
2160
|
closed = false;
|
|
1620
2161
|
exitError;
|
|
2162
|
+
closedPromise;
|
|
2163
|
+
resolveClosed;
|
|
2164
|
+
disposalAttempt;
|
|
1621
2165
|
stderrRing = [];
|
|
1622
2166
|
unmappedFrameCounts = /* @__PURE__ */ new Map();
|
|
1623
2167
|
sessionId;
|
|
1624
2168
|
initWaiter;
|
|
1625
2169
|
constructor(options) {
|
|
1626
2170
|
const spawnFn = options.spawnFn ?? spawn;
|
|
1627
|
-
this.child = spawnFn(options.command, options.args, {
|
|
2171
|
+
this.child = spawnFn(options.command, options.args, withOwnedProcessTree({
|
|
1628
2172
|
cwd: options.cwd,
|
|
1629
2173
|
env: options.env,
|
|
1630
2174
|
stdio: ["pipe", "pipe", "pipe"]
|
|
2175
|
+
}));
|
|
2176
|
+
this.closedPromise = new Promise((resolve) => {
|
|
2177
|
+
this.resolveClosed = resolve;
|
|
1631
2178
|
});
|
|
1632
2179
|
this.child.stdout.setEncoding("utf8");
|
|
1633
2180
|
this.child.stdout.on("data", (chunk) => this.onData(chunk));
|
|
@@ -1683,6 +2230,10 @@ var ClaudeProcessClient = class {
|
|
|
1683
2230
|
get events() {
|
|
1684
2231
|
return this.eventQueue;
|
|
1685
2232
|
}
|
|
2233
|
+
/** Local transport diagnostic retained when the process closes; consumers classify it at the session boundary. */
|
|
2234
|
+
get terminalError() {
|
|
2235
|
+
return this.exitError;
|
|
2236
|
+
}
|
|
1686
2237
|
/**
|
|
1687
2238
|
* Record a claude stream-json frame/subtype/content-block label that
|
|
1688
2239
|
* `ClaudeSession`'s event iterator (`../claude-adapter.ts`) decided has
|
|
@@ -1702,15 +2253,37 @@ var ClaudeProcessClient = class {
|
|
|
1702
2253
|
);
|
|
1703
2254
|
}
|
|
1704
2255
|
}
|
|
1705
|
-
/**
|
|
2256
|
+
/**
|
|
2257
|
+
* Immediate process-tree termination request. `dispose()` is the settlement
|
|
2258
|
+
* receipt, so this stays fire-and-forget: an interrupt must not block on a
|
|
2259
|
+
* terminator. A request that could not be spawned is left unrecorded, so
|
|
2260
|
+
* `dispose()` re-issues it and raises the typed `stage:'signal'` failure —
|
|
2261
|
+
* swallowing it here loses nothing.
|
|
2262
|
+
*/
|
|
1706
2263
|
kill() {
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
2264
|
+
void requestOwnedProcessTreeTermination(this.processTreeOptions()).catch(() => {
|
|
2265
|
+
});
|
|
2266
|
+
}
|
|
2267
|
+
waitClosed() {
|
|
2268
|
+
return this.closedPromise;
|
|
2269
|
+
}
|
|
2270
|
+
dispose() {
|
|
2271
|
+
if (!this.disposalAttempt) {
|
|
2272
|
+
const attempt = disposeOwnedProcessTree(this.processTreeOptions());
|
|
2273
|
+
this.disposalAttempt = attempt.catch((error) => {
|
|
2274
|
+
this.disposalAttempt = void 0;
|
|
2275
|
+
throw error;
|
|
2276
|
+
});
|
|
1713
2277
|
}
|
|
2278
|
+
return this.disposalAttempt;
|
|
2279
|
+
}
|
|
2280
|
+
processTreeOptions() {
|
|
2281
|
+
return {
|
|
2282
|
+
child: this.child,
|
|
2283
|
+
waitClosed: () => this.closedPromise,
|
|
2284
|
+
isClosed: () => this.closed,
|
|
2285
|
+
label: "claude"
|
|
2286
|
+
};
|
|
1714
2287
|
}
|
|
1715
2288
|
onData(chunk) {
|
|
1716
2289
|
this.buffer += chunk;
|
|
@@ -1761,6 +2334,7 @@ var ClaudeProcessClient = class {
|
|
|
1761
2334
|
if (this.closed) return;
|
|
1762
2335
|
this.closed = true;
|
|
1763
2336
|
this.exitError = err;
|
|
2337
|
+
this.resolveClosed();
|
|
1764
2338
|
this.initWaiter?.reject(err);
|
|
1765
2339
|
this.initWaiter = void 0;
|
|
1766
2340
|
this.eventQueue.end();
|
|
@@ -1772,18 +2346,37 @@ var APPROVAL_TOOL_NAME = "approval_prompt";
|
|
|
1772
2346
|
var APPROVAL_MCP_SERVER_NAME = "byokapproval";
|
|
1773
2347
|
var execFileAsync2 = promisify(execFile);
|
|
1774
2348
|
var DETECT_TIMEOUT_MS2 = 5e3;
|
|
2349
|
+
function errorMessage2(err) {
|
|
2350
|
+
return err instanceof Error ? err.message : String(err);
|
|
2351
|
+
}
|
|
1775
2352
|
async function cleanupMcpConfigDir(dir) {
|
|
1776
2353
|
if (!dir) return;
|
|
1777
|
-
|
|
1778
|
-
|
|
2354
|
+
try {
|
|
2355
|
+
await promises.rm(dir, { recursive: true, force: true });
|
|
2356
|
+
} catch (cause) {
|
|
2357
|
+
throw new RuntimeDisposalFailure({
|
|
2358
|
+
stage: "cleanup",
|
|
2359
|
+
reason: "claude task-scoped MCP configuration could not be removed"
|
|
2360
|
+
}, { cause });
|
|
2361
|
+
}
|
|
1779
2362
|
}
|
|
1780
2363
|
var ClaudeAdapter = class {
|
|
1781
2364
|
constructor(options = {}) {
|
|
1782
2365
|
this.options = options;
|
|
1783
2366
|
}
|
|
1784
2367
|
options;
|
|
1785
|
-
|
|
1786
|
-
|
|
2368
|
+
descriptor = freezeRuntimeAdapterDescriptor({
|
|
2369
|
+
id: "claude",
|
|
2370
|
+
supportsDispatchSelection: true,
|
|
2371
|
+
capabilities: {
|
|
2372
|
+
steer: false,
|
|
2373
|
+
resume: true,
|
|
2374
|
+
approvalInteractive: true,
|
|
2375
|
+
mcpToolsets: true,
|
|
2376
|
+
permissionModes: ["auto", "readonly", "plan", "confirm"]
|
|
2377
|
+
},
|
|
2378
|
+
environmentRequirements: { credentialNames: [] }
|
|
2379
|
+
});
|
|
1787
2380
|
async detect() {
|
|
1788
2381
|
const bin = this.resolveBin();
|
|
1789
2382
|
try {
|
|
@@ -1795,54 +2388,73 @@ var ClaudeAdapter = class {
|
|
|
1795
2388
|
return { present: false };
|
|
1796
2389
|
}
|
|
1797
2390
|
}
|
|
1798
|
-
|
|
2391
|
+
async prepare(input) {
|
|
2392
|
+
const mapping = mapPermissionPolicyToClaudeArgs(input.policy);
|
|
2393
|
+
if (!mapping.ok) return { kind: "reject", reason: mapping.reason ?? "policy rejected by claude adapter", retryable: false };
|
|
2394
|
+
let modelId;
|
|
2395
|
+
try {
|
|
2396
|
+
modelId = subscriptionModel(input.offer.dispatchSelection, "claude");
|
|
2397
|
+
} catch (error) {
|
|
2398
|
+
return { kind: "reject", reason: error instanceof Error ? error.message : String(error), retryable: false };
|
|
2399
|
+
}
|
|
2400
|
+
if (mapping.needsApprovalMcp && Object.prototype.hasOwnProperty.call(input.mcpServers ?? {}, APPROVAL_MCP_SERVER_NAME)) {
|
|
2401
|
+
return { kind: "reject", reason: `MCP server name "${APPROVAL_MCP_SERVER_NAME}" is reserved by the claude approval channel`, retryable: false };
|
|
2402
|
+
}
|
|
2403
|
+
let bin;
|
|
2404
|
+
try {
|
|
2405
|
+
bin = this.resolveBin();
|
|
2406
|
+
} catch (error) {
|
|
2407
|
+
return { kind: "reject", reason: error instanceof Error ? error.message : String(error), retryable: true };
|
|
2408
|
+
}
|
|
2409
|
+
let approvalMcpBin;
|
|
2410
|
+
if (mapping.needsApprovalMcp) {
|
|
2411
|
+
try {
|
|
2412
|
+
approvalMcpBin = (this.options.resolveApprovalMcpBin ?? resolveApprovalMcpBin)();
|
|
2413
|
+
} catch (error) {
|
|
2414
|
+
return { kind: "reject", reason: error instanceof Error ? error.message : String(error), retryable: true };
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
1799
2417
|
return {
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
permissionModes: ["auto", "readonly", "plan", "confirm"]
|
|
2418
|
+
kind: "prepared",
|
|
2419
|
+
operation: {
|
|
2420
|
+
start: (startInput) => this.startPrepared(startInput, mapping, modelId, bin, approvalMcpBin)
|
|
2421
|
+
}
|
|
1805
2422
|
};
|
|
1806
2423
|
}
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
return { credentialNames: [] };
|
|
1822
|
-
}
|
|
1823
|
-
async start(task, ctx) {
|
|
1824
|
-
if (typeof task.instruction !== "string") {
|
|
1825
|
-
throw new PolicyUnsupportedError("claude adapter only supports string instructions in M2 (no blob-ref fetch yet)");
|
|
1826
|
-
}
|
|
1827
|
-
const mapping = mapPermissionPolicyToClaudeArgs(ctx.policy);
|
|
1828
|
-
if (!mapping.ok) {
|
|
1829
|
-
throw new PolicyUnsupportedError(mapping.reason ?? "policy rejected by claude adapter");
|
|
2424
|
+
async startPrepared(startInput, initialMapping, modelId, bin, approvalMcpBin) {
|
|
2425
|
+
if (!initialMapping.ok) throw new RuntimeExecutionFailure({
|
|
2426
|
+
phase: "start",
|
|
2427
|
+
category: "authority",
|
|
2428
|
+
retry: "non-retryable",
|
|
2429
|
+
reason: "prepared claude permission mapping was invalid"
|
|
2430
|
+
});
|
|
2431
|
+
if (typeof startInput.instruction !== "string") {
|
|
2432
|
+
throw new RuntimeExecutionFailure({
|
|
2433
|
+
phase: "start",
|
|
2434
|
+
category: "authority",
|
|
2435
|
+
retry: "non-retryable",
|
|
2436
|
+
reason: "prepared claude operation requires a resolved string instruction"
|
|
2437
|
+
});
|
|
1830
2438
|
}
|
|
1831
|
-
const
|
|
2439
|
+
const mapping = { ...initialMapping, args: [...initialMapping.args] };
|
|
1832
2440
|
let mcpConfigDir;
|
|
1833
|
-
const taskMcpServers =
|
|
2441
|
+
const taskMcpServers = startInput.mcpServers ?? {};
|
|
1834
2442
|
const needsMcpConfig = mapping.needsApprovalMcp || Object.keys(taskMcpServers).length > 0;
|
|
1835
2443
|
if (mapping.needsApprovalMcp) {
|
|
1836
|
-
if (!
|
|
1837
|
-
throw new
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
`MCP server name "${APPROVAL_MCP_SERVER_NAME}" is reserved by the claude approval channel`
|
|
1844
|
-
);
|
|
2444
|
+
if (!startInput.approvalChannel) {
|
|
2445
|
+
throw new RuntimeExecutionFailure({
|
|
2446
|
+
phase: "start",
|
|
2447
|
+
category: "authority",
|
|
2448
|
+
retry: "non-retryable",
|
|
2449
|
+
reason: 'claude adapter requires policy.mode "confirm" to be started with an approval channel'
|
|
2450
|
+
});
|
|
1845
2451
|
}
|
|
2452
|
+
if (!approvalMcpBin) throw new RuntimeExecutionFailure({
|
|
2453
|
+
phase: "start",
|
|
2454
|
+
category: "authority",
|
|
2455
|
+
retry: "non-retryable",
|
|
2456
|
+
reason: "prepared claude approval MCP binary was not resolved"
|
|
2457
|
+
});
|
|
1846
2458
|
}
|
|
1847
2459
|
if (needsMcpConfig) {
|
|
1848
2460
|
mcpConfigDir = await promises.mkdtemp(path20.join(os.tmpdir(), "byok-mcp-"));
|
|
@@ -1851,12 +2463,23 @@ var ClaudeAdapter = class {
|
|
|
1851
2463
|
const mcpConfigPath = path20.join(mcpConfigDir, "mcp-config.json");
|
|
1852
2464
|
const mcpServers = { ...taskMcpServers };
|
|
1853
2465
|
if (mapping.needsApprovalMcp) {
|
|
1854
|
-
const approvalChannel =
|
|
1855
|
-
if (!approvalChannel) throw new
|
|
1856
|
-
|
|
2466
|
+
const approvalChannel = startInput.approvalChannel;
|
|
2467
|
+
if (!approvalChannel) throw new RuntimeExecutionFailure({
|
|
2468
|
+
phase: "start",
|
|
2469
|
+
category: "authority",
|
|
2470
|
+
retry: "non-retryable",
|
|
2471
|
+
reason: "prepared claude approval channel was not available"
|
|
2472
|
+
});
|
|
2473
|
+
const preparedApprovalMcpBin = approvalMcpBin;
|
|
2474
|
+
if (!preparedApprovalMcpBin) throw new RuntimeExecutionFailure({
|
|
2475
|
+
phase: "start",
|
|
2476
|
+
category: "authority",
|
|
2477
|
+
retry: "non-retryable",
|
|
2478
|
+
reason: "prepared claude approval MCP binary was not resolved"
|
|
2479
|
+
});
|
|
1857
2480
|
mcpServers[APPROVAL_MCP_SERVER_NAME] = {
|
|
1858
|
-
command:
|
|
1859
|
-
args:
|
|
2481
|
+
command: preparedApprovalMcpBin.command,
|
|
2482
|
+
args: preparedApprovalMcpBin.args,
|
|
1860
2483
|
env: {
|
|
1861
2484
|
BYOK_STORE_DIR: approvalChannel.storeDir,
|
|
1862
2485
|
BYOK_PRODUCT_ID: approvalChannel.productId,
|
|
@@ -1876,8 +2499,26 @@ var ClaudeAdapter = class {
|
|
|
1876
2499
|
"--strict-mcp-config"
|
|
1877
2500
|
];
|
|
1878
2501
|
}
|
|
1879
|
-
const
|
|
1880
|
-
|
|
2502
|
+
const resumeSessionId = startInput.manifest.sessionRef;
|
|
2503
|
+
let manifestModelId;
|
|
2504
|
+
try {
|
|
2505
|
+
manifestModelId = subscriptionModel(startInput.manifest.dispatchSelection, "claude");
|
|
2506
|
+
} catch (cause) {
|
|
2507
|
+
throw new RuntimeExecutionFailure({
|
|
2508
|
+
phase: "start",
|
|
2509
|
+
category: "authority",
|
|
2510
|
+
retry: "non-retryable",
|
|
2511
|
+
reason: "prepared claude operation received an invalid runtime selection manifest"
|
|
2512
|
+
}, { cause });
|
|
2513
|
+
}
|
|
2514
|
+
if (manifestModelId !== modelId) {
|
|
2515
|
+
throw new RuntimeExecutionFailure({
|
|
2516
|
+
phase: "start",
|
|
2517
|
+
category: "authority",
|
|
2518
|
+
retry: "non-retryable",
|
|
2519
|
+
reason: "prepared claude operation received a manifest with different runtime selection"
|
|
2520
|
+
});
|
|
2521
|
+
}
|
|
1881
2522
|
const args = [
|
|
1882
2523
|
"-p",
|
|
1883
2524
|
"--input-format",
|
|
@@ -1889,40 +2530,71 @@ var ClaudeAdapter = class {
|
|
|
1889
2530
|
// "Error: When using --print, --output-format=stream-json requires
|
|
1890
2531
|
// --verbose", before spawning any model call.
|
|
1891
2532
|
"--verbose",
|
|
1892
|
-
...
|
|
2533
|
+
...manifestModelId ? ["--model", manifestModelId] : [],
|
|
1893
2534
|
...resumeSessionId ? ["--resume", resumeSessionId] : [],
|
|
1894
2535
|
...mapping.args
|
|
1895
2536
|
];
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
2537
|
+
let client;
|
|
2538
|
+
try {
|
|
2539
|
+
client = new ClaudeProcessClient({
|
|
2540
|
+
command: bin.command,
|
|
2541
|
+
args,
|
|
2542
|
+
cwd: startInput.manifest.workspace.workspaceDir,
|
|
2543
|
+
env: withoutProviderCredentials(startInput.env),
|
|
2544
|
+
spawnFn: this.options.spawnFn
|
|
2545
|
+
});
|
|
2546
|
+
} catch (cause) {
|
|
2547
|
+
await cleanupMcpConfigDir(mcpConfigDir);
|
|
2548
|
+
throw new RuntimeExecutionFailure({
|
|
2549
|
+
phase: "start",
|
|
2550
|
+
category: "infrastructure",
|
|
2551
|
+
retry: "retryable",
|
|
2552
|
+
reason: "claude runtime process could not be spawned"
|
|
2553
|
+
}, { cause });
|
|
2554
|
+
}
|
|
2555
|
+
try {
|
|
2556
|
+
client.writeUserMessage(startInput.instruction);
|
|
2557
|
+
} catch (cause) {
|
|
2558
|
+
client.kill();
|
|
2559
|
+
await cleanupMcpConfigDir(mcpConfigDir);
|
|
2560
|
+
throw new RuntimeExecutionFailure({
|
|
2561
|
+
phase: "start",
|
|
2562
|
+
category: "infrastructure",
|
|
2563
|
+
retry: "retryable",
|
|
2564
|
+
reason: "claude initial instruction transport failed"
|
|
2565
|
+
}, { cause });
|
|
2566
|
+
}
|
|
1904
2567
|
let sessionRef;
|
|
1905
2568
|
try {
|
|
1906
2569
|
sessionRef = await client.waitForInit();
|
|
1907
2570
|
} catch (err) {
|
|
1908
2571
|
client.kill();
|
|
1909
2572
|
await cleanupMcpConfigDir(mcpConfigDir);
|
|
1910
|
-
throw err;
|
|
2573
|
+
if (isRuntimeExecutionFailure(err)) throw err;
|
|
2574
|
+
throw new RuntimeExecutionFailure({
|
|
2575
|
+
phase: "start",
|
|
2576
|
+
category: "infrastructure",
|
|
2577
|
+
retry: "retryable",
|
|
2578
|
+
reason: `claude exited before yielding an authoritative session id: ${errorMessage2(err)}`
|
|
2579
|
+
}, { cause: err });
|
|
1911
2580
|
}
|
|
1912
2581
|
if (resumeSessionId !== void 0 && sessionRef !== resumeSessionId) {
|
|
1913
2582
|
client.kill();
|
|
1914
2583
|
await cleanupMcpConfigDir(mcpConfigDir);
|
|
1915
|
-
throw new
|
|
1916
|
-
|
|
1917
|
-
|
|
2584
|
+
throw new RuntimeExecutionFailure({
|
|
2585
|
+
phase: "start",
|
|
2586
|
+
category: "authority",
|
|
2587
|
+
retry: "non-retryable",
|
|
2588
|
+
reason: `claude --resume echoed a different session id than requested (requested ${resumeSessionId}, got ${sessionRef})`
|
|
2589
|
+
});
|
|
1918
2590
|
}
|
|
1919
2591
|
return new ClaudeSession(
|
|
1920
2592
|
sessionRef,
|
|
1921
2593
|
client,
|
|
1922
|
-
|
|
1923
|
-
|
|
2594
|
+
startInput.manifest.workspace.workspaceDir,
|
|
2595
|
+
startInput.approvalChannel,
|
|
1924
2596
|
mcpConfigDir,
|
|
1925
|
-
|
|
2597
|
+
manifestModelId
|
|
1926
2598
|
);
|
|
1927
2599
|
}
|
|
1928
2600
|
/**
|
|
@@ -1957,8 +2629,7 @@ var ClaudeAdapter = class {
|
|
|
1957
2629
|
return (this.options.resolveBin ?? resolveClaudeBin)();
|
|
1958
2630
|
}
|
|
1959
2631
|
};
|
|
1960
|
-
function subscriptionModel(
|
|
1961
|
-
const selection = task.dispatchSelection;
|
|
2632
|
+
function subscriptionModel(selection, runtimeId) {
|
|
1962
2633
|
if (selection === void 0) return void 0;
|
|
1963
2634
|
if (selection.lane !== "subscription" || selection.runtimeId !== runtimeId) {
|
|
1964
2635
|
throw new PolicyUnsupportedError(
|
|
@@ -1983,6 +2654,7 @@ var ClaudeSession = class {
|
|
|
1983
2654
|
mcpConfigDir;
|
|
1984
2655
|
modelId;
|
|
1985
2656
|
correlation = createToolUseCorrelation();
|
|
2657
|
+
closeAttempt;
|
|
1986
2658
|
get events() {
|
|
1987
2659
|
const client = this.client;
|
|
1988
2660
|
const correlation = this.correlation;
|
|
@@ -1991,17 +2663,40 @@ var ClaudeSession = class {
|
|
|
1991
2663
|
[Symbol.asyncIterator]() {
|
|
1992
2664
|
const inner = client.events[Symbol.asyncIterator]();
|
|
1993
2665
|
let pending = [];
|
|
2666
|
+
let terminalFailure;
|
|
1994
2667
|
let turnSettled = false;
|
|
1995
2668
|
return {
|
|
1996
2669
|
async next() {
|
|
1997
2670
|
for (; ; ) {
|
|
1998
2671
|
const buffered = pending.shift();
|
|
1999
2672
|
if (buffered) return { value: buffered, done: false };
|
|
2000
|
-
if (turnSettled)
|
|
2001
|
-
|
|
2002
|
-
|
|
2673
|
+
if (turnSettled) {
|
|
2674
|
+
if (terminalFailure) throw terminalFailure;
|
|
2675
|
+
return { value: void 0, done: true };
|
|
2676
|
+
}
|
|
2677
|
+
let raw;
|
|
2678
|
+
try {
|
|
2679
|
+
raw = await inner.next();
|
|
2680
|
+
} catch (cause) {
|
|
2681
|
+
throw new RuntimeExecutionFailure({
|
|
2682
|
+
phase: "run",
|
|
2683
|
+
category: "infrastructure",
|
|
2684
|
+
retry: "retryable",
|
|
2685
|
+
reason: "claude runtime event transport failed"
|
|
2686
|
+
}, { cause });
|
|
2687
|
+
}
|
|
2688
|
+
const { value, done } = raw;
|
|
2689
|
+
if (done) {
|
|
2690
|
+
throw new RuntimeExecutionFailure({
|
|
2691
|
+
phase: "run",
|
|
2692
|
+
category: "infrastructure",
|
|
2693
|
+
retry: "retryable",
|
|
2694
|
+
reason: "claude runtime process ended before a terminal result frame"
|
|
2695
|
+
}, { cause: client.terminalError });
|
|
2696
|
+
}
|
|
2003
2697
|
if (value.type === "result") turnSettled = true;
|
|
2004
2698
|
const mapped = mapClaudeMessageToAgentEvents(value, correlation, { workspaceDir });
|
|
2699
|
+
terminalFailure = mapped.terminalFailure ?? terminalFailure;
|
|
2005
2700
|
if (mapped.unmappedLabel) {
|
|
2006
2701
|
client.recordUnmappedFrame(mapped.unmappedLabel);
|
|
2007
2702
|
}
|
|
@@ -2032,7 +2727,7 @@ var ClaudeSession = class {
|
|
|
2032
2727
|
if (typeof task.instruction !== "string") {
|
|
2033
2728
|
throw new PolicyUnsupportedError("claude adapter only supports string instructions in M2 (no blob-ref fetch yet)");
|
|
2034
2729
|
}
|
|
2035
|
-
const requestedModel = subscriptionModel(task, "claude");
|
|
2730
|
+
const requestedModel = subscriptionModel(task.dispatchSelection, "claude");
|
|
2036
2731
|
if (requestedModel !== void 0 && requestedModel !== this.modelId) {
|
|
2037
2732
|
throw new PolicyUnsupportedError(
|
|
2038
2733
|
`claude persistent session cannot change model from ${this.modelId ?? "(legacy default)"} to ${requestedModel}`
|
|
@@ -2056,8 +2751,17 @@ var ClaudeSession = class {
|
|
|
2056
2751
|
this.client.kill();
|
|
2057
2752
|
}
|
|
2058
2753
|
async close() {
|
|
2059
|
-
this.
|
|
2060
|
-
|
|
2754
|
+
if (!this.closeAttempt) {
|
|
2755
|
+
const attempt = (async () => {
|
|
2756
|
+
await this.client.dispose();
|
|
2757
|
+
await cleanupMcpConfigDir(this.mcpConfigDir);
|
|
2758
|
+
})();
|
|
2759
|
+
this.closeAttempt = attempt.catch((error) => {
|
|
2760
|
+
this.closeAttempt = void 0;
|
|
2761
|
+
throw error;
|
|
2762
|
+
});
|
|
2763
|
+
}
|
|
2764
|
+
await this.closeAttempt;
|
|
2061
2765
|
}
|
|
2062
2766
|
/**
|
|
2063
2767
|
* M4 Phase 3: routes into the out-of-band approval channel `start()`
|
|
@@ -2274,14 +2978,15 @@ var CodexProcessRunner = class {
|
|
|
2274
2978
|
exitSignal = null;
|
|
2275
2979
|
closedPromise;
|
|
2276
2980
|
resolveClosed;
|
|
2981
|
+
disposalAttempt;
|
|
2277
2982
|
constructor(options) {
|
|
2278
2983
|
this.onEvent = options.onEvent;
|
|
2279
2984
|
const spawnFn = options.spawnFn ?? spawn;
|
|
2280
|
-
this.child = spawnFn(options.command, options.args, {
|
|
2985
|
+
this.child = spawnFn(options.command, options.args, withOwnedProcessTree({
|
|
2281
2986
|
cwd: options.cwd,
|
|
2282
2987
|
env: options.env,
|
|
2283
2988
|
stdio: ["ignore", "pipe", "pipe"]
|
|
2284
|
-
});
|
|
2989
|
+
}));
|
|
2285
2990
|
this.closedPromise = new Promise((resolve) => {
|
|
2286
2991
|
this.resolveClosed = resolve;
|
|
2287
2992
|
});
|
|
@@ -2311,7 +3016,7 @@ var CodexProcessRunner = class {
|
|
|
2311
3016
|
return this.closed;
|
|
2312
3017
|
}
|
|
2313
3018
|
/**
|
|
2314
|
-
*
|
|
3019
|
+
* Immediate tree termination request. SIGTERM on POSIX: SIGINT was empirically confirmed
|
|
2315
3020
|
* to be silently ignored by `codex exec` (a real, direct test — a 60s
|
|
2316
3021
|
* shell `sleep` ran to full, unaffected completion despite SIGINT sent at
|
|
2317
3022
|
* t=4s) — a genuine, evidence-based correction to this task's own initial
|
|
@@ -2322,15 +3027,33 @@ var CodexProcessRunner = class {
|
|
|
2322
3027
|
* cleanly resumable afterward via `codex exec resume` (no corruption from
|
|
2323
3028
|
* killing mid-turn). `taskkill /T /F` on Windows, mirroring
|
|
2324
3029
|
* `../pi/rpc-client.ts`'s own cross-platform convention.
|
|
3030
|
+
*
|
|
3031
|
+
* Fire-and-forget by design: an interrupt must not block on a terminator,
|
|
3032
|
+
* and `dispose()` is the settlement receipt. A request that could not be
|
|
3033
|
+
* spawned is left unrecorded, so `dispose()` re-issues it and raises the
|
|
3034
|
+
* typed `stage:'signal'` failure — swallowing it here loses nothing.
|
|
2325
3035
|
*/
|
|
2326
3036
|
kill() {
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
this.
|
|
3037
|
+
void requestOwnedProcessTreeTermination(this.processTreeOptions()).catch(() => {
|
|
3038
|
+
});
|
|
3039
|
+
}
|
|
3040
|
+
dispose() {
|
|
3041
|
+
if (!this.disposalAttempt) {
|
|
3042
|
+
const attempt = disposeOwnedProcessTree(this.processTreeOptions());
|
|
3043
|
+
this.disposalAttempt = attempt.catch((error) => {
|
|
3044
|
+
this.disposalAttempt = void 0;
|
|
3045
|
+
throw error;
|
|
3046
|
+
});
|
|
2333
3047
|
}
|
|
3048
|
+
return this.disposalAttempt;
|
|
3049
|
+
}
|
|
3050
|
+
processTreeOptions() {
|
|
3051
|
+
return {
|
|
3052
|
+
child: this.child,
|
|
3053
|
+
waitClosed: () => this.closedPromise,
|
|
3054
|
+
isClosed: () => this.closed,
|
|
3055
|
+
label: "codex"
|
|
3056
|
+
};
|
|
2334
3057
|
}
|
|
2335
3058
|
/** 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. */
|
|
2336
3059
|
buildExitError(context) {
|
|
@@ -2381,8 +3104,17 @@ var CodexAdapter = class {
|
|
|
2381
3104
|
this.options = options;
|
|
2382
3105
|
}
|
|
2383
3106
|
options;
|
|
2384
|
-
|
|
2385
|
-
|
|
3107
|
+
descriptor = freezeRuntimeAdapterDescriptor({
|
|
3108
|
+
id: "codex",
|
|
3109
|
+
supportsDispatchSelection: true,
|
|
3110
|
+
capabilities: {
|
|
3111
|
+
steer: false,
|
|
3112
|
+
resume: true,
|
|
3113
|
+
approvalInteractive: false,
|
|
3114
|
+
permissionModes: ["auto", "readonly"]
|
|
3115
|
+
},
|
|
3116
|
+
environmentRequirements: { credentialNames: [] }
|
|
3117
|
+
});
|
|
2386
3118
|
async detect() {
|
|
2387
3119
|
const bin = this.resolveBin();
|
|
2388
3120
|
try {
|
|
@@ -2431,61 +3163,100 @@ ${result.stderr}`);
|
|
|
2431
3163
|
${withStreams.stderr ?? ""}`);
|
|
2432
3164
|
}
|
|
2433
3165
|
}
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
* API-key passthrough remains a separate, pending product decision. No
|
|
2443
|
-
* `baseNames` either: nothing in this adapter reads a codex-specific
|
|
2444
|
-
* config-discovery variable (e.g. `CODEX_HOME`) today.
|
|
2445
|
-
*/
|
|
2446
|
-
environmentRequirements() {
|
|
2447
|
-
return { credentialNames: [] };
|
|
2448
|
-
}
|
|
2449
|
-
async start(task, ctx) {
|
|
2450
|
-
if (typeof task.instruction !== "string") {
|
|
2451
|
-
throw new PolicyUnsupportedError("codex adapter only supports string instructions in M2 (no blob-ref fetch yet)");
|
|
3166
|
+
async prepare(input) {
|
|
3167
|
+
const mapping = mapPermissionPolicyToCodexArgs(input.policy);
|
|
3168
|
+
if (!mapping.ok) return { kind: "reject", reason: mapping.reason ?? "policy rejected by codex adapter", retryable: false };
|
|
3169
|
+
let modelId;
|
|
3170
|
+
try {
|
|
3171
|
+
modelId = subscriptionModel2(input.offer.dispatchSelection);
|
|
3172
|
+
} catch (error) {
|
|
3173
|
+
return { kind: "reject", reason: error instanceof Error ? error.message : String(error), retryable: false };
|
|
2452
3174
|
}
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
3175
|
+
let command;
|
|
3176
|
+
try {
|
|
3177
|
+
command = this.resolveBin().command;
|
|
3178
|
+
} catch (error) {
|
|
3179
|
+
return { kind: "reject", reason: error instanceof Error ? error.message : String(error), retryable: true };
|
|
3180
|
+
}
|
|
3181
|
+
return {
|
|
3182
|
+
kind: "prepared",
|
|
3183
|
+
operation: {
|
|
3184
|
+
start: (startInput) => this.startPrepared(startInput, mapping.args, modelId, command)
|
|
3185
|
+
}
|
|
3186
|
+
};
|
|
3187
|
+
}
|
|
3188
|
+
async startPrepared(startInput, policyArgs, modelId, command) {
|
|
3189
|
+
if (typeof startInput.instruction !== "string") {
|
|
3190
|
+
throw new RuntimeExecutionFailure({
|
|
3191
|
+
phase: "start",
|
|
3192
|
+
category: "authority",
|
|
3193
|
+
retry: "non-retryable",
|
|
3194
|
+
reason: "prepared codex operation requires a resolved string instruction"
|
|
3195
|
+
});
|
|
2456
3196
|
}
|
|
2457
|
-
const modelId = subscriptionModel2(task);
|
|
2458
|
-
const bin = this.resolveBin();
|
|
2459
3197
|
const queue = new AsyncQueue();
|
|
3198
|
+
const terminal = {};
|
|
2460
3199
|
const recordUnmapped = makeUnmappedFrameRecorder(/* @__PURE__ */ new Map());
|
|
2461
|
-
|
|
2462
|
-
|
|
3200
|
+
let workspaceDir;
|
|
3201
|
+
try {
|
|
3202
|
+
workspaceDir = await resolveRealWorkspaceDir(startInput.manifest.workspace.workspaceDir);
|
|
3203
|
+
} catch (cause) {
|
|
3204
|
+
throw new RuntimeExecutionFailure({
|
|
3205
|
+
phase: "start",
|
|
3206
|
+
category: "infrastructure",
|
|
3207
|
+
retry: "retryable",
|
|
3208
|
+
reason: "codex runtime workspace could not be resolved"
|
|
3209
|
+
}, { cause });
|
|
3210
|
+
}
|
|
3211
|
+
const runtimeEnv = withoutProviderCredentials(startInput.env);
|
|
3212
|
+
let manifestModelId;
|
|
3213
|
+
try {
|
|
3214
|
+
manifestModelId = subscriptionModel2(startInput.manifest.dispatchSelection);
|
|
3215
|
+
} catch (cause) {
|
|
3216
|
+
throw new RuntimeExecutionFailure({
|
|
3217
|
+
phase: "start",
|
|
3218
|
+
category: "authority",
|
|
3219
|
+
retry: "non-retryable",
|
|
3220
|
+
reason: "prepared codex operation received an invalid runtime selection manifest"
|
|
3221
|
+
}, { cause });
|
|
3222
|
+
}
|
|
3223
|
+
if (manifestModelId !== modelId) {
|
|
3224
|
+
throw new RuntimeExecutionFailure({
|
|
3225
|
+
phase: "start",
|
|
3226
|
+
category: "authority",
|
|
3227
|
+
retry: "non-retryable",
|
|
3228
|
+
reason: "prepared codex operation received a manifest with different runtime selection"
|
|
3229
|
+
});
|
|
3230
|
+
}
|
|
2463
3231
|
const { sessionRef, runner } = await runCodexTurn({
|
|
2464
|
-
command
|
|
2465
|
-
resumeRef:
|
|
2466
|
-
instruction:
|
|
2467
|
-
modelId,
|
|
2468
|
-
policyArgs:
|
|
2469
|
-
cwd:
|
|
3232
|
+
command,
|
|
3233
|
+
resumeRef: startInput.manifest.sessionRef,
|
|
3234
|
+
instruction: startInput.instruction,
|
|
3235
|
+
modelId: manifestModelId,
|
|
3236
|
+
policyArgs: [...policyArgs],
|
|
3237
|
+
cwd: startInput.manifest.workspace.workspaceDir,
|
|
2470
3238
|
env: runtimeEnv,
|
|
2471
3239
|
spawnFn: this.options.spawnFn,
|
|
2472
3240
|
workspaceDir,
|
|
2473
3241
|
queue,
|
|
2474
3242
|
recordUnmapped,
|
|
2475
|
-
expectedSessionRef:
|
|
2476
|
-
preparedGit:
|
|
3243
|
+
expectedSessionRef: startInput.manifest.sessionRef,
|
|
3244
|
+
preparedGit: startInput.manifest.workspace.workspaceId !== void 0,
|
|
3245
|
+
failurePhase: "start",
|
|
3246
|
+
terminal
|
|
2477
3247
|
});
|
|
2478
3248
|
return new CodexSession({
|
|
2479
3249
|
sessionRef,
|
|
2480
|
-
command
|
|
3250
|
+
command,
|
|
2481
3251
|
workspaceDir,
|
|
2482
|
-
env:
|
|
3252
|
+
env: startInput.env,
|
|
2483
3253
|
spawnFn: this.options.spawnFn,
|
|
2484
3254
|
queue,
|
|
2485
3255
|
recordUnmapped,
|
|
2486
3256
|
initialRunner: runner,
|
|
2487
|
-
preparedGit:
|
|
2488
|
-
modelId
|
|
3257
|
+
preparedGit: startInput.manifest.workspace.workspaceId !== void 0,
|
|
3258
|
+
modelId: manifestModelId,
|
|
3259
|
+
terminal
|
|
2489
3260
|
});
|
|
2490
3261
|
}
|
|
2491
3262
|
resolveBin() {
|
|
@@ -2533,37 +3304,69 @@ async function runCodexTurn(params) {
|
|
|
2533
3304
|
rejectFirstLine = reject;
|
|
2534
3305
|
});
|
|
2535
3306
|
let turnEnded = false;
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
if (
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
3307
|
+
let runner;
|
|
3308
|
+
try {
|
|
3309
|
+
runner = new CodexProcessRunner({
|
|
3310
|
+
command: params.command,
|
|
3311
|
+
args: argv,
|
|
3312
|
+
cwd: params.cwd,
|
|
3313
|
+
env: params.env,
|
|
3314
|
+
spawnFn: params.spawnFn,
|
|
3315
|
+
onEvent: (evt) => {
|
|
3316
|
+
if (!firstLineSettled) {
|
|
3317
|
+
firstLineSettled = true;
|
|
3318
|
+
if (evt.type === "thread.started" && typeof evt.thread_id === "string" && evt.thread_id.length > 0) {
|
|
3319
|
+
resolveFirstLine(evt.thread_id);
|
|
3320
|
+
} else {
|
|
3321
|
+
rejectFirstLine(
|
|
3322
|
+
new RuntimeExecutionFailure({
|
|
3323
|
+
phase: params.failurePhase,
|
|
3324
|
+
category: "authority",
|
|
3325
|
+
retry: "non-retryable",
|
|
3326
|
+
reason: `codex did not yield thread.started as its first event (got ${JSON.stringify(evt).slice(0, 200)})`
|
|
3327
|
+
})
|
|
3328
|
+
);
|
|
3329
|
+
}
|
|
3330
|
+
return;
|
|
3331
|
+
}
|
|
3332
|
+
const mapped = mapCodexEventToAgentEvents(evt, params.workspaceDir);
|
|
3333
|
+
for (const agentEvent of mapped) {
|
|
3334
|
+
if (agentEvent.type === "turn_end") turnEnded = true;
|
|
3335
|
+
params.queue.push(agentEvent);
|
|
3336
|
+
}
|
|
3337
|
+
if (evt.type === "turn.failed") {
|
|
3338
|
+
params.terminal.failure = new RuntimeExecutionFailure({
|
|
3339
|
+
phase: "run",
|
|
3340
|
+
category: "semantic",
|
|
3341
|
+
retry: "non-retryable",
|
|
3342
|
+
reason: "codex reported terminal task failure"
|
|
3343
|
+
});
|
|
3344
|
+
params.queue.end();
|
|
3345
|
+
}
|
|
3346
|
+
if (mapped.length === 0 && !isRoutineCodexEvent(evt)) {
|
|
3347
|
+
params.recordUnmapped(unmappedFrameKey(evt));
|
|
2551
3348
|
}
|
|
2552
|
-
return;
|
|
2553
|
-
}
|
|
2554
|
-
const mapped = mapCodexEventToAgentEvents(evt, params.workspaceDir);
|
|
2555
|
-
for (const agentEvent of mapped) {
|
|
2556
|
-
if (agentEvent.type === "turn_end") turnEnded = true;
|
|
2557
|
-
params.queue.push(agentEvent);
|
|
2558
|
-
}
|
|
2559
|
-
if (mapped.length === 0 && !isRoutineCodexEvent(evt)) {
|
|
2560
|
-
params.recordUnmapped(unmappedFrameKey(evt));
|
|
2561
3349
|
}
|
|
2562
|
-
}
|
|
2563
|
-
})
|
|
3350
|
+
});
|
|
3351
|
+
} catch (cause) {
|
|
3352
|
+
throw new RuntimeExecutionFailure({
|
|
3353
|
+
phase: params.failurePhase,
|
|
3354
|
+
category: "infrastructure",
|
|
3355
|
+
retry: "retryable",
|
|
3356
|
+
reason: "codex runtime process could not be spawned"
|
|
3357
|
+
}, { cause });
|
|
3358
|
+
}
|
|
3359
|
+
params.onRunnerCreated?.(runner);
|
|
2564
3360
|
void runner.waitClosed().then(() => {
|
|
2565
|
-
if (turnEnded) return;
|
|
2566
|
-
|
|
3361
|
+
if (turnEnded || params.terminal.failure) return;
|
|
3362
|
+
const cause = runner.buildExitError("codex exited without completing the turn");
|
|
3363
|
+
params.terminal.failure = new RuntimeExecutionFailure({
|
|
3364
|
+
phase: "run",
|
|
3365
|
+
category: "infrastructure",
|
|
3366
|
+
retry: "retryable",
|
|
3367
|
+
reason: cause.message
|
|
3368
|
+
}, { cause });
|
|
3369
|
+
params.queue.push({ type: "error", message: cause.message });
|
|
2567
3370
|
params.queue.end();
|
|
2568
3371
|
});
|
|
2569
3372
|
let sessionRef;
|
|
@@ -2587,7 +3390,13 @@ async function runCodexTurn(params) {
|
|
|
2587
3390
|
void runner.waitClosed().then(() => {
|
|
2588
3391
|
if (!settled) {
|
|
2589
3392
|
settled = true;
|
|
2590
|
-
|
|
3393
|
+
const cause = runner.buildExitError("codex exited before yielding an authoritative thread id");
|
|
3394
|
+
reject(new RuntimeExecutionFailure({
|
|
3395
|
+
phase: params.failurePhase,
|
|
3396
|
+
category: "infrastructure",
|
|
3397
|
+
retry: "retryable",
|
|
3398
|
+
reason: cause.message
|
|
3399
|
+
}, { cause }));
|
|
2591
3400
|
}
|
|
2592
3401
|
});
|
|
2593
3402
|
});
|
|
@@ -2597,9 +3406,12 @@ async function runCodexTurn(params) {
|
|
|
2597
3406
|
}
|
|
2598
3407
|
if (params.expectedSessionRef !== void 0 && sessionRef !== params.expectedSessionRef) {
|
|
2599
3408
|
runner.kill();
|
|
2600
|
-
throw new
|
|
2601
|
-
|
|
2602
|
-
|
|
3409
|
+
throw new RuntimeExecutionFailure({
|
|
3410
|
+
phase: params.failurePhase,
|
|
3411
|
+
category: "authority",
|
|
3412
|
+
retry: "non-retryable",
|
|
3413
|
+
reason: `codex exec resume echoed a different thread id than requested (requested ${params.expectedSessionRef}, got ${sessionRef})`
|
|
3414
|
+
});
|
|
2603
3415
|
}
|
|
2604
3416
|
return { sessionRef, runner };
|
|
2605
3417
|
}
|
|
@@ -2626,8 +3438,12 @@ var CodexSession = class {
|
|
|
2626
3438
|
recordUnmapped;
|
|
2627
3439
|
preparedGit;
|
|
2628
3440
|
modelId;
|
|
3441
|
+
terminal;
|
|
2629
3442
|
currentRunner;
|
|
3443
|
+
ownedRunners = /* @__PURE__ */ new Set();
|
|
3444
|
+
followUpAttempts = /* @__PURE__ */ new Set();
|
|
2630
3445
|
closed = false;
|
|
3446
|
+
closeAttempt;
|
|
2631
3447
|
constructor(options) {
|
|
2632
3448
|
this.sessionRef = options.sessionRef;
|
|
2633
3449
|
this.command = options.command;
|
|
@@ -2638,11 +3454,36 @@ var CodexSession = class {
|
|
|
2638
3454
|
this.recordUnmapped = options.recordUnmapped;
|
|
2639
3455
|
this.preparedGit = options.preparedGit;
|
|
2640
3456
|
this.modelId = options.modelId;
|
|
3457
|
+
this.terminal = options.terminal;
|
|
2641
3458
|
this.currentRunner = options.initialRunner;
|
|
3459
|
+
this.ownedRunners.add(options.initialRunner);
|
|
2642
3460
|
void this.forgetRunnerOnceClosed(options.initialRunner);
|
|
2643
3461
|
}
|
|
2644
3462
|
get events() {
|
|
2645
|
-
|
|
3463
|
+
const queue = this.queue;
|
|
3464
|
+
const session = this;
|
|
3465
|
+
return {
|
|
3466
|
+
[Symbol.asyncIterator]() {
|
|
3467
|
+
const inner = queue[Symbol.asyncIterator]();
|
|
3468
|
+
return {
|
|
3469
|
+
async next() {
|
|
3470
|
+
let result;
|
|
3471
|
+
try {
|
|
3472
|
+
result = await inner.next();
|
|
3473
|
+
} catch (cause) {
|
|
3474
|
+
throw new RuntimeExecutionFailure({
|
|
3475
|
+
phase: "run",
|
|
3476
|
+
category: "infrastructure",
|
|
3477
|
+
retry: "retryable",
|
|
3478
|
+
reason: "codex runtime event transport failed"
|
|
3479
|
+
}, { cause });
|
|
3480
|
+
}
|
|
3481
|
+
if (result.done && session.terminal.failure) throw session.terminal.failure;
|
|
3482
|
+
return result;
|
|
3483
|
+
}
|
|
3484
|
+
};
|
|
3485
|
+
}
|
|
3486
|
+
};
|
|
2646
3487
|
}
|
|
2647
3488
|
async forgetRunnerOnceClosed(runner) {
|
|
2648
3489
|
await runner.waitClosed();
|
|
@@ -2687,7 +3528,14 @@ var CodexSession = class {
|
|
|
2687
3528
|
* stale id even after codex had moved on) — it just can now only ever be
|
|
2688
3529
|
* the SAME id this call asked to resume, never a silently-different one.
|
|
2689
3530
|
*/
|
|
2690
|
-
|
|
3531
|
+
followUp(task) {
|
|
3532
|
+
const attempt = this.runFollowUp(task);
|
|
3533
|
+
this.followUpAttempts.add(attempt);
|
|
3534
|
+
void attempt.finally(() => this.followUpAttempts.delete(attempt)).catch(() => {
|
|
3535
|
+
});
|
|
3536
|
+
return attempt;
|
|
3537
|
+
}
|
|
3538
|
+
async runFollowUp(task) {
|
|
2691
3539
|
if (typeof task.instruction !== "string") {
|
|
2692
3540
|
throw new PolicyUnsupportedError("codex adapter only supports string instructions in M2 (no blob-ref fetch yet)");
|
|
2693
3541
|
}
|
|
@@ -2698,7 +3546,7 @@ var CodexSession = class {
|
|
|
2698
3546
|
if (!mapping.ok) {
|
|
2699
3547
|
throw new PolicyUnsupportedError(mapping.reason ?? "policy rejected by codex adapter");
|
|
2700
3548
|
}
|
|
2701
|
-
const requestedModel = subscriptionModel2(task);
|
|
3549
|
+
const requestedModel = subscriptionModel2(task.dispatchSelection);
|
|
2702
3550
|
if (requestedModel !== void 0 && requestedModel !== this.modelId) {
|
|
2703
3551
|
throw new PolicyUnsupportedError(
|
|
2704
3552
|
`codex persistent session cannot change model from ${this.modelId ?? "(legacy default)"} to ${requestedModel}`
|
|
@@ -2708,6 +3556,7 @@ var CodexSession = class {
|
|
|
2708
3556
|
const resumeRef = this.sessionRef;
|
|
2709
3557
|
let sessionRef;
|
|
2710
3558
|
let runner;
|
|
3559
|
+
const terminal = {};
|
|
2711
3560
|
try {
|
|
2712
3561
|
({ sessionRef, runner } = await runCodexTurn({
|
|
2713
3562
|
command: this.command,
|
|
@@ -2722,25 +3571,54 @@ var CodexSession = class {
|
|
|
2722
3571
|
queue: this.queue,
|
|
2723
3572
|
recordUnmapped: this.recordUnmapped,
|
|
2724
3573
|
expectedSessionRef: resumeRef,
|
|
2725
|
-
preparedGit: this.preparedGit
|
|
3574
|
+
preparedGit: this.preparedGit,
|
|
3575
|
+
failurePhase: "run",
|
|
3576
|
+
terminal,
|
|
3577
|
+
onRunnerCreated: (created) => {
|
|
3578
|
+
this.ownedRunners.add(created);
|
|
3579
|
+
void this.forgetRunnerOnceClosed(created);
|
|
3580
|
+
}
|
|
2726
3581
|
}));
|
|
2727
3582
|
} catch (err) {
|
|
2728
3583
|
this.queue.end();
|
|
3584
|
+
this.terminal = {
|
|
3585
|
+
failure: isRuntimeExecutionFailure(err) ? err : new RuntimeExecutionFailure({
|
|
3586
|
+
phase: "run",
|
|
3587
|
+
category: "authority",
|
|
3588
|
+
retry: "non-retryable",
|
|
3589
|
+
reason: "codex follow-up violated the runtime adapter contract"
|
|
3590
|
+
}, { cause: err })
|
|
3591
|
+
};
|
|
2729
3592
|
throw err;
|
|
2730
3593
|
}
|
|
3594
|
+
if (this.closed) {
|
|
3595
|
+
await runner.dispose();
|
|
3596
|
+
throw new Error("codex session closed while follow-up was starting");
|
|
3597
|
+
}
|
|
3598
|
+
this.terminal = terminal;
|
|
2731
3599
|
this.sessionRef = sessionRef;
|
|
2732
3600
|
this.currentRunner = runner;
|
|
2733
|
-
void this.forgetRunnerOnceClosed(runner);
|
|
2734
3601
|
}
|
|
2735
3602
|
/** 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. */
|
|
2736
3603
|
async interrupt() {
|
|
2737
3604
|
this.currentRunner?.kill();
|
|
2738
3605
|
}
|
|
2739
3606
|
async close() {
|
|
2740
|
-
if (this.
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
3607
|
+
if (!this.closeAttempt) {
|
|
3608
|
+
this.closed = true;
|
|
3609
|
+
this.queue.end();
|
|
3610
|
+
const attempt = (async () => {
|
|
3611
|
+
const runners = [...this.ownedRunners];
|
|
3612
|
+
await Promise.all(runners.map((runner) => runner.dispose()));
|
|
3613
|
+
for (const runner of runners) this.ownedRunners.delete(runner);
|
|
3614
|
+
await Promise.allSettled([...this.followUpAttempts]);
|
|
3615
|
+
})();
|
|
3616
|
+
this.closeAttempt = attempt.catch((error) => {
|
|
3617
|
+
this.closeAttempt = void 0;
|
|
3618
|
+
throw error;
|
|
3619
|
+
});
|
|
3620
|
+
}
|
|
3621
|
+
await this.closeAttempt;
|
|
2744
3622
|
}
|
|
2745
3623
|
/**
|
|
2746
3624
|
* `codex exec` has no in-band channel to inject text into an already-
|
|
@@ -2776,8 +3654,7 @@ var CodexSession = class {
|
|
|
2776
3654
|
);
|
|
2777
3655
|
}
|
|
2778
3656
|
};
|
|
2779
|
-
function subscriptionModel2(
|
|
2780
|
-
const selection = task.dispatchSelection;
|
|
3657
|
+
function subscriptionModel2(selection) {
|
|
2781
3658
|
if (selection === void 0) return void 0;
|
|
2782
3659
|
if (selection.lane !== "subscription" || selection.runtimeId !== "codex") {
|
|
2783
3660
|
throw new PolicyUnsupportedError(
|
|
@@ -2842,7 +3719,7 @@ var ApprovalRegistry = class {
|
|
|
2842
3719
|
* `requestApproval` timeout and `finish()` fail-closed cleanup
|
|
2843
3720
|
* (`task-runner.ts`) — resolves a decision this device made on its own.
|
|
2844
3721
|
* The one exception, a server-sent wire `task.approve`/`task.reject`
|
|
2845
|
-
* relayed through `
|
|
3722
|
+
* relayed through `RuntimeOperationStartInput.approvalChannel.resolve`
|
|
2846
3723
|
* (`task-runner.ts`'s `handleOffer`), passes `'wire'` explicitly.
|
|
2847
3724
|
*/
|
|
2848
3725
|
resolve(approvalId, decision, reason, origin = "local") {
|
|
@@ -3473,7 +4350,10 @@ var PresencePublisher = class {
|
|
|
3473
4350
|
{
|
|
3474
4351
|
method: "PUT",
|
|
3475
4352
|
headers: { "content-type": "application/json" },
|
|
3476
|
-
body: JSON.stringify({
|
|
4353
|
+
body: JSON.stringify({
|
|
4354
|
+
level: "online",
|
|
4355
|
+
...this.opts.configuredToolsets === void 0 ? {} : { configuredToolsets: this.opts.configuredToolsets }
|
|
4356
|
+
})
|
|
3477
4357
|
},
|
|
3478
4358
|
this.opts.auth
|
|
3479
4359
|
);
|
|
@@ -3667,12 +4547,12 @@ var AnotherControlServerRunningError = class extends Error {
|
|
|
3667
4547
|
}
|
|
3668
4548
|
};
|
|
3669
4549
|
var MAX_HALF_OPEN_CONNECTIONS = 8;
|
|
3670
|
-
function
|
|
4550
|
+
function errorMessage3(err) {
|
|
3671
4551
|
return err instanceof Error ? err.message : String(err);
|
|
3672
4552
|
}
|
|
3673
4553
|
function toControlErrorShape(err) {
|
|
3674
4554
|
if (err instanceof ControlError) return { code: err.code, message: err.message };
|
|
3675
|
-
return { code: "internal_error", message:
|
|
4555
|
+
return { code: "internal_error", message: errorMessage3(err) };
|
|
3676
4556
|
}
|
|
3677
4557
|
function probeUnixSocketAlive(socketPath) {
|
|
3678
4558
|
return new Promise((resolve) => {
|
|
@@ -3879,12 +4759,19 @@ async function startControlServer(opts) {
|
|
|
3879
4759
|
}
|
|
3880
4760
|
throw err;
|
|
3881
4761
|
}
|
|
4762
|
+
let stopServingPromise;
|
|
4763
|
+
async function stopServing() {
|
|
4764
|
+
stopServingPromise ??= (async () => {
|
|
4765
|
+
for (const socket of sockets) socket.destroy();
|
|
4766
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
4767
|
+
})();
|
|
4768
|
+
await stopServingPromise;
|
|
4769
|
+
}
|
|
3882
4770
|
let closed = false;
|
|
3883
4771
|
async function close() {
|
|
3884
4772
|
if (closed) return;
|
|
3885
4773
|
closed = true;
|
|
3886
|
-
|
|
3887
|
-
await new Promise((resolve) => server.close(() => resolve()));
|
|
4774
|
+
await stopServing();
|
|
3888
4775
|
if (process.platform !== "win32") {
|
|
3889
4776
|
await promises.rm(endpoint, { force: true }).catch(() => {
|
|
3890
4777
|
});
|
|
@@ -3892,7 +4779,7 @@ async function startControlServer(opts) {
|
|
|
3892
4779
|
await promises.rm(tokenPath, { force: true }).catch(() => {
|
|
3893
4780
|
});
|
|
3894
4781
|
}
|
|
3895
|
-
return { endpoint, close };
|
|
4782
|
+
return { endpoint, stopServing, close };
|
|
3896
4783
|
}
|
|
3897
4784
|
function deterministicJitterMs(input) {
|
|
3898
4785
|
const ratio = input.ratio ?? 0.2;
|
|
@@ -3928,14 +4815,17 @@ function parseLooseEventsPollResponse(raw) {
|
|
|
3928
4815
|
if (typeof raw !== "object" || raw === null) {
|
|
3929
4816
|
throw new Error("events poll response is not an object");
|
|
3930
4817
|
}
|
|
3931
|
-
const { events, cursor } = raw;
|
|
4818
|
+
const { events, cursor, capabilities } = raw;
|
|
3932
4819
|
if (!Array.isArray(events)) {
|
|
3933
4820
|
throw new Error("events poll response.events is not an array");
|
|
3934
4821
|
}
|
|
3935
4822
|
if (typeof cursor !== "number" || !Number.isInteger(cursor)) {
|
|
3936
4823
|
throw new Error("events poll response.cursor is not an integer");
|
|
3937
4824
|
}
|
|
3938
|
-
|
|
4825
|
+
if (capabilities !== void 0 && (!Array.isArray(capabilities) || capabilities.some((flag) => typeof flag !== "string"))) {
|
|
4826
|
+
throw new Error("events poll response.capabilities is not an array of strings");
|
|
4827
|
+
}
|
|
4828
|
+
return { events, cursor, capabilities: capabilities ?? [] };
|
|
3939
4829
|
}
|
|
3940
4830
|
function extractSkippableSeq(raw) {
|
|
3941
4831
|
if (typeof raw !== "object" || raw === null) return void 0;
|
|
@@ -4015,12 +4905,14 @@ var LongPollClient = class {
|
|
|
4015
4905
|
if (cursor !== void 0) url.searchParams.set("cursor", String(cursor));
|
|
4016
4906
|
const res = await authedFetch(url, { method: "GET" }, this.opts.auth);
|
|
4017
4907
|
if (!res.ok) {
|
|
4908
|
+
this.opts.onServerCapabilities?.([]);
|
|
4018
4909
|
this.opts.onOperationalOutcome?.("failure");
|
|
4019
4910
|
const baseMs = this.opts.retryDelayMs ?? 2e3;
|
|
4020
4911
|
await sleep(this.opts.retryDelayForAttempt?.(retryAttempt++, baseMs) ?? baseMs);
|
|
4021
4912
|
continue;
|
|
4022
4913
|
}
|
|
4023
4914
|
const parsed = parseLooseEventsPollResponse(await res.json());
|
|
4915
|
+
this.opts.onServerCapabilities?.(parsed.capabilities);
|
|
4024
4916
|
let hadValidationFailureThisBatch = false;
|
|
4025
4917
|
for (const raw of parsed.events) {
|
|
4026
4918
|
let envelope;
|
|
@@ -4064,6 +4956,7 @@ var LongPollClient = class {
|
|
|
4064
4956
|
this.opts.onOperationalOutcome?.("success");
|
|
4065
4957
|
}
|
|
4066
4958
|
} catch (err) {
|
|
4959
|
+
this.opts.onServerCapabilities?.([]);
|
|
4067
4960
|
if (err instanceof DeviceRevokedError) {
|
|
4068
4961
|
this.running = false;
|
|
4069
4962
|
this.opts.onRevoked?.();
|
|
@@ -4186,6 +5079,7 @@ var WsTransport = class {
|
|
|
4186
5079
|
deviceId: this.opts.deviceId,
|
|
4187
5080
|
productId: this.opts.productId,
|
|
4188
5081
|
runtimes: this.opts.runtimes,
|
|
5082
|
+
configuredToolsets: this.opts.configuredToolsets === void 0 ? void 0 : [...this.opts.configuredToolsets],
|
|
4189
5083
|
cursor: this.opts.getCursor?.()
|
|
4190
5084
|
});
|
|
4191
5085
|
socket.send(encodeEnvelope(hello));
|
|
@@ -4275,6 +5169,7 @@ var ConnectionManager = class {
|
|
|
4275
5169
|
productId: opts.productId,
|
|
4276
5170
|
capabilities: opts.capabilities,
|
|
4277
5171
|
runtimes: opts.runtimes,
|
|
5172
|
+
configuredToolsets: opts.configuredToolsets,
|
|
4278
5173
|
getCursor: () => this.cursor,
|
|
4279
5174
|
onEnvelope: (envelope) => this.deliver(envelope),
|
|
4280
5175
|
onStateChange: (state) => {
|
|
@@ -4297,6 +5192,9 @@ var ConnectionManager = class {
|
|
|
4297
5192
|
// re-attempted.
|
|
4298
5193
|
getCursor: () => this.dedupWatermark(),
|
|
4299
5194
|
onEnvelope: (envelope) => this.deliver(envelope),
|
|
5195
|
+
onServerCapabilities: (capabilities) => {
|
|
5196
|
+
if (this.mode === "long-poll") this.serverCapabilities = capabilities;
|
|
5197
|
+
},
|
|
4300
5198
|
onRevoked: () => this.enterRevoked(),
|
|
4301
5199
|
// M4 Phase 4 (version-negotiation drill fix): a batch entry
|
|
4302
5200
|
// LongPollClient couldn't parse into a known Envelope at all (an
|
|
@@ -4397,7 +5295,7 @@ var ConnectionManager = class {
|
|
|
4397
5295
|
* already-delivered seqs (see its own doc comment) so the failed seq's own
|
|
4398
5296
|
* redelivery can get through — but that same frozen watermark also means
|
|
4399
5297
|
* every OTHER seq above it rides along on every re-poll too. Without this,
|
|
4400
|
-
* a seq already mid-flight (e.g. a `task.offer` whose
|
|
5298
|
+
* a seq already mid-flight (e.g. a `task.offer` whose prepared operation start()
|
|
4401
5299
|
* hasn't resolved yet) would be re-enqueued into `processingChain` on
|
|
4402
5300
|
* every such re-poll, piling up duplicate copies that — once the first
|
|
4403
5301
|
* finally resolves and the chain unwinds through them — run its handler
|
|
@@ -4428,25 +5326,24 @@ var ConnectionManager = class {
|
|
|
4428
5326
|
*/
|
|
4429
5327
|
cancelPendingDrainRetry;
|
|
4430
5328
|
/**
|
|
4431
|
-
* The capabilities the
|
|
4432
|
-
* `
|
|
4433
|
-
*
|
|
4434
|
-
*
|
|
4435
|
-
* successful handshake.
|
|
5329
|
+
* The capabilities the CURRENT transport's server advertised — untyped
|
|
5330
|
+
* `string[]` for forward compatibility. WS populates it from `conn.ack`;
|
|
5331
|
+
* long-poll populates it from each successful events response. Empty until
|
|
5332
|
+
* the active transport supplies an advertisement.
|
|
4436
5333
|
*
|
|
4437
5334
|
* Finding R2 (cross-model re-review — was P1): strictly PER-CONNECTION,
|
|
4438
5335
|
* not per-daemon-lifetime. Cleared to `[]` the instant the acked WS
|
|
4439
5336
|
* connection ends for ANY reason — an ordinary disconnect (`onWsOutcome`'s
|
|
4440
5337
|
* `acked` branch), `stop()`, or a transport switch to long-poll
|
|
4441
|
-
* (`enterLongPoll`) — and only
|
|
5338
|
+
* (`enterLongPoll`) — and only repopulated by a fresh advertisement from
|
|
5339
|
+
* the transport that is still current.
|
|
4442
5340
|
* The previous version of this doc comment claimed long-poll mode simply
|
|
4443
5341
|
* "stays at whatever the last real WS `conn.ack` said" — that was the bug:
|
|
4444
5342
|
* a daemon that once learned e.g. `approval_resolved` from an earlier WS
|
|
4445
5343
|
* session kept believing it applied to whatever it's connected to NOW,
|
|
4446
5344
|
* even after a disconnect/degrade where nothing has actually confirmed
|
|
4447
5345
|
* that's still true (a reconnect could land on a DIFFERENT server behind a
|
|
4448
|
-
* load balancer
|
|
4449
|
-
* handshake at all). Concretely, `TaskRunner.sendApprovalResolved` gates
|
|
5346
|
+
* load balancer). Concretely, `TaskRunner.sendApprovalResolved` gates
|
|
4450
5347
|
* `task.approval_resolved` on this list — sending it to a server that
|
|
4451
5348
|
* doesn't actually understand it over the long-poll path would get a
|
|
4452
5349
|
* batch-level 400 from `MessagesSendRequestSchema` (protocol §8.2), which
|
|
@@ -4566,13 +5463,10 @@ var ConnectionManager = class {
|
|
|
4566
5463
|
return this.mode === "long-poll";
|
|
4567
5464
|
}
|
|
4568
5465
|
/**
|
|
4569
|
-
* The capabilities the
|
|
4570
|
-
* `conn.ack`
|
|
4571
|
-
*
|
|
4572
|
-
*
|
|
4573
|
-
* first handshake completes, AND (finding R2) once again empty after any
|
|
4574
|
-
* disconnect/degrade — see `serverCapabilities`'s own doc comment for why
|
|
4575
|
-
* this is strictly per-connection rather than "sticky" across one.
|
|
5466
|
+
* The capabilities the CURRENT transport's server advertised: from
|
|
5467
|
+
* `conn.ack` on WS, or the latest successful `GET /byok/events` response
|
|
5468
|
+
* on long-poll. Empty before either transport has supplied its current
|
|
5469
|
+
* advertisement, and cleared across disconnect/switch boundaries.
|
|
4576
5470
|
*/
|
|
4577
5471
|
getServerCapabilities() {
|
|
4578
5472
|
return this.serverCapabilities;
|
|
@@ -5273,6 +6167,12 @@ var RECLAIM_FILENAME = `${DAEMON_OWNER_FILENAME}.reclaim`;
|
|
|
5273
6167
|
var MAX_OWNER_BYTES = 4096;
|
|
5274
6168
|
var RECLAIM_MALFORMED_GRACE_MS = 3e4;
|
|
5275
6169
|
var SELF_PROCESS_STARTED_AT = new Date(Date.now() - process.uptime() * 1e3).toISOString();
|
|
6170
|
+
function endOwnershipProbe(socket, response) {
|
|
6171
|
+
socket.on("error", () => {
|
|
6172
|
+
});
|
|
6173
|
+
if (response === void 0) socket.end();
|
|
6174
|
+
else socket.end(response);
|
|
6175
|
+
}
|
|
5276
6176
|
var STORE_MUTEX_ID_PREFIX = "byok-store-mutex-v1:";
|
|
5277
6177
|
var STORE_MUTEX_PROBE_TIMEOUT_MS = 1e3;
|
|
5278
6178
|
var STORE_MUTEX_SOCKET_FILENAME = "mutex.sock";
|
|
@@ -5379,7 +6279,7 @@ async function portIsBound(port) {
|
|
|
5379
6279
|
});
|
|
5380
6280
|
}
|
|
5381
6281
|
async function createLivenessListener() {
|
|
5382
|
-
const server = createServer((socket) => socket
|
|
6282
|
+
const server = createServer((socket) => endOwnershipProbe(socket));
|
|
5383
6283
|
const port = await new Promise((resolve, reject) => {
|
|
5384
6284
|
server.once("error", reject);
|
|
5385
6285
|
server.listen({ host: "127.0.0.1", port: 0, exclusive: true }, () => {
|
|
@@ -5464,7 +6364,7 @@ async function acquireStoreMutex(canonicalStoreDir) {
|
|
|
5464
6364
|
}
|
|
5465
6365
|
await clearStaleStoreMutexSocket(endpoint, identity);
|
|
5466
6366
|
}
|
|
5467
|
-
const server = createServer((socket) => socket
|
|
6367
|
+
const server = createServer((socket) => endOwnershipProbe(socket, `${STORE_MUTEX_ID_PREFIX}${identity}
|
|
5468
6368
|
`));
|
|
5469
6369
|
try {
|
|
5470
6370
|
await new Promise((resolve, reject) => {
|
|
@@ -5606,7 +6506,7 @@ function toRuntimeInfoCapabilities(caps) {
|
|
|
5606
6506
|
resume: caps.resume,
|
|
5607
6507
|
approvalInteractive: caps.approvalInteractive,
|
|
5608
6508
|
...caps.mcpToolsets === void 0 ? {} : { mcpToolsets: caps.mcpToolsets },
|
|
5609
|
-
permissionModes: caps.permissionModes
|
|
6509
|
+
permissionModes: [...caps.permissionModes]
|
|
5610
6510
|
};
|
|
5611
6511
|
}
|
|
5612
6512
|
var CursorStore = class {
|
|
@@ -5846,6 +6746,9 @@ var DaemonObserver = class {
|
|
|
5846
6746
|
noteGitWorkspace(event) {
|
|
5847
6747
|
this.emit({ kind: "git-workspace", ts: nowIso(), ...event });
|
|
5848
6748
|
}
|
|
6749
|
+
noteRuntimeDisposalFailure(event) {
|
|
6750
|
+
this.emit({ kind: "runtime-disposal-failed", ts: nowIso(), ...event });
|
|
6751
|
+
}
|
|
5849
6752
|
/**
|
|
5850
6753
|
* Finding F4: wired from `TaskRunnerDeps.onApprovalDispatched`, called
|
|
5851
6754
|
* synchronously by `TaskRunner.dispatchApproval` BEFORE its own
|
|
@@ -7499,20 +8402,20 @@ function isKnownRuntimeId(id) {
|
|
|
7499
8402
|
var DEFAULT_RUNTIME_PREFERENCE = ["claude", "codex", "pi"];
|
|
7500
8403
|
function orderByPreference(candidates, preference) {
|
|
7501
8404
|
const rank = new Map(preference.map((id, index) => [id, index]));
|
|
7502
|
-
return [...candidates].sort((a, b) => (rank.get(a.id) ?? preference.length) - (rank.get(b.id) ?? preference.length));
|
|
8405
|
+
return [...candidates].sort((a, b) => (rank.get(a.descriptor.id) ?? preference.length) - (rank.get(b.descriptor.id) ?? preference.length));
|
|
7503
8406
|
}
|
|
7504
|
-
function adapterSupportsMode(
|
|
7505
|
-
return
|
|
8407
|
+
function adapterSupportsMode(descriptor, mode) {
|
|
8408
|
+
return descriptor.capabilities.permissionModes.includes(mode);
|
|
7506
8409
|
}
|
|
7507
|
-
function adapterSupportsMcpToolsets(
|
|
7508
|
-
return
|
|
8410
|
+
function adapterSupportsMcpToolsets(descriptor) {
|
|
8411
|
+
return descriptor.capabilities.mcpToolsets === true;
|
|
7509
8412
|
}
|
|
7510
8413
|
function withoutRequiredToolsets(payload) {
|
|
7511
8414
|
if (!("requiredToolsets" in payload)) return payload;
|
|
7512
8415
|
const { requiredToolsets, ...offer } = payload;
|
|
7513
8416
|
return offer;
|
|
7514
8417
|
}
|
|
7515
|
-
function
|
|
8418
|
+
function errorMessage4(err) {
|
|
7516
8419
|
return err instanceof Error ? err.message : String(err);
|
|
7517
8420
|
}
|
|
7518
8421
|
function raceSettleFirst(fn, timeoutMs) {
|
|
@@ -7565,7 +8468,7 @@ async function openArtifact(workspaceDir, name) {
|
|
|
7565
8468
|
try {
|
|
7566
8469
|
handle = await promises.open(candidate, constants.O_RDONLY | O_NOFOLLOW);
|
|
7567
8470
|
} catch (err) {
|
|
7568
|
-
return { ok: false, reason: `artifact "${name}" could not be opened: ${
|
|
8471
|
+
return { ok: false, reason: `artifact "${name}" could not be opened: ${errorMessage4(err)}` };
|
|
7569
8472
|
}
|
|
7570
8473
|
try {
|
|
7571
8474
|
const st = await handle.stat();
|
|
@@ -7577,7 +8480,7 @@ async function openArtifact(workspaceDir, name) {
|
|
|
7577
8480
|
} catch (err) {
|
|
7578
8481
|
await handle.close().catch(() => {
|
|
7579
8482
|
});
|
|
7580
|
-
return { ok: false, reason: `artifact "${name}" could not be verified: ${
|
|
8483
|
+
return { ok: false, reason: `artifact "${name}" could not be verified: ${errorMessage4(err)}` };
|
|
7581
8484
|
}
|
|
7582
8485
|
return { ok: true, handle };
|
|
7583
8486
|
}
|
|
@@ -7591,13 +8494,13 @@ var TaskRunner = class {
|
|
|
7591
8494
|
* Finding F4 (cancel lost during the offer-processing window): a
|
|
7592
8495
|
* `task.cancel` for a taskId that hasn't finished `handleOffer` yet (still
|
|
7593
8496
|
* awaiting adapter detection / instruction resolution / workspace setup /
|
|
7594
|
-
* `
|
|
8497
|
+
* prepared operation `start()`) has no `this.tasks` entry to land on — it used to be
|
|
7595
8498
|
* silently dropped, and the runtime session `handleOffer` was about to
|
|
7596
8499
|
* register would then run an unsupervised ("zombie") turn nobody asked
|
|
7597
8500
|
* for anymore. Recording the taskId here lets `handleOffer` consult it at
|
|
7598
8501
|
* the two points where it can still safely react (see its body): before
|
|
7599
8502
|
* claiming at all (decline instead of ever starting a session), and right
|
|
7600
|
-
* after
|
|
8503
|
+
* after the prepared operation resolves but before this task is registered as
|
|
7601
8504
|
* active (tear the just-started session down immediately, before its
|
|
7602
8505
|
* event loop ever pumps a single event). Consumed (deleted) at whichever
|
|
7603
8506
|
* checkpoint handles it; a cancel for a taskId that's already active,
|
|
@@ -7623,12 +8526,12 @@ var TaskRunner = class {
|
|
|
7623
8526
|
* checkpoint-2 cancel-teardown, or successful registration into
|
|
7624
8527
|
* `this.tasks`). Bounded eviction on `pendingCancelled` (below) must never
|
|
7625
8528
|
* remove an entry for a taskId in this set: doing so is exactly the bug —
|
|
7626
|
-
* block task A in `
|
|
8529
|
+
* block task A in prepared-operation `start()`, deliver A's own `task.cancel` (so
|
|
7627
8530
|
* `pendingCancelled` gets an entry for A while A is still in-flight),
|
|
7628
8531
|
* then deliver `MAX_TRACKED_TASK_IDS` more cancels for unrelated taskIds
|
|
7629
8532
|
* nobody ever offered — under naive oldest-wins eviction, A's entry (the
|
|
7630
8533
|
* single oldest) gets evicted purely because of unrelated churn, so when
|
|
7631
|
-
*
|
|
8534
|
+
* the prepared operation finally resolves, checkpoint 2 finds no cancel marker
|
|
7632
8535
|
* and the already-cancelled task starts a real session. See
|
|
7633
8536
|
* `evictPendingCancelled` below for the fix, and
|
|
7634
8537
|
* `task-runner-bounded-collections.test.ts` for a test mirroring this
|
|
@@ -7648,7 +8551,7 @@ var TaskRunner = class {
|
|
|
7648
8551
|
* explicitly relies on redelivered handlers being idempotent for exactly
|
|
7649
8552
|
* this reason). `handleOffer` must treat a redelivered offer for a taskId
|
|
7650
8553
|
* that's already active (`this.tasks`) or already finished (this set) as
|
|
7651
|
-
* a no-op — never a second `
|
|
8554
|
+
* a no-op — never a second prepared-operation `start()` call, which would orphan the
|
|
7652
8555
|
* first session.
|
|
7653
8556
|
*
|
|
7654
8557
|
* M3-B: unbounded otherwise — a long-lived daemon that's finished many
|
|
@@ -7717,10 +8620,9 @@ var TaskRunner = class {
|
|
|
7717
8620
|
this.stoppingOffers = true;
|
|
7718
8621
|
}
|
|
7719
8622
|
/**
|
|
7720
|
-
*
|
|
7721
|
-
*
|
|
7722
|
-
*
|
|
7723
|
-
* terminal message is sent either way) but reports `task.fail` rather than
|
|
8623
|
+
* Shutdown of every currently ACTIVE task for the control socket's
|
|
8624
|
+
* `shutdown` RPC. Soft interrupt remains bounded, but each task's
|
|
8625
|
+
* authoritative close receipt must settle successfully. Reports `task.fail` rather than
|
|
7724
8626
|
* `task.cancelled` — these tasks aren't ending because the SERVER
|
|
7725
8627
|
* cancelled them, they're ending because this device is shutting down.
|
|
7726
8628
|
* `retryable: true` throughout: nothing about the task/policy itself was
|
|
@@ -7774,28 +8676,13 @@ var TaskRunner = class {
|
|
|
7774
8676
|
* unconditionally, so a hung `interrupt()` (a misbehaving adapter) can
|
|
7775
8677
|
* never block `task.fail` from being sent at all.
|
|
7776
8678
|
*
|
|
7777
|
-
*
|
|
7778
|
-
*
|
|
7779
|
-
*
|
|
7780
|
-
*
|
|
7781
|
-
* far more here than it used to for the pre-existing graceful-shutdown-only
|
|
7782
|
-
* caller, since THAT path is additionally bounded by an outer deadline
|
|
7783
|
-
* (`SHUTDOWN_TASK_TEARDOWN_DEADLINE_MS`/`DaemonConfig.shutdownGraceMs`,
|
|
7784
|
-
* `create-daemon.ts`), while resource-limit enforcement fires during
|
|
7785
|
-
* ordinary operation with no such outer bound watching it). `close()` is
|
|
7786
|
-
* every adapter's harder teardown primitive — an actual process-level kill
|
|
7787
|
-
* (SIGTERM, or `taskkill /F` on Windows — see e.g.
|
|
7788
|
-
* `ClaudeProcessClient.kill()`/`PiRpcClient.kill()`) as opposed to pi's own
|
|
7789
|
-
* soft in-band `interrupt()` (an RPC `abort` message that leaves the
|
|
7790
|
-
* process alive and resumable) — so escalating to it is the closest thing
|
|
7791
|
-
* to a "hard kill" the `Session` interface exposes. `finish()` below calls
|
|
7792
|
-
* `session.close()` again regardless (documented idempotent) — this isn't
|
|
7793
|
-
* a substitute for that, only an earlier, bounded attempt at actually
|
|
7794
|
-
* stopping a stuck runtime before this method gives up and reports failure
|
|
7795
|
-
* anyway.
|
|
8679
|
+
* After the bounded soft interrupt, `finish()` always awaits the authoritative
|
|
8680
|
+
* `Session.close()` receipt. A failed receipt retains active/Git ownership;
|
|
8681
|
+
* shutdown surfaces the rejection while resource enforcement leaves local
|
|
8682
|
+
* evidence for a later retry.
|
|
7796
8683
|
*
|
|
7797
8684
|
* Re-checks task identity (`this.tasks.get(...) === active`) immediately
|
|
7798
|
-
* before sending `task.fail`: the interrupt
|
|
8685
|
+
* before sending `task.fail`: the interrupt race above has await
|
|
7799
8686
|
* points during which a DIFFERENT path (a racing `task.cancel`/
|
|
7800
8687
|
* `task.reject`, or the session completing normally on its own) may have
|
|
7801
8688
|
* already finished this exact task and sent its own terminal message.
|
|
@@ -7804,20 +8691,25 @@ var TaskRunner = class {
|
|
|
7804
8691
|
* identity-check guard for the same class of race.
|
|
7805
8692
|
*/
|
|
7806
8693
|
async teardownActiveTask(active, reason, retryable) {
|
|
8694
|
+
if (active.finalizationStarted) return this.finish(active.taskId);
|
|
8695
|
+
if (!this.reserveSemanticTerminal(active)) return active.semanticTerminalSettled ?? false;
|
|
7807
8696
|
active.beingTornDown = true;
|
|
7808
8697
|
await this.observeGit(active, "salvage");
|
|
7809
8698
|
const timeoutMs = this.deps.shutdownInterruptTimeoutMs ?? DEFAULT_SHUTDOWN_INTERRUPT_TIMEOUT_MS;
|
|
7810
|
-
|
|
7811
|
-
if (
|
|
7812
|
-
await raceSettleFirst(() => active.session.close(), timeoutMs);
|
|
7813
|
-
}
|
|
7814
|
-
if (this.tasks.get(active.taskId) !== active) return;
|
|
8699
|
+
await raceSettleFirst(() => active.session.interrupt(), timeoutMs);
|
|
8700
|
+
if (this.tasks.get(active.taskId) !== active) return true;
|
|
7815
8701
|
this.deps.send(createEnvelope("task.fail", { reason, retryable }, { taskId: active.taskId }));
|
|
7816
|
-
|
|
8702
|
+
return this.finish(active.taskId);
|
|
7817
8703
|
}
|
|
7818
8704
|
/** 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. */
|
|
7819
8705
|
async shutdownTask(active, reason) {
|
|
7820
|
-
await this.teardownActiveTask(active, `daemon shutting down: ${reason}`, true);
|
|
8706
|
+
const disposed = await this.teardownActiveTask(active, `daemon shutting down: ${reason}`, true);
|
|
8707
|
+
if (!disposed) {
|
|
8708
|
+
throw new RuntimeDisposalFailure({
|
|
8709
|
+
stage: "quiescence",
|
|
8710
|
+
reason: `${active.adapter.descriptor.id} runtime ownership remains active after shutdown disposal failed`
|
|
8711
|
+
});
|
|
8712
|
+
}
|
|
7821
8713
|
}
|
|
7822
8714
|
/**
|
|
7823
8715
|
* M5 batch-3 (workstream 2): shared entry point for both resource-limit
|
|
@@ -7937,15 +8829,33 @@ var TaskRunner = class {
|
|
|
7937
8829
|
this.decline(taskId, resolvedMcp.reason, true);
|
|
7938
8830
|
return;
|
|
7939
8831
|
}
|
|
8832
|
+
const decision = computeEffectivePolicy(payload.policy, this.deps.permissionDefaults);
|
|
8833
|
+
if (!decision.ok) {
|
|
8834
|
+
this.decline(taskId, decision.reason ?? "policy rejected", false);
|
|
8835
|
+
return;
|
|
8836
|
+
}
|
|
8837
|
+
const offered = withoutRequiredToolsets(payload);
|
|
7940
8838
|
const requestedRuntime = payload.dispatchSelection?.runtimeId ?? payload.runtime;
|
|
7941
8839
|
const pick = await this.pickAdapter(requestedRuntime, payload.policy.mode, requiredToolsets !== void 0);
|
|
7942
8840
|
if (!pick.ok) {
|
|
7943
8841
|
this.decline(taskId, pick.reason, pick.retryable);
|
|
7944
8842
|
return;
|
|
7945
8843
|
}
|
|
7946
|
-
|
|
7947
|
-
|
|
7948
|
-
|
|
8844
|
+
let prepared;
|
|
8845
|
+
try {
|
|
8846
|
+
prepared = await pick.adapter.prepare({
|
|
8847
|
+
offer: offered,
|
|
8848
|
+
policy: decision.policy,
|
|
8849
|
+
descriptor: pick.descriptor,
|
|
8850
|
+
requiredToolsetIds: requiredToolsets ?? [],
|
|
8851
|
+
...resolvedMcp?.ok ? { mcpServers: resolvedMcp.servers } : {}
|
|
8852
|
+
});
|
|
8853
|
+
} catch (error) {
|
|
8854
|
+
this.decline(taskId, `runtime preparation failed: ${errorMessage4(error)}`, true);
|
|
8855
|
+
return;
|
|
8856
|
+
}
|
|
8857
|
+
if (prepared.kind === "reject") {
|
|
8858
|
+
this.decline(taskId, prepared.reason, prepared.retryable);
|
|
7949
8859
|
return;
|
|
7950
8860
|
}
|
|
7951
8861
|
let known = void 0;
|
|
@@ -7980,6 +8890,7 @@ var TaskRunner = class {
|
|
|
7980
8890
|
}
|
|
7981
8891
|
} else {
|
|
7982
8892
|
workspaceDir = path20.join(this.deps.workspaceRoot, taskId);
|
|
8893
|
+
gitWorkspaceId = randomUUID();
|
|
7983
8894
|
}
|
|
7984
8895
|
try {
|
|
7985
8896
|
gitLease = await gitManager.acquireLease(workspaceDir, payload.sessionRef);
|
|
@@ -7995,6 +8906,26 @@ var TaskRunner = class {
|
|
|
7995
8906
|
this.decline(taskId, "workspace mode is unavailable", true);
|
|
7996
8907
|
return;
|
|
7997
8908
|
}
|
|
8909
|
+
const env = buildRuntimeEnv({
|
|
8910
|
+
ambient: process.env,
|
|
8911
|
+
requirements: pick.descriptor.environmentRequirements,
|
|
8912
|
+
locallyAllowedNames: this.deps.runtimeEnvironment?.[pick.descriptor.id]?.allow
|
|
8913
|
+
});
|
|
8914
|
+
const manifest = sealRuntimeOperationManifest({
|
|
8915
|
+
taskId,
|
|
8916
|
+
runtimeId: pick.descriptor.id,
|
|
8917
|
+
descriptor: pick.descriptor,
|
|
8918
|
+
policy: decision.policy,
|
|
8919
|
+
requiredToolsetIds: requiredToolsets ?? [],
|
|
8920
|
+
...offered.dispatchSelection === void 0 ? {} : { dispatchSelection: offered.dispatchSelection },
|
|
8921
|
+
...known === void 0 || payload.sessionRef === void 0 ? {} : { sessionRef: payload.sessionRef },
|
|
8922
|
+
workspace: {
|
|
8923
|
+
workspaceDir,
|
|
8924
|
+
...gitWorkspaceId === void 0 ? {} : { workspaceId: gitWorkspaceId },
|
|
8925
|
+
...gitBaseline === void 0 ? {} : { baseline: gitBaseline }
|
|
8926
|
+
},
|
|
8927
|
+
forwardedEnvironmentNames: Object.freeze(Object.keys(env).sort())
|
|
8928
|
+
});
|
|
7998
8929
|
this.deps.send(
|
|
7999
8930
|
createEnvelope(
|
|
8000
8931
|
"task.claim",
|
|
@@ -8010,7 +8941,7 @@ var TaskRunner = class {
|
|
|
8010
8941
|
// (the merely REQUESTED runtime): this is what closes the gap
|
|
8011
8942
|
// where an auto-selected task left the server never learning
|
|
8012
8943
|
// which runtime actually ran.
|
|
8013
|
-
runtime: isKnownRuntimeId(
|
|
8944
|
+
runtime: isKnownRuntimeId(manifest.descriptor.id) ? manifest.descriptor.id : void 0,
|
|
8014
8945
|
// S0/D-4 (`task.claim.capabilities`, docs/protocol.md §2.4): the
|
|
8015
8946
|
// selected adapter's own capability self-report, carried on the
|
|
8016
8947
|
// same message that establishes the task↔runtime binding. The
|
|
@@ -8027,7 +8958,7 @@ var TaskRunner = class {
|
|
|
8027
8958
|
// Gating them would silently strip a custom steer-capable
|
|
8028
8959
|
// adapter's own truth and leave the server fail-closing on it
|
|
8029
8960
|
// forever.
|
|
8030
|
-
capabilities: toRuntimeInfoCapabilities(
|
|
8961
|
+
capabilities: toRuntimeInfoCapabilities(manifest.descriptor.capabilities)
|
|
8031
8962
|
},
|
|
8032
8963
|
{ taskId }
|
|
8033
8964
|
)
|
|
@@ -8038,7 +8969,7 @@ var TaskRunner = class {
|
|
|
8038
8969
|
if (plainWorkspaceNeedsResolve) workspaceDir = await this.resolveWorkspaceDir(taskId, known?.workspaceDir);
|
|
8039
8970
|
} catch (err) {
|
|
8040
8971
|
gitLease?.release();
|
|
8041
|
-
await this.fail(taskId, `failed to resolve instruction blob: ${
|
|
8972
|
+
await this.fail(taskId, `failed to resolve instruction blob: ${errorMessage4(err)}`, true);
|
|
8042
8973
|
return;
|
|
8043
8974
|
}
|
|
8044
8975
|
if (this.deps.gitWorkspaceManager && gitLease) {
|
|
@@ -8047,7 +8978,7 @@ var TaskRunner = class {
|
|
|
8047
8978
|
if (gitExisting) {
|
|
8048
8979
|
observation = await this.deps.gitWorkspaceManager.validateExisting(workspaceDir);
|
|
8049
8980
|
} else {
|
|
8050
|
-
const workspaceId2 = randomUUID();
|
|
8981
|
+
const workspaceId2 = gitWorkspaceId ?? randomUUID();
|
|
8051
8982
|
const now2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
8052
8983
|
gitWorkspaceId = workspaceId2;
|
|
8053
8984
|
await this.deps.gitWorkspaceStore?.upsert({
|
|
@@ -8098,30 +9029,11 @@ var TaskRunner = class {
|
|
|
8098
9029
|
return;
|
|
8099
9030
|
}
|
|
8100
9031
|
}
|
|
8101
|
-
const
|
|
8102
|
-
|
|
8103
|
-
|
|
9032
|
+
const startInput = {
|
|
9033
|
+
manifest,
|
|
9034
|
+
instruction: gitWorkspaceId ? prependGitWorkspaceGuidance(resolvedInstruction) : resolvedInstruction,
|
|
9035
|
+
env,
|
|
8104
9036
|
...resolvedMcp?.ok ? { mcpServers: resolvedMcp.servers } : {},
|
|
8105
|
-
...gitWorkspaceId ? { gitWorkspace: { workspaceId: gitWorkspaceId, baseline: gitBaseline } } : {},
|
|
8106
|
-
// M5: no longer `process.env` verbatim (see `environment.ts`'s own
|
|
8107
|
-
// module doc comment for the credential-leak gap that closed) —
|
|
8108
|
-
// built fresh per task from the SPECIFIC adapter `pickAdapter`
|
|
8109
|
-
// above already selected, so this always runs after adapter
|
|
8110
|
-
// selection: `pick.adapter.environmentRequirements?.()` (undefined
|
|
8111
|
-
// ⇒ platform baseline only, fail-closed) plus this device's own
|
|
8112
|
-
// `runtimeEnvironment` override, keyed by that same adapter's `id`.
|
|
8113
|
-
env: buildRuntimeEnv({
|
|
8114
|
-
ambient: process.env,
|
|
8115
|
-
requirements: pick.adapter.environmentRequirements?.(),
|
|
8116
|
-
locallyAllowedNames: this.deps.runtimeEnvironment?.[pick.adapter.id]?.allow
|
|
8117
|
-
}),
|
|
8118
|
-
// M4 Phase 3: adapter-agnostic and cheap to always populate — only an
|
|
8119
|
-
// adapter whose runtime genuinely supports an out-of-band approval
|
|
8120
|
-
// pause (claude, today) ever reads this. `resolve` is a closure over
|
|
8121
|
-
// `taskId` (not a pre-bound approvalId): it looks up whichever
|
|
8122
|
-
// approval is CURRENTLY pending for this task at call time, since one
|
|
8123
|
-
// task/session can face several approval requests, one at a time,
|
|
8124
|
-
// over its life. See `types.ts`'s `ApprovalChannel` doc comment.
|
|
8125
9037
|
approvalChannel: {
|
|
8126
9038
|
taskId,
|
|
8127
9039
|
storeDir: this.deps.storeDir,
|
|
@@ -8137,45 +9049,19 @@ var TaskRunner = class {
|
|
|
8137
9049
|
}
|
|
8138
9050
|
}
|
|
8139
9051
|
};
|
|
8140
|
-
const effectiveOffer = {
|
|
8141
|
-
...withoutRequiredToolsets(payload),
|
|
8142
|
-
instruction: gitWorkspaceId ? prependGitWorkspaceGuidance(resolvedInstruction) : resolvedInstruction,
|
|
8143
|
-
// Never forward a sessionRef this device has no recorded workspace
|
|
8144
|
-
// for (stale, from another device, or simply made up) — an adapter
|
|
8145
|
-
// that tries to resume an id it never minted fails outright (pi:
|
|
8146
|
-
// "No session found matching '<id>'", exit 1, empirically confirmed)
|
|
8147
|
-
// instead of silently starting fresh, so an unresolvable sessionRef
|
|
8148
|
-
// must look identical to "none supplied" by the time it reaches the
|
|
8149
|
-
// adapter, not get forwarded as a resume attempt doomed to fail.
|
|
8150
|
-
sessionRef: known ? payload.sessionRef : void 0
|
|
8151
|
-
};
|
|
8152
9052
|
let session;
|
|
8153
9053
|
try {
|
|
8154
|
-
session = await
|
|
9054
|
+
session = await prepared.operation.start(startInput);
|
|
8155
9055
|
} catch (err) {
|
|
8156
|
-
const
|
|
8157
|
-
|
|
8158
|
-
|
|
8159
|
-
await this.fail(taskId, `adapter failed to start: ${errorMessage3(err)}`, retryable);
|
|
8160
|
-
return;
|
|
8161
|
-
}
|
|
8162
|
-
if (this.pendingCancelled.has(taskId)) {
|
|
8163
|
-
const reason = this.pendingCancelled.get(taskId);
|
|
8164
|
-
this.pendingCancelled.delete(taskId);
|
|
8165
|
-
try {
|
|
8166
|
-
await session.interrupt();
|
|
8167
|
-
} catch {
|
|
8168
|
-
}
|
|
8169
|
-
try {
|
|
8170
|
-
await session.close();
|
|
8171
|
-
} catch {
|
|
9056
|
+
const failure = projectRuntimeBoundaryFailure(err, "start");
|
|
9057
|
+
if (failure.contractViolation) {
|
|
9058
|
+
console.error("[byok/client] runtime adapter start() returned an untyped failure", err);
|
|
8172
9059
|
}
|
|
9060
|
+
await this.updateGitPhaseBestEffort(gitWorkspaceId, "failed", "repository-invalid");
|
|
8173
9061
|
gitLease?.release();
|
|
8174
|
-
await this.
|
|
8175
|
-
this.deps.send(createEnvelope("task.cancelled", { reason }, { taskId }));
|
|
9062
|
+
await this.fail(taskId, failure.reason, failure.retryable);
|
|
8176
9063
|
return;
|
|
8177
9064
|
}
|
|
8178
|
-
this.deps.send(createEnvelope("task.started", {}, { taskId }));
|
|
8179
9065
|
const active = {
|
|
8180
9066
|
taskId,
|
|
8181
9067
|
adapter: pick.adapter,
|
|
@@ -8192,6 +9078,21 @@ var TaskRunner = class {
|
|
|
8192
9078
|
approvalQueue: [],
|
|
8193
9079
|
outputBytesSoFar: 0
|
|
8194
9080
|
};
|
|
9081
|
+
if (this.pendingCancelled.has(taskId)) {
|
|
9082
|
+
const reason = this.pendingCancelled.get(taskId);
|
|
9083
|
+
this.pendingCancelled.delete(taskId);
|
|
9084
|
+
this.tasks.set(taskId, active);
|
|
9085
|
+
this.reserveSemanticTerminal(active);
|
|
9086
|
+
try {
|
|
9087
|
+
await session.interrupt();
|
|
9088
|
+
} catch {
|
|
9089
|
+
}
|
|
9090
|
+
await this.updateGitPhaseBestEffort(gitWorkspaceId, "cancelled");
|
|
9091
|
+
this.deps.send(createEnvelope("task.cancelled", { reason }, { taskId }));
|
|
9092
|
+
await this.finish(taskId);
|
|
9093
|
+
return;
|
|
9094
|
+
}
|
|
9095
|
+
this.deps.send(createEnvelope("task.started", {}, { taskId }));
|
|
8195
9096
|
this.tasks.set(taskId, active);
|
|
8196
9097
|
if (payload.limits?.maxDurationMs !== void 0) {
|
|
8197
9098
|
this.armMaxDurationTimer(active, payload.limits.maxDurationMs);
|
|
@@ -8249,6 +9150,7 @@ var TaskRunner = class {
|
|
|
8249
9150
|
async pump(active) {
|
|
8250
9151
|
try {
|
|
8251
9152
|
for await (const event of active.session.events) {
|
|
9153
|
+
if (this.tasks.get(active.taskId) !== active || active.beingTornDown) return;
|
|
8252
9154
|
if (this.tasks.get(active.taskId) !== active) return;
|
|
8253
9155
|
active.outputBytesSoFar += estimateEventBytes(event);
|
|
8254
9156
|
if (active.outputBytesSoFar > this.maxTaskOutputBytes) {
|
|
@@ -8271,7 +9173,7 @@ var TaskRunner = class {
|
|
|
8271
9173
|
try {
|
|
8272
9174
|
await active.session.resolveApproval(approved, reason);
|
|
8273
9175
|
} catch (err) {
|
|
8274
|
-
await this.fail(taskId, `failed to resume session after approval decision: ${
|
|
9176
|
+
await this.fail(taskId, `failed to resume session after approval decision: ${errorMessage4(err)}`, false);
|
|
8275
9177
|
}
|
|
8276
9178
|
});
|
|
8277
9179
|
continue;
|
|
@@ -8283,6 +9185,7 @@ var TaskRunner = class {
|
|
|
8283
9185
|
const outcome = await this.resolveResultDocument(active, finalOutput);
|
|
8284
9186
|
if (!outcome.deliver) return;
|
|
8285
9187
|
await this.observeGit(active, "completed");
|
|
9188
|
+
if (this.tasks.get(active.taskId) !== active || active.beingTornDown) return;
|
|
8286
9189
|
if (outcome.document !== void 0 && !this.hasResultDocumentCapability()) {
|
|
8287
9190
|
await this.fail(
|
|
8288
9191
|
active.taskId,
|
|
@@ -8291,6 +9194,7 @@ var TaskRunner = class {
|
|
|
8291
9194
|
);
|
|
8292
9195
|
return;
|
|
8293
9196
|
}
|
|
9197
|
+
if (!this.reserveSemanticTerminal(active)) return;
|
|
8294
9198
|
this.deps.send(
|
|
8295
9199
|
createEnvelope(
|
|
8296
9200
|
"task.complete",
|
|
@@ -8326,11 +9230,17 @@ var TaskRunner = class {
|
|
|
8326
9230
|
active.batcher.push(event);
|
|
8327
9231
|
}
|
|
8328
9232
|
if (this.tasks.get(active.taskId) !== active || active.beingTornDown) return;
|
|
8329
|
-
|
|
9233
|
+
const failure = projectRuntimeBoundaryFailure(void 0, "run");
|
|
9234
|
+
console.error("[byok/client] runtime adapter events iterable ended without terminal authority");
|
|
9235
|
+
await this.fail(active.taskId, failure.reason, failure.retryable);
|
|
8330
9236
|
} catch (err) {
|
|
8331
9237
|
if (this.tasks.get(active.taskId) !== active || active.beingTornDown) return;
|
|
8332
9238
|
active.batcher.flush();
|
|
8333
|
-
|
|
9239
|
+
const failure = projectRuntimeBoundaryFailure(err, "run");
|
|
9240
|
+
if (failure.contractViolation) {
|
|
9241
|
+
console.error("[byok/client] runtime adapter events iterable returned an untyped failure", err);
|
|
9242
|
+
}
|
|
9243
|
+
await this.fail(active.taskId, failure.reason, failure.retryable);
|
|
8334
9244
|
}
|
|
8335
9245
|
}
|
|
8336
9246
|
/**
|
|
@@ -8369,7 +9279,7 @@ var TaskRunner = class {
|
|
|
8369
9279
|
try {
|
|
8370
9280
|
bytes = await opened.handle.readFile();
|
|
8371
9281
|
} catch (err) {
|
|
8372
|
-
this.reportArtifactError(active, name, `failed to read artifact "${name}": ${
|
|
9282
|
+
this.reportArtifactError(active, name, `failed to read artifact "${name}": ${errorMessage4(err)}`);
|
|
8373
9283
|
return;
|
|
8374
9284
|
} finally {
|
|
8375
9285
|
await opened.handle.close().catch(() => {
|
|
@@ -8386,7 +9296,7 @@ var TaskRunner = class {
|
|
|
8386
9296
|
const blobRef = await this.deps.blobClient.uploadArtifact(bytes, contentType);
|
|
8387
9297
|
this.deps.send(createEnvelope("task.artifact", { name, contentType, blobRef }, { taskId: active.taskId }));
|
|
8388
9298
|
} catch (err) {
|
|
8389
|
-
this.reportArtifactError(active, name, `failed to upload artifact "${name}": ${
|
|
9299
|
+
this.reportArtifactError(active, name, `failed to upload artifact "${name}": ${errorMessage4(err)}`);
|
|
8390
9300
|
}
|
|
8391
9301
|
}
|
|
8392
9302
|
/** 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. */
|
|
@@ -8400,6 +9310,14 @@ var TaskRunner = class {
|
|
|
8400
9310
|
this.setPendingCancelled(taskId, reason);
|
|
8401
9311
|
return;
|
|
8402
9312
|
}
|
|
9313
|
+
if (active.finalizationStarted) {
|
|
9314
|
+
await this.finish(taskId);
|
|
9315
|
+
return;
|
|
9316
|
+
}
|
|
9317
|
+
if (!this.reserveSemanticTerminal(active)) {
|
|
9318
|
+
await active.semanticTerminalSettled;
|
|
9319
|
+
return;
|
|
9320
|
+
}
|
|
8403
9321
|
try {
|
|
8404
9322
|
await active.session.interrupt();
|
|
8405
9323
|
} catch {
|
|
@@ -8428,7 +9346,7 @@ var TaskRunner = class {
|
|
|
8428
9346
|
*
|
|
8429
9347
|
* `inFlightOffers` is naturally tiny (bounded by this device's real
|
|
8430
9348
|
* concurrent-offer-processing count — normally single digits, driven by
|
|
8431
|
-
* how many `task.offer`s are simultaneously mid
|
|
9349
|
+
* how many `task.offer`s are simultaneously mid-prepared-operation start() — nowhere
|
|
8432
9350
|
* near `MAX_TRACKED_TASK_IDS`), so this scan is cheap in practice: it
|
|
8433
9351
|
* finds a safe entry at or near the front almost always. The only case
|
|
8434
9352
|
* where NO entry is safe to evict is every single tracked cancel
|
|
@@ -8781,7 +9699,7 @@ var TaskRunner = class {
|
|
|
8781
9699
|
this.deps.onStaleApprovalDecision?.(taskId, "approve");
|
|
8782
9700
|
return;
|
|
8783
9701
|
}
|
|
8784
|
-
await this.fail(taskId, `failed to resume session after approval: ${
|
|
9702
|
+
await this.fail(taskId, `failed to resume session after approval: ${errorMessage4(err)}`, false);
|
|
8785
9703
|
return;
|
|
8786
9704
|
}
|
|
8787
9705
|
this.clearPendingApproval(resolvedId, "approve", void 0);
|
|
@@ -8812,6 +9730,10 @@ var TaskRunner = class {
|
|
|
8812
9730
|
async handleReject(taskId, reason, approvalId) {
|
|
8813
9731
|
const active = this.tasks.get(taskId);
|
|
8814
9732
|
if (!active) return;
|
|
9733
|
+
if (active.finalizationStarted) {
|
|
9734
|
+
await this.finish(taskId);
|
|
9735
|
+
return;
|
|
9736
|
+
}
|
|
8815
9737
|
if (approvalId !== void 0 && approvalId !== active.pendingApprovalId) {
|
|
8816
9738
|
this.deps.onStaleApprovalDecision?.(
|
|
8817
9739
|
taskId,
|
|
@@ -8830,6 +9752,10 @@ var TaskRunner = class {
|
|
|
8830
9752
|
}
|
|
8831
9753
|
}
|
|
8832
9754
|
this.clearPendingApproval(resolvedId, "reject", reason);
|
|
9755
|
+
if (!this.reserveSemanticTerminal(active)) {
|
|
9756
|
+
await active.semanticTerminalSettled;
|
|
9757
|
+
return;
|
|
9758
|
+
}
|
|
8833
9759
|
try {
|
|
8834
9760
|
await active.session.interrupt();
|
|
8835
9761
|
} catch {
|
|
@@ -8844,6 +9770,14 @@ var TaskRunner = class {
|
|
|
8844
9770
|
}
|
|
8845
9771
|
async fail(taskId, reason, retryable) {
|
|
8846
9772
|
const active = this.tasks.get(taskId);
|
|
9773
|
+
if (active?.finalizationStarted) {
|
|
9774
|
+
await this.finish(taskId);
|
|
9775
|
+
return;
|
|
9776
|
+
}
|
|
9777
|
+
if (active && !this.reserveSemanticTerminal(active)) {
|
|
9778
|
+
await active.semanticTerminalSettled;
|
|
9779
|
+
return;
|
|
9780
|
+
}
|
|
8847
9781
|
if (active) await this.observeGit(active, "salvage");
|
|
8848
9782
|
this.deps.send(createEnvelope("task.fail", { reason, retryable }, { taskId }));
|
|
8849
9783
|
await this.finish(taskId);
|
|
@@ -8903,7 +9837,7 @@ var TaskRunner = class {
|
|
|
8903
9837
|
} catch (err) {
|
|
8904
9838
|
await this.fail(
|
|
8905
9839
|
active.taskId,
|
|
8906
|
-
`${RESULT_DOCUMENT_UNDELIVERABLE_REASON_PREFIX}: the configured resultDocument.extract threw: ${
|
|
9840
|
+
`${RESULT_DOCUMENT_UNDELIVERABLE_REASON_PREFIX}: the configured resultDocument.extract threw: ${errorMessage4(err)}`,
|
|
8907
9841
|
false
|
|
8908
9842
|
);
|
|
8909
9843
|
return { deliver: false };
|
|
@@ -8936,9 +9870,9 @@ var TaskRunner = class {
|
|
|
8936
9870
|
/**
|
|
8937
9871
|
* Whether the CURRENTLY connected server advertised `result-document` —
|
|
8938
9872
|
* read fresh on every call, never captured, because the answer changes
|
|
8939
|
-
* across a reconnect (`ConnectionManager
|
|
8940
|
-
*
|
|
8941
|
-
*
|
|
9873
|
+
* across a reconnect or transport switch (`ConnectionManager` clears the
|
|
9874
|
+
* old advertisement at the boundary, then repopulates it from a fresh WS
|
|
9875
|
+
* ack or successful poll response). An absent `getServerCapabilities` seam is
|
|
8942
9876
|
* "no capabilities", the fail-closed reading.
|
|
8943
9877
|
*/
|
|
8944
9878
|
hasResultDocumentCapability() {
|
|
@@ -8978,32 +9912,64 @@ var TaskRunner = class {
|
|
|
8978
9912
|
}
|
|
8979
9913
|
async finish(taskId) {
|
|
8980
9914
|
const active = this.tasks.get(taskId);
|
|
8981
|
-
if (!active) return;
|
|
8982
|
-
if (active.
|
|
8983
|
-
|
|
8984
|
-
active.
|
|
8985
|
-
|
|
8986
|
-
|
|
8987
|
-
|
|
8988
|
-
|
|
8989
|
-
|
|
8990
|
-
|
|
8991
|
-
|
|
8992
|
-
|
|
8993
|
-
|
|
8994
|
-
|
|
8995
|
-
|
|
8996
|
-
|
|
8997
|
-
|
|
8998
|
-
|
|
8999
|
-
|
|
9915
|
+
if (!active) return true;
|
|
9916
|
+
if (!active.finalizationStarted) {
|
|
9917
|
+
active.finalizationStarted = true;
|
|
9918
|
+
active.beingTornDown = true;
|
|
9919
|
+
if (active.maxDurationTimer) {
|
|
9920
|
+
clearTimeout(active.maxDurationTimer);
|
|
9921
|
+
active.maxDurationTimer = void 0;
|
|
9922
|
+
}
|
|
9923
|
+
active.batcher.stop();
|
|
9924
|
+
this.addFinishedTaskId(taskId);
|
|
9925
|
+
const queued = active.approvalQueue.splice(0);
|
|
9926
|
+
for (const request of queued) {
|
|
9927
|
+
request.resolve({
|
|
9928
|
+
approved: false,
|
|
9929
|
+
reason: `task ${taskId} finished before this queued approval request could be dispatched`
|
|
9930
|
+
});
|
|
9931
|
+
}
|
|
9932
|
+
if (active.pendingApprovalId !== void 0) {
|
|
9933
|
+
try {
|
|
9934
|
+
this.deps.approvalRegistry.resolve(active.pendingApprovalId, "reject", `task ${taskId} finished`);
|
|
9935
|
+
} catch {
|
|
9936
|
+
}
|
|
9000
9937
|
}
|
|
9001
9938
|
}
|
|
9002
|
-
|
|
9939
|
+
const attempt = active.disposalAttempt ?? active.session.close();
|
|
9940
|
+
active.disposalAttempt = attempt;
|
|
9003
9941
|
try {
|
|
9004
|
-
await
|
|
9005
|
-
} catch {
|
|
9942
|
+
await attempt;
|
|
9943
|
+
} catch (caught) {
|
|
9944
|
+
if (active.disposalAttempt === attempt) active.disposalAttempt = void 0;
|
|
9945
|
+
const failure = isRuntimeDisposalFailure(caught) ? caught : new RuntimeDisposalFailure({
|
|
9946
|
+
stage: "quiescence",
|
|
9947
|
+
reason: `${active.adapter.descriptor.id} session.close() returned an untyped disposal failure`
|
|
9948
|
+
}, { cause: caught });
|
|
9949
|
+
console.error(`[byok/client] runtime disposal failed for task ${taskId}: ${failure.message}`);
|
|
9950
|
+
this.deps.onRuntimeDisposalFailure?.({
|
|
9951
|
+
taskId,
|
|
9952
|
+
runtimeId: active.adapter.descriptor.id,
|
|
9953
|
+
stage: failure.stage,
|
|
9954
|
+
reason: failure.message
|
|
9955
|
+
});
|
|
9956
|
+
active.resolveSemanticTerminalSettled?.(false);
|
|
9957
|
+
return false;
|
|
9006
9958
|
}
|
|
9959
|
+
if (this.tasks.get(taskId) !== active) return true;
|
|
9960
|
+
active.gitLease?.release();
|
|
9961
|
+
this.tasks.delete(taskId);
|
|
9962
|
+
active.resolveSemanticTerminalSettled?.(true);
|
|
9963
|
+
return true;
|
|
9964
|
+
}
|
|
9965
|
+
reserveSemanticTerminal(active) {
|
|
9966
|
+
if (active.semanticTerminalReserved || active.finalizationStarted) return false;
|
|
9967
|
+
active.semanticTerminalReserved = true;
|
|
9968
|
+
active.beingTornDown = true;
|
|
9969
|
+
active.semanticTerminalSettled = new Promise((resolve) => {
|
|
9970
|
+
active.resolveSemanticTerminalSettled = resolve;
|
|
9971
|
+
});
|
|
9972
|
+
return true;
|
|
9007
9973
|
}
|
|
9008
9974
|
/** 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). */
|
|
9009
9975
|
addFinishedTaskId(taskId) {
|
|
@@ -9056,18 +10022,19 @@ var TaskRunner = class {
|
|
|
9056
10022
|
retryable: false
|
|
9057
10023
|
};
|
|
9058
10024
|
}
|
|
9059
|
-
const adapter = this.deps.adapters.find((a) => a.id === requestedRuntime);
|
|
10025
|
+
const adapter = this.deps.adapters.find((a) => a.descriptor.id === requestedRuntime);
|
|
9060
10026
|
if (!adapter) {
|
|
9061
10027
|
return { ok: false, reason: `unknown runtime "${requestedRuntime}"`, retryable: false };
|
|
9062
10028
|
}
|
|
9063
|
-
|
|
10029
|
+
const descriptor = freezeRuntimeAdapterDescriptor(adapter.descriptor);
|
|
10030
|
+
if (!adapterSupportsMode(descriptor, policyMode)) {
|
|
9064
10031
|
return {
|
|
9065
10032
|
ok: false,
|
|
9066
10033
|
reason: `runtime "${requestedRuntime}" cannot express permission mode "${policyMode}"`,
|
|
9067
10034
|
retryable: false
|
|
9068
10035
|
};
|
|
9069
10036
|
}
|
|
9070
|
-
if (requiresMcpToolsets && !adapterSupportsMcpToolsets(
|
|
10037
|
+
if (requiresMcpToolsets && !adapterSupportsMcpToolsets(descriptor)) {
|
|
9071
10038
|
return {
|
|
9072
10039
|
ok: false,
|
|
9073
10040
|
reason: `runtime "${requestedRuntime}" cannot project required MCP toolsets`,
|
|
@@ -9082,15 +10049,16 @@ var TaskRunner = class {
|
|
|
9082
10049
|
retryable: true
|
|
9083
10050
|
};
|
|
9084
10051
|
}
|
|
9085
|
-
return { ok: true, adapter };
|
|
10052
|
+
return { ok: true, adapter, descriptor };
|
|
9086
10053
|
}
|
|
9087
|
-
const eligible = allowlist ? this.deps.adapters.filter((a) => allowlist.includes(a.id)) : this.deps.adapters;
|
|
10054
|
+
const eligible = allowlist ? this.deps.adapters.filter((a) => allowlist.includes(a.descriptor.id)) : this.deps.adapters;
|
|
9088
10055
|
const candidates = orderByPreference(eligible, this.deps.runtimePreference ?? DEFAULT_RUNTIME_PREFERENCE);
|
|
9089
10056
|
for (const adapter of candidates) {
|
|
9090
|
-
|
|
9091
|
-
if (
|
|
10057
|
+
const descriptor = freezeRuntimeAdapterDescriptor(adapter.descriptor);
|
|
10058
|
+
if (!adapterSupportsMode(descriptor, policyMode)) continue;
|
|
10059
|
+
if (requiresMcpToolsets && !adapterSupportsMcpToolsets(descriptor)) continue;
|
|
9092
10060
|
const detected = await adapter.detect();
|
|
9093
|
-
if (detected.present) return { ok: true, adapter };
|
|
10061
|
+
if (detected.present) return { ok: true, adapter, descriptor };
|
|
9094
10062
|
}
|
|
9095
10063
|
return {
|
|
9096
10064
|
ok: false,
|
|
@@ -9135,27 +10103,27 @@ async function detectRuntimes(adapters) {
|
|
|
9135
10103
|
const detections = await Promise.all(adapters.map(async (adapter) => ({ adapter, detected: await adapter.detect() })));
|
|
9136
10104
|
const runtimes = [];
|
|
9137
10105
|
for (const { adapter, detected } of detections) {
|
|
9138
|
-
if (!detected.present || !isRuntimeId(adapter.id)) continue;
|
|
9139
|
-
const info = { id: adapter.id };
|
|
10106
|
+
if (!detected.present || !isRuntimeId(adapter.descriptor.id)) continue;
|
|
10107
|
+
const info = { id: adapter.descriptor.id };
|
|
9140
10108
|
if (detected.version !== void 0) info.version = detected.version;
|
|
9141
10109
|
if (detected.authPresent !== void 0) info.authPresent = detected.authPresent;
|
|
9142
|
-
info.capabilities = toRuntimeInfoCapabilities(adapter.capabilities
|
|
10110
|
+
info.capabilities = toRuntimeInfoCapabilities(adapter.descriptor.capabilities);
|
|
9143
10111
|
runtimes.push(info);
|
|
9144
10112
|
}
|
|
9145
10113
|
return runtimes;
|
|
9146
10114
|
}
|
|
9147
10115
|
function computeCapabilities(adapters) {
|
|
9148
10116
|
const flags = [];
|
|
9149
|
-
if (adapters.some((adapter) => adapter.capabilities
|
|
10117
|
+
if (adapters.some((adapter) => adapter.descriptor.capabilities.steer)) flags.push("steer");
|
|
9150
10118
|
flags.push("blob-upload");
|
|
9151
10119
|
flags.push("approval-targeting");
|
|
9152
10120
|
const selectionAdapters = adapters.filter(
|
|
9153
|
-
(adapter) => ALL_RUNTIME_IDS.includes(adapter.id)
|
|
10121
|
+
(adapter) => ALL_RUNTIME_IDS.includes(adapter.descriptor.id)
|
|
9154
10122
|
);
|
|
9155
|
-
if (selectionAdapters.length > 0 && selectionAdapters.every((adapter) => adapter.supportsDispatchSelection === true)) {
|
|
10123
|
+
if (selectionAdapters.length > 0 && selectionAdapters.every((adapter) => adapter.descriptor.supportsDispatchSelection === true)) {
|
|
9156
10124
|
flags.push("dispatch-selection");
|
|
9157
10125
|
}
|
|
9158
|
-
if (adapters.some((adapter) => adapter.capabilities
|
|
10126
|
+
if (adapters.some((adapter) => adapter.descriptor.capabilities.mcpToolsets === true)) {
|
|
9159
10127
|
flags.push("toolset-selection");
|
|
9160
10128
|
}
|
|
9161
10129
|
return flags;
|
|
@@ -9215,7 +10183,6 @@ function validatePiByokLauncherConfig(launcher) {
|
|
|
9215
10183
|
throw new Error("DaemonConfig.piByokLauncher.args must contain only non-empty single-line strings");
|
|
9216
10184
|
}
|
|
9217
10185
|
}
|
|
9218
|
-
var MAX_LOCAL_MCP_TOOLSETS = 64;
|
|
9219
10186
|
var MAX_LOCAL_MCP_SERVERS_PER_TOOLSET = 16;
|
|
9220
10187
|
var MAX_LOCAL_MCP_ARGS = 64;
|
|
9221
10188
|
var MAX_LOCAL_MCP_TOKEN_CHARS = 4096;
|
|
@@ -9228,8 +10195,10 @@ function resolveMcpToolsets(configured) {
|
|
|
9228
10195
|
throw new Error("DaemonConfig.mcpToolsets must be an object keyed by logical toolset id");
|
|
9229
10196
|
}
|
|
9230
10197
|
const toolsetEntries = Object.entries(configured);
|
|
9231
|
-
if (toolsetEntries.length >
|
|
9232
|
-
throw new Error(
|
|
10198
|
+
if (toolsetEntries.length > CONFIGURED_TOOLSETS_MAX_ITEMS) {
|
|
10199
|
+
throw new Error(
|
|
10200
|
+
`DaemonConfig.mcpToolsets may contain at most ${CONFIGURED_TOOLSETS_MAX_ITEMS} toolsets`
|
|
10201
|
+
);
|
|
9233
10202
|
}
|
|
9234
10203
|
const resolved = /* @__PURE__ */ new Map();
|
|
9235
10204
|
for (const [toolsetId, rawToolset] of toolsetEntries) {
|
|
@@ -9342,6 +10311,9 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
9342
10311
|
}
|
|
9343
10312
|
function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProbe) {
|
|
9344
10313
|
const mcpToolsets = resolveMcpToolsets(config.mcpToolsets);
|
|
10314
|
+
const configuredToolsets = Object.freeze(
|
|
10315
|
+
[...mcpToolsets?.keys() ?? []].sort()
|
|
10316
|
+
);
|
|
9345
10317
|
if (config.piByokLauncher !== void 0) {
|
|
9346
10318
|
validatePiByokLauncherConfig(config.piByokLauncher);
|
|
9347
10319
|
}
|
|
@@ -9648,12 +10620,13 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
9648
10620
|
dirty: event.observation ? { staged: event.observation.staged, unstaged: event.observation.unstaged, untracked: event.observation.untracked, conflicted: event.observation.conflicted } : void 0,
|
|
9649
10621
|
errorCategory: event.errorCategory
|
|
9650
10622
|
}),
|
|
10623
|
+
onRuntimeDisposalFailure: (event) => observer.noteRuntimeDisposalFailure(event),
|
|
9651
10624
|
// M4 Phase 3: the SAME `ApprovalRegistry` instance the control
|
|
9652
10625
|
// socket's own `approvals.list`/`approvals.resolve` methods already
|
|
9653
10626
|
// share (see that field's own construction above) — `TaskRunner
|
|
9654
10627
|
// .requestApproval` registers into it directly, so a decision arriving
|
|
9655
10628
|
// via either the server wire or the local CLI resolves the identical
|
|
9656
|
-
// entry. `storeDir`/`productId` let
|
|
10629
|
+
// entry. `storeDir`/`productId` let the prepared operation approval channel
|
|
9657
10630
|
// (populated per-task by `TaskRunner`) tell an out-of-process helper
|
|
9658
10631
|
// (`bin/byok-approval-mcp.ts`) exactly which control socket to dial.
|
|
9659
10632
|
approvalRegistry,
|
|
@@ -9705,6 +10678,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
9705
10678
|
productId: config.productId,
|
|
9706
10679
|
capabilities,
|
|
9707
10680
|
runtimes,
|
|
10681
|
+
configuredToolsets,
|
|
9708
10682
|
auth,
|
|
9709
10683
|
cursorStore,
|
|
9710
10684
|
// Finding F3: return (not void-and-forget) so ConnectionManager can
|
|
@@ -9791,6 +10765,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
9791
10765
|
presencePublisher ??= new PresencePublisher({
|
|
9792
10766
|
serverUrl: config.serverUrl,
|
|
9793
10767
|
auth,
|
|
10768
|
+
configuredToolsets,
|
|
9794
10769
|
...presenceCadence,
|
|
9795
10770
|
onDegraded: (reason) => console.warn(`[byok/client] ${reason}`)
|
|
9796
10771
|
});
|
|
@@ -9884,8 +10859,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
9884
10859
|
await attempt(() => auth.stop(), true);
|
|
9885
10860
|
connectionState = "closed";
|
|
9886
10861
|
await attempt(async () => {
|
|
9887
|
-
await controlServerHandle?.
|
|
9888
|
-
controlServerHandle = void 0;
|
|
10862
|
+
await controlServerHandle?.stopServing();
|
|
9889
10863
|
}, true);
|
|
9890
10864
|
if (mutationBarrierComplete && daemonOwnerLease) {
|
|
9891
10865
|
await attempt(() => operationalHealth.markCleanStop(), false);
|
|
@@ -9894,6 +10868,12 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
9894
10868
|
daemonOwnerLease = void 0;
|
|
9895
10869
|
}, false);
|
|
9896
10870
|
}
|
|
10871
|
+
if (daemonOwnerLease === void 0) {
|
|
10872
|
+
await attempt(async () => {
|
|
10873
|
+
await controlServerHandle?.close();
|
|
10874
|
+
controlServerHandle = void 0;
|
|
10875
|
+
}, false);
|
|
10876
|
+
}
|
|
9897
10877
|
if (errors.length === 1) throw errors[0];
|
|
9898
10878
|
if (errors.length > 1) {
|
|
9899
10879
|
throw new AggregateError(errors, "daemon shutdown completed with errors");
|
|
@@ -9973,7 +10953,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
9973
10953
|
deviceId: auth.deviceId,
|
|
9974
10954
|
transport: connectionState,
|
|
9975
10955
|
activeTasks,
|
|
9976
|
-
runtimeIds: adapters.map((adapter) => adapter.id),
|
|
10956
|
+
runtimeIds: adapters.map((adapter) => adapter.descriptor.id),
|
|
9977
10957
|
// M4 Phase 4 (part B.3): queue watermarks come from TaskRunner's own
|
|
9978
10958
|
// active-task map (distinct from `observer.tasks()` above, which is
|
|
9979
10959
|
// derived from the envelope feed) — see `TaskRunner.getQueueWatermarks`'s
|
|
@@ -10181,7 +11161,7 @@ function createDaemon(config) {
|
|
|
10181
11161
|
return createDaemonWithAdapters(config, buildDefaultAdapters(config));
|
|
10182
11162
|
}
|
|
10183
11163
|
var MAX_CONTROL_TOKEN_BYTES = 256;
|
|
10184
|
-
function
|
|
11164
|
+
function errorMessage5(err) {
|
|
10185
11165
|
return err instanceof Error ? err.message : String(err);
|
|
10186
11166
|
}
|
|
10187
11167
|
function sameFileState3(left, right) {
|
|
@@ -10234,7 +11214,7 @@ async function connectControlClient(opts) {
|
|
|
10234
11214
|
}
|
|
10235
11215
|
token = read;
|
|
10236
11216
|
} catch (err) {
|
|
10237
|
-
return { ok: false, reason: `could not read the control token: ${
|
|
11217
|
+
return { ok: false, reason: `could not read the control token: ${errorMessage5(err)}` };
|
|
10238
11218
|
}
|
|
10239
11219
|
if (!token) {
|
|
10240
11220
|
return { ok: false, reason: "control token file is empty" };
|
|
@@ -10244,7 +11224,7 @@ async function connectControlClient(opts) {
|
|
|
10244
11224
|
const client = await connectAndHandshake(endpoint, token, opts);
|
|
10245
11225
|
return { ok: true, client };
|
|
10246
11226
|
} catch (err) {
|
|
10247
|
-
return { ok: false, reason: `daemon control socket not reachable: ${
|
|
11227
|
+
return { ok: false, reason: `daemon control socket not reachable: ${errorMessage5(err)}` };
|
|
10248
11228
|
}
|
|
10249
11229
|
}
|
|
10250
11230
|
function connectAndHandshake(endpoint, token, opts) {
|
|
@@ -10863,19 +11843,7 @@ function quote(text) {
|
|
|
10863
11843
|
function redactedByteCountPlaceholder(text) {
|
|
10864
11844
|
return `[redacted: ${Buffer.byteLength(text, "utf8")} bytes]`;
|
|
10865
11845
|
}
|
|
10866
|
-
var STABLE_GIT_ERROR_CATEGORIES =
|
|
10867
|
-
"git-unavailable",
|
|
10868
|
-
"git-timeout",
|
|
10869
|
-
"git-output-limit",
|
|
10870
|
-
"git-command-failed",
|
|
10871
|
-
"workspace-root-invalid",
|
|
10872
|
-
"workspace-root-conflict",
|
|
10873
|
-
"workspace-not-owned",
|
|
10874
|
-
"repository-root-mismatch",
|
|
10875
|
-
"repository-invalid",
|
|
10876
|
-
"lease-busy",
|
|
10877
|
-
"ledger-invalid"
|
|
10878
|
-
]);
|
|
11846
|
+
var STABLE_GIT_ERROR_CATEGORIES = new Set(GIT_ERROR_CATEGORIES);
|
|
10879
11847
|
function stableGitErrorCategory(value) {
|
|
10880
11848
|
return value !== void 0 && STABLE_GIT_ERROR_CATEGORIES.has(value) ? value : void 0;
|
|
10881
11849
|
}
|
|
@@ -10941,6 +11909,8 @@ function formatDaemonEventLine(event, options = {}) {
|
|
|
10941
11909
|
return `${prefix} shutdown-complete reason=${quote(event.reason)}${event.undeliveredOutboxCount !== void 0 ? ` undeliveredOutboxCount=${event.undeliveredOutboxCount}` : ""}`;
|
|
10942
11910
|
case "stale-approval-decision":
|
|
10943
11911
|
return `${prefix} stale-approval-decision taskId=${event.taskId} decision=${event.decision}${event.reason ? ` reason=${quote(event.reason)}` : ""}`;
|
|
11912
|
+
case "runtime-disposal-failed":
|
|
11913
|
+
return `${prefix} runtime-disposal-failed taskId=${event.taskId} runtime=${event.runtimeId} stage=${event.stage} reason=${quote(event.reason)}`;
|
|
10944
11914
|
case "git-workspace": {
|
|
10945
11915
|
const parts = [
|
|
10946
11916
|
`${prefix} git-workspace taskId=${event.taskId}`,
|
|
@@ -11219,8 +12189,8 @@ async function probeRuntimes(adapters, options = {}) {
|
|
|
11219
12189
|
let resume = false;
|
|
11220
12190
|
let permissionModes = [];
|
|
11221
12191
|
try {
|
|
11222
|
-
id = boundedSingleLine(adapter.id, MAX_RUNTIME_ID_CHARS);
|
|
11223
|
-
const caps = adapter.capabilities
|
|
12192
|
+
id = boundedSingleLine(adapter.descriptor.id, MAX_RUNTIME_ID_CHARS);
|
|
12193
|
+
const caps = adapter.descriptor.capabilities;
|
|
11224
12194
|
steer = caps.steer === true;
|
|
11225
12195
|
resume = caps.resume === true;
|
|
11226
12196
|
permissionModes = caps.permissionModes.slice(0, MAX_PERMISSION_MODES).map((mode) => boundedSingleLine(mode, MAX_PERMISSION_MODE_CHARS));
|
|
@@ -12085,19 +13055,7 @@ function valueByteSize(value) {
|
|
|
12085
13055
|
function placeholderFor(size) {
|
|
12086
13056
|
return size === void 0 ? "[redacted]" : `[redacted: ${size} bytes]`;
|
|
12087
13057
|
}
|
|
12088
|
-
var STABLE_GIT_ERROR_CATEGORIES2 =
|
|
12089
|
-
"git-unavailable",
|
|
12090
|
-
"git-timeout",
|
|
12091
|
-
"git-output-limit",
|
|
12092
|
-
"git-command-failed",
|
|
12093
|
-
"workspace-root-invalid",
|
|
12094
|
-
"workspace-root-conflict",
|
|
12095
|
-
"workspace-not-owned",
|
|
12096
|
-
"repository-root-mismatch",
|
|
12097
|
-
"repository-invalid",
|
|
12098
|
-
"lease-busy",
|
|
12099
|
-
"ledger-invalid"
|
|
12100
|
-
]);
|
|
13058
|
+
var STABLE_GIT_ERROR_CATEGORIES2 = new Set(GIT_ERROR_CATEGORIES);
|
|
12101
13059
|
function stableGitErrorCategory2(value) {
|
|
12102
13060
|
return typeof value === "string" && STABLE_GIT_ERROR_CATEGORIES2.has(value) ? value : void 0;
|
|
12103
13061
|
}
|
|
@@ -12196,6 +13154,8 @@ function redactForAudit(event) {
|
|
|
12196
13154
|
return { ...base, reason: event.reason, undeliveredOutboxCount: event.undeliveredOutboxCount };
|
|
12197
13155
|
case "stale-approval-decision":
|
|
12198
13156
|
return { ...base, taskId: event.taskId, decision: event.decision, reasonSize: byteSize(event.reason) };
|
|
13157
|
+
case "runtime-disposal-failed":
|
|
13158
|
+
return { ...base, taskId: event.taskId, runtimeId: event.runtimeId, stage: event.stage, reason: event.reason };
|
|
12199
13159
|
case "device-assertion":
|
|
12200
13160
|
return event.result === "issued" ? {
|
|
12201
13161
|
...base,
|
|
@@ -12344,6 +13304,15 @@ function reconstructDaemonEvent(raw) {
|
|
|
12344
13304
|
reason: reasonSize === void 0 ? void 0 : placeholderFor(reasonSize)
|
|
12345
13305
|
};
|
|
12346
13306
|
}
|
|
13307
|
+
case "runtime-disposal-failed":
|
|
13308
|
+
return {
|
|
13309
|
+
kind: "runtime-disposal-failed",
|
|
13310
|
+
ts,
|
|
13311
|
+
taskId: str(raw.taskId),
|
|
13312
|
+
runtimeId: str(raw.runtimeId),
|
|
13313
|
+
stage: str(raw.stage),
|
|
13314
|
+
reason: str(raw.reason)
|
|
13315
|
+
};
|
|
12347
13316
|
case "device-assertion": {
|
|
12348
13317
|
if (raw.result === "issued") {
|
|
12349
13318
|
return {
|
|
@@ -12616,20 +13585,8 @@ async function runStartCommand(config, deps) {
|
|
|
12616
13585
|
throw err;
|
|
12617
13586
|
}
|
|
12618
13587
|
}
|
|
12619
|
-
var STABLE_GIT_PHASES =
|
|
12620
|
-
var STABLE_GIT_ERROR_CATEGORIES3 =
|
|
12621
|
-
"git-unavailable",
|
|
12622
|
-
"git-timeout",
|
|
12623
|
-
"git-output-limit",
|
|
12624
|
-
"git-command-failed",
|
|
12625
|
-
"workspace-root-invalid",
|
|
12626
|
-
"workspace-root-conflict",
|
|
12627
|
-
"workspace-not-owned",
|
|
12628
|
-
"repository-root-mismatch",
|
|
12629
|
-
"repository-invalid",
|
|
12630
|
-
"lease-busy",
|
|
12631
|
-
"ledger-invalid"
|
|
12632
|
-
]);
|
|
13588
|
+
var STABLE_GIT_PHASES = new Set(GIT_WORKSPACE_PHASES);
|
|
13589
|
+
var STABLE_GIT_ERROR_CATEGORIES3 = new Set(GIT_ERROR_CATEGORIES);
|
|
12633
13590
|
function nonNegativeSafeInteger(value) {
|
|
12634
13591
|
return value !== void 0 && Number.isSafeInteger(value) && value >= 0 ? value : void 0;
|
|
12635
13592
|
}
|
|
@@ -13128,19 +14085,7 @@ async function runUnpairCommand(daemon, deps = {}) {
|
|
|
13128
14085
|
}
|
|
13129
14086
|
|
|
13130
14087
|
// src/bin/commands/workspaces.ts
|
|
13131
|
-
var STABLE_ERROR_CATEGORIES =
|
|
13132
|
-
"git-unavailable",
|
|
13133
|
-
"git-timeout",
|
|
13134
|
-
"git-output-limit",
|
|
13135
|
-
"git-command-failed",
|
|
13136
|
-
"workspace-root-invalid",
|
|
13137
|
-
"workspace-root-conflict",
|
|
13138
|
-
"workspace-not-owned",
|
|
13139
|
-
"repository-root-mismatch",
|
|
13140
|
-
"repository-invalid",
|
|
13141
|
-
"lease-busy",
|
|
13142
|
-
"ledger-invalid"
|
|
13143
|
-
]);
|
|
14088
|
+
var STABLE_ERROR_CATEGORIES = new Set(GIT_ERROR_CATEGORIES);
|
|
13144
14089
|
function abbreviateCommit(value) {
|
|
13145
14090
|
if (!value) return "-";
|
|
13146
14091
|
return value.length > 8 ? value.slice(0, 8) : value;
|