@byok-sdk/client 0.4.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 +1 -1
- package/dist/adapters/claude/process-client.d.ts +7 -1
- package/dist/adapters/codex/process-runner.d.ts +5 -0
- package/dist/adapters/index.js +175 -44
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/pi/resolve-bin.d.ts +1 -1
- package/dist/adapters/pi/rpc-client.d.ts +7 -1
- package/dist/adapters/process-tree.d.ts +45 -4
- package/dist/adapters/taskkill-pid-set.d.ts +34 -0
- package/dist/bin/byok-agent.js +200 -64
- package/dist/bin/byok-agent.js.map +1 -1
- package/dist/daemon/connection-manager.d.ts +11 -15
- package/dist/daemon/long-poll-transport.d.ts +6 -0
- package/dist/daemon/task-runner.d.ts +7 -6
- package/dist/index.js +200 -64
- package/dist/index.js.map +1 -1
- package/package.json +14 -14
package/README.md
CHANGED
|
@@ -97,7 +97,13 @@ export declare class ClaudeProcessClient {
|
|
|
97
97
|
* post-mortem without separate log scraping.
|
|
98
98
|
*/
|
|
99
99
|
recordUnmappedFrame(label: string): void;
|
|
100
|
-
/**
|
|
100
|
+
/**
|
|
101
|
+
* Immediate process-tree termination request. `dispose()` is the settlement
|
|
102
|
+
* receipt, so this stays fire-and-forget: an interrupt must not block on a
|
|
103
|
+
* terminator. A request that could not be spawned is left unrecorded, so
|
|
104
|
+
* `dispose()` re-issues it and raises the typed `stage:'signal'` failure —
|
|
105
|
+
* swallowing it here loses nothing.
|
|
106
|
+
*/
|
|
101
107
|
kill(): void;
|
|
102
108
|
waitClosed(): Promise<void>;
|
|
103
109
|
dispose(): Promise<void>;
|
|
@@ -77,6 +77,11 @@ export declare class CodexProcessRunner {
|
|
|
77
77
|
* cleanly resumable afterward via `codex exec resume` (no corruption from
|
|
78
78
|
* killing mid-turn). `taskkill /T /F` on Windows, mirroring
|
|
79
79
|
* `../pi/rpc-client.ts`'s own cross-platform convention.
|
|
80
|
+
*
|
|
81
|
+
* Fire-and-forget by design: an interrupt must not block on a terminator,
|
|
82
|
+
* and `dispose()` is the settlement receipt. A request that could not be
|
|
83
|
+
* spawned is left unrecorded, so `dispose()` re-issues it and raises the
|
|
84
|
+
* typed `stage:'signal'` failure — swallowing it here loses nothing.
|
|
80
85
|
*/
|
|
81
86
|
kill(): void;
|
|
82
87
|
dispose(): Promise<void>;
|
package/dist/adapters/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { execFile, spawn
|
|
1
|
+
import { execFile, spawn } from 'child_process';
|
|
2
2
|
import { promisify } from 'util';
|
|
3
3
|
import { promises, existsSync, readFileSync, realpathSync } from 'fs';
|
|
4
4
|
import path3 from 'path';
|
|
@@ -128,12 +128,12 @@ function resolvePiBin() {
|
|
|
128
128
|
}
|
|
129
129
|
} catch (cause) {
|
|
130
130
|
throw new Error(
|
|
131
|
-
`Required ${PI_PACKAGE_NAME} could not be resolved; install @byok-sdk/client dependencies or set BYOK_PI_BIN to a Node 22.
|
|
131
|
+
`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`,
|
|
132
132
|
{ cause }
|
|
133
133
|
);
|
|
134
134
|
}
|
|
135
135
|
throw new Error(
|
|
136
|
-
`Required ${PI_PACKAGE_NAME} does not expose the pi CLI; reinstall the pinned dependency or set BYOK_PI_BIN to a Node 22.
|
|
136
|
+
`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`
|
|
137
137
|
);
|
|
138
138
|
}
|
|
139
139
|
|
|
@@ -338,11 +338,54 @@ var AsyncQueue = class {
|
|
|
338
338
|
};
|
|
339
339
|
}
|
|
340
340
|
};
|
|
341
|
+
|
|
342
|
+
// src/adapters/taskkill-pid-set.ts
|
|
343
|
+
var INTEGER_PATTERN = /\d+/g;
|
|
344
|
+
function isCandidatePid(value) {
|
|
345
|
+
return Number.isSafeInteger(value) && value > 0;
|
|
346
|
+
}
|
|
347
|
+
function walkTaskkillPidSet(text, rootPid, excludedPids = []) {
|
|
348
|
+
const excluded = new Set(excludedPids);
|
|
349
|
+
const accepted = /* @__PURE__ */ new Set();
|
|
350
|
+
if (isCandidatePid(rootPid)) accepted.add(rootPid);
|
|
351
|
+
const lines = text.split(/\r?\n/).map((line) => {
|
|
352
|
+
const pids = [];
|
|
353
|
+
for (const match of line.matchAll(INTEGER_PATTERN)) {
|
|
354
|
+
const pid = Number(match[0]);
|
|
355
|
+
if (isCandidatePid(pid) && !excluded.has(pid)) pids.push(pid);
|
|
356
|
+
}
|
|
357
|
+
return pids;
|
|
358
|
+
});
|
|
359
|
+
let changed = true;
|
|
360
|
+
while (changed) {
|
|
361
|
+
changed = false;
|
|
362
|
+
for (const pids of lines) {
|
|
363
|
+
if (!pids.some((pid) => accepted.has(pid))) continue;
|
|
364
|
+
for (const pid of pids) {
|
|
365
|
+
if (accepted.has(pid)) continue;
|
|
366
|
+
accepted.add(pid);
|
|
367
|
+
changed = true;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return accepted;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// src/adapters/process-tree.ts
|
|
341
375
|
var DEFAULT_TERM_GRACE_MS = 750;
|
|
342
376
|
var DEFAULT_KILL_GRACE_MS = 2e3;
|
|
343
377
|
var POLL_MS = 20;
|
|
344
|
-
var
|
|
345
|
-
|
|
378
|
+
var terminationState = /* @__PURE__ */ new WeakMap();
|
|
379
|
+
function stateFor(child) {
|
|
380
|
+
const existing = terminationState.get(child);
|
|
381
|
+
if (existing) return existing;
|
|
382
|
+
const created = { requested: false, acceptedPids: /* @__PURE__ */ new Set() };
|
|
383
|
+
terminationState.set(child, created);
|
|
384
|
+
return created;
|
|
385
|
+
}
|
|
386
|
+
function defaultKill(pid, signal) {
|
|
387
|
+
process.kill(pid, signal);
|
|
388
|
+
}
|
|
346
389
|
function withOwnedProcessTree(options) {
|
|
347
390
|
return {
|
|
348
391
|
...options,
|
|
@@ -360,9 +403,9 @@ function positivePid(child, label) {
|
|
|
360
403
|
}
|
|
361
404
|
return pid;
|
|
362
405
|
}
|
|
363
|
-
function groupExists(pid, label) {
|
|
406
|
+
function groupExists(pid, label, kill) {
|
|
364
407
|
try {
|
|
365
|
-
|
|
408
|
+
kill(-pid, 0);
|
|
366
409
|
return true;
|
|
367
410
|
} catch (cause) {
|
|
368
411
|
const code = cause.code;
|
|
@@ -374,9 +417,23 @@ function groupExists(pid, label) {
|
|
|
374
417
|
}, { cause });
|
|
375
418
|
}
|
|
376
419
|
}
|
|
377
|
-
function
|
|
420
|
+
function processExists(pid, label, kill) {
|
|
378
421
|
try {
|
|
379
|
-
|
|
422
|
+
kill(pid, 0);
|
|
423
|
+
return true;
|
|
424
|
+
} catch (cause) {
|
|
425
|
+
const code = cause.code;
|
|
426
|
+
if (code === "ESRCH") return false;
|
|
427
|
+
if (code === "EPERM") return true;
|
|
428
|
+
throw new RuntimeDisposalFailure({
|
|
429
|
+
stage: "quiescence",
|
|
430
|
+
reason: `${label} runtime process ${pid} state could not be verified`
|
|
431
|
+
}, { cause });
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
function signalGroup(pid, signal, label, kill) {
|
|
435
|
+
try {
|
|
436
|
+
kill(-pid, signal);
|
|
380
437
|
} catch (cause) {
|
|
381
438
|
const code = cause.code;
|
|
382
439
|
if (code === "ESRCH" || code === "EPERM") return;
|
|
@@ -386,6 +443,39 @@ function signalGroup(pid, signal, label) {
|
|
|
386
443
|
}, { cause });
|
|
387
444
|
}
|
|
388
445
|
}
|
|
446
|
+
async function runTaskkill(pid, options) {
|
|
447
|
+
const spawnFn = options.spawnFn ?? spawn;
|
|
448
|
+
return new Promise((resolve, reject) => {
|
|
449
|
+
const signalFailure = (cause) => {
|
|
450
|
+
reject(new RuntimeDisposalFailure({
|
|
451
|
+
stage: "signal",
|
|
452
|
+
reason: `${options.label} runtime process tree termination could not be requested`
|
|
453
|
+
}, { cause }));
|
|
454
|
+
};
|
|
455
|
+
let taskkill;
|
|
456
|
+
try {
|
|
457
|
+
taskkill = spawnFn("taskkill", ["/PID", String(pid), "/T", "/F"], {
|
|
458
|
+
windowsHide: true,
|
|
459
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
460
|
+
});
|
|
461
|
+
} catch (cause) {
|
|
462
|
+
signalFailure(cause);
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
const chunks = [];
|
|
466
|
+
taskkill.stdout?.on("data", (chunk) => chunks.push(chunk));
|
|
467
|
+
taskkill.stderr?.on("data", (chunk) => chunks.push(chunk));
|
|
468
|
+
taskkill.once("error", signalFailure);
|
|
469
|
+
taskkill.once("close", () => resolve(Buffer.concat(chunks).toString("latin1")));
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
function liveAcceptedPids(accepted, label, kill) {
|
|
473
|
+
const live = [];
|
|
474
|
+
for (const pid of accepted) {
|
|
475
|
+
if (processExists(pid, label, kill)) live.push(pid);
|
|
476
|
+
}
|
|
477
|
+
return live;
|
|
478
|
+
}
|
|
389
479
|
async function waitUntil(predicate, timeoutMs) {
|
|
390
480
|
const deadline = Date.now() + timeoutMs;
|
|
391
481
|
while (predicate()) {
|
|
@@ -423,29 +513,27 @@ async function waitWithDeadline(promise, timeoutMs) {
|
|
|
423
513
|
);
|
|
424
514
|
});
|
|
425
515
|
}
|
|
426
|
-
function requestOwnedProcessTreeTermination(options) {
|
|
427
|
-
|
|
516
|
+
async function requestOwnedProcessTreeTermination(options) {
|
|
517
|
+
const platform = options.platform ?? process.platform;
|
|
518
|
+
if (platform !== "win32" && options.isClosed()) return;
|
|
428
519
|
const pid = positivePid(options.child, options.label);
|
|
429
520
|
if (pid === void 0) return;
|
|
430
|
-
if (
|
|
431
|
-
const
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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);
|
|
521
|
+
if (platform === "win32") {
|
|
522
|
+
const output = await runTaskkill(pid, options);
|
|
523
|
+
const state = stateFor(options.child);
|
|
524
|
+
for (const walked of walkTaskkillPidSet(output, pid, [process.pid])) state.acceptedPids.add(walked);
|
|
525
|
+
state.requested = true;
|
|
440
526
|
return;
|
|
441
527
|
}
|
|
442
|
-
signalGroup(pid, "SIGTERM", options.label);
|
|
443
|
-
|
|
528
|
+
signalGroup(pid, "SIGTERM", options.label, options.killFn ?? defaultKill);
|
|
529
|
+
stateFor(options.child).requested = true;
|
|
444
530
|
}
|
|
445
531
|
async function disposeOwnedProcessTree(options) {
|
|
446
532
|
const pid = positivePid(options.child, options.label);
|
|
447
533
|
const termGraceMs = options.termGraceMs ?? DEFAULT_TERM_GRACE_MS;
|
|
448
534
|
const killGraceMs = options.killGraceMs ?? DEFAULT_KILL_GRACE_MS;
|
|
535
|
+
const platform = options.platform ?? process.platform;
|
|
536
|
+
const kill = options.killFn ?? defaultKill;
|
|
449
537
|
if (pid === void 0) {
|
|
450
538
|
if (await waitWithDeadline(options.waitClosed(), killGraceMs)) return;
|
|
451
539
|
throw new RuntimeDisposalFailure({
|
|
@@ -453,27 +541,50 @@ async function disposeOwnedProcessTree(options) {
|
|
|
453
541
|
reason: `${options.label} runtime process did not settle after spawn failure`
|
|
454
542
|
});
|
|
455
543
|
}
|
|
456
|
-
if (
|
|
457
|
-
if (!
|
|
458
|
-
|
|
459
|
-
|
|
544
|
+
if (platform === "win32") {
|
|
545
|
+
if (!terminationState.get(options.child)?.requested) {
|
|
546
|
+
await requestOwnedProcessTreeTermination(options);
|
|
547
|
+
}
|
|
548
|
+
const accepted = terminationState.get(options.child)?.acceptedPids ?? /* @__PURE__ */ new Set();
|
|
549
|
+
const deadline = Date.now() + killGraceMs;
|
|
550
|
+
const resweepAt = Date.now() + Math.floor(killGraceMs / 2);
|
|
551
|
+
let reswept = false;
|
|
552
|
+
let live = liveAcceptedPids(accepted, options.label, kill);
|
|
553
|
+
while (live.length > 0) {
|
|
554
|
+
if (Date.now() >= deadline) {
|
|
555
|
+
throw new RuntimeDisposalFailure({
|
|
556
|
+
stage: "quiescence",
|
|
557
|
+
reason: `${options.label} runtime process tree did not quiesce: ${live.length} of ${accepted.size} walked process ids were still alive at the disposal deadline`
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
if (!reswept && Date.now() >= resweepAt) {
|
|
561
|
+
reswept = true;
|
|
562
|
+
for (const livePid of live) {
|
|
563
|
+
for (const walked of walkTaskkillPidSet(await runTaskkill(livePid, options), livePid, [process.pid])) {
|
|
564
|
+
accepted.add(walked);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
await new Promise((resolve) => {
|
|
569
|
+
setTimeout(resolve, POLL_MS);
|
|
570
|
+
});
|
|
571
|
+
live = liveAcceptedPids(accepted, options.label, kill);
|
|
572
|
+
}
|
|
573
|
+
if (!await waitWithDeadline(options.waitClosed(), killGraceMs)) {
|
|
460
574
|
throw new RuntimeDisposalFailure({
|
|
461
|
-
stage: "
|
|
462
|
-
reason: `${options.label} runtime
|
|
575
|
+
stage: "quiescence",
|
|
576
|
+
reason: `${options.label} runtime root did not emit close after its process tree quiesced`
|
|
463
577
|
});
|
|
464
578
|
}
|
|
465
|
-
|
|
466
|
-
stage: "quiescence",
|
|
467
|
-
reason: `${options.label} runtime process tree did not close before the disposal deadline`
|
|
468
|
-
});
|
|
579
|
+
return;
|
|
469
580
|
}
|
|
470
|
-
if (groupExists(pid, options.label) && !
|
|
471
|
-
signalGroup(pid, "SIGTERM", options.label);
|
|
472
|
-
|
|
581
|
+
if (groupExists(pid, options.label, kill) && !terminationState.get(options.child)?.requested) {
|
|
582
|
+
signalGroup(pid, "SIGTERM", options.label, kill);
|
|
583
|
+
stateFor(options.child).requested = true;
|
|
473
584
|
}
|
|
474
|
-
if (!await waitUntil(() => groupExists(pid, options.label), termGraceMs)) {
|
|
475
|
-
signalGroup(pid, "SIGKILL", options.label);
|
|
476
|
-
if (!await waitUntil(() => groupExists(pid, options.label), killGraceMs)) {
|
|
585
|
+
if (!await waitUntil(() => groupExists(pid, options.label, kill), termGraceMs)) {
|
|
586
|
+
signalGroup(pid, "SIGKILL", options.label, kill);
|
|
587
|
+
if (!await waitUntil(() => groupExists(pid, options.label, kill), killGraceMs)) {
|
|
477
588
|
throw new RuntimeDisposalFailure({
|
|
478
589
|
stage: "quiescence",
|
|
479
590
|
reason: `${options.label} runtime process group remained live after SIGKILL`
|
|
@@ -571,9 +682,16 @@ var PiRpcClient = class {
|
|
|
571
682
|
);
|
|
572
683
|
}
|
|
573
684
|
}
|
|
574
|
-
/**
|
|
685
|
+
/**
|
|
686
|
+
* Immediate process-tree termination request. `dispose()` is the settlement
|
|
687
|
+
* receipt, so this stays fire-and-forget: an interrupt must not block on a
|
|
688
|
+
* terminator. A request that could not be spawned is left unrecorded, so
|
|
689
|
+
* `dispose()` re-issues it and raises the typed `stage:'signal'` failure —
|
|
690
|
+
* swallowing it here loses nothing.
|
|
691
|
+
*/
|
|
575
692
|
kill() {
|
|
576
|
-
requestOwnedProcessTreeTermination(this.processTreeOptions())
|
|
693
|
+
void requestOwnedProcessTreeTermination(this.processTreeOptions()).catch(() => {
|
|
694
|
+
});
|
|
577
695
|
}
|
|
578
696
|
waitClosed() {
|
|
579
697
|
return this.closedPromise;
|
|
@@ -1376,9 +1494,16 @@ var ClaudeProcessClient = class {
|
|
|
1376
1494
|
);
|
|
1377
1495
|
}
|
|
1378
1496
|
}
|
|
1379
|
-
/**
|
|
1497
|
+
/**
|
|
1498
|
+
* Immediate process-tree termination request. `dispose()` is the settlement
|
|
1499
|
+
* receipt, so this stays fire-and-forget: an interrupt must not block on a
|
|
1500
|
+
* terminator. A request that could not be spawned is left unrecorded, so
|
|
1501
|
+
* `dispose()` re-issues it and raises the typed `stage:'signal'` failure —
|
|
1502
|
+
* swallowing it here loses nothing.
|
|
1503
|
+
*/
|
|
1380
1504
|
kill() {
|
|
1381
|
-
requestOwnedProcessTreeTermination(this.processTreeOptions())
|
|
1505
|
+
void requestOwnedProcessTreeTermination(this.processTreeOptions()).catch(() => {
|
|
1506
|
+
});
|
|
1382
1507
|
}
|
|
1383
1508
|
waitClosed() {
|
|
1384
1509
|
return this.closedPromise;
|
|
@@ -2143,9 +2268,15 @@ var CodexProcessRunner = class {
|
|
|
2143
2268
|
* cleanly resumable afterward via `codex exec resume` (no corruption from
|
|
2144
2269
|
* killing mid-turn). `taskkill /T /F` on Windows, mirroring
|
|
2145
2270
|
* `../pi/rpc-client.ts`'s own cross-platform convention.
|
|
2271
|
+
*
|
|
2272
|
+
* Fire-and-forget by design: an interrupt must not block on a terminator,
|
|
2273
|
+
* and `dispose()` is the settlement receipt. A request that could not be
|
|
2274
|
+
* spawned is left unrecorded, so `dispose()` re-issues it and raises the
|
|
2275
|
+
* typed `stage:'signal'` failure — swallowing it here loses nothing.
|
|
2146
2276
|
*/
|
|
2147
2277
|
kill() {
|
|
2148
|
-
requestOwnedProcessTreeTermination(this.processTreeOptions())
|
|
2278
|
+
void requestOwnedProcessTreeTermination(this.processTreeOptions()).catch(() => {
|
|
2279
|
+
});
|
|
2149
2280
|
}
|
|
2150
2281
|
dispose() {
|
|
2151
2282
|
if (!this.disposalAttempt) {
|