@byok-sdk/client 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -0
- package/dist/adapters/claude/claude-adapter.d.ts +4 -20
- package/dist/adapters/claude/events.d.ts +3 -0
- package/dist/adapters/claude/process-client.d.ts +9 -1
- package/dist/adapters/codex/codex-adapter.d.ts +4 -16
- package/dist/adapters/codex/process-runner.d.ts +4 -1
- package/dist/adapters/index.d.ts +3 -1
- package/dist/adapters/index.js +923 -258
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/pi/pi-adapter.d.ts +3 -16
- package/dist/adapters/pi/rpc-client.d.ts +9 -1
- package/dist/adapters/process-tree.d.ts +19 -0
- package/dist/bin/audit-log.d.ts +12 -0
- package/dist/bin/byok-agent.js +1293 -484
- 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 +4 -2
- 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/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 +27 -34
- package/dist/daemon/ws-transport.d.ts +3 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +1254 -432
- 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 +4 -4
package/dist/adapters/index.js
CHANGED
|
@@ -6,9 +6,80 @@ import { fileURLToPath } from 'url';
|
|
|
6
6
|
import os from 'os';
|
|
7
7
|
import 'readline';
|
|
8
8
|
|
|
9
|
-
// src/
|
|
9
|
+
// src/runtime-failure.ts
|
|
10
|
+
var RUNTIME_EXECUTION_FAILURE_BRAND = /* @__PURE__ */ Symbol.for("@byok-sdk/client/RuntimeExecutionFailure/v1");
|
|
11
|
+
var RUNTIME_DISPOSAL_FAILURE_BRAND = /* @__PURE__ */ Symbol.for("@byok-sdk/client/RuntimeDisposalFailure/v1");
|
|
12
|
+
var RuntimeDisposalFailure = class extends Error {
|
|
13
|
+
stage;
|
|
14
|
+
constructor(input, options) {
|
|
15
|
+
if (!isRuntimeDisposalStage(input.stage) || typeof input.reason !== "string" || input.reason.length === 0) {
|
|
16
|
+
throw new TypeError("invalid RuntimeDisposalFailure input");
|
|
17
|
+
}
|
|
18
|
+
super(input.reason, options);
|
|
19
|
+
this.name = "RuntimeDisposalFailure";
|
|
20
|
+
this.stage = input.stage;
|
|
21
|
+
Object.defineProperty(this, RUNTIME_DISPOSAL_FAILURE_BRAND, { value: true });
|
|
22
|
+
Object.freeze(this);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
function isRuntimeDisposalStage(value) {
|
|
26
|
+
return value === "signal" || value === "quiescence" || value === "cleanup";
|
|
27
|
+
}
|
|
28
|
+
var RuntimeExecutionFailure = class extends Error {
|
|
29
|
+
phase;
|
|
30
|
+
category;
|
|
31
|
+
retry;
|
|
32
|
+
constructor(input, options) {
|
|
33
|
+
if (!isRuntimeFailurePhase(input.phase) || !isRuntimeFailureCategory(input.category) || !isRuntimeRetryDisposition(input.retry) || typeof input.reason !== "string" || input.reason.length === 0) {
|
|
34
|
+
throw new TypeError("invalid RuntimeExecutionFailure input");
|
|
35
|
+
}
|
|
36
|
+
super(input.reason, options);
|
|
37
|
+
this.name = "RuntimeExecutionFailure";
|
|
38
|
+
this.phase = input.phase;
|
|
39
|
+
this.category = input.category;
|
|
40
|
+
this.retry = input.retry;
|
|
41
|
+
Object.defineProperty(this, RUNTIME_EXECUTION_FAILURE_BRAND, { value: true });
|
|
42
|
+
Object.freeze(this);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
function isRuntimeFailurePhase(value) {
|
|
46
|
+
return value === "start" || value === "run";
|
|
47
|
+
}
|
|
48
|
+
function isRuntimeFailureCategory(value) {
|
|
49
|
+
return value === "semantic" || value === "infrastructure" || value === "authority";
|
|
50
|
+
}
|
|
51
|
+
function isRuntimeRetryDisposition(value) {
|
|
52
|
+
return value === "retryable" || value === "non-retryable";
|
|
53
|
+
}
|
|
54
|
+
function isRuntimeExecutionFailure(value) {
|
|
55
|
+
if (typeof value !== "object" || value === null) return false;
|
|
56
|
+
const candidate = value;
|
|
57
|
+
return candidate[RUNTIME_EXECUTION_FAILURE_BRAND] === true && isRuntimeFailurePhase(candidate.phase) && isRuntimeFailureCategory(candidate.category) && isRuntimeRetryDisposition(candidate.retry) && typeof candidate.message === "string" && candidate.message.length > 0;
|
|
58
|
+
}
|
|
10
59
|
|
|
11
60
|
// src/types.ts
|
|
61
|
+
function frozenStrings(values) {
|
|
62
|
+
return values === void 0 ? void 0 : Object.freeze([...values]);
|
|
63
|
+
}
|
|
64
|
+
function freezeRuntimeAdapterDescriptor(descriptor) {
|
|
65
|
+
const baseNames = frozenStrings(descriptor.environmentRequirements.baseNames);
|
|
66
|
+
const credentialNames = frozenStrings(descriptor.environmentRequirements.credentialNames);
|
|
67
|
+
return Object.freeze({
|
|
68
|
+
id: descriptor.id,
|
|
69
|
+
supportsDispatchSelection: descriptor.supportsDispatchSelection === true,
|
|
70
|
+
capabilities: Object.freeze({
|
|
71
|
+
steer: descriptor.capabilities.steer === true,
|
|
72
|
+
resume: descriptor.capabilities.resume === true,
|
|
73
|
+
approvalInteractive: descriptor.capabilities.approvalInteractive === true,
|
|
74
|
+
...descriptor.capabilities.mcpToolsets === void 0 ? {} : { mcpToolsets: descriptor.capabilities.mcpToolsets === true },
|
|
75
|
+
permissionModes: Object.freeze([...descriptor.capabilities.permissionModes])
|
|
76
|
+
}),
|
|
77
|
+
environmentRequirements: Object.freeze({
|
|
78
|
+
...baseNames === void 0 ? {} : { baseNames },
|
|
79
|
+
...credentialNames === void 0 ? {} : { credentialNames }
|
|
80
|
+
})
|
|
81
|
+
});
|
|
82
|
+
}
|
|
12
83
|
var PolicyUnsupportedError = class extends Error {
|
|
13
84
|
constructor(message) {
|
|
14
85
|
super(message);
|
|
@@ -16,7 +87,7 @@ var PolicyUnsupportedError = class extends Error {
|
|
|
16
87
|
}
|
|
17
88
|
};
|
|
18
89
|
var SteerUnsupportedError = class extends Error {
|
|
19
|
-
/** The `RuntimeAdapter.id` that cannot steer (e.g. `claude`, `codex`). */
|
|
90
|
+
/** The `RuntimeAdapter.descriptor.id` that cannot steer (e.g. `claude`, `codex`). */
|
|
20
91
|
runtimeId;
|
|
21
92
|
constructor(runtimeId, message) {
|
|
22
93
|
super(message);
|
|
@@ -267,6 +338,155 @@ var AsyncQueue = class {
|
|
|
267
338
|
};
|
|
268
339
|
}
|
|
269
340
|
};
|
|
341
|
+
var DEFAULT_TERM_GRACE_MS = 750;
|
|
342
|
+
var DEFAULT_KILL_GRACE_MS = 2e3;
|
|
343
|
+
var POLL_MS = 20;
|
|
344
|
+
var terminationRequested = /* @__PURE__ */ new WeakSet();
|
|
345
|
+
var terminationRequestFailed = /* @__PURE__ */ new WeakSet();
|
|
346
|
+
function withOwnedProcessTree(options) {
|
|
347
|
+
return {
|
|
348
|
+
...options,
|
|
349
|
+
...process.platform === "win32" ? { windowsHide: true } : { detached: true }
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
function positivePid(child, label) {
|
|
353
|
+
const pid = child.pid;
|
|
354
|
+
if (pid === void 0) return void 0;
|
|
355
|
+
if (!Number.isSafeInteger(pid) || pid <= 0 || pid === process.pid) {
|
|
356
|
+
throw new RuntimeDisposalFailure({
|
|
357
|
+
stage: "signal",
|
|
358
|
+
reason: `${label} runtime process has an unsafe owned pid`
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
return pid;
|
|
362
|
+
}
|
|
363
|
+
function groupExists(pid, label) {
|
|
364
|
+
try {
|
|
365
|
+
process.kill(-pid, 0);
|
|
366
|
+
return true;
|
|
367
|
+
} catch (cause) {
|
|
368
|
+
const code = cause.code;
|
|
369
|
+
if (code === "ESRCH") return false;
|
|
370
|
+
if (code === "EPERM") return true;
|
|
371
|
+
throw new RuntimeDisposalFailure({
|
|
372
|
+
stage: "quiescence",
|
|
373
|
+
reason: `${label} runtime process-group state could not be verified`
|
|
374
|
+
}, { cause });
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
function signalGroup(pid, signal, label) {
|
|
378
|
+
try {
|
|
379
|
+
process.kill(-pid, signal);
|
|
380
|
+
} catch (cause) {
|
|
381
|
+
const code = cause.code;
|
|
382
|
+
if (code === "ESRCH" || code === "EPERM") return;
|
|
383
|
+
throw new RuntimeDisposalFailure({
|
|
384
|
+
stage: "signal",
|
|
385
|
+
reason: `${label} runtime process group ${pid} could not receive ${signal} (${code ?? "unknown"})`
|
|
386
|
+
}, { cause });
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
async function waitUntil(predicate, timeoutMs) {
|
|
390
|
+
const deadline = Date.now() + timeoutMs;
|
|
391
|
+
while (predicate()) {
|
|
392
|
+
if (Date.now() >= deadline) return false;
|
|
393
|
+
await new Promise((resolve) => {
|
|
394
|
+
setTimeout(resolve, POLL_MS);
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
return true;
|
|
398
|
+
}
|
|
399
|
+
async function waitWithDeadline(promise, timeoutMs) {
|
|
400
|
+
return new Promise((resolve) => {
|
|
401
|
+
let settled = false;
|
|
402
|
+
const timer = setTimeout(() => {
|
|
403
|
+
if (!settled) {
|
|
404
|
+
settled = true;
|
|
405
|
+
resolve(false);
|
|
406
|
+
}
|
|
407
|
+
}, timeoutMs);
|
|
408
|
+
void promise.then(
|
|
409
|
+
() => {
|
|
410
|
+
if (!settled) {
|
|
411
|
+
settled = true;
|
|
412
|
+
clearTimeout(timer);
|
|
413
|
+
resolve(true);
|
|
414
|
+
}
|
|
415
|
+
},
|
|
416
|
+
() => {
|
|
417
|
+
if (!settled) {
|
|
418
|
+
settled = true;
|
|
419
|
+
clearTimeout(timer);
|
|
420
|
+
resolve(false);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
);
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
function requestOwnedProcessTreeTermination(options) {
|
|
427
|
+
if (options.isClosed()) return;
|
|
428
|
+
const pid = positivePid(options.child, options.label);
|
|
429
|
+
if (pid === void 0) return;
|
|
430
|
+
if (process.platform === "win32") {
|
|
431
|
+
const result = spawnSync("taskkill", ["/PID", String(pid), "/T", "/F"], { windowsHide: true });
|
|
432
|
+
if (result.error) {
|
|
433
|
+
throw new RuntimeDisposalFailure({
|
|
434
|
+
stage: "signal",
|
|
435
|
+
reason: `${options.label} runtime process tree could not be terminated`
|
|
436
|
+
}, { cause: result.error });
|
|
437
|
+
}
|
|
438
|
+
terminationRequested.add(options.child);
|
|
439
|
+
if (result.status !== 0) terminationRequestFailed.add(options.child);
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
signalGroup(pid, "SIGTERM", options.label);
|
|
443
|
+
terminationRequested.add(options.child);
|
|
444
|
+
}
|
|
445
|
+
async function disposeOwnedProcessTree(options) {
|
|
446
|
+
const pid = positivePid(options.child, options.label);
|
|
447
|
+
const termGraceMs = options.termGraceMs ?? DEFAULT_TERM_GRACE_MS;
|
|
448
|
+
const killGraceMs = options.killGraceMs ?? DEFAULT_KILL_GRACE_MS;
|
|
449
|
+
if (pid === void 0) {
|
|
450
|
+
if (await waitWithDeadline(options.waitClosed(), killGraceMs)) return;
|
|
451
|
+
throw new RuntimeDisposalFailure({
|
|
452
|
+
stage: "quiescence",
|
|
453
|
+
reason: `${options.label} runtime process did not settle after spawn failure`
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
if (process.platform === "win32") {
|
|
457
|
+
if (!options.isClosed() && !terminationRequested.has(options.child)) requestOwnedProcessTreeTermination(options);
|
|
458
|
+
if (await waitWithDeadline(options.waitClosed(), killGraceMs)) return;
|
|
459
|
+
if (terminationRequestFailed.has(options.child)) {
|
|
460
|
+
throw new RuntimeDisposalFailure({
|
|
461
|
+
stage: "signal",
|
|
462
|
+
reason: `${options.label} runtime process tree could not be terminated`
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
throw new RuntimeDisposalFailure({
|
|
466
|
+
stage: "quiescence",
|
|
467
|
+
reason: `${options.label} runtime process tree did not close before the disposal deadline`
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
if (groupExists(pid, options.label) && !terminationRequested.has(options.child)) {
|
|
471
|
+
signalGroup(pid, "SIGTERM", options.label);
|
|
472
|
+
terminationRequested.add(options.child);
|
|
473
|
+
}
|
|
474
|
+
if (!await waitUntil(() => groupExists(pid, options.label), termGraceMs)) {
|
|
475
|
+
signalGroup(pid, "SIGKILL", options.label);
|
|
476
|
+
if (!await waitUntil(() => groupExists(pid, options.label), killGraceMs)) {
|
|
477
|
+
throw new RuntimeDisposalFailure({
|
|
478
|
+
stage: "quiescence",
|
|
479
|
+
reason: `${options.label} runtime process group remained live after SIGKILL`
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
if (!await waitWithDeadline(options.waitClosed(), killGraceMs)) {
|
|
484
|
+
throw new RuntimeDisposalFailure({
|
|
485
|
+
stage: "quiescence",
|
|
486
|
+
reason: `${options.label} runtime root did not emit close after its process group exited`
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
}
|
|
270
490
|
|
|
271
491
|
// src/adapters/pi/rpc-client.ts
|
|
272
492
|
var STDERR_RING_CAPACITY = 20;
|
|
@@ -279,16 +499,22 @@ var PiRpcClient = class {
|
|
|
279
499
|
eventQueue = new AsyncQueue();
|
|
280
500
|
closed = false;
|
|
281
501
|
exitError;
|
|
502
|
+
closedPromise;
|
|
503
|
+
resolveClosed;
|
|
504
|
+
disposalAttempt;
|
|
282
505
|
/** 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`. */
|
|
283
506
|
stderrRing = [];
|
|
284
507
|
/** Count of pi RPC message types `PiSession` (pi-adapter.ts) has told us have no `AgentEvent` mapping and aren't routine bookkeeping — see `recordUnmappedFrame`. */
|
|
285
508
|
unmappedFrameCounts = /* @__PURE__ */ new Map();
|
|
286
509
|
constructor(options) {
|
|
287
510
|
const spawnFn = options.spawnFn ?? spawn;
|
|
288
|
-
this.child = spawnFn(options.command, options.args, {
|
|
511
|
+
this.child = spawnFn(options.command, options.args, withOwnedProcessTree({
|
|
289
512
|
cwd: options.cwd,
|
|
290
513
|
env: options.env,
|
|
291
514
|
stdio: ["pipe", "pipe", "pipe"]
|
|
515
|
+
}));
|
|
516
|
+
this.closedPromise = new Promise((resolve) => {
|
|
517
|
+
this.resolveClosed = resolve;
|
|
292
518
|
});
|
|
293
519
|
this.child.stdout.setEncoding("utf8");
|
|
294
520
|
this.child.stdout.on("data", (chunk) => this.onData(chunk));
|
|
@@ -323,6 +549,10 @@ var PiRpcClient = class {
|
|
|
323
549
|
get events() {
|
|
324
550
|
return this.eventQueue;
|
|
325
551
|
}
|
|
552
|
+
/** Local transport diagnostic retained when the process closes; consumers must classify it explicitly. */
|
|
553
|
+
get terminalError() {
|
|
554
|
+
return this.exitError;
|
|
555
|
+
}
|
|
326
556
|
/**
|
|
327
557
|
* Record a pi RPC message `type` that `PiSession` (pi-adapter.ts) decided
|
|
328
558
|
* has no `AgentEvent` mapping and isn't routine bookkeeping (see
|
|
@@ -341,15 +571,30 @@ var PiRpcClient = class {
|
|
|
341
571
|
);
|
|
342
572
|
}
|
|
343
573
|
}
|
|
344
|
-
/**
|
|
574
|
+
/** Immediate process-tree termination request. `dispose()` is the settlement receipt. */
|
|
345
575
|
kill() {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
576
|
+
requestOwnedProcessTreeTermination(this.processTreeOptions());
|
|
577
|
+
}
|
|
578
|
+
waitClosed() {
|
|
579
|
+
return this.closedPromise;
|
|
580
|
+
}
|
|
581
|
+
dispose() {
|
|
582
|
+
if (!this.disposalAttempt) {
|
|
583
|
+
const attempt = disposeOwnedProcessTree(this.processTreeOptions());
|
|
584
|
+
this.disposalAttempt = attempt.catch((error) => {
|
|
585
|
+
this.disposalAttempt = void 0;
|
|
586
|
+
throw error;
|
|
587
|
+
});
|
|
352
588
|
}
|
|
589
|
+
return this.disposalAttempt;
|
|
590
|
+
}
|
|
591
|
+
processTreeOptions() {
|
|
592
|
+
return {
|
|
593
|
+
child: this.child,
|
|
594
|
+
waitClosed: () => this.closedPromise,
|
|
595
|
+
isClosed: () => this.closed,
|
|
596
|
+
label: "pi"
|
|
597
|
+
};
|
|
353
598
|
}
|
|
354
599
|
onData(chunk) {
|
|
355
600
|
this.buffer += chunk;
|
|
@@ -435,6 +680,7 @@ var PiRpcClient = class {
|
|
|
435
680
|
if (this.closed) return;
|
|
436
681
|
this.closed = true;
|
|
437
682
|
this.exitError = err;
|
|
683
|
+
this.resolveClosed();
|
|
438
684
|
for (const [, waiter] of this.pending) waiter.reject(err);
|
|
439
685
|
this.pending.clear();
|
|
440
686
|
this.eventQueue.end();
|
|
@@ -505,8 +751,17 @@ var PiAdapter = class {
|
|
|
505
751
|
this.options = options;
|
|
506
752
|
}
|
|
507
753
|
options;
|
|
508
|
-
|
|
509
|
-
|
|
754
|
+
descriptor = freezeRuntimeAdapterDescriptor({
|
|
755
|
+
id: "pi",
|
|
756
|
+
supportsDispatchSelection: true,
|
|
757
|
+
capabilities: {
|
|
758
|
+
steer: true,
|
|
759
|
+
resume: true,
|
|
760
|
+
approvalInteractive: false,
|
|
761
|
+
permissionModes: ["auto", "readonly"]
|
|
762
|
+
},
|
|
763
|
+
environmentRequirements: { credentialNames: PROVIDER_CREDENTIAL_ENV_NAMES }
|
|
764
|
+
});
|
|
510
765
|
async detect() {
|
|
511
766
|
try {
|
|
512
767
|
const bin = this.resolveBin();
|
|
@@ -518,49 +773,26 @@ var PiAdapter = class {
|
|
|
518
773
|
return { present: false };
|
|
519
774
|
}
|
|
520
775
|
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
}
|
|
524
|
-
/**
|
|
525
|
-
* M5: pi authenticates to its ~30 supported providers via env-var API
|
|
526
|
-
* keys — `detect()`'s own `authPresent` probe above checks this identical
|
|
527
|
-
* list — so these MUST keep flowing into pi's spawned process or pi auth
|
|
528
|
-
* breaks entirely. `KNOWN_PROVIDER_ENV_VARS` above is the single source
|
|
529
|
-
* of truth, reused here rather than duplicated. No `baseNames`: nothing
|
|
530
|
-
* in this adapter or `rpc-client.ts` reads a pi-specific config-discovery
|
|
531
|
-
* variable beyond the platform baseline (`daemon/environment.ts`).
|
|
532
|
-
*/
|
|
533
|
-
environmentRequirements() {
|
|
534
|
-
return { credentialNames: PROVIDER_CREDENTIAL_ENV_NAMES };
|
|
535
|
-
}
|
|
536
|
-
async start(task, ctx) {
|
|
537
|
-
if (typeof task.instruction !== "string") {
|
|
538
|
-
throw new PolicyUnsupportedError("pi adapter only supports string instructions in M0 (no blob-ref fetch yet)");
|
|
539
|
-
}
|
|
540
|
-
const mapping = mapPermissionPolicyToPiArgs(ctx.policy);
|
|
776
|
+
async prepare(input) {
|
|
777
|
+
const mapping = mapPermissionPolicyToPiArgs(input.policy);
|
|
541
778
|
if (!mapping.ok) {
|
|
542
|
-
|
|
779
|
+
return { kind: "reject", reason: mapping.reason ?? "policy rejected by pi adapter", retryable: false };
|
|
543
780
|
}
|
|
544
781
|
const bin = this.resolveBin();
|
|
545
|
-
const
|
|
546
|
-
const
|
|
547
|
-
const selection = task.dispatchSelection;
|
|
782
|
+
const selection = input.offer.dispatchSelection;
|
|
783
|
+
const pinnedSelection = selection === void 0 ? void 0 : Object.freeze({ ...selection });
|
|
548
784
|
let command = bin.command;
|
|
549
|
-
let
|
|
550
|
-
if (
|
|
551
|
-
if (
|
|
552
|
-
|
|
553
|
-
`pi adapter cannot execute ${selection.lane} selection for runtime ${selection.runtimeId}`
|
|
554
|
-
);
|
|
785
|
+
let launcherArgs;
|
|
786
|
+
if (pinnedSelection !== void 0) {
|
|
787
|
+
if (pinnedSelection.lane !== "byok" || pinnedSelection.runtimeId !== "pi") {
|
|
788
|
+
return { kind: "reject", reason: `pi adapter cannot execute ${pinnedSelection.lane} selection for runtime ${pinnedSelection.runtimeId}`, retryable: false };
|
|
555
789
|
}
|
|
556
790
|
const launcher = this.options.byokLauncher;
|
|
557
791
|
if (launcher === void 0) {
|
|
558
|
-
|
|
559
|
-
"pi BYOK selection requires a configured credential-custody launcher"
|
|
560
|
-
);
|
|
792
|
+
return { kind: "reject", reason: "pi BYOK selection requires a configured credential-custody launcher", retryable: false };
|
|
561
793
|
}
|
|
562
794
|
command = launcher.command;
|
|
563
|
-
|
|
795
|
+
launcherArgs = [
|
|
564
796
|
...launcher.args ?? [],
|
|
565
797
|
"--pi-bin",
|
|
566
798
|
bin.command,
|
|
@@ -570,62 +802,136 @@ var PiAdapter = class {
|
|
|
570
802
|
launcher.sessionDir,
|
|
571
803
|
...launcher.secretServicePrefix ? ["--secret-service-prefix", launcher.secretServicePrefix] : [],
|
|
572
804
|
"--provider",
|
|
573
|
-
|
|
805
|
+
pinnedSelection.providerId,
|
|
574
806
|
"--model",
|
|
575
|
-
|
|
576
|
-
"--",
|
|
577
|
-
...piArgs
|
|
807
|
+
pinnedSelection.modelId
|
|
578
808
|
];
|
|
579
809
|
}
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
810
|
+
return {
|
|
811
|
+
kind: "prepared",
|
|
812
|
+
operation: {
|
|
813
|
+
start: async (startInput) => {
|
|
814
|
+
const manifestSelection = startInput.manifest.dispatchSelection;
|
|
815
|
+
if (!sameDispatchSelection(manifestSelection, pinnedSelection)) {
|
|
816
|
+
throw new RuntimeExecutionFailure({
|
|
817
|
+
phase: "start",
|
|
818
|
+
category: "authority",
|
|
819
|
+
retry: "non-retryable",
|
|
820
|
+
reason: "prepared pi operation received a manifest with different runtime selection"
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
if (typeof startInput.instruction !== "string") {
|
|
824
|
+
throw new RuntimeExecutionFailure({
|
|
825
|
+
phase: "start",
|
|
826
|
+
category: "authority",
|
|
827
|
+
retry: "non-retryable",
|
|
828
|
+
reason: "prepared pi operation requires a resolved string instruction"
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
const resumeSessionId = startInput.manifest.sessionRef;
|
|
832
|
+
const piArgs = ["--mode", "rpc", ...resumeSessionId ? ["--session", resumeSessionId] : [], ...mapping.args];
|
|
833
|
+
const args = launcherArgs === void 0 ? piArgs : [...launcherArgs, "--", ...piArgs];
|
|
834
|
+
let rpc;
|
|
835
|
+
try {
|
|
836
|
+
rpc = new PiRpcClient({
|
|
837
|
+
command,
|
|
838
|
+
args,
|
|
839
|
+
cwd: startInput.manifest.workspace.workspaceDir,
|
|
840
|
+
env: manifestSelection === void 0 ? startInput.env : withoutProviderCredentials(startInput.env),
|
|
841
|
+
spawnFn: this.options.spawnFn
|
|
842
|
+
});
|
|
843
|
+
} catch (cause) {
|
|
844
|
+
throw new RuntimeExecutionFailure({
|
|
845
|
+
phase: "start",
|
|
846
|
+
category: "infrastructure",
|
|
847
|
+
retry: "retryable",
|
|
848
|
+
reason: "pi runtime process could not be spawned"
|
|
849
|
+
}, { cause });
|
|
850
|
+
}
|
|
851
|
+
let response;
|
|
852
|
+
try {
|
|
853
|
+
response = await rpc.send({ type: "prompt", message: startInput.instruction });
|
|
854
|
+
} catch (cause) {
|
|
855
|
+
rpc.kill();
|
|
856
|
+
throw new RuntimeExecutionFailure({
|
|
857
|
+
phase: "start",
|
|
858
|
+
category: "infrastructure",
|
|
859
|
+
retry: "retryable",
|
|
860
|
+
reason: `pi initial prompt transport failed: ${errorMessage(cause)}`
|
|
861
|
+
}, { cause });
|
|
862
|
+
}
|
|
863
|
+
if (response.success === false) {
|
|
864
|
+
rpc.kill();
|
|
865
|
+
throw new RuntimeExecutionFailure({
|
|
866
|
+
phase: "start",
|
|
867
|
+
category: "semantic",
|
|
868
|
+
retry: "non-retryable",
|
|
869
|
+
reason: typeof response.error === "string" ? response.error : "pi rejected the initial prompt"
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
let sessionRef;
|
|
873
|
+
try {
|
|
874
|
+
sessionRef = await resolveAuthoritativeSessionId(rpc);
|
|
875
|
+
} catch (err) {
|
|
876
|
+
rpc.kill();
|
|
877
|
+
throw err;
|
|
878
|
+
}
|
|
879
|
+
if (resumeSessionId !== void 0 && sessionRef !== resumeSessionId) {
|
|
880
|
+
rpc.kill();
|
|
881
|
+
throw new RuntimeExecutionFailure({
|
|
882
|
+
phase: "start",
|
|
883
|
+
category: "authority",
|
|
884
|
+
retry: "non-retryable",
|
|
885
|
+
reason: "pi resumed a different authoritative session than requested"
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
return new PiSession(sessionRef, rpc, manifestSelection);
|
|
889
|
+
}
|
|
601
890
|
}
|
|
602
|
-
}
|
|
603
|
-
return new PiSession(sessionRef, rpc, selection);
|
|
891
|
+
};
|
|
604
892
|
}
|
|
605
893
|
resolveBin() {
|
|
606
894
|
return (this.options.resolveBin ?? resolvePiBin)();
|
|
607
895
|
}
|
|
608
896
|
};
|
|
609
|
-
|
|
897
|
+
function sameDispatchSelection(left, right) {
|
|
898
|
+
if (left === void 0 || right === void 0) return left === right;
|
|
899
|
+
return left.lane === right.lane && left.runtimeId === right.runtimeId && left.providerId === right.providerId && left.modelId === right.modelId;
|
|
900
|
+
}
|
|
901
|
+
async function resolveAuthoritativeSessionId(rpc) {
|
|
610
902
|
let state;
|
|
611
903
|
try {
|
|
612
904
|
state = await rpc.send({ type: "get_state" });
|
|
613
905
|
} catch (err) {
|
|
614
|
-
|
|
906
|
+
if (isRuntimeExecutionFailure(err)) throw err;
|
|
907
|
+
throw new RuntimeExecutionFailure({
|
|
908
|
+
phase: "start",
|
|
909
|
+
category: "infrastructure",
|
|
910
|
+
retry: "retryable",
|
|
911
|
+
reason: `pi transport ended before yielding an authoritative session id: ${errorMessage(err)}`
|
|
912
|
+
}, {
|
|
615
913
|
cause: err
|
|
616
914
|
});
|
|
617
915
|
}
|
|
618
916
|
if (state.success === false) {
|
|
619
917
|
const reason = typeof state.error === "string" ? state.error : "get_state reported failure";
|
|
620
|
-
throw new
|
|
918
|
+
throw new RuntimeExecutionFailure({
|
|
919
|
+
phase: "start",
|
|
920
|
+
category: "authority",
|
|
921
|
+
retry: "non-retryable",
|
|
922
|
+
reason: `pi did not yield an authoritative session id: ${reason}`
|
|
923
|
+
});
|
|
621
924
|
}
|
|
622
925
|
const data = state.data;
|
|
623
926
|
if (typeof data?.sessionId === "string" && data.sessionId.length > 0) {
|
|
624
927
|
return data.sessionId;
|
|
625
928
|
}
|
|
626
|
-
throw new
|
|
627
|
-
|
|
628
|
-
|
|
929
|
+
throw new RuntimeExecutionFailure({
|
|
930
|
+
phase: "start",
|
|
931
|
+
category: "authority",
|
|
932
|
+
retry: "non-retryable",
|
|
933
|
+
reason: "pi get_state reported no authoritative session id"
|
|
934
|
+
});
|
|
629
935
|
}
|
|
630
936
|
var PiSession = class {
|
|
631
937
|
constructor(sessionRef, rpc, selection) {
|
|
@@ -641,12 +947,40 @@ var PiSession = class {
|
|
|
641
947
|
return {
|
|
642
948
|
[Symbol.asyncIterator]() {
|
|
643
949
|
const inner = rpc.events[Symbol.asyncIterator]();
|
|
950
|
+
let terminalFailure;
|
|
644
951
|
return {
|
|
645
952
|
async next() {
|
|
646
953
|
for (; ; ) {
|
|
647
|
-
|
|
648
|
-
|
|
954
|
+
if (terminalFailure) throw terminalFailure;
|
|
955
|
+
let result;
|
|
956
|
+
try {
|
|
957
|
+
result = await inner.next();
|
|
958
|
+
} catch (cause) {
|
|
959
|
+
throw new RuntimeExecutionFailure({
|
|
960
|
+
phase: "run",
|
|
961
|
+
category: "infrastructure",
|
|
962
|
+
retry: "retryable",
|
|
963
|
+
reason: "pi runtime event transport failed"
|
|
964
|
+
}, { cause });
|
|
965
|
+
}
|
|
966
|
+
const { value, done } = result;
|
|
967
|
+
if (done) {
|
|
968
|
+
throw new RuntimeExecutionFailure({
|
|
969
|
+
phase: "run",
|
|
970
|
+
category: "infrastructure",
|
|
971
|
+
retry: "retryable",
|
|
972
|
+
reason: "pi runtime process ended before agent_settled"
|
|
973
|
+
}, { cause: rpc.terminalError });
|
|
974
|
+
}
|
|
649
975
|
const mapped = mapPiMessageToAgentEvent(value);
|
|
976
|
+
if (value.type === "auto_retry_end" && value.success === false) {
|
|
977
|
+
terminalFailure = new RuntimeExecutionFailure({
|
|
978
|
+
phase: "run",
|
|
979
|
+
category: "semantic",
|
|
980
|
+
retry: "non-retryable",
|
|
981
|
+
reason: "pi exhausted its native retry policy"
|
|
982
|
+
});
|
|
983
|
+
}
|
|
650
984
|
if (mapped) return { value: mapped, done: false };
|
|
651
985
|
if (!ROUTINE_PI_EVENT_TYPES.has(value.type)) {
|
|
652
986
|
rpc.recordUnmappedFrame(value.type);
|
|
@@ -676,7 +1010,7 @@ var PiSession = class {
|
|
|
676
1010
|
await this.rpc.send({ type: "abort" });
|
|
677
1011
|
}
|
|
678
1012
|
async close() {
|
|
679
|
-
this.rpc.
|
|
1013
|
+
await this.rpc.dispose();
|
|
680
1014
|
}
|
|
681
1015
|
async resolveApproval() {
|
|
682
1016
|
throw new Error("pi adapter does not support approval resume: pi never emits needs_approval in M0/M1");
|
|
@@ -888,7 +1222,15 @@ function mapResult(msg) {
|
|
|
888
1222
|
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`
|
|
889
1223
|
);
|
|
890
1224
|
const events = usageEvent ? [usageEvent, { type: "error", message }] : [{ type: "error", message }];
|
|
891
|
-
return {
|
|
1225
|
+
return {
|
|
1226
|
+
events,
|
|
1227
|
+
terminalFailure: new RuntimeExecutionFailure({
|
|
1228
|
+
phase: "run",
|
|
1229
|
+
category: msg.is_error === true ? "semantic" : "authority",
|
|
1230
|
+
retry: "non-retryable",
|
|
1231
|
+
reason: msg.is_error === true ? "claude reported terminal task failure" : "claude emitted a malformed terminal result frame"
|
|
1232
|
+
})
|
|
1233
|
+
};
|
|
892
1234
|
}
|
|
893
1235
|
var RESULT_DIAGNOSTIC_MAX_CHARS = 2e3;
|
|
894
1236
|
function truncateResultDiagnostic(text) {
|
|
@@ -940,16 +1282,22 @@ var ClaudeProcessClient = class {
|
|
|
940
1282
|
eventQueue = new AsyncQueue();
|
|
941
1283
|
closed = false;
|
|
942
1284
|
exitError;
|
|
1285
|
+
closedPromise;
|
|
1286
|
+
resolveClosed;
|
|
1287
|
+
disposalAttempt;
|
|
943
1288
|
stderrRing = [];
|
|
944
1289
|
unmappedFrameCounts = /* @__PURE__ */ new Map();
|
|
945
1290
|
sessionId;
|
|
946
1291
|
initWaiter;
|
|
947
1292
|
constructor(options) {
|
|
948
1293
|
const spawnFn = options.spawnFn ?? spawn;
|
|
949
|
-
this.child = spawnFn(options.command, options.args, {
|
|
1294
|
+
this.child = spawnFn(options.command, options.args, withOwnedProcessTree({
|
|
950
1295
|
cwd: options.cwd,
|
|
951
1296
|
env: options.env,
|
|
952
1297
|
stdio: ["pipe", "pipe", "pipe"]
|
|
1298
|
+
}));
|
|
1299
|
+
this.closedPromise = new Promise((resolve) => {
|
|
1300
|
+
this.resolveClosed = resolve;
|
|
953
1301
|
});
|
|
954
1302
|
this.child.stdout.setEncoding("utf8");
|
|
955
1303
|
this.child.stdout.on("data", (chunk) => this.onData(chunk));
|
|
@@ -1005,6 +1353,10 @@ var ClaudeProcessClient = class {
|
|
|
1005
1353
|
get events() {
|
|
1006
1354
|
return this.eventQueue;
|
|
1007
1355
|
}
|
|
1356
|
+
/** Local transport diagnostic retained when the process closes; consumers classify it at the session boundary. */
|
|
1357
|
+
get terminalError() {
|
|
1358
|
+
return this.exitError;
|
|
1359
|
+
}
|
|
1008
1360
|
/**
|
|
1009
1361
|
* Record a claude stream-json frame/subtype/content-block label that
|
|
1010
1362
|
* `ClaudeSession`'s event iterator (`../claude-adapter.ts`) decided has
|
|
@@ -1024,15 +1376,30 @@ var ClaudeProcessClient = class {
|
|
|
1024
1376
|
);
|
|
1025
1377
|
}
|
|
1026
1378
|
}
|
|
1027
|
-
/**
|
|
1379
|
+
/** Immediate process-tree termination request. `dispose()` is the settlement receipt. */
|
|
1028
1380
|
kill() {
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1381
|
+
requestOwnedProcessTreeTermination(this.processTreeOptions());
|
|
1382
|
+
}
|
|
1383
|
+
waitClosed() {
|
|
1384
|
+
return this.closedPromise;
|
|
1385
|
+
}
|
|
1386
|
+
dispose() {
|
|
1387
|
+
if (!this.disposalAttempt) {
|
|
1388
|
+
const attempt = disposeOwnedProcessTree(this.processTreeOptions());
|
|
1389
|
+
this.disposalAttempt = attempt.catch((error) => {
|
|
1390
|
+
this.disposalAttempt = void 0;
|
|
1391
|
+
throw error;
|
|
1392
|
+
});
|
|
1035
1393
|
}
|
|
1394
|
+
return this.disposalAttempt;
|
|
1395
|
+
}
|
|
1396
|
+
processTreeOptions() {
|
|
1397
|
+
return {
|
|
1398
|
+
child: this.child,
|
|
1399
|
+
waitClosed: () => this.closedPromise,
|
|
1400
|
+
isClosed: () => this.closed,
|
|
1401
|
+
label: "claude"
|
|
1402
|
+
};
|
|
1036
1403
|
}
|
|
1037
1404
|
onData(chunk) {
|
|
1038
1405
|
this.buffer += chunk;
|
|
@@ -1083,6 +1450,7 @@ var ClaudeProcessClient = class {
|
|
|
1083
1450
|
if (this.closed) return;
|
|
1084
1451
|
this.closed = true;
|
|
1085
1452
|
this.exitError = err;
|
|
1453
|
+
this.resolveClosed();
|
|
1086
1454
|
this.initWaiter?.reject(err);
|
|
1087
1455
|
this.initWaiter = void 0;
|
|
1088
1456
|
this.eventQueue.end();
|
|
@@ -1094,18 +1462,37 @@ var APPROVAL_TOOL_NAME = "approval_prompt";
|
|
|
1094
1462
|
var APPROVAL_MCP_SERVER_NAME = "byokapproval";
|
|
1095
1463
|
var execFileAsync2 = promisify(execFile);
|
|
1096
1464
|
var DETECT_TIMEOUT_MS2 = 5e3;
|
|
1465
|
+
function errorMessage2(err) {
|
|
1466
|
+
return err instanceof Error ? err.message : String(err);
|
|
1467
|
+
}
|
|
1097
1468
|
async function cleanupMcpConfigDir(dir) {
|
|
1098
1469
|
if (!dir) return;
|
|
1099
|
-
|
|
1100
|
-
|
|
1470
|
+
try {
|
|
1471
|
+
await promises.rm(dir, { recursive: true, force: true });
|
|
1472
|
+
} catch (cause) {
|
|
1473
|
+
throw new RuntimeDisposalFailure({
|
|
1474
|
+
stage: "cleanup",
|
|
1475
|
+
reason: "claude task-scoped MCP configuration could not be removed"
|
|
1476
|
+
}, { cause });
|
|
1477
|
+
}
|
|
1101
1478
|
}
|
|
1102
1479
|
var ClaudeAdapter = class {
|
|
1103
1480
|
constructor(options = {}) {
|
|
1104
1481
|
this.options = options;
|
|
1105
1482
|
}
|
|
1106
1483
|
options;
|
|
1107
|
-
|
|
1108
|
-
|
|
1484
|
+
descriptor = freezeRuntimeAdapterDescriptor({
|
|
1485
|
+
id: "claude",
|
|
1486
|
+
supportsDispatchSelection: true,
|
|
1487
|
+
capabilities: {
|
|
1488
|
+
steer: false,
|
|
1489
|
+
resume: true,
|
|
1490
|
+
approvalInteractive: true,
|
|
1491
|
+
mcpToolsets: true,
|
|
1492
|
+
permissionModes: ["auto", "readonly", "plan", "confirm"]
|
|
1493
|
+
},
|
|
1494
|
+
environmentRequirements: { credentialNames: [] }
|
|
1495
|
+
});
|
|
1109
1496
|
async detect() {
|
|
1110
1497
|
const bin = this.resolveBin();
|
|
1111
1498
|
try {
|
|
@@ -1117,54 +1504,73 @@ var ClaudeAdapter = class {
|
|
|
1117
1504
|
return { present: false };
|
|
1118
1505
|
}
|
|
1119
1506
|
}
|
|
1120
|
-
|
|
1507
|
+
async prepare(input) {
|
|
1508
|
+
const mapping = mapPermissionPolicyToClaudeArgs(input.policy);
|
|
1509
|
+
if (!mapping.ok) return { kind: "reject", reason: mapping.reason ?? "policy rejected by claude adapter", retryable: false };
|
|
1510
|
+
let modelId;
|
|
1511
|
+
try {
|
|
1512
|
+
modelId = subscriptionModel(input.offer.dispatchSelection, "claude");
|
|
1513
|
+
} catch (error) {
|
|
1514
|
+
return { kind: "reject", reason: error instanceof Error ? error.message : String(error), retryable: false };
|
|
1515
|
+
}
|
|
1516
|
+
if (mapping.needsApprovalMcp && Object.prototype.hasOwnProperty.call(input.mcpServers ?? {}, APPROVAL_MCP_SERVER_NAME)) {
|
|
1517
|
+
return { kind: "reject", reason: `MCP server name "${APPROVAL_MCP_SERVER_NAME}" is reserved by the claude approval channel`, retryable: false };
|
|
1518
|
+
}
|
|
1519
|
+
let bin;
|
|
1520
|
+
try {
|
|
1521
|
+
bin = this.resolveBin();
|
|
1522
|
+
} catch (error) {
|
|
1523
|
+
return { kind: "reject", reason: error instanceof Error ? error.message : String(error), retryable: true };
|
|
1524
|
+
}
|
|
1525
|
+
let approvalMcpBin;
|
|
1526
|
+
if (mapping.needsApprovalMcp) {
|
|
1527
|
+
try {
|
|
1528
|
+
approvalMcpBin = (this.options.resolveApprovalMcpBin ?? resolveApprovalMcpBin)();
|
|
1529
|
+
} catch (error) {
|
|
1530
|
+
return { kind: "reject", reason: error instanceof Error ? error.message : String(error), retryable: true };
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1121
1533
|
return {
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
permissionModes: ["auto", "readonly", "plan", "confirm"]
|
|
1534
|
+
kind: "prepared",
|
|
1535
|
+
operation: {
|
|
1536
|
+
start: (startInput) => this.startPrepared(startInput, mapping, modelId, bin, approvalMcpBin)
|
|
1537
|
+
}
|
|
1127
1538
|
};
|
|
1128
1539
|
}
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
return { credentialNames: [] };
|
|
1144
|
-
}
|
|
1145
|
-
async start(task, ctx) {
|
|
1146
|
-
if (typeof task.instruction !== "string") {
|
|
1147
|
-
throw new PolicyUnsupportedError("claude adapter only supports string instructions in M2 (no blob-ref fetch yet)");
|
|
1148
|
-
}
|
|
1149
|
-
const mapping = mapPermissionPolicyToClaudeArgs(ctx.policy);
|
|
1150
|
-
if (!mapping.ok) {
|
|
1151
|
-
throw new PolicyUnsupportedError(mapping.reason ?? "policy rejected by claude adapter");
|
|
1540
|
+
async startPrepared(startInput, initialMapping, modelId, bin, approvalMcpBin) {
|
|
1541
|
+
if (!initialMapping.ok) throw new RuntimeExecutionFailure({
|
|
1542
|
+
phase: "start",
|
|
1543
|
+
category: "authority",
|
|
1544
|
+
retry: "non-retryable",
|
|
1545
|
+
reason: "prepared claude permission mapping was invalid"
|
|
1546
|
+
});
|
|
1547
|
+
if (typeof startInput.instruction !== "string") {
|
|
1548
|
+
throw new RuntimeExecutionFailure({
|
|
1549
|
+
phase: "start",
|
|
1550
|
+
category: "authority",
|
|
1551
|
+
retry: "non-retryable",
|
|
1552
|
+
reason: "prepared claude operation requires a resolved string instruction"
|
|
1553
|
+
});
|
|
1152
1554
|
}
|
|
1153
|
-
const
|
|
1555
|
+
const mapping = { ...initialMapping, args: [...initialMapping.args] };
|
|
1154
1556
|
let mcpConfigDir;
|
|
1155
|
-
const taskMcpServers =
|
|
1557
|
+
const taskMcpServers = startInput.mcpServers ?? {};
|
|
1156
1558
|
const needsMcpConfig = mapping.needsApprovalMcp || Object.keys(taskMcpServers).length > 0;
|
|
1157
1559
|
if (mapping.needsApprovalMcp) {
|
|
1158
|
-
if (!
|
|
1159
|
-
throw new
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
`MCP server name "${APPROVAL_MCP_SERVER_NAME}" is reserved by the claude approval channel`
|
|
1166
|
-
);
|
|
1560
|
+
if (!startInput.approvalChannel) {
|
|
1561
|
+
throw new RuntimeExecutionFailure({
|
|
1562
|
+
phase: "start",
|
|
1563
|
+
category: "authority",
|
|
1564
|
+
retry: "non-retryable",
|
|
1565
|
+
reason: 'claude adapter requires policy.mode "confirm" to be started with an approval channel'
|
|
1566
|
+
});
|
|
1167
1567
|
}
|
|
1568
|
+
if (!approvalMcpBin) throw new RuntimeExecutionFailure({
|
|
1569
|
+
phase: "start",
|
|
1570
|
+
category: "authority",
|
|
1571
|
+
retry: "non-retryable",
|
|
1572
|
+
reason: "prepared claude approval MCP binary was not resolved"
|
|
1573
|
+
});
|
|
1168
1574
|
}
|
|
1169
1575
|
if (needsMcpConfig) {
|
|
1170
1576
|
mcpConfigDir = await promises.mkdtemp(path3.join(os.tmpdir(), "byok-mcp-"));
|
|
@@ -1173,12 +1579,23 @@ var ClaudeAdapter = class {
|
|
|
1173
1579
|
const mcpConfigPath = path3.join(mcpConfigDir, "mcp-config.json");
|
|
1174
1580
|
const mcpServers = { ...taskMcpServers };
|
|
1175
1581
|
if (mapping.needsApprovalMcp) {
|
|
1176
|
-
const approvalChannel =
|
|
1177
|
-
if (!approvalChannel) throw new
|
|
1178
|
-
|
|
1582
|
+
const approvalChannel = startInput.approvalChannel;
|
|
1583
|
+
if (!approvalChannel) throw new RuntimeExecutionFailure({
|
|
1584
|
+
phase: "start",
|
|
1585
|
+
category: "authority",
|
|
1586
|
+
retry: "non-retryable",
|
|
1587
|
+
reason: "prepared claude approval channel was not available"
|
|
1588
|
+
});
|
|
1589
|
+
const preparedApprovalMcpBin = approvalMcpBin;
|
|
1590
|
+
if (!preparedApprovalMcpBin) throw new RuntimeExecutionFailure({
|
|
1591
|
+
phase: "start",
|
|
1592
|
+
category: "authority",
|
|
1593
|
+
retry: "non-retryable",
|
|
1594
|
+
reason: "prepared claude approval MCP binary was not resolved"
|
|
1595
|
+
});
|
|
1179
1596
|
mcpServers[APPROVAL_MCP_SERVER_NAME] = {
|
|
1180
|
-
command:
|
|
1181
|
-
args:
|
|
1597
|
+
command: preparedApprovalMcpBin.command,
|
|
1598
|
+
args: preparedApprovalMcpBin.args,
|
|
1182
1599
|
env: {
|
|
1183
1600
|
BYOK_STORE_DIR: approvalChannel.storeDir,
|
|
1184
1601
|
BYOK_PRODUCT_ID: approvalChannel.productId,
|
|
@@ -1198,8 +1615,26 @@ var ClaudeAdapter = class {
|
|
|
1198
1615
|
"--strict-mcp-config"
|
|
1199
1616
|
];
|
|
1200
1617
|
}
|
|
1201
|
-
const
|
|
1202
|
-
|
|
1618
|
+
const resumeSessionId = startInput.manifest.sessionRef;
|
|
1619
|
+
let manifestModelId;
|
|
1620
|
+
try {
|
|
1621
|
+
manifestModelId = subscriptionModel(startInput.manifest.dispatchSelection, "claude");
|
|
1622
|
+
} catch (cause) {
|
|
1623
|
+
throw new RuntimeExecutionFailure({
|
|
1624
|
+
phase: "start",
|
|
1625
|
+
category: "authority",
|
|
1626
|
+
retry: "non-retryable",
|
|
1627
|
+
reason: "prepared claude operation received an invalid runtime selection manifest"
|
|
1628
|
+
}, { cause });
|
|
1629
|
+
}
|
|
1630
|
+
if (manifestModelId !== modelId) {
|
|
1631
|
+
throw new RuntimeExecutionFailure({
|
|
1632
|
+
phase: "start",
|
|
1633
|
+
category: "authority",
|
|
1634
|
+
retry: "non-retryable",
|
|
1635
|
+
reason: "prepared claude operation received a manifest with different runtime selection"
|
|
1636
|
+
});
|
|
1637
|
+
}
|
|
1203
1638
|
const args = [
|
|
1204
1639
|
"-p",
|
|
1205
1640
|
"--input-format",
|
|
@@ -1211,40 +1646,71 @@ var ClaudeAdapter = class {
|
|
|
1211
1646
|
// "Error: When using --print, --output-format=stream-json requires
|
|
1212
1647
|
// --verbose", before spawning any model call.
|
|
1213
1648
|
"--verbose",
|
|
1214
|
-
...
|
|
1649
|
+
...manifestModelId ? ["--model", manifestModelId] : [],
|
|
1215
1650
|
...resumeSessionId ? ["--resume", resumeSessionId] : [],
|
|
1216
1651
|
...mapping.args
|
|
1217
1652
|
];
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1653
|
+
let client;
|
|
1654
|
+
try {
|
|
1655
|
+
client = new ClaudeProcessClient({
|
|
1656
|
+
command: bin.command,
|
|
1657
|
+
args,
|
|
1658
|
+
cwd: startInput.manifest.workspace.workspaceDir,
|
|
1659
|
+
env: withoutProviderCredentials(startInput.env),
|
|
1660
|
+
spawnFn: this.options.spawnFn
|
|
1661
|
+
});
|
|
1662
|
+
} catch (cause) {
|
|
1663
|
+
await cleanupMcpConfigDir(mcpConfigDir);
|
|
1664
|
+
throw new RuntimeExecutionFailure({
|
|
1665
|
+
phase: "start",
|
|
1666
|
+
category: "infrastructure",
|
|
1667
|
+
retry: "retryable",
|
|
1668
|
+
reason: "claude runtime process could not be spawned"
|
|
1669
|
+
}, { cause });
|
|
1670
|
+
}
|
|
1671
|
+
try {
|
|
1672
|
+
client.writeUserMessage(startInput.instruction);
|
|
1673
|
+
} catch (cause) {
|
|
1674
|
+
client.kill();
|
|
1675
|
+
await cleanupMcpConfigDir(mcpConfigDir);
|
|
1676
|
+
throw new RuntimeExecutionFailure({
|
|
1677
|
+
phase: "start",
|
|
1678
|
+
category: "infrastructure",
|
|
1679
|
+
retry: "retryable",
|
|
1680
|
+
reason: "claude initial instruction transport failed"
|
|
1681
|
+
}, { cause });
|
|
1682
|
+
}
|
|
1226
1683
|
let sessionRef;
|
|
1227
1684
|
try {
|
|
1228
1685
|
sessionRef = await client.waitForInit();
|
|
1229
1686
|
} catch (err) {
|
|
1230
1687
|
client.kill();
|
|
1231
1688
|
await cleanupMcpConfigDir(mcpConfigDir);
|
|
1232
|
-
throw err;
|
|
1689
|
+
if (isRuntimeExecutionFailure(err)) throw err;
|
|
1690
|
+
throw new RuntimeExecutionFailure({
|
|
1691
|
+
phase: "start",
|
|
1692
|
+
category: "infrastructure",
|
|
1693
|
+
retry: "retryable",
|
|
1694
|
+
reason: `claude exited before yielding an authoritative session id: ${errorMessage2(err)}`
|
|
1695
|
+
}, { cause: err });
|
|
1233
1696
|
}
|
|
1234
1697
|
if (resumeSessionId !== void 0 && sessionRef !== resumeSessionId) {
|
|
1235
1698
|
client.kill();
|
|
1236
1699
|
await cleanupMcpConfigDir(mcpConfigDir);
|
|
1237
|
-
throw new
|
|
1238
|
-
|
|
1239
|
-
|
|
1700
|
+
throw new RuntimeExecutionFailure({
|
|
1701
|
+
phase: "start",
|
|
1702
|
+
category: "authority",
|
|
1703
|
+
retry: "non-retryable",
|
|
1704
|
+
reason: `claude --resume echoed a different session id than requested (requested ${resumeSessionId}, got ${sessionRef})`
|
|
1705
|
+
});
|
|
1240
1706
|
}
|
|
1241
1707
|
return new ClaudeSession(
|
|
1242
1708
|
sessionRef,
|
|
1243
1709
|
client,
|
|
1244
|
-
|
|
1245
|
-
|
|
1710
|
+
startInput.manifest.workspace.workspaceDir,
|
|
1711
|
+
startInput.approvalChannel,
|
|
1246
1712
|
mcpConfigDir,
|
|
1247
|
-
|
|
1713
|
+
manifestModelId
|
|
1248
1714
|
);
|
|
1249
1715
|
}
|
|
1250
1716
|
/**
|
|
@@ -1279,8 +1745,7 @@ var ClaudeAdapter = class {
|
|
|
1279
1745
|
return (this.options.resolveBin ?? resolveClaudeBin)();
|
|
1280
1746
|
}
|
|
1281
1747
|
};
|
|
1282
|
-
function subscriptionModel(
|
|
1283
|
-
const selection = task.dispatchSelection;
|
|
1748
|
+
function subscriptionModel(selection, runtimeId) {
|
|
1284
1749
|
if (selection === void 0) return void 0;
|
|
1285
1750
|
if (selection.lane !== "subscription" || selection.runtimeId !== runtimeId) {
|
|
1286
1751
|
throw new PolicyUnsupportedError(
|
|
@@ -1305,6 +1770,7 @@ var ClaudeSession = class {
|
|
|
1305
1770
|
mcpConfigDir;
|
|
1306
1771
|
modelId;
|
|
1307
1772
|
correlation = createToolUseCorrelation();
|
|
1773
|
+
closeAttempt;
|
|
1308
1774
|
get events() {
|
|
1309
1775
|
const client = this.client;
|
|
1310
1776
|
const correlation = this.correlation;
|
|
@@ -1313,17 +1779,40 @@ var ClaudeSession = class {
|
|
|
1313
1779
|
[Symbol.asyncIterator]() {
|
|
1314
1780
|
const inner = client.events[Symbol.asyncIterator]();
|
|
1315
1781
|
let pending = [];
|
|
1782
|
+
let terminalFailure;
|
|
1316
1783
|
let turnSettled = false;
|
|
1317
1784
|
return {
|
|
1318
1785
|
async next() {
|
|
1319
1786
|
for (; ; ) {
|
|
1320
1787
|
const buffered = pending.shift();
|
|
1321
1788
|
if (buffered) return { value: buffered, done: false };
|
|
1322
|
-
if (turnSettled)
|
|
1323
|
-
|
|
1324
|
-
|
|
1789
|
+
if (turnSettled) {
|
|
1790
|
+
if (terminalFailure) throw terminalFailure;
|
|
1791
|
+
return { value: void 0, done: true };
|
|
1792
|
+
}
|
|
1793
|
+
let raw;
|
|
1794
|
+
try {
|
|
1795
|
+
raw = await inner.next();
|
|
1796
|
+
} catch (cause) {
|
|
1797
|
+
throw new RuntimeExecutionFailure({
|
|
1798
|
+
phase: "run",
|
|
1799
|
+
category: "infrastructure",
|
|
1800
|
+
retry: "retryable",
|
|
1801
|
+
reason: "claude runtime event transport failed"
|
|
1802
|
+
}, { cause });
|
|
1803
|
+
}
|
|
1804
|
+
const { value, done } = raw;
|
|
1805
|
+
if (done) {
|
|
1806
|
+
throw new RuntimeExecutionFailure({
|
|
1807
|
+
phase: "run",
|
|
1808
|
+
category: "infrastructure",
|
|
1809
|
+
retry: "retryable",
|
|
1810
|
+
reason: "claude runtime process ended before a terminal result frame"
|
|
1811
|
+
}, { cause: client.terminalError });
|
|
1812
|
+
}
|
|
1325
1813
|
if (value.type === "result") turnSettled = true;
|
|
1326
1814
|
const mapped = mapClaudeMessageToAgentEvents(value, correlation, { workspaceDir });
|
|
1815
|
+
terminalFailure = mapped.terminalFailure ?? terminalFailure;
|
|
1327
1816
|
if (mapped.unmappedLabel) {
|
|
1328
1817
|
client.recordUnmappedFrame(mapped.unmappedLabel);
|
|
1329
1818
|
}
|
|
@@ -1354,7 +1843,7 @@ var ClaudeSession = class {
|
|
|
1354
1843
|
if (typeof task.instruction !== "string") {
|
|
1355
1844
|
throw new PolicyUnsupportedError("claude adapter only supports string instructions in M2 (no blob-ref fetch yet)");
|
|
1356
1845
|
}
|
|
1357
|
-
const requestedModel = subscriptionModel(task, "claude");
|
|
1846
|
+
const requestedModel = subscriptionModel(task.dispatchSelection, "claude");
|
|
1358
1847
|
if (requestedModel !== void 0 && requestedModel !== this.modelId) {
|
|
1359
1848
|
throw new PolicyUnsupportedError(
|
|
1360
1849
|
`claude persistent session cannot change model from ${this.modelId ?? "(legacy default)"} to ${requestedModel}`
|
|
@@ -1378,8 +1867,17 @@ var ClaudeSession = class {
|
|
|
1378
1867
|
this.client.kill();
|
|
1379
1868
|
}
|
|
1380
1869
|
async close() {
|
|
1381
|
-
this.
|
|
1382
|
-
|
|
1870
|
+
if (!this.closeAttempt) {
|
|
1871
|
+
const attempt = (async () => {
|
|
1872
|
+
await this.client.dispose();
|
|
1873
|
+
await cleanupMcpConfigDir(this.mcpConfigDir);
|
|
1874
|
+
})();
|
|
1875
|
+
this.closeAttempt = attempt.catch((error) => {
|
|
1876
|
+
this.closeAttempt = void 0;
|
|
1877
|
+
throw error;
|
|
1878
|
+
});
|
|
1879
|
+
}
|
|
1880
|
+
await this.closeAttempt;
|
|
1383
1881
|
}
|
|
1384
1882
|
/**
|
|
1385
1883
|
* M4 Phase 3: routes into the out-of-band approval channel `start()`
|
|
@@ -1596,14 +2094,15 @@ var CodexProcessRunner = class {
|
|
|
1596
2094
|
exitSignal = null;
|
|
1597
2095
|
closedPromise;
|
|
1598
2096
|
resolveClosed;
|
|
2097
|
+
disposalAttempt;
|
|
1599
2098
|
constructor(options) {
|
|
1600
2099
|
this.onEvent = options.onEvent;
|
|
1601
2100
|
const spawnFn = options.spawnFn ?? spawn;
|
|
1602
|
-
this.child = spawnFn(options.command, options.args, {
|
|
2101
|
+
this.child = spawnFn(options.command, options.args, withOwnedProcessTree({
|
|
1603
2102
|
cwd: options.cwd,
|
|
1604
2103
|
env: options.env,
|
|
1605
2104
|
stdio: ["ignore", "pipe", "pipe"]
|
|
1606
|
-
});
|
|
2105
|
+
}));
|
|
1607
2106
|
this.closedPromise = new Promise((resolve) => {
|
|
1608
2107
|
this.resolveClosed = resolve;
|
|
1609
2108
|
});
|
|
@@ -1633,7 +2132,7 @@ var CodexProcessRunner = class {
|
|
|
1633
2132
|
return this.closed;
|
|
1634
2133
|
}
|
|
1635
2134
|
/**
|
|
1636
|
-
*
|
|
2135
|
+
* Immediate tree termination request. SIGTERM on POSIX: SIGINT was empirically confirmed
|
|
1637
2136
|
* to be silently ignored by `codex exec` (a real, direct test — a 60s
|
|
1638
2137
|
* shell `sleep` ran to full, unaffected completion despite SIGINT sent at
|
|
1639
2138
|
* t=4s) — a genuine, evidence-based correction to this task's own initial
|
|
@@ -1646,13 +2145,25 @@ var CodexProcessRunner = class {
|
|
|
1646
2145
|
* `../pi/rpc-client.ts`'s own cross-platform convention.
|
|
1647
2146
|
*/
|
|
1648
2147
|
kill() {
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
this.
|
|
2148
|
+
requestOwnedProcessTreeTermination(this.processTreeOptions());
|
|
2149
|
+
}
|
|
2150
|
+
dispose() {
|
|
2151
|
+
if (!this.disposalAttempt) {
|
|
2152
|
+
const attempt = disposeOwnedProcessTree(this.processTreeOptions());
|
|
2153
|
+
this.disposalAttempt = attempt.catch((error) => {
|
|
2154
|
+
this.disposalAttempt = void 0;
|
|
2155
|
+
throw error;
|
|
2156
|
+
});
|
|
1655
2157
|
}
|
|
2158
|
+
return this.disposalAttempt;
|
|
2159
|
+
}
|
|
2160
|
+
processTreeOptions() {
|
|
2161
|
+
return {
|
|
2162
|
+
child: this.child,
|
|
2163
|
+
waitClosed: () => this.closedPromise,
|
|
2164
|
+
isClosed: () => this.closed,
|
|
2165
|
+
label: "codex"
|
|
2166
|
+
};
|
|
1656
2167
|
}
|
|
1657
2168
|
/** 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. */
|
|
1658
2169
|
buildExitError(context) {
|
|
@@ -1703,8 +2214,17 @@ var CodexAdapter = class {
|
|
|
1703
2214
|
this.options = options;
|
|
1704
2215
|
}
|
|
1705
2216
|
options;
|
|
1706
|
-
|
|
1707
|
-
|
|
2217
|
+
descriptor = freezeRuntimeAdapterDescriptor({
|
|
2218
|
+
id: "codex",
|
|
2219
|
+
supportsDispatchSelection: true,
|
|
2220
|
+
capabilities: {
|
|
2221
|
+
steer: false,
|
|
2222
|
+
resume: true,
|
|
2223
|
+
approvalInteractive: false,
|
|
2224
|
+
permissionModes: ["auto", "readonly"]
|
|
2225
|
+
},
|
|
2226
|
+
environmentRequirements: { credentialNames: [] }
|
|
2227
|
+
});
|
|
1708
2228
|
async detect() {
|
|
1709
2229
|
const bin = this.resolveBin();
|
|
1710
2230
|
try {
|
|
@@ -1753,61 +2273,100 @@ ${result.stderr}`);
|
|
|
1753
2273
|
${withStreams.stderr ?? ""}`);
|
|
1754
2274
|
}
|
|
1755
2275
|
}
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
* API-key passthrough remains a separate, pending product decision. No
|
|
1765
|
-
* `baseNames` either: nothing in this adapter reads a codex-specific
|
|
1766
|
-
* config-discovery variable (e.g. `CODEX_HOME`) today.
|
|
1767
|
-
*/
|
|
1768
|
-
environmentRequirements() {
|
|
1769
|
-
return { credentialNames: [] };
|
|
1770
|
-
}
|
|
1771
|
-
async start(task, ctx) {
|
|
1772
|
-
if (typeof task.instruction !== "string") {
|
|
1773
|
-
throw new PolicyUnsupportedError("codex adapter only supports string instructions in M2 (no blob-ref fetch yet)");
|
|
2276
|
+
async prepare(input) {
|
|
2277
|
+
const mapping = mapPermissionPolicyToCodexArgs(input.policy);
|
|
2278
|
+
if (!mapping.ok) return { kind: "reject", reason: mapping.reason ?? "policy rejected by codex adapter", retryable: false };
|
|
2279
|
+
let modelId;
|
|
2280
|
+
try {
|
|
2281
|
+
modelId = subscriptionModel2(input.offer.dispatchSelection);
|
|
2282
|
+
} catch (error) {
|
|
2283
|
+
return { kind: "reject", reason: error instanceof Error ? error.message : String(error), retryable: false };
|
|
1774
2284
|
}
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
2285
|
+
let command;
|
|
2286
|
+
try {
|
|
2287
|
+
command = this.resolveBin().command;
|
|
2288
|
+
} catch (error) {
|
|
2289
|
+
return { kind: "reject", reason: error instanceof Error ? error.message : String(error), retryable: true };
|
|
2290
|
+
}
|
|
2291
|
+
return {
|
|
2292
|
+
kind: "prepared",
|
|
2293
|
+
operation: {
|
|
2294
|
+
start: (startInput) => this.startPrepared(startInput, mapping.args, modelId, command)
|
|
2295
|
+
}
|
|
2296
|
+
};
|
|
2297
|
+
}
|
|
2298
|
+
async startPrepared(startInput, policyArgs, modelId, command) {
|
|
2299
|
+
if (typeof startInput.instruction !== "string") {
|
|
2300
|
+
throw new RuntimeExecutionFailure({
|
|
2301
|
+
phase: "start",
|
|
2302
|
+
category: "authority",
|
|
2303
|
+
retry: "non-retryable",
|
|
2304
|
+
reason: "prepared codex operation requires a resolved string instruction"
|
|
2305
|
+
});
|
|
1778
2306
|
}
|
|
1779
|
-
const modelId = subscriptionModel2(task);
|
|
1780
|
-
const bin = this.resolveBin();
|
|
1781
2307
|
const queue = new AsyncQueue();
|
|
2308
|
+
const terminal = {};
|
|
1782
2309
|
const recordUnmapped = makeUnmappedFrameRecorder(/* @__PURE__ */ new Map());
|
|
1783
|
-
|
|
1784
|
-
|
|
2310
|
+
let workspaceDir;
|
|
2311
|
+
try {
|
|
2312
|
+
workspaceDir = await resolveRealWorkspaceDir(startInput.manifest.workspace.workspaceDir);
|
|
2313
|
+
} catch (cause) {
|
|
2314
|
+
throw new RuntimeExecutionFailure({
|
|
2315
|
+
phase: "start",
|
|
2316
|
+
category: "infrastructure",
|
|
2317
|
+
retry: "retryable",
|
|
2318
|
+
reason: "codex runtime workspace could not be resolved"
|
|
2319
|
+
}, { cause });
|
|
2320
|
+
}
|
|
2321
|
+
const runtimeEnv = withoutProviderCredentials(startInput.env);
|
|
2322
|
+
let manifestModelId;
|
|
2323
|
+
try {
|
|
2324
|
+
manifestModelId = subscriptionModel2(startInput.manifest.dispatchSelection);
|
|
2325
|
+
} catch (cause) {
|
|
2326
|
+
throw new RuntimeExecutionFailure({
|
|
2327
|
+
phase: "start",
|
|
2328
|
+
category: "authority",
|
|
2329
|
+
retry: "non-retryable",
|
|
2330
|
+
reason: "prepared codex operation received an invalid runtime selection manifest"
|
|
2331
|
+
}, { cause });
|
|
2332
|
+
}
|
|
2333
|
+
if (manifestModelId !== modelId) {
|
|
2334
|
+
throw new RuntimeExecutionFailure({
|
|
2335
|
+
phase: "start",
|
|
2336
|
+
category: "authority",
|
|
2337
|
+
retry: "non-retryable",
|
|
2338
|
+
reason: "prepared codex operation received a manifest with different runtime selection"
|
|
2339
|
+
});
|
|
2340
|
+
}
|
|
1785
2341
|
const { sessionRef, runner } = await runCodexTurn({
|
|
1786
|
-
command
|
|
1787
|
-
resumeRef:
|
|
1788
|
-
instruction:
|
|
1789
|
-
modelId,
|
|
1790
|
-
policyArgs:
|
|
1791
|
-
cwd:
|
|
2342
|
+
command,
|
|
2343
|
+
resumeRef: startInput.manifest.sessionRef,
|
|
2344
|
+
instruction: startInput.instruction,
|
|
2345
|
+
modelId: manifestModelId,
|
|
2346
|
+
policyArgs: [...policyArgs],
|
|
2347
|
+
cwd: startInput.manifest.workspace.workspaceDir,
|
|
1792
2348
|
env: runtimeEnv,
|
|
1793
2349
|
spawnFn: this.options.spawnFn,
|
|
1794
2350
|
workspaceDir,
|
|
1795
2351
|
queue,
|
|
1796
2352
|
recordUnmapped,
|
|
1797
|
-
expectedSessionRef:
|
|
1798
|
-
preparedGit:
|
|
2353
|
+
expectedSessionRef: startInput.manifest.sessionRef,
|
|
2354
|
+
preparedGit: startInput.manifest.workspace.workspaceId !== void 0,
|
|
2355
|
+
failurePhase: "start",
|
|
2356
|
+
terminal
|
|
1799
2357
|
});
|
|
1800
2358
|
return new CodexSession({
|
|
1801
2359
|
sessionRef,
|
|
1802
|
-
command
|
|
2360
|
+
command,
|
|
1803
2361
|
workspaceDir,
|
|
1804
|
-
env:
|
|
2362
|
+
env: startInput.env,
|
|
1805
2363
|
spawnFn: this.options.spawnFn,
|
|
1806
2364
|
queue,
|
|
1807
2365
|
recordUnmapped,
|
|
1808
2366
|
initialRunner: runner,
|
|
1809
|
-
preparedGit:
|
|
1810
|
-
modelId
|
|
2367
|
+
preparedGit: startInput.manifest.workspace.workspaceId !== void 0,
|
|
2368
|
+
modelId: manifestModelId,
|
|
2369
|
+
terminal
|
|
1811
2370
|
});
|
|
1812
2371
|
}
|
|
1813
2372
|
resolveBin() {
|
|
@@ -1855,37 +2414,69 @@ async function runCodexTurn(params) {
|
|
|
1855
2414
|
rejectFirstLine = reject;
|
|
1856
2415
|
});
|
|
1857
2416
|
let turnEnded = false;
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
if (
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
2417
|
+
let runner;
|
|
2418
|
+
try {
|
|
2419
|
+
runner = new CodexProcessRunner({
|
|
2420
|
+
command: params.command,
|
|
2421
|
+
args: argv,
|
|
2422
|
+
cwd: params.cwd,
|
|
2423
|
+
env: params.env,
|
|
2424
|
+
spawnFn: params.spawnFn,
|
|
2425
|
+
onEvent: (evt) => {
|
|
2426
|
+
if (!firstLineSettled) {
|
|
2427
|
+
firstLineSettled = true;
|
|
2428
|
+
if (evt.type === "thread.started" && typeof evt.thread_id === "string" && evt.thread_id.length > 0) {
|
|
2429
|
+
resolveFirstLine(evt.thread_id);
|
|
2430
|
+
} else {
|
|
2431
|
+
rejectFirstLine(
|
|
2432
|
+
new RuntimeExecutionFailure({
|
|
2433
|
+
phase: params.failurePhase,
|
|
2434
|
+
category: "authority",
|
|
2435
|
+
retry: "non-retryable",
|
|
2436
|
+
reason: `codex did not yield thread.started as its first event (got ${JSON.stringify(evt).slice(0, 200)})`
|
|
2437
|
+
})
|
|
2438
|
+
);
|
|
2439
|
+
}
|
|
2440
|
+
return;
|
|
2441
|
+
}
|
|
2442
|
+
const mapped = mapCodexEventToAgentEvents(evt, params.workspaceDir);
|
|
2443
|
+
for (const agentEvent of mapped) {
|
|
2444
|
+
if (agentEvent.type === "turn_end") turnEnded = true;
|
|
2445
|
+
params.queue.push(agentEvent);
|
|
2446
|
+
}
|
|
2447
|
+
if (evt.type === "turn.failed") {
|
|
2448
|
+
params.terminal.failure = new RuntimeExecutionFailure({
|
|
2449
|
+
phase: "run",
|
|
2450
|
+
category: "semantic",
|
|
2451
|
+
retry: "non-retryable",
|
|
2452
|
+
reason: "codex reported terminal task failure"
|
|
2453
|
+
});
|
|
2454
|
+
params.queue.end();
|
|
2455
|
+
}
|
|
2456
|
+
if (mapped.length === 0 && !isRoutineCodexEvent(evt)) {
|
|
2457
|
+
params.recordUnmapped(unmappedFrameKey(evt));
|
|
1873
2458
|
}
|
|
1874
|
-
return;
|
|
1875
|
-
}
|
|
1876
|
-
const mapped = mapCodexEventToAgentEvents(evt, params.workspaceDir);
|
|
1877
|
-
for (const agentEvent of mapped) {
|
|
1878
|
-
if (agentEvent.type === "turn_end") turnEnded = true;
|
|
1879
|
-
params.queue.push(agentEvent);
|
|
1880
|
-
}
|
|
1881
|
-
if (mapped.length === 0 && !isRoutineCodexEvent(evt)) {
|
|
1882
|
-
params.recordUnmapped(unmappedFrameKey(evt));
|
|
1883
2459
|
}
|
|
1884
|
-
}
|
|
1885
|
-
})
|
|
2460
|
+
});
|
|
2461
|
+
} catch (cause) {
|
|
2462
|
+
throw new RuntimeExecutionFailure({
|
|
2463
|
+
phase: params.failurePhase,
|
|
2464
|
+
category: "infrastructure",
|
|
2465
|
+
retry: "retryable",
|
|
2466
|
+
reason: "codex runtime process could not be spawned"
|
|
2467
|
+
}, { cause });
|
|
2468
|
+
}
|
|
2469
|
+
params.onRunnerCreated?.(runner);
|
|
1886
2470
|
void runner.waitClosed().then(() => {
|
|
1887
|
-
if (turnEnded) return;
|
|
1888
|
-
|
|
2471
|
+
if (turnEnded || params.terminal.failure) return;
|
|
2472
|
+
const cause = runner.buildExitError("codex exited without completing the turn");
|
|
2473
|
+
params.terminal.failure = new RuntimeExecutionFailure({
|
|
2474
|
+
phase: "run",
|
|
2475
|
+
category: "infrastructure",
|
|
2476
|
+
retry: "retryable",
|
|
2477
|
+
reason: cause.message
|
|
2478
|
+
}, { cause });
|
|
2479
|
+
params.queue.push({ type: "error", message: cause.message });
|
|
1889
2480
|
params.queue.end();
|
|
1890
2481
|
});
|
|
1891
2482
|
let sessionRef;
|
|
@@ -1909,7 +2500,13 @@ async function runCodexTurn(params) {
|
|
|
1909
2500
|
void runner.waitClosed().then(() => {
|
|
1910
2501
|
if (!settled) {
|
|
1911
2502
|
settled = true;
|
|
1912
|
-
|
|
2503
|
+
const cause = runner.buildExitError("codex exited before yielding an authoritative thread id");
|
|
2504
|
+
reject(new RuntimeExecutionFailure({
|
|
2505
|
+
phase: params.failurePhase,
|
|
2506
|
+
category: "infrastructure",
|
|
2507
|
+
retry: "retryable",
|
|
2508
|
+
reason: cause.message
|
|
2509
|
+
}, { cause }));
|
|
1913
2510
|
}
|
|
1914
2511
|
});
|
|
1915
2512
|
});
|
|
@@ -1919,9 +2516,12 @@ async function runCodexTurn(params) {
|
|
|
1919
2516
|
}
|
|
1920
2517
|
if (params.expectedSessionRef !== void 0 && sessionRef !== params.expectedSessionRef) {
|
|
1921
2518
|
runner.kill();
|
|
1922
|
-
throw new
|
|
1923
|
-
|
|
1924
|
-
|
|
2519
|
+
throw new RuntimeExecutionFailure({
|
|
2520
|
+
phase: params.failurePhase,
|
|
2521
|
+
category: "authority",
|
|
2522
|
+
retry: "non-retryable",
|
|
2523
|
+
reason: `codex exec resume echoed a different thread id than requested (requested ${params.expectedSessionRef}, got ${sessionRef})`
|
|
2524
|
+
});
|
|
1925
2525
|
}
|
|
1926
2526
|
return { sessionRef, runner };
|
|
1927
2527
|
}
|
|
@@ -1948,8 +2548,12 @@ var CodexSession = class {
|
|
|
1948
2548
|
recordUnmapped;
|
|
1949
2549
|
preparedGit;
|
|
1950
2550
|
modelId;
|
|
2551
|
+
terminal;
|
|
1951
2552
|
currentRunner;
|
|
2553
|
+
ownedRunners = /* @__PURE__ */ new Set();
|
|
2554
|
+
followUpAttempts = /* @__PURE__ */ new Set();
|
|
1952
2555
|
closed = false;
|
|
2556
|
+
closeAttempt;
|
|
1953
2557
|
constructor(options) {
|
|
1954
2558
|
this.sessionRef = options.sessionRef;
|
|
1955
2559
|
this.command = options.command;
|
|
@@ -1960,11 +2564,36 @@ var CodexSession = class {
|
|
|
1960
2564
|
this.recordUnmapped = options.recordUnmapped;
|
|
1961
2565
|
this.preparedGit = options.preparedGit;
|
|
1962
2566
|
this.modelId = options.modelId;
|
|
2567
|
+
this.terminal = options.terminal;
|
|
1963
2568
|
this.currentRunner = options.initialRunner;
|
|
2569
|
+
this.ownedRunners.add(options.initialRunner);
|
|
1964
2570
|
void this.forgetRunnerOnceClosed(options.initialRunner);
|
|
1965
2571
|
}
|
|
1966
2572
|
get events() {
|
|
1967
|
-
|
|
2573
|
+
const queue = this.queue;
|
|
2574
|
+
const session = this;
|
|
2575
|
+
return {
|
|
2576
|
+
[Symbol.asyncIterator]() {
|
|
2577
|
+
const inner = queue[Symbol.asyncIterator]();
|
|
2578
|
+
return {
|
|
2579
|
+
async next() {
|
|
2580
|
+
let result;
|
|
2581
|
+
try {
|
|
2582
|
+
result = await inner.next();
|
|
2583
|
+
} catch (cause) {
|
|
2584
|
+
throw new RuntimeExecutionFailure({
|
|
2585
|
+
phase: "run",
|
|
2586
|
+
category: "infrastructure",
|
|
2587
|
+
retry: "retryable",
|
|
2588
|
+
reason: "codex runtime event transport failed"
|
|
2589
|
+
}, { cause });
|
|
2590
|
+
}
|
|
2591
|
+
if (result.done && session.terminal.failure) throw session.terminal.failure;
|
|
2592
|
+
return result;
|
|
2593
|
+
}
|
|
2594
|
+
};
|
|
2595
|
+
}
|
|
2596
|
+
};
|
|
1968
2597
|
}
|
|
1969
2598
|
async forgetRunnerOnceClosed(runner) {
|
|
1970
2599
|
await runner.waitClosed();
|
|
@@ -2009,7 +2638,14 @@ var CodexSession = class {
|
|
|
2009
2638
|
* stale id even after codex had moved on) — it just can now only ever be
|
|
2010
2639
|
* the SAME id this call asked to resume, never a silently-different one.
|
|
2011
2640
|
*/
|
|
2012
|
-
|
|
2641
|
+
followUp(task) {
|
|
2642
|
+
const attempt = this.runFollowUp(task);
|
|
2643
|
+
this.followUpAttempts.add(attempt);
|
|
2644
|
+
void attempt.finally(() => this.followUpAttempts.delete(attempt)).catch(() => {
|
|
2645
|
+
});
|
|
2646
|
+
return attempt;
|
|
2647
|
+
}
|
|
2648
|
+
async runFollowUp(task) {
|
|
2013
2649
|
if (typeof task.instruction !== "string") {
|
|
2014
2650
|
throw new PolicyUnsupportedError("codex adapter only supports string instructions in M2 (no blob-ref fetch yet)");
|
|
2015
2651
|
}
|
|
@@ -2020,7 +2656,7 @@ var CodexSession = class {
|
|
|
2020
2656
|
if (!mapping.ok) {
|
|
2021
2657
|
throw new PolicyUnsupportedError(mapping.reason ?? "policy rejected by codex adapter");
|
|
2022
2658
|
}
|
|
2023
|
-
const requestedModel = subscriptionModel2(task);
|
|
2659
|
+
const requestedModel = subscriptionModel2(task.dispatchSelection);
|
|
2024
2660
|
if (requestedModel !== void 0 && requestedModel !== this.modelId) {
|
|
2025
2661
|
throw new PolicyUnsupportedError(
|
|
2026
2662
|
`codex persistent session cannot change model from ${this.modelId ?? "(legacy default)"} to ${requestedModel}`
|
|
@@ -2030,6 +2666,7 @@ var CodexSession = class {
|
|
|
2030
2666
|
const resumeRef = this.sessionRef;
|
|
2031
2667
|
let sessionRef;
|
|
2032
2668
|
let runner;
|
|
2669
|
+
const terminal = {};
|
|
2033
2670
|
try {
|
|
2034
2671
|
({ sessionRef, runner } = await runCodexTurn({
|
|
2035
2672
|
command: this.command,
|
|
@@ -2044,25 +2681,54 @@ var CodexSession = class {
|
|
|
2044
2681
|
queue: this.queue,
|
|
2045
2682
|
recordUnmapped: this.recordUnmapped,
|
|
2046
2683
|
expectedSessionRef: resumeRef,
|
|
2047
|
-
preparedGit: this.preparedGit
|
|
2684
|
+
preparedGit: this.preparedGit,
|
|
2685
|
+
failurePhase: "run",
|
|
2686
|
+
terminal,
|
|
2687
|
+
onRunnerCreated: (created) => {
|
|
2688
|
+
this.ownedRunners.add(created);
|
|
2689
|
+
void this.forgetRunnerOnceClosed(created);
|
|
2690
|
+
}
|
|
2048
2691
|
}));
|
|
2049
2692
|
} catch (err) {
|
|
2050
2693
|
this.queue.end();
|
|
2694
|
+
this.terminal = {
|
|
2695
|
+
failure: isRuntimeExecutionFailure(err) ? err : new RuntimeExecutionFailure({
|
|
2696
|
+
phase: "run",
|
|
2697
|
+
category: "authority",
|
|
2698
|
+
retry: "non-retryable",
|
|
2699
|
+
reason: "codex follow-up violated the runtime adapter contract"
|
|
2700
|
+
}, { cause: err })
|
|
2701
|
+
};
|
|
2051
2702
|
throw err;
|
|
2052
2703
|
}
|
|
2704
|
+
if (this.closed) {
|
|
2705
|
+
await runner.dispose();
|
|
2706
|
+
throw new Error("codex session closed while follow-up was starting");
|
|
2707
|
+
}
|
|
2708
|
+
this.terminal = terminal;
|
|
2053
2709
|
this.sessionRef = sessionRef;
|
|
2054
2710
|
this.currentRunner = runner;
|
|
2055
|
-
void this.forgetRunnerOnceClosed(runner);
|
|
2056
2711
|
}
|
|
2057
2712
|
/** 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. */
|
|
2058
2713
|
async interrupt() {
|
|
2059
2714
|
this.currentRunner?.kill();
|
|
2060
2715
|
}
|
|
2061
2716
|
async close() {
|
|
2062
|
-
if (this.
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2717
|
+
if (!this.closeAttempt) {
|
|
2718
|
+
this.closed = true;
|
|
2719
|
+
this.queue.end();
|
|
2720
|
+
const attempt = (async () => {
|
|
2721
|
+
const runners = [...this.ownedRunners];
|
|
2722
|
+
await Promise.all(runners.map((runner) => runner.dispose()));
|
|
2723
|
+
for (const runner of runners) this.ownedRunners.delete(runner);
|
|
2724
|
+
await Promise.allSettled([...this.followUpAttempts]);
|
|
2725
|
+
})();
|
|
2726
|
+
this.closeAttempt = attempt.catch((error) => {
|
|
2727
|
+
this.closeAttempt = void 0;
|
|
2728
|
+
throw error;
|
|
2729
|
+
});
|
|
2730
|
+
}
|
|
2731
|
+
await this.closeAttempt;
|
|
2066
2732
|
}
|
|
2067
2733
|
/**
|
|
2068
2734
|
* `codex exec` has no in-band channel to inject text into an already-
|
|
@@ -2098,8 +2764,7 @@ var CodexSession = class {
|
|
|
2098
2764
|
);
|
|
2099
2765
|
}
|
|
2100
2766
|
};
|
|
2101
|
-
function subscriptionModel2(
|
|
2102
|
-
const selection = task.dispatchSelection;
|
|
2767
|
+
function subscriptionModel2(selection) {
|
|
2103
2768
|
if (selection === void 0) return void 0;
|
|
2104
2769
|
if (selection.lane !== "subscription" || selection.runtimeId !== "codex") {
|
|
2105
2770
|
throw new PolicyUnsupportedError(
|
|
@@ -2109,6 +2774,6 @@ function subscriptionModel2(task) {
|
|
|
2109
2774
|
return selection.modelId;
|
|
2110
2775
|
}
|
|
2111
2776
|
|
|
2112
|
-
export { ClaudeAdapter, CodexAdapter, PI_PACKAGE_NAME, PiAdapter };
|
|
2777
|
+
export { ClaudeAdapter, CodexAdapter, PI_PACKAGE_NAME, PiAdapter, RuntimeDisposalFailure, RuntimeExecutionFailure };
|
|
2113
2778
|
//# sourceMappingURL=index.js.map
|
|
2114
2779
|
//# sourceMappingURL=index.js.map
|