@code-yeongyu/senpi 2026.10.2 → 2026.10.3
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/CHANGELOG.md +22 -0
- package/dist/bundle/chunks/{app-server-command-SZJAJX5J.js → app-server-command-VSOBULUS.js} +1 -1
- package/dist/bundle/chunks/{chunk-PA7DXEUM.js → chunk-2EYJJDES.js} +13 -30
- package/dist/bundle/chunks/{chunk-LACVPFMM.js → chunk-ATACOV2Q.js} +3 -3
- package/dist/bundle/chunks/{chunk-2NAN2JVY.js → chunk-E6TL3R3I.js} +1 -1
- package/dist/bundle/chunks/chunk-Q23IMPRD.js +6 -0
- package/dist/bundle/chunks/{chunk-GIK2N57G.js → chunk-WIBTUARS.js} +20 -2
- package/dist/bundle/chunks/chunk-XOUTBUAV.js +24 -0
- package/dist/bundle/chunks/{cli-main-B66BJ6WS.js → cli-main-RH4BAWCJ.js} +1 -1
- package/dist/bundle/chunks/{host-command-ESRPUXI2.js → host-command-2ZP7B3DA.js} +1 -1
- package/dist/bundle/chunks/{host-lifecycle-P65B4FWL.js → host-lifecycle-3ZZLFX55.js} +1 -1
- package/dist/bundle/chunks/host-lifecycle.js +2 -2
- package/dist/bundle/chunks/{interactive-mode-LBS4DGTM.js → interactive-mode-LBBIPYPZ.js} +1 -1
- package/dist/bundle/chunks/{multi-session-host-5C2TC4BN.js → multi-session-host-GFWO2JUC.js} +1 -1
- package/dist/bundle/chunks/{package-manager-cli-K3JGGB65.js → package-manager-cli-VHCS5NXO.js} +1 -1
- package/dist/bundle/chunks/{rpc-mode-FJGYYLVG.js → rpc-mode-KAZWRS2K.js} +1 -1
- package/dist/bundle/chunks/{schedule-command-BT36FE6F.js → schedule-command-OUALJDGD.js} +1 -1
- package/dist/bundle/chunks/{session-picker-MMTSHPR5.js → session-picker-UYF62GAF.js} +1 -1
- package/dist/bundle/chunks/session-worker.js +35 -34
- package/dist/bundle/cli.js +1 -1
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/bundle/runtime-manifest.json +1 -1
- package/dist/core/agent-session.d.ts +4 -0
- package/dist/core/agent-session.js +60 -27
- package/dist/core/extensions/builtin/mcp/index.js +19 -10
- package/dist/core/extensions/builtin/mcp/service-register.d.ts +1 -0
- package/dist/core/extensions/builtin/mcp/service-register.js +2 -4
- package/dist/core/extensions/builtin/mcp/service-tools-changed.d.ts +11 -2
- package/dist/core/extensions/builtin/mcp/service-tools-changed.js +33 -6
- package/dist/core/extensions/builtin/mcp/service-types.d.ts +0 -2
- package/dist/core/extensions/builtin/mcp/service.d.ts +18 -8
- package/dist/core/extensions/builtin/mcp/service.js +231 -76
- package/dist/core/settings-manager.d.ts +2 -0
- package/dist/core/settings-manager.js +7 -0
- package/dist/core/skill-invocation.d.ts +10 -2
- package/dist/core/skill-invocation.js +13 -3
- package/dist/core/webview/native-webview.d.ts +1 -0
- package/dist/core/webview/webview-client.d.ts +5 -2
- package/dist/core/webview/webview-client.js +1 -1
- package/dist/core/webview/webview-readiness.d.ts +41 -0
- package/dist/core/webview/webview-readiness.js +48 -0
- package/dist/core/webview/webview-service.d.ts +8 -1
- package/dist/core/webview/webview-service.js +56 -16
- package/dist/modes/rpc/host-exec-argv.d.ts +7 -0
- package/dist/modes/rpc/host-exec-argv.js +45 -0
- package/dist/modes/rpc/host-launch.js +3 -2
- package/dist/modes/rpc/host-lifecycle.js +2 -1
- package/docs/rpc.md +2 -0
- package/docs/settings.md +1 -0
- package/docs/skills.md +4 -2
- package/package.json +6 -6
- package/dist/bundle/chunks/chunk-UB2RTUY7.js +0 -24
- package/dist/bundle/chunks/chunk-XOZFAFCS.js +0 -6
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { appendFileSync } from "node:fs";
|
|
2
|
+
// Healthy first navigations on windows-latest measured p95 3.4 s and at most 18 s (the first Chrome
|
|
3
|
+
// of a cold runner); two bounded launches fit inside the eval cell's 60 s default budget.
|
|
4
|
+
export const DEFAULT_READINESS = { attachBoundMs: 20_000, launchAttempts: 2 };
|
|
5
|
+
export const READINESS_URL = "about:blank";
|
|
6
|
+
/** Appends one line per launch outcome to the file `SENPI_WEBVIEW_READINESS_LOG` names (CI diagnostics). */
|
|
7
|
+
export function readinessLogFromEnvironment() {
|
|
8
|
+
const path = process.env.SENPI_WEBVIEW_READINESS_LOG;
|
|
9
|
+
if (!path)
|
|
10
|
+
return undefined;
|
|
11
|
+
return (event) => {
|
|
12
|
+
const outcome = event.type === "ready"
|
|
13
|
+
? `ready launch=${event.launch} attachMs=${event.attachMs}`
|
|
14
|
+
: `stalled phase=${event.phase} launch=${event.launch} (Chrome retired)`;
|
|
15
|
+
appendFileSync(path, `${new Date().toISOString()} pid=${process.pid} webview ${outcome}\n`);
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export class WebViewNotReadyError extends Error {
|
|
19
|
+
constructor(launches, policy) {
|
|
20
|
+
super(`Chrome-backed WebView never became ready: phase cdp-target-attach (Chrome did not attach a CDP session to the new view within ${policy.attachBoundMs} ms) on ${launches} launch(es); each stalled Chrome was retired`);
|
|
21
|
+
this.code = "ERR_WEBVIEW_NOT_READY";
|
|
22
|
+
this.phase = "cdp-target-attach";
|
|
23
|
+
this.name = "WebViewNotReadyError";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export function timedDeadline(boundMs) {
|
|
27
|
+
let timer;
|
|
28
|
+
const reached = new Promise((resolve) => {
|
|
29
|
+
timer = setTimeout(resolve, boundMs);
|
|
30
|
+
});
|
|
31
|
+
return { reached, cancel: () => clearTimeout(timer) };
|
|
32
|
+
}
|
|
33
|
+
/** Resolves the readiness navigation's duration once it settled, undefined when the deadline came first. */
|
|
34
|
+
export async function attachedWithin(view, deadline) {
|
|
35
|
+
const startedAt = performance.now();
|
|
36
|
+
const bound = deadline.reached.then(() => undefined);
|
|
37
|
+
// Bun throws synchronously for some invalid states; the async wrapper turns that into a rejection.
|
|
38
|
+
const navigation = (async () => await view.navigate(READINESS_URL))().then(() => Math.round(performance.now() - startedAt));
|
|
39
|
+
try {
|
|
40
|
+
return await Promise.race([navigation, bound]);
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
deadline.cancel();
|
|
44
|
+
// A stalled navigation is abandoned; closing its view rejects it with nobody left waiting.
|
|
45
|
+
navigation.catch(() => { });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=webview-readiness.js.map
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { type MessagePort } from "node:worker_threads";
|
|
2
2
|
import { type NativeWebViewClass } from "./native-webview.ts";
|
|
3
|
+
import { type AttachDeadline, type ReadinessEvent, type WebViewReadinessPolicy } from "./webview-readiness.ts";
|
|
4
|
+
export interface WebViewServiceOptions {
|
|
5
|
+
readonly readiness?: Partial<WebViewReadinessPolicy>;
|
|
6
|
+
readonly onReadiness?: (event: ReadinessEvent) => void;
|
|
7
|
+
/** The readiness bound of each launch; defaults to `attachBoundMs`. Tests signal it instead of waiting. */
|
|
8
|
+
readonly attachDeadline?: (launch: number) => AttachDeadline;
|
|
9
|
+
}
|
|
3
10
|
export interface WebViewClientGrant {
|
|
4
11
|
readonly clientId: string;
|
|
5
12
|
readonly port: MessagePort;
|
|
@@ -11,7 +18,7 @@ export interface WebViewClientGrant {
|
|
|
11
18
|
*/
|
|
12
19
|
export declare class WebViewService {
|
|
13
20
|
#private;
|
|
14
|
-
constructor(webViewClass: NativeWebViewClass);
|
|
21
|
+
constructor(webViewClass: NativeWebViewClass, options?: WebViewServiceOptions);
|
|
15
22
|
get viewCount(): number;
|
|
16
23
|
connect(owner: object): WebViewClientGrant;
|
|
17
24
|
/**
|
|
@@ -3,6 +3,7 @@ import { MessageChannel } from "node:worker_threads";
|
|
|
3
3
|
import { retireBunChrome, settleDeadBunChrome } from "./bun-chrome.js";
|
|
4
4
|
import { mainThreadWebViewClass } from "./native-webview.js";
|
|
5
5
|
import { closeQuietly, WebViewServiceClient } from "./webview-client.js";
|
|
6
|
+
import { attachedWithin, DEFAULT_READINESS, readinessLogFromEnvironment, timedDeadline, WebViewNotReadyError, } from "./webview-readiness.js";
|
|
6
7
|
/**
|
|
7
8
|
* Serves Chrome-backed `Bun.WebView`s on the process main thread (Bun allows that backend nowhere
|
|
8
9
|
* else) for eval kernels running in worker threads. Every kernel gets its own client: a private
|
|
@@ -10,13 +11,20 @@ import { closeQuietly, WebViewServiceClient } from "./webview-client.js";
|
|
|
10
11
|
*/
|
|
11
12
|
export class WebViewService {
|
|
12
13
|
#webViewClass;
|
|
14
|
+
#readiness;
|
|
15
|
+
#onReadiness;
|
|
16
|
+
#attachDeadline;
|
|
13
17
|
#clients = new Map();
|
|
14
18
|
#retiring = Promise.resolve();
|
|
15
19
|
#chromeInUse = false;
|
|
16
20
|
// Views being constructed and not yet adopted by their client: Chrome must outlive them.
|
|
17
21
|
#launching = 0;
|
|
18
|
-
constructor(webViewClass) {
|
|
22
|
+
constructor(webViewClass, options = {}) {
|
|
19
23
|
this.#webViewClass = webViewClass;
|
|
24
|
+
this.#readiness = { ...DEFAULT_READINESS, ...options.readiness };
|
|
25
|
+
this.#onReadiness = options.onReadiness ?? (() => { });
|
|
26
|
+
const boundMs = this.#readiness.attachBoundMs;
|
|
27
|
+
this.#attachDeadline = options.attachDeadline ?? (() => timedDeadline(boundMs));
|
|
20
28
|
}
|
|
21
29
|
get viewCount() {
|
|
22
30
|
let count = 0;
|
|
@@ -28,7 +36,7 @@ export class WebViewService {
|
|
|
28
36
|
const clientId = randomUUID();
|
|
29
37
|
const channel = new MessageChannel();
|
|
30
38
|
const client = new WebViewServiceClient(clientId, owner, channel.port1, {
|
|
31
|
-
createView: (options, onConsole, adopt) => this.#createView(options, onConsole, adopt),
|
|
39
|
+
createView: (options, onConsole, adopt, wanted) => this.#createView(options, onConsole, adopt, wanted),
|
|
32
40
|
onClientClosed: (closed) => void this.#drop(closed),
|
|
33
41
|
});
|
|
34
42
|
this.#clients.set(clientId, client);
|
|
@@ -51,27 +59,58 @@ export class WebViewService {
|
|
|
51
59
|
await this.#retiring;
|
|
52
60
|
}
|
|
53
61
|
/**
|
|
54
|
-
* Launches a view and hands it to `adopt` in the same turn, so no
|
|
55
|
-
* launch and the client's bookkeeping. A launch that fails,
|
|
56
|
-
* was in flight, retires the Chrome it started
|
|
62
|
+
* Launches a view, waits until it is ready, and hands it to `adopt` in the same turn, so no
|
|
63
|
+
* retirement can slip between the launch and the client's bookkeeping. A launch that fails, never
|
|
64
|
+
* becomes ready, or whose client was released while it was in flight, retires the Chrome it started
|
|
65
|
+
* unless another view still needs it; a launch that never became ready is retried once on a fresh
|
|
66
|
+
* Chrome before the create fails naming the phase it stalled in.
|
|
57
67
|
*/
|
|
58
|
-
async #createView(options, onConsole, adopt) {
|
|
59
|
-
|
|
60
|
-
let
|
|
68
|
+
async #createView(options, onConsole, adopt, wanted) {
|
|
69
|
+
const viewOptions = onConsole ? { ...options, console: onConsole } : options;
|
|
70
|
+
for (let launch = 1;; launch++) {
|
|
71
|
+
this.#launching++;
|
|
72
|
+
let view;
|
|
73
|
+
try {
|
|
74
|
+
view = await this.#launchReady(viewOptions, launch);
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
this.#launching--;
|
|
78
|
+
await this.#retireIfIdle();
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
this.#launching--;
|
|
82
|
+
if (view) {
|
|
83
|
+
if (adopt(view))
|
|
84
|
+
return view;
|
|
85
|
+
closeQuietly(view);
|
|
86
|
+
await this.#retireIfIdle();
|
|
87
|
+
throw new Error("WebView client released");
|
|
88
|
+
}
|
|
89
|
+
await this.#retireIfIdle();
|
|
90
|
+
if (!wanted())
|
|
91
|
+
throw new Error("WebView client released");
|
|
92
|
+
if (launch >= this.#readiness.launchAttempts)
|
|
93
|
+
throw new WebViewNotReadyError(launch, this.#readiness);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/** A launched view whose readiness navigation settled, or undefined (the view closed) when it stalled. */
|
|
97
|
+
async #launchReady(viewOptions, launch) {
|
|
98
|
+
const view = await this.#launch(viewOptions);
|
|
99
|
+
let attachMs;
|
|
61
100
|
try {
|
|
62
|
-
|
|
101
|
+
attachMs = await attachedWithin(view, this.#attachDeadline(launch));
|
|
63
102
|
}
|
|
64
103
|
catch (error) {
|
|
65
|
-
|
|
66
|
-
await this.#retireIfIdle();
|
|
104
|
+
closeQuietly(view);
|
|
67
105
|
throw error;
|
|
68
106
|
}
|
|
69
|
-
|
|
70
|
-
|
|
107
|
+
if (attachMs !== undefined) {
|
|
108
|
+
this.#onReadiness({ type: "ready", launch, attachMs });
|
|
71
109
|
return view;
|
|
110
|
+
}
|
|
72
111
|
closeQuietly(view);
|
|
73
|
-
|
|
74
|
-
|
|
112
|
+
this.#onReadiness({ type: "stalled", phase: "cdp-target-attach", launch });
|
|
113
|
+
return undefined;
|
|
75
114
|
}
|
|
76
115
|
async #launch(viewOptions) {
|
|
77
116
|
// No retirement is scheduled while a launch is pending, so the one awaited here is the last.
|
|
@@ -129,7 +168,8 @@ export function mainThreadWebViewService() {
|
|
|
129
168
|
const webViewClass = mainThreadWebViewClass();
|
|
130
169
|
if (!webViewClass)
|
|
131
170
|
return undefined;
|
|
132
|
-
const
|
|
171
|
+
const onReadiness = readinessLogFromEnvironment();
|
|
172
|
+
const service = new WebViewService(webViewClass, onReadiness ? { onReadiness } : {});
|
|
133
173
|
Reflect.set(globalThis, SERVICE_KEY, service);
|
|
134
174
|
return service;
|
|
135
175
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-enter a host script with the caller's runtime options, not its entry mode.
|
|
3
|
+
* Forwarding eval/print code can execute the embedding caller again and recursively
|
|
4
|
+
* spawn hosts; input-type is only valid for eval/stdin, and interactive keeps a REPL alive.
|
|
5
|
+
*/
|
|
6
|
+
export declare function rpcHostExecArgv(execArgv?: readonly string[], bun?: boolean): string[];
|
|
7
|
+
//# sourceMappingURL=host-exec-argv.d.ts.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-enter a host script with the caller's runtime options, not its entry mode.
|
|
3
|
+
* Forwarding eval/print code can execute the embedding caller again and recursively
|
|
4
|
+
* spawn hosts; input-type is only valid for eval/stdin, and interactive keeps a REPL alive.
|
|
5
|
+
*/
|
|
6
|
+
export function rpcHostExecArgv(execArgv = process.execArgv, bun = process.versions.bun !== undefined) {
|
|
7
|
+
const args = [];
|
|
8
|
+
for (let index = 0; index < execArgv.length; index++) {
|
|
9
|
+
const arg = execArgv[index];
|
|
10
|
+
if (bun) {
|
|
11
|
+
// Bun always takes the next token as the code, even when it starts with "-".
|
|
12
|
+
if (arg === "-e" || arg === "--eval" || arg === "-p" || arg === "--print" || arg === "-pe") {
|
|
13
|
+
index++;
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
// Bun reads any other -e…/-p… token as code glued to the flag: -eCODE, -e=CODE, -pCODE,
|
|
17
|
+
// and even -expose-gc (code "xpose-gc"). Node rejects these forms, so this is Bun-only.
|
|
18
|
+
if (arg.startsWith("-e") || arg.startsWith("-p"))
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (arg === "-e" || arg === "--eval" || arg === "-pe" || arg === "--input-type" || arg === "--input_type") {
|
|
22
|
+
index++;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (arg === "-p" || arg === "--print") {
|
|
26
|
+
// The expression is optional: `-p -e CODE` and `-p --trace-warnings` are valid.
|
|
27
|
+
if (execArgv[index + 1] !== undefined && !execArgv[index + 1].startsWith("-"))
|
|
28
|
+
index++;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (arg === "-i" ||
|
|
32
|
+
arg === "--interactive" ||
|
|
33
|
+
arg.startsWith("--interactive=") ||
|
|
34
|
+
arg.startsWith("--eval=") ||
|
|
35
|
+
arg.startsWith("--print=") ||
|
|
36
|
+
arg.startsWith("--input-type=") ||
|
|
37
|
+
arg.startsWith("--input_type=")) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
// Under Node, do not match arbitrary -e/-p prefixes: V8 accepts -expose-gc and -predictable.
|
|
41
|
+
args.push(arg);
|
|
42
|
+
}
|
|
43
|
+
return args;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=host-exec-argv.js.map
|
|
@@ -11,6 +11,7 @@ import { basename, dirname, resolve } from "node:path";
|
|
|
11
11
|
import { fileURLToPath } from "node:url";
|
|
12
12
|
import { isBunBinary } from "../../config.js";
|
|
13
13
|
import { CUSTOM_UNSUPPORTED_CAPABILITY, EXTENSION_EVENTS_CAPABILITY } from "./custom-capability.js";
|
|
14
|
+
import { rpcHostExecArgv } from "./host-exec-argv.js";
|
|
14
15
|
import { INTERNAL_SUPERVISOR_FLAG, resolveCliMainPath } from "./host-lifecycle.js";
|
|
15
16
|
/**
|
|
16
17
|
* Every ensured host starts with this installation-wide profile, independent of the first
|
|
@@ -35,7 +36,7 @@ sibling = resolveHostLifecycleEntryPath() ?? null) {
|
|
|
35
36
|
if (compiled)
|
|
36
37
|
return { command: process.execPath, args: [INTERNAL_SUPERVISOR_FLAG, ...supervisorArgs] };
|
|
37
38
|
if (sibling !== null)
|
|
38
|
-
return { command: process.execPath, args: [...
|
|
39
|
+
return { command: process.execPath, args: [...rpcHostExecArgv(), sibling, ...supervisorArgs] };
|
|
39
40
|
// Bundled, the host-lifecycle entry beside us is a bundler chunk, not the standalone
|
|
40
41
|
// program the unbundled tree ships: run directly it returns immediately without ever
|
|
41
42
|
// listening, so ensure saw "exited with code 0 before answering get_protocol_info".
|
|
@@ -44,7 +45,7 @@ sibling = resolveHostLifecycleEntryPath() ?? null) {
|
|
|
44
45
|
// counting "..", which lands on the package root once this module is bundled.
|
|
45
46
|
return {
|
|
46
47
|
command: process.execPath,
|
|
47
|
-
args: [...
|
|
48
|
+
args: [...rpcHostExecArgv(), resolveCliMainPath(), INTERNAL_SUPERVISOR_FLAG, ...supervisorArgs],
|
|
48
49
|
};
|
|
49
50
|
}
|
|
50
51
|
/**
|
|
@@ -51,6 +51,7 @@ import { getAgentDir, isBunBinary, isBundledNode } from "../../config.js";
|
|
|
51
51
|
import { processIsLive, readProcessStartTime } from "../app-server/daemon/process.js";
|
|
52
52
|
import { classifyChildExit, noteChildExit } from "./host-child-exit.js";
|
|
53
53
|
import { ClientOccupancy } from "./host-client-occupancy.js";
|
|
54
|
+
import { rpcHostExecArgv } from "./host-exec-argv.js";
|
|
54
55
|
import { DEFAULT_HANDOFF_GRACE_MS, HANDOFF_GRACE_MS_ENV, IdleExitDecider, parseIdleExitMs, resolveHostPolicy, } from "./host-lifecycle-policy.js";
|
|
55
56
|
// The exit verdict moved to ./host-child-exit.ts with the crash recording it now feeds; it stays
|
|
56
57
|
// exported from here so every existing importer keeps resolving it at its original home.
|
|
@@ -257,7 +258,7 @@ export function resolveHostChildLaunch(launch, internalSocket, compiled = isBunB
|
|
|
257
258
|
return {
|
|
258
259
|
command: process.execPath,
|
|
259
260
|
args: [
|
|
260
|
-
...(compiled ? [] : [...
|
|
261
|
+
...(compiled ? [] : [...rpcHostExecArgv(), resolveCliMainPath()]),
|
|
261
262
|
"--mode",
|
|
262
263
|
"rpc",
|
|
263
264
|
"--multi-session",
|
package/docs/rpc.md
CHANGED
|
@@ -882,6 +882,8 @@ The command response reports only `{ cancelled }`, so this event is the only pus
|
|
|
882
882
|
|
|
883
883
|
The lifecycle supervisor is also available to bundled/rebranded runtimes through the hidden internal launch route `--internal-rpc-host-supervisor`. This route is wire-invisible and intended only for desktop launchers: it receives the public socket, ownership directory, and the runtime command/arguments to wrap, then runs the same `host-lifecycle.ts` implementation used by `ensureHost()`. Normal CLI modes do not use or advertise this route. Compiled standalone binaries also re-enter themselves through this route automatically: a bun executable always boots its embedded entrypoint, so the script-path re-entry used under a JS runtime would be parsed as CLI arguments (`Unknown option: --socket`) and the host could never start.
|
|
884
884
|
|
|
885
|
+
When a Node.js or Bun caller starts a host, both spawn levels retain runtime options such as loaders and memory limits, but remove eval/print expressions, `--input-type`, and interactive mode from `process.execArgv`. This lets embedding code launched with `node -e` or `bun -e` start the intended host entry instead of replaying itself. Under Bun, any token beginning with `-e` or `-p` is removed as well, because Bun reads it as code glued to the flag (`-eCODE`, `-e=CODE`, `-pCODE`); Node rejects those forms, so single-dash V8 options such as `-expose-gc` are kept there. Explicit child commands keep their supplied arguments.
|
|
886
|
+
|
|
885
887
|
On win32 the supervisor's internal hop lives under `<daemonDir>/internal-<uuid>`, and that directory is created recursively. Before allocating the internal hop or spawning a child, the supervisor ensures `<publicSocket>.secret` exists, creating its parent directories and a 32-byte secret with mode `0600` when needed. An existing valid secret, including one written by `ensureHost()`, is reused unchanged. Direct launch therefore works on a fresh profile without caller-side secret provisioning. Provisioning failures identify the bootstrap step and secret path; the public endpoint still requires the secret handshake before forwarding RPC traffic.
|
|
886
888
|
|
|
887
889
|
Hosts started through `ensureHost()` are wrapped by a lifecycle supervisor that owns the public socket and spawns the
|
package/docs/settings.md
CHANGED
|
@@ -693,6 +693,7 @@ Paths in `~/.senpi/agent/settings.json` resolve relative to `~/.senpi/agent`. Pa
|
|
|
693
693
|
| `prompts` | string[] | `[]` | Local prompt template paths or directories |
|
|
694
694
|
| `themes` | string[] | `[]` | Local theme file paths or directories |
|
|
695
695
|
| `enableSkillCommands` | boolean | `true` | Register skills as `/skill:name` commands |
|
|
696
|
+
| `maxSkillExpansionsPerPrompt` | number | `5` | Distinct skills one prompt may expand; later skill commands stay literal. Must be a positive integer, otherwise the default applies. The parser reads at most 64 invocation tokens per prompt |
|
|
696
697
|
|
|
697
698
|
Arrays support glob patterns and exclusions. Use `!pattern` to exclude. Use `+path` to force-include an exact path and `-path` to force-exclude an exact path.
|
|
698
699
|
|
package/docs/skills.md
CHANGED
|
@@ -92,13 +92,15 @@ do not become skill invocations.
|
|
|
92
92
|
|
|
93
93
|
After resolving the explicit tokens, Senpi removes only those tokens and wraps the remaining text once
|
|
94
94
|
as the user request. Unknown tokens stay literal, duplicates are skipped, and at most five distinct
|
|
95
|
-
skills expand per prompt.
|
|
95
|
+
skills expand per prompt. Raise that limit with `maxSkillExpansionsPerPrompt` in `settings.json`
|
|
96
|
+
when you deliberately compose more skills.
|
|
96
97
|
|
|
97
98
|
Toggle skill commands via `/settings` in interactive mode or in `settings.json`:
|
|
98
99
|
|
|
99
100
|
```json
|
|
100
101
|
{
|
|
101
|
-
"enableSkillCommands": true
|
|
102
|
+
"enableSkillCommands": true,
|
|
103
|
+
"maxSkillExpansionsPerPrompt": 5
|
|
102
104
|
}
|
|
103
105
|
```
|
|
104
106
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@code-yeongyu/senpi",
|
|
3
|
-
"version": "2026.10.
|
|
3
|
+
"version": "2026.10.3",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"piConfig": {
|
|
@@ -56,9 +56,9 @@
|
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
58
|
"@earendil-works/chord": "0.99.1",
|
|
59
|
-
"@earendil-works/pi-agent-core": "npm:@code-yeongyu/senpi-agent-core@2026.10.
|
|
60
|
-
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.10.
|
|
61
|
-
"@earendil-works/pi-tui": "npm:@code-yeongyu/senpi-tui@2026.10.
|
|
59
|
+
"@earendil-works/pi-agent-core": "npm:@code-yeongyu/senpi-agent-core@2026.10.3",
|
|
60
|
+
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.10.3",
|
|
61
|
+
"@earendil-works/pi-tui": "npm:@code-yeongyu/senpi-tui@2026.10.3",
|
|
62
62
|
"@silvia-odwyer/photon-node": "0.3.4",
|
|
63
63
|
"chalk": "6.0.0",
|
|
64
64
|
"cross-spawn": "7.0.6",
|
|
@@ -77,8 +77,8 @@
|
|
|
77
77
|
"yaml": "2.9.1",
|
|
78
78
|
"@anthropic-ai/claude-agent-sdk": "0.3.286",
|
|
79
79
|
"@anthropic-ai/sdk": "0.127.0",
|
|
80
|
-
"@code-yeongyu/senpi-codemode": "2026.10.
|
|
81
|
-
"@earendil-works/pi-pty": "npm:@code-yeongyu/senpi-pty@2026.10.
|
|
80
|
+
"@code-yeongyu/senpi-codemode": "2026.10.3",
|
|
81
|
+
"@earendil-works/pi-pty": "npm:@code-yeongyu/senpi-pty@2026.10.3",
|
|
82
82
|
"@modelcontextprotocol/sdk": "1.30.0",
|
|
83
83
|
"@mozilla/readability": "0.6.0",
|
|
84
84
|
"linkedom": "0.18.13",
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { createRequire as __piCreateRequire } from "node:module"; const require = __piCreateRequire(import.meta.url); { const __piWorkerThreads = require("node:worker_threads"); if (typeof __piWorkerThreads.markAsUncloneable !== "function") { __piWorkerThreads.markAsUncloneable = () => {}; } }
|
|
2
|
-
import{CUSTOM_UNSUPPORTED_CAPABILITY,EXTENSION_EVENTS_CAPABILITY,RPC_CLIENT_CAPABILITIES_ENV,SESSION_CONTEXT_CAPABILITY,SESSION_KIND_CAPABILITY,hostLaunchProfile,socketHostLaunchProfile}from"./chunk-LJWC5IVX.js";import{BUILTIN_PATH_PREFIX}from"./chunk-QMRJYMNH.js";import{acquireHostEnsureLock,acquireOwnershipSafeLock,anyGenerationLive,endpointKindOfSocket,gcHostEndpoints,hostEnsureLockOptions,hostEnsureLockTarget,listHostEndpoints,socketNeedsHandshake}from"./chunk-6KIZRGOT.js";import{DEFAULT_HOST_IDLE_EXIT_MS,INTERNAL_SUPERVISOR_FLAG,OBSERVE_REQUEST_FIELD,readHostCrashRecords,resolveCliMainPath}from"./chunk-LACVPFMM.js";import{HOST_GENERATION_ENV,HOST_INSTANCE_ID_ENV}from"./chunk-CIRZTP7W.js";import{HOST_DAEMON_DIR_ENV,HOST_STATE_FILE_MODE,MAX_SOCKET_PATH_BYTES,ProcessIdentityUnreadableError,claimOwnerIsLive,clearHostRegistration,createDaemonDirectories,createHostDaemonPaths,createSocketSecret,ensureEndpointIdentity,generationBindPath,generationPaths,hostDaemonDirectoryPaths,parseDaemonPidFile,parseJson,parseShardSocket,processIsLive,processMatchesPidFile,provenOwner,pruneDeadGenerations,readFileOrUndefined,readGenerationRows,readHostProcessMetrics,readHostRegistration,readHostSettings,readProcessStartTime,readSessionPathClaims,readSocketSecret,releaseGeneration,resolveSocketTransportAddress,sameEndpoint,sendSocketHandshake,sessionDirectoryRemoved,shardKey,shardSocketPathForKey,socketSecretPath,statSocketIdentity,waitForStartTime,writeGenerationRecord,writeHostRegistration,writeHostSettings,writeStateFile,writtenByThisProcess}from"./chunk-22DAHAHO.js";import{compareEngineOrdinal,engineBuildIdentity}from"./chunk-XYDZ3LIE.js";import{isSessionRuntimeKind}from"./chunk-MH2BFYGN.js";import{APP_NAME,ENV_AGENT_DIR,envValue,getAgentDir,isBunBinary}from"./chunk-463OK2Y4.js";import{writeSync}from"node:fs";import{join as join5,resolve as resolve3}from"node:path";import{readFile,realpath,stat}from"node:fs/promises";import{dirname,isAbsolute,relative,resolve}from"node:path";var SPEC_ENV_NAME=/^(SENPI|OMO|PI)_[A-Z0-9_]+$/u,HostLaunchSpecError=class extends Error{constructor(reason,detail){super(`${reason}: ${detail}`),this.name="HostLaunchSpecError",this.reason=reason,this.detail=detail}},DEFAULT_HOST_LAUNCH_SPEC={hostArgs:[],policy:{},env:{}};async function loadHostLaunchSpec(specPath,platform=process.platform){let absolute=resolve(specPath);await assertTrustedFile(absolute,platform);let spec=parseHostLaunchSpec(await readSpecText(absolute)),env=trustedEnv(spec.env),extensions=await resolveExtensions(spec.core.extensions,dirname(absolute));return{hostArgs:["--session-runtime",spec.core.session_runtime,...extensions.flatMap(path=>["--extension",path])],policy:{...spec.tunables?.idleExitMs!==void 0&&{idleExitMs:spec.tunables.idleExitMs},...spec.tunables?.coldStart!==void 0&&{coldStart:spec.tunables.coldStart}},env}}function parseHostLaunchSpec(text){let parsed;try{parsed=JSON.parse(text)}catch(error){throw new HostLaunchSpecError("launch_spec_invalid",error instanceof Error?error.message:"not JSON")}if(!isRecord(parsed))throw new HostLaunchSpecError("launch_spec_invalid","the spec is not a JSON object");if(parsed.spec_version!==1)throw new HostLaunchSpecError("launch_spec_invalid",`unsupported spec_version ${String(parsed.spec_version)}`);let core=parsed.core;if(!isRecord(core))throw new HostLaunchSpecError("launch_spec_invalid","core must be an object");if(typeof core.session_runtime!="string"||!isSessionRuntimeKind(core.session_runtime))throw new HostLaunchSpecError("launch_spec_invalid","core.session_runtime must be in-process or worker");if(core.multi_session!==!0)throw new HostLaunchSpecError("launch_spec_invalid","core.multi_session must be true");if(!Array.isArray(core.extensions)||!core.extensions.every(entry=>typeof entry=="string"))throw new HostLaunchSpecError("launch_spec_invalid","core.extensions must be an array of strings");let tunables=parsedTunables(parsed.tunables);return{spec_version:1,core:{session_runtime:core.session_runtime,multi_session:!0,extensions:core.extensions},...tunables&&{tunables},...isRecord(parsed.env)&&{env:parsedEnv(parsed.env)}}}async function readSpecText(absolute){try{return await readFile(absolute,"utf8")}catch(error){throw new HostLaunchSpecError("launch_spec_unreadable",error instanceof Error?error.message:absolute)}}async function assertTrustedFile(absolute,platform){if(platform==="win32")return;let stats=await stat(absolute).catch(error=>{throw new HostLaunchSpecError("launch_spec_unreadable",error instanceof Error?error.message:absolute)}),uid=process.getuid?.();if(uid!==void 0&&stats.uid!==uid)throw new HostLaunchSpecError("launch_spec_insecure",`${absolute} is owned by uid ${stats.uid}, not ${uid}`);if((stats.mode&18)!==0)throw new HostLaunchSpecError("launch_spec_insecure",`${absolute} is group/world writable (mode ${(stats.mode&511).toString(8)})`)}function trustedEnv(env){for(let name of Object.keys(env??{}))if(!SPEC_ENV_NAME.test(name))throw new HostLaunchSpecError("launch_spec_env_denied",`${name} is outside ${SPEC_ENV_NAME.source}`);return env??{}}async function resolveExtensions(extensions,specDir){let rootReal=await realpath(specDir).catch(()=>specDir),resolved=[];for(let entry of extensions){let target=resolve(specDir,entry);if(escapes(specDir,target))throw new HostLaunchSpecError("launch_spec_path_escape",`${entry} resolves outside ${specDir}`);let real=await realpath(target).catch(()=>{});if(real===void 0)throw new HostLaunchSpecError("launch_spec_missing_extension",`${target} does not exist`);if(escapes(rootReal,real))throw new HostLaunchSpecError("launch_spec_path_escape",`${entry} links outside ${specDir}`);resolved.push(real)}return resolved}function escapes(root,target){let step=relative(root,target);return step.startsWith("..")||isAbsolute(step)}function parsedTunables(value){if(!isRecord(value))return;let idleExitMs=value.idleExitMs,coldStart=value.coldStart;return{...typeof idleExitMs=="number"&&Number.isFinite(idleExitMs)&&idleExitMs>0&&{idleExitMs},...(coldStart==="transient"||coldStart==="persistent")&&{coldStart}}}function parsedEnv(value){let env={};for(let[name,entry]of Object.entries(value)){if(typeof entry!="string")throw new HostLaunchSpecError("launch_spec_invalid",`env.${name} must be a string`);env[name]=entry}return env}function isRecord(value){return typeof value=="object"&&value!==null&&!Array.isArray(value)}import{join}from"node:path";var ALLOWED_ENV_NAMES=[/^(PATH|HOME|USER|LOGNAME|SHELL|TMPDIR|TERM|LANG)$/u,/^(LC|XDG)_/u,/^(SENPI|OMO|PI)_/u,/^(HTTPS?_PROXY|NO_PROXY)$/u,/^[A-Z0-9_]+_API_KEY$/u,/^(ANTHROPIC|OPENAI|GOOGLE|GEMINI|AZURE|AWS|OPENROUTER|XAI|MISTRAL|DEEPSEEK|GROQ|CEREBRAS|MINIMAX)_/u],WINDOWS_SYSTEM_ENV_NAMES=new Set(["SYSTEMROOT","SYSTEMDRIVE","WINDIR","COMSPEC","PATHEXT","TEMP","TMP","USERPROFILE","APPDATA","LOCALAPPDATA","PROGRAMFILES","PROGRAMFILES(X86)","PROGRAMDATA","HOMEDRIVE","HOMEPATH","NUMBER_OF_PROCESSORS","PROCESSOR_ARCHITECTURE","OS"]),TRANSIENT_ENV_NAMES=new Set(["PI_SESSION_ID","PI_SESSION_FILE","PI_SESSION_CWD","PI_GOAL_STORE_FILE","PI_PROVIDER","PI_MODEL","PI_REASONING_LEVEL","PI_PROMPT_CACHE_SAFE_WAIT_SECONDS","SENPI_PY_KERNEL_PARENT_PID","SENPI_RPC_HOST_WATCH_FD","SENPI_RPC_HOST_WATCH_PPID","SENPI_RPC_HOST_SCRATCH_DIR","SENPI_RPC_HOST_CLEANUP_PATHS","SENPI_RPC_HOST_PUBLIC_SOCKET","SENPI_RPC_HOST_INSTANCE_ID","SENPI_RPC_HOST_GENERATION","SENPI_RPC_HOST_DAEMON_DIR","SENPI_RPC_HOST_EXPECTED_RUNTIME_BUILD_ID"]);function daemonEnvIsAllowed(name,platform=process.platform){let upper=name.toUpperCase();return TRANSIENT_ENV_NAMES.has(platform==="win32"?upper:name)?!1:platform!=="win32"?ALLOWED_ENV_NAMES.some(pattern=>pattern.test(name)):WINDOWS_SYSTEM_ENV_NAMES.has(upper)||ALLOWED_ENV_NAMES.some(pattern=>pattern.test(upper))}function daemonEnvOverrides(processEnv,specEnv={},platform=process.platform){let overrides={};for(let name of Object.keys(processEnv))daemonEnvIsAllowed(name,platform)||(overrides[name]=null);for(let[name,value]of Object.entries(specEnv))daemonEnvIsAllowed(name,platform)&&(overrides[name]=value);return overrides}function daemonEnvironment(processEnv,overrides={},forced={},platform=process.platform){let env={};for(let[name,value]of Object.entries(processEnv))value!==void 0&&daemonEnvIsAllowed(name,platform)&&(env[name]=value);for(let[name,value]of Object.entries(overrides))daemonEnvIsAllowed(name,platform)&&(deleteEnvironmentName(env,name,platform),value!==null&&(env[name]=value));for(let[name,value]of Object.entries(forced))deleteEnvironmentName(env,name,platform),env[name]=value;return env}function daemonEnvKeys(processEnv,specEnv={},platform=process.platform){let kept=Object.keys(processEnv).filter(name=>daemonEnvIsAllowed(name,platform)),explicit=Object.keys(specEnv).filter(name=>daemonEnvIsAllowed(name,platform));return[...new Set([...kept,...explicit])].sort()}async function writeDaemonEnvKeys(paths,keys){await writeStateFile(daemonEnvKeysFile(paths),{env_keys:keys})}async function readDaemonEnvKeys(paths){let keys=parseJson(await readFileOrUndefined(daemonEnvKeysFile(paths)))?.env_keys;return Array.isArray(keys)?keys.filter(key=>typeof key=="string"):[]}function daemonEnvKeysFile(paths){return join(paths.dir,"env-keys.json")}function deleteEnvironmentName(env,name,platform){if(platform!=="win32"){delete env[name];return}let upper=name.toUpperCase();for(let existing of Object.keys(env))existing.toUpperCase()===upper&&delete env[existing]}import{createHash}from"node:crypto";import{existsSync}from"node:fs";import{readdir,readFile as readFile2,realpath as realpath2,stat as stat2}from"node:fs/promises";import{basename,dirname as dirname2,join as join2,relative as relative2,sep}from"node:path";import{fileURLToPath}from"node:url";var RUNTIME_IDENTITY_HANDOVER_CAPABILITY="runtime_identity_handover",RUNTIME_BUILD_ID=/^sha256:[0-9a-f]{64}$/,EXCLUDED_FILE=/\.(?:d\.[cm]?ts|map)$/,EXCLUDED_NAMES=new Set(["node_modules","runtime-manifest.json","runtime-snapshot.json"]),READ_CONCURRENCY=32;function parseRuntimeBuildId(value){return typeof value=="string"&&RUNTIME_BUILD_ID.test(value)?value:void 0}async function computeRuntimeBuildId(input){let source=input.source??loadedRuntimeSource(),plugins=await Promise.all(input.profile.extensions.map(path=>extensionDigest(path))),manifest={schema:1,flavour:source.flavour,platform:input.platform??process.platform,arch:input.arch??process.arch,engine:input.engine??engineBuildIdentity().text,runtime:await treeDigest(source.root),plugins:plugins.sort(),profile:{multi_session:input.profile.multi_session,session_runtime:input.profile.session_runtime}};return`sha256:${sha256(JSON.stringify(manifest))}`}function loadedRuntimeSource(){if(isBunBinary)return{flavour:"compiled",root:process.execPath};let moduleDir=dirname2(fileURLToPath(import.meta.url)),packageRoot=nearestPackageRoot(moduleDir),[top,next]=relative2(packageRoot,moduleDir).split(sep);return top==="dist"?{flavour:"packaged",root:join2(packageRoot,next==="bundle"?join2("dist","bundle"):"dist")}:{flavour:"dev",root:join2(packageRoot,top??"")}}function nearestPackageRoot(start){for(let dir=start;;dir=dirname2(dir)){if(existsSync(join2(dir,"package.json")))return dir;if(dirname2(dir)===dir)return start}}function extensionDigest(path){let name=basename(path);return name.startsWith(BUILTIN_PATH_PREFIX)?Promise.resolve(sha256(name)):treeDigest(path)}async function treeDigest(root){let files=[];(await stat2(root)).isFile()?files.push(["",root]):await collect(await realpath2(root),"",files,new Set);let digests=await mapLimited(files,READ_CONCURRENCY,async([path,file])=>[path,sha256(await readFile2(file))]);return digests.sort(([a],[b])=>a<b?-1:a>b?1:0),sha256(JSON.stringify(digests))}async function collect(dir,prefix,files,seen){if(seen.has(dir))return;seen.add(dir);let entries=(await readdir(dir,{withFileTypes:!0})).sort((a,b)=>a.name<b.name?-1:a.name>b.name?1:0);for(let entry of entries){if(entry.name.startsWith(".")||EXCLUDED_NAMES.has(entry.name))continue;let path=join2(dir,entry.name),name=prefix===""?entry.name:`${prefix}/${entry.name}`,kind=entry.isSymbolicLink()?await stat2(path).catch(()=>{}):entry;kind!==void 0&&(kind.isDirectory()?await collect(await realpath2(path),name,files,seen):kind.isFile()&&!EXCLUDED_FILE.test(entry.name)&&files.push([name,path]))}}async function mapLimited(items,limit,map){let results=new Array(items.length),next=0,worker=async()=>{for(let index=next++;index<items.length;index=next++)results[index]=await map(items[index])};return await Promise.all(Array.from({length:Math.min(limit,items.length)},worker)),results}function sha256(data){return createHash("sha256").update(data).digest("hex")}function parseHostProtocolInfo(data){if(!isRecord2(data)||typeof data.serverVersion!="string"||!Array.isArray(data.capabilities)||!data.capabilities.every(entry=>typeof entry=="string"))return;let ordinal=parseOrdinal(data.engineOrdinal),launchProfile=parseLaunchProfile(data.launch_profile),runtimeBuildId=parseRuntimeBuildId(data.runtimeBuildId);return{protocolVersion:typeof data.protocolVersion=="number"?data.protocolVersion:0,serverVersion:data.serverVersion,capabilities:data.capabilities,...typeof data.instanceId=="string"&&{instanceId:data.instanceId},...typeof data.generation=="number"&&Number.isSafeInteger(data.generation)&&{generation:data.generation},...typeof data.engineVersion=="string"&&{engineVersion:data.engineVersion},...ordinal&&{engineOrdinal:ordinal},...launchProfile&&{launch_profile:launchProfile},...typeof data.memory_pressure=="boolean"&&{memory_pressure:data.memory_pressure},...runtimeBuildId!==void 0&&{runtimeBuildId},...isRecord2(data.handover)&&{handover:data.handover}}}function parseOrdinal(value){if(!(!Array.isArray(value)||value.length!==5||!value.every(part=>typeof part=="number")))return[value[0],value[1],value[2],value[3],value[4]]}function parseLaunchProfile(value){if(!isRecord2(value)||typeof value.profile_id!="string"||!isRecord2(value.core))return;let{extensions,multi_session,session_runtime}=value.core;if(!(!Array.isArray(extensions)||!extensions.every(entry=>typeof entry=="string"))&&typeof multi_session=="boolean"&&!(session_runtime!=="in-process"&&session_runtime!=="worker"))return{profile_id:value.profile_id,core:{extensions,multi_session,session_runtime}}}function isRecord2(value){return typeof value=="object"&&value!==null&&!Array.isArray(value)}var HOST_PROTOCOL_VERSION=1,GENERATION_HANDOFF_CAPABILITY="generation_handoff",REQUIRED_HOST_CAPABILITIES=["multi_session",EXTENSION_EVENTS_CAPABILITY,SESSION_CONTEXT_CAPABILITY,SESSION_KIND_CAPABILITY],HostEnsureRefusedError=class extends Error{constructor(socket,reason,host,detail){super(`RPC socket ${socket} refused: ${reason} - ${refusalDetail(reason,host)}${detail?`: ${detail}`:""}`),this.name="HostEnsureRefusedError",this.socket=socket,this.reason=reason,this.detail=detail}};function refusalDetail(reason,host){switch(reason){case"protocol":return`the running host speaks protocol version ${host?.protocolVersion??"unknown"}, this build speaks ${HOST_PROTOCOL_VERSION}`;case"capability":{let missing=REQUIRED_HOST_CAPABILITIES.filter(capability=>!host?.capabilities.includes(capability));return`the running host is missing ${JSON.stringify(missing)}; it owns the socket, so no second host is started`}case"foreign_writer":return"its pidfile was written by another process, so this one may not signal it; stop that host explicitly instead";case"legacy_host":return"a host from before the per-socket daemon directory is still running; it is drained and replaced only while it holds no session, and no second host is started beside it";case"host_busy":return"its socket accepts connections but did not answer inside the probe budget: a live host under load, which is never ended to make room for a replacement";default:return assertNever(reason)}}function decideHostAction(client,host,policy){if(host===void 0)return{action:"start",reason:client.startedByUs?"restart_own_host":"no_host",upgradeable:!1};if(host.protocolVersion!==client.protocolVersion)return{action:"refuse",reason:"protocol",upgradeable:!1};if(!client.requiredCapabilities.every(capability=>host.capabilities.includes(capability)))return policy==="fallback"?{action:"fallback",reason:"capability",upgradeable:!1}:{action:"refuse",reason:"capability",upgradeable:!1};let upgradeable=client.platform!=="win32"&&host.capabilities.includes(GENERATION_HANDOFF_CAPABILITY);if(policy==="fallback"&&host.engineVersion!==client.identity.text)return{action:"fallback",reason:"engine_mismatch",upgradeable};let warning=profileWarning(client,host);return client.platform==="win32"?{action:"reuse",reason:"win32_attach_only",upgradeable:!1,...warning&&{warning}}:upgradeable?policy==="upgrade"&&client.launchProfile&&host.launch_profile&&supersedes(client,host)?{action:"handoff",reason:sameRelease(client.identity.ordinal,host.engineOrdinal)?"profile":"newer_engine",upgradeable:!0}:{action:"reuse",reason:"compatible",upgradeable,...warning&&{warning}}:{action:"reuse",reason:"handoff_unsupported",upgradeable:!1,...warning&&{warning}}}function supersedes(client,host){if(host.engineOrdinal===void 0)return!1;let hostIdentity={text:host.engineVersion??"",ordinal:host.engineOrdinal,scheme:host.engineOrdinal[4]>0?"epoch":"nodef"};return compareEngineOrdinal(client.identity,hostIdentity)<=0?!1:covers(client.launchProfile,host.launch_profile)}function profileWarning(client,host){if(client.launchProfile===void 0||host.launch_profile===void 0)return"profile_mismatch_attached";if(client.launchProfile.profile_id!==host.launch_profile.profile_id)return covers(client.launchProfile,host.launch_profile)?"profile_mismatch_attached":"profile_narrower_attached"}function covers(candidate,running){return candidate===void 0||running===void 0?!1:running.core.extensions.every(extension=>candidate.core.extensions.includes(extension))}function sameRelease(candidate,running){return running!==void 0&&candidate.slice(0,4).every((part,index)=>part===running[index])}function assertNever(value){throw new Error(`unreachable host decision: ${JSON.stringify(value)}`)}import{spawn as spawn2}from"node:child_process";import{randomUUID as randomUUID2}from"node:crypto";import{mkdir,open as open2,readdir as readdir2,readFile as readFile3,rm as rm2,writeFile as writeFile2}from"node:fs/promises";import{tmpdir}from"node:os";import{dirname as dirname5,join as join4}from"node:path";import{createConnection}from"node:net";function holdAttachment(socket){return socket.removeAllListeners("data"),socket.resume(),socket.on("error",()=>socket.destroy()),socket.unref(),{release:()=>socket.destroy()}}var DEFAULT_PROBE_TIMEOUT_MS=1e4,PROBE_REQUEST_ID="ensure-host-probe";function probeHost(options){return probeProtocolInfo(options.socket,options.timeoutMs??DEFAULT_PROBE_TIMEOUT_MS)}async function probeProtocolInfo(socketPath,timeoutMs){let reply=await requestOnSocket(socketPath,{type:"get_protocol_info"},timeoutMs);return reply===void 0?void 0:parseHostProtocolInfo(reply)}async function observeProtocolInfo(socketPath,timeoutMs){let reply=await requestOnSocket(socketPath,{type:"get_protocol_info",[OBSERVE_REQUEST_FIELD]:!0},timeoutMs);return reply===void 0?void 0:parseHostProtocolInfo(reply)}async function holdProtocolInfo(socketPath,timeoutMs){let outcome=await connectAndAsk(socketPath,{id:PROBE_REQUEST_ID,type:"get_protocol_info"},timeoutMs,!0),info=outcome.answer===void 0?void 0:parseHostProtocolInfo(outcome.answer);if(outcome.kept===void 0)return;let hold=holdAttachment(outcome.kept);if(info!==void 0)return{info,hold};hold.release()}async function probeSocketReachable(socketPath,timeoutMs){return(await connectAndAsk(socketPath,{id:PROBE_REQUEST_ID,type:"get_protocol_info"},timeoutMs)).connected}async function probeSessionCount(socketPath,timeoutMs){let reply=await requestOnSocket(socketPath,{type:"list_sessions",include_workers:!0},timeoutMs);if(!(!isRecord3(reply)||!Array.isArray(reply.sessions)))return reply.sessions.length}async function requestOnSocket(socketPath,command,timeoutMs){return(await connectAndAsk(socketPath,{id:PROBE_REQUEST_ID,...command},timeoutMs)).answer}async function connectAndAsk(socketPath,request,timeoutMs,keep=!1){let secret;if(socketNeedsHandshake(socketPath))try{secret=await readSocketSecret(socketSecretPath(socketPath))}catch{return{connected:!1,answer:void 0}}return new Promise(resolveProbe=>{let socket=createConnection(resolveSocketTransportAddress(socketPath,process.platform,secret)),buffer="",settled=!1,connected=!1,finish=value=>{if(!settled){if(settled=!0,clearTimeout(timeout),keep&&value!==void 0&&!socket.destroyed){resolveProbe({connected,answer:value,kept:socket});return}socket.destroy(),resolveProbe({connected,answer:value})}},timeout=setTimeout(()=>finish(),timeoutMs);socket.once("connect",()=>{connected=!0,socket.write(`${JSON.stringify(request)}
|
|
3
|
-
`)}),socket.on("data",chunk=>{for(buffer+=chunk.toString("utf8");;){let newline=buffer.indexOf(`
|
|
4
|
-
`);if(newline===-1)return;let line=buffer.slice(0,newline);buffer=buffer.slice(newline+1);let answer2=readAnswer(line);if(answer2!==void 0)return finish(answer2)}}),socket.once("error",()=>finish()),socket.once("close",()=>finish()),secret&&sendSocketHandshake(socket,secret)})}function readAnswer(text){let parsed;try{parsed=JSON.parse(text)}catch{return}if(!(!isRecord3(parsed)||parsed.id!==PROBE_REQUEST_ID||parsed.success!==!0))return parsed.data??{}}function isRecord3(value){return typeof value=="object"&&value!==null&&!Array.isArray(value)}import{spawn}from"node:child_process";import{randomUUID}from"node:crypto";import{open,rm,writeFile}from"node:fs/promises";import{existsSync as existsSync2}from"node:fs";import{basename as basename2,dirname as dirname3,resolve as resolve2}from"node:path";import{fileURLToPath as fileURLToPath2}from"node:url";var PINNED_HOST_CLIENT_CAPABILITIES=[EXTENSION_EVENTS_CAPABILITY,CUSTOM_UNSUPPORTED_CAPABILITY];function defaultHostLaunch(supervisorArgs,compiled=isBunBinary,sibling=resolveHostLifecycleEntryPath()??null){return compiled?{command:process.execPath,args:[INTERNAL_SUPERVISOR_FLAG,...supervisorArgs]}:sibling!==null?{command:process.execPath,args:[...process.execArgv,sibling,...supervisorArgs]}:{command:process.execPath,args:[...process.execArgv,resolveCliMainPath(),INTERNAL_SUPERVISOR_FLAG,...supervisorArgs]}}function resolveHostLifecycleEntryPath(){let modulePath=fileURLToPath2(import.meta.url);if(basename2(dirname3(modulePath))==="chunks")return;let extension=modulePath.endsWith(".ts")?".ts":".js",sibling=resolve2(dirname3(modulePath),`host-lifecycle${extension}`);return existsSync2(sibling)?sibling:void 0}var EXPECTED_RUNTIME_BUILD_ID_ENV="SENPI_RPC_HOST_EXPECTED_RUNTIME_BUILD_ID",NEW_WORK_COMMANDS=new Set(["prompt","steer","follow_up","send_custom_message","append_user_message","bash","compact","wake","edit_assistant_message","edit_user_message"]),DEFAULT_POLL_MS=200,HostIdleHandover=class{constructor(options){this.options=options}view(){return this.operation?.view}admits(commandType){let state=this.operation?.view.state;return state===void 0||state==="handover_blocked"||!NEW_WORK_COMMANDS.has(commandType)}async begin(request){let current=this.operation;if(current!==void 0&¤t.view.operationId===request.operationId)return sameTerms(current.request,request)?answer(current):{kind:"refused",reason:"operation_conflict",detail:"this operation id names other terms"};if(current!==void 0&¤t.view.state!=="handover_blocked")return current.request.targetRuntimeBuildId===request.targetRuntimeBuildId?answer(current):{kind:"refused",reason:"handover_in_progress",detail:current.view.operationId};let self=this.options.identity();if(self.instanceId!==request.ifInstanceId||self.generation!==request.ifGeneration)return{kind:"refused",reason:"stale_generation",detail:`this host is ${self.instanceId} generation ${self.generation}`};if(self.runtimeBuildId===request.targetRuntimeBuildId)return{kind:"refused",reason:"already_current",detail:self.runtimeBuildId};let operation={request,view:{operationId:request.operationId,state:"handover_pending",targetRuntimeBuildId:request.targetRuntimeBuildId}};return this.operation=operation,this.options.log?.(`handover ${request.operationId} pending: no new work is admitted`),this.idleNow()?(await this.switchOver(operation),answer(operation)):(this.timer=setInterval(()=>this.observe(),this.options.pollMs??DEFAULT_POLL_MS),this.timer.unref?.(),answer(operation))}observe(){let operation=this.operation;operation?.view.state==="handover_pending"&&this.idleNow()&&this.switchOver(operation)}dispose(){this.stopTimer()}idleNow(){try{return this.options.isIdle()}catch(cause){return this.options.log?.(`handover idle check failed, waiting: ${String(cause)}`),!1}}async switchOver(operation){if(operation.view.state!=="handover_pending")return;this.stopTimer(),operation.view={...operation.view,state:"handover_switching"};let outcome=await this.options.perform(operation.request).catch(cause=>({ok:!1,reason:`successor_unavailable: ${String(cause)}`}));operation.view=outcome.ok?{...operation.view,state:"handover_completed",successor:outcome.successor}:{...operation.view,state:"handover_blocked",reason:outcome.reason},this.options.log?.(`handover ${operation.view.operationId} ${operation.view.state}`)}stopTimer(){this.timer!==void 0&&clearInterval(this.timer),this.timer=void 0}};function sameTerms(a,b){return a.targetRuntimeBuildId===b.targetRuntimeBuildId&&a.ifInstanceId===b.ifInstanceId&&a.ifGeneration===b.ifGeneration}function answer(operation){return{kind:"operation",view:operation.view}}function initialHostEnvironment(options){return daemonEnvironment(process.env,options.env,{[ENV_AGENT_DIR]:options.agentDir??getAgentDir(),[RPC_CLIENT_CAPABILITIES_ENV]:PINNED_HOST_CLIENT_CAPABILITIES.join(","),[HOST_INSTANCE_ID_ENV]:options.instanceId,[HOST_GENERATION_ENV]:String(options.generation),[HOST_DAEMON_DIR_ENV]:options.paths.dir})}function successorHostEnvironment(options){return daemonEnvironment(process.env,options.env,{[RPC_CLIENT_CAPABILITIES_ENV]:PINNED_HOST_CLIENT_CAPABILITIES.join(","),...options.agentDir?{[ENV_AGENT_DIR]:options.agentDir}:{},[HOST_INSTANCE_ID_ENV]:options.instanceId,[HOST_GENERATION_ENV]:String(options.generation),[HOST_DAEMON_DIR_ENV]:options.paths.dir,...options.expectedRuntimeBuildId!==void 0&&{[EXPECTED_RUNTIME_BUILD_ID_ENV]:options.expectedRuntimeBuildId}})}import{dirname as dirname4,join as join3}from"node:path";var LEGACY_PROBE_TIMEOUT_MS=1e4,DRAIN_POLL_MS=50;async function retireIdleLegacyHost(paths,probe,drainTimeoutMs){let judged=await judgeLegacyHost(paths,probe);if(judged.verdict==="absent")return;if(judged.verdict==="held")return judged.detail;let{record,socket}=judged.host;if(!await drainAndWait(record,probe,drainTimeoutMs))return`pid ${record.pid} (${socket}) was asked to drain and is still running after ${drainTimeoutMs}ms; retry once it exits`}async function readLegacyHost(paths){let text=await readFileOrUndefined(paths.legacyPidFile),record=text===void 0?void 0:parseDaemonPidFile(text);if(record===void 0)return;let stamped=parseJson(text)?.socket;return{record,socket:typeof stamped=="string"?stamped:join3(dirname4(paths.flatDir),"rpc","rpc.sock")}}async function provenLegacyOwner(paths,socket){let legacy=await readLegacyHost(paths),startTime=legacy?.record.processStartTime;if(!legacy||startTime===null||startTime===void 0||!sameEndpoint(legacy.socket,socket))return;let identity={pid:legacy.record.pid,processStartTime:startTime};return await processMatchesPidFile(identity,readProcessStartTime).catch(()=>!1)?identity:void 0}async function judgeLegacyHost(paths,probe,platform=process.platform){let host=await readLegacyHost(paths);if(host===void 0)return{verdict:"absent"};let{pid}=host.record,identity=await processMatchesPidFile(host.record,probe).then(owns=>owns?"proven":"gone",error=>{if(error instanceof ProcessIdentityUnreadableError)return"unreadable";throw error});if(identity==="gone")return{verdict:"absent"};let where=`pid ${pid} (${host.socket})`;if(identity==="unreadable")return{verdict:"held",detail:`${where} cannot be proven to be the process its record names; stop it by hand`};if(platform==="win32")return{verdict:"held",detail:`${where} cannot be drained on win32; it exits once idle, then retry`};let protocol=await probeProtocolInfo(host.socket,LEGACY_PROBE_TIMEOUT_MS);if(!protocol?.capabilities.includes(GENERATION_HANDOFF_CAPABILITY))return{verdict:"held",detail:`${where} ${protocol?"cannot drain":"does not answer"}; it is never signalled - stop it by hand, then retry`};let sessions=await probeSessionCount(host.socket,LEGACY_PROBE_TIMEOUT_MS);if(sessions===0)return{verdict:"idle",host};let held=sessions===void 0?"an unknown number of sessions":`${sessions} open session${sessions===1?"":"s"}`;return{verdict:"held",detail:`${where} holds ${held}; run \`${APP_NAME} host stop --drain --socket ${host.socket}\` to let it finish that work and exit, then retry`}}async function drainAndWait(record,probe,timeoutMs){try{process.kill(record.pid,"SIGUSR1")}catch(error){if(!(error instanceof Error&&"code"in error&&error.code==="ESRCH"))throw error}let stillRunning=()=>processMatchesPidFile(record,probe,processIsLive,{attempts:1}).catch(error=>{if(error instanceof ProcessIdentityUnreadableError)return!0;throw error}),deadline=Date.now()+timeoutMs;for(;await stillRunning();){if(Date.now()>deadline)return!1;await new Promise(resolve4=>setTimeout(resolve4,DRAIN_POLL_MS))}return!0}async function stopHost(options){let paths=createHostDaemonPaths({socket:options.socket,...options.agentDir?{agentDir:options.agentDir}:{}}),registered=await readHostRegistration(paths),owner=await provenOwner(registered,options.socket);if(options.drain===!0){let pid=owner?.pid??(await provenLegacyOwner(paths,options.socket))?.pid;return pid===void 0?{action:"refuse",reason:"unknown_owner"}:!(await probeProtocolInfo(options.socket,options.timeoutMs??1e4))?.capabilities.includes(GENERATION_HANDOFF_CAPABILITY)||process.platform==="win32"?{action:"refuse",reason:"drain_unsupported"}:(signalGeneration(pid,"SIGUSR1"),{action:"drained",pid})}if(!owner)return{action:"refuse",reason:"unknown_owner"};let host=await probeProtocolInfo(options.socket,options.timeoutMs??1e4),sessions=await probeSessionCount(options.socket,options.timeoutMs??1e4);return options.force!==!0&&host!==void 0&&sessions!==void 0&&sessions>0?{action:"refuse",reason:"sessions_live"}:(signalGeneration(owner.pid,"SIGTERM"),await releaseGeneration(paths,{instanceId:owner.instanceId,pid:owner.pid}),{action:"stopped",pid:owner.pid})}function signalGeneration(pid,signal){try{return process.kill(pid,signal),!0}catch(error){if(error instanceof Error&&"code"in error&&error.code==="ESRCH")return!1;throw error}}var DEFAULT_HANDOFF_READINESS_MS=3e4,SUCCESSOR_EXIT_WAIT_MS=5e3,SUCCESSOR_START_BUDGET_MS=1e4+DEFAULT_HANDOFF_READINESS_MS+2e3+SUCCESSOR_EXIT_WAIT_MS;async function startSuccessor(context){let{options,paths,host,owner}=context,generation=(host.generation??0)+1,instanceId=randomUUID(),bindSocket=generationBindPath(options.socket,generation);if(Buffer.byteLength(bindSocket)>MAX_SOCKET_PATH_BYTES)return{action:"refuse",reason:"socket_path_too_long",upgradeable:!0,detail:bindSocket};let replaced=await statSocketIdentity(options.socket);if(!replaced)return{action:"refuse",reason:"socket_replaced",upgradeable:!0};let running=await readHostSettings(paths),bootSettings=await readFileOrUndefined(paths.settingsFile);await writeHostSettings(paths,{socket:options.socket,capabilities:PINNED_HOST_CLIENT_CAPABILITIES,coldStart:options.policy?.coldStart??running?.coldStart??"transient",idleExitMs:options.policy?.idleExitMs??running?.idleExitMs??9e5,generation,instanceId});let child,exited;try{await options._test?.beforeSpawn?.();let argv=["--socket",options.socket,"--bind",bindSocket,"--replace",`${replaced.dev}:${replaced.ino}`,...options.hostArgs??[]],launch=(options._test?.launch??options.launch)?.(argv)??defaultHostLaunch(argv),stderr=await open(paths.stderrLog,"a",384),spawned=spawn(launch.command,[...launch.args],{detached:!0,windowsHide:!0,env:successorHostEnvironment({agentDir:options.agentDir,env:options.env,expectedRuntimeBuildId:options.expectedRuntimeBuildId,paths,generation,instanceId}),stdio:["ignore","ignore",stderr.fd]});if(await stderr.close(),child=spawned,exited=new Promise(resolve4=>spawned.once("exit",()=>resolve4())),child.pid===void 0)throw new Error("failed to spawn the successor generation");let processStartTime=await waitForStartTime(child.pid,1e4).catch(()=>{})??null,registration={record:{pid:child.pid,processStartTime},socket:options.socket,instanceId,generation,launchProfileId:hostLaunchProfile(["--mode","rpc","--multi-session",...options.hostArgs??[]],process.cwd()).profile_id};await writeGenerationRecord(paths,registration),await options._test?.afterSpawn?.(child.pid);let answer2=await awaitSuccessor(options,host,exited);if(!answer2){let reason=await abortReason(options.socket,replaced),cleanupFailure=await abandonSuccessor({child,exited,paths,instanceId,bootSettings});return{action:"refuse",reason,upgradeable:!0,...cleanupFailure&&{detail:cleanupFailure}}}return await options._test?.beforeRegistration?.(),await writeHostRegistration(paths,registration),child.unref(),signalGeneration(owner.pid,"SIGUSR1"),{action:"handoff",pid:child.pid,socket:options.socket,generation:answer2.generation??generation,instanceId:answer2.instanceId??""}}catch(cause){let cleanupFailure=await abandonSuccessor({child,exited,paths,instanceId,bootSettings}),detail=cause instanceof Error?cause.message:String(cause);return{action:"refuse",reason:"successor_unavailable",upgradeable:!0,detail:cleanupFailure?`${detail}; ${cleanupFailure}`:detail}}}async function abandonSuccessor(context){let{child,exited,paths,instanceId,bootSettings}=context;child!==void 0&&child.exitCode===null&&child.signalCode===null&&child.kill("SIGKILL");try{bootSettings===void 0?await rm(paths.settingsFile,{force:!0}):await writeFile(paths.settingsFile,bootSettings,{mode:HOST_STATE_FILE_MODE}),child?.pid===void 0?await rm(generationPaths(paths,instanceId).dir,{recursive:!0,force:!0}):exited!==void 0&&await exitedWithin(exited,SUCCESSOR_EXIT_WAIT_MS)&&await releaseGeneration(paths,{instanceId,pid:child.pid});return}catch(cause){return`successor cleanup failed: ${cause instanceof Error?cause.message:String(cause)}`}}async function exitedWithin(exited,ms){let timer,timedOut=new Promise(resolve4=>{timer=setTimeout(()=>resolve4(!1),ms)});try{return await Promise.race([exited.then(()=>!0),timedOut])}finally{clearTimeout(timer)}}async function awaitSuccessor(options,previous,exited){let deadline=Date.now()+(options._test?.readinessTimeoutMs??DEFAULT_HANDOFF_READINESS_MS),childGone=!1;for(exited.then(()=>{childGone=!0});Date.now()<=deadline;){let answer2=await probeProtocolInfo(options.socket,2e3);if(answer2&&answer2.instanceId!==void 0&&answer2.instanceId!==previous.instanceId)return answer2;if(childGone)return;await delay(50)}}async function abortReason(socket,replaced){let current=await statSocketIdentity(socket).catch(()=>{});return current===void 0||current.dev!==replaced.dev||current.ino!==replaced.ino?"socket_replaced":"successor_unavailable"}function delay(ms){return new Promise(resolve4=>setTimeout(resolve4,ms))}var HANDOFF_PROBE_TIMEOUT_MS=1e4,HANDOFF_LOCK_HOLD_MS=HANDOFF_PROBE_TIMEOUT_MS+SUCCESSOR_START_BUDGET_MS,HANDOFF_LOCK_WAIT_MS=HANDOFF_PROBE_TIMEOUT_MS+HANDOFF_LOCK_HOLD_MS+1e4;async function handoffHost(options){if(handoffUnsupported(options))return{action:"refuse",reason:"upgrade_unsupported",upgradeable:!1};let release=await acquireHostEnsureLock(options.socket,HANDOFF_LOCK_WAIT_MS);try{return await handoffHostLocked(options)}finally{await release()}}async function handoffHostLocked(options){if(handoffUnsupported(options))return{action:"refuse",reason:"upgrade_unsupported",upgradeable:!1};let paths=createHostDaemonPaths({socket:options.socket,...options.agentDir?{agentDir:options.agentDir}:{}});await createDaemonDirectories(paths);let host=await probeProtocolInfo(options.socket,HANDOFF_PROBE_TIMEOUT_MS);if(!host)return{action:"refuse",reason:"no_host",upgradeable:!1};if(!host.capabilities.includes(GENERATION_HANDOFF_CAPABILITY))return{action:"refuse",reason:"handoff_unsupported",upgradeable:!1};let registered=await readHostRegistration(paths),owner=await provenOwner(registered,options.socket);return owner?startSuccessor({options,paths,host,owner}):{action:"refuse",reason:"unknown_owner",upgradeable:!0}}function handoffUnsupported(options){return(options._test?.platform??process.platform)==="win32"}var hostGenerationScopes=0;function isHostGenerationProcess(){return hostGenerationScopes>0}async function runAsHostGenerationProcess(action){hostGenerationScopes+=1;try{return await action()}finally{hostGenerationScopes-=1}}var SPAWNED_HOST_PROBE_TIMEOUT_MS=1e4;async function pollProtocolInfo(socket,timeoutMs,isCompatible2,childExit){let deadline=Date.now()+timeoutMs,lastProtocol;for(;Date.now()<=deadline;){let probe=holdProtocolInfo(socket,Math.min(SPAWNED_HOST_PROBE_TIMEOUT_MS,Math.max(1,deadline-Date.now()))),raced=childExit?await Promise.race([probe,childExit]):await probe;if(isChildExit(raced)){let held=await probe;if(!held)return{ready:!1,protocol:lastProtocol,exited:raced};if(lastProtocol=held.info,isCompatible2(held.info))return{ready:!0,protocol:held.info,hold:held.hold};held.hold.release()}else if(raced){if(lastProtocol=raced.info,isCompatible2(raced.info))return{ready:!0,protocol:raced.info,hold:raced.hold};raced.hold.release()}await delay2(50)}return{ready:!1,protocol:lastProtocol}}function isChildExit(value){return!!value&&"code"in value&&"signal"in value}function delay2(ms){return new Promise(resolveDelay=>setTimeout(resolveDelay,ms))}var EXISTING_HOST_PROBE_TIMEOUT_MS=1e4,DEFAULT_READINESS_TIMEOUT_MS=1e4,DEFAULT_STOP_TIMEOUT_MS=1e4,SIGKILL_GRACE_MS=2e3,ENSURE_LOCK_WAIT_MS=EXISTING_HOST_PROBE_TIMEOUT_MS+Math.max(DEFAULT_STOP_TIMEOUT_MS+SIGKILL_GRACE_MS+DEFAULT_READINESS_TIMEOUT_MS,HANDOFF_LOCK_HOLD_MS)+1e4,lockOptions=hostEnsureLockOptions(ENSURE_LOCK_WAIT_MS);async function ensureHost(options){let socket=normalizeSocketPath(options.socket),paths=createHostDaemonPaths({socket,...options.agentDir?{agentDir:options.agentDir}:{}});await createDaemonDirectories(paths);let lockTarget=hostEnsureLockTarget(socket);await mkdir(dirname5(lockTarget),{recursive:!0}),await writeFile2(lockTarget,"",{flag:"a",mode:384}),await reapOrphanedInternalHostDirs();let release=await acquireOwnershipSafeLock(`${lockTarget}.lock`,lockOptions);try{return await options._test?.afterLockAcquired?.(),await ensureHostLocked(paths,socket,options)}finally{await release()}}async function ensureHostLocked(paths,socket,options){await ensureEndpointIdentity(paths,socket,{repair:!0});let testOptions=options._test,registered=await readHostRegistration(paths),registeredHere=registersSocket(registered,socket),held=await holdProtocolInfo(socket,EXISTING_HOST_PROBE_TIMEOUT_MS),protocol=held?.info,startedByUs=registeredHere&&await writtenByThisProcess(registered?.writer,testOptions?.readProcessStartTime),attachedPid=registeredHere?registered?.record.pid??0:0,decision=decide(options,startedByUs,protocol);if(decision.action==="reuse"&&held)return{pid:attachedPid,socket,reused:!0,release:held.hold.release};switch(held?.hold.release(),decision.action){case"reuse":throw new Error(`host at ${socket} was reused without answering its probe`);case"refuse":throw new HostEnsureRefusedError(socket,decision.reason,protocol);case"handoff":return upgradeGeneration(paths,socket,options,attachedPid);case"start":break;default:return assertNever2(decision)}let probe=testOptions?.readProcessStartTime??readProcessStartTime,pidMatches=registeredHere&®istered?await matchesPidFileOrUnknown(registered.record,probe):!1,stranded;if(registered&&pidMatches)if(startedByUs){if(await probeSocketReachable(socket,EXISTING_HOST_PROBE_TIMEOUT_MS))throw new HostEnsureRefusedError(socket,"host_busy",protocol);await stopManagedHost(registered.record,testOptions?.stopTimeoutMs??DEFAULT_STOP_TIMEOUT_MS,probe)}else{if(await publicEndpointAccepts(socket))throw new HostEnsureRefusedError(socket,"foreign_writer",protocol);stranded=registered}let legacyRefusal=await retireIdleLegacyHost(paths,probe,testOptions?.stopTimeoutMs??DEFAULT_STOP_TIMEOUT_MS);if(legacyRefusal!==void 0)throw new HostEnsureRefusedError(socket,"legacy_host",protocol,legacyRefusal);return stranded!==void 0?startHost(paths,socket,options,stranded.generation+1):(registeredHere&&await clearHostRegistration(paths),startHost(paths,socket,options))}var FOREIGN_ENDPOINT_PROBE_TIMEOUT_MS=2e3;async function publicEndpointAccepts(socket){if(process.platform==="win32"||socket.startsWith("\0"))return!0;let entry=await statSocketIdentity(socket).then(identity=>identity===void 0?"absent":"present",()=>"unknown");return entry==="absent"?!1:entry==="unknown"?!0:probeSocketReachable(socket,FOREIGN_ENDPOINT_PROBE_TIMEOUT_MS)}function decide(options,startedByUs,protocol){if(options.upgrade!=="if-engine-differs"||isHostGenerationProcess())return decideHostAction(ensureClient(options,startedByUs),protocol,"never");let decision=decideHostAction(ensureClient(options,startedByUs),protocol,"upgrade");return decision.action==="fallback"?{action:"reuse",reason:"compatible",upgradeable:!1}:decision}async function upgradeGeneration(paths,socket,options,attachedPid){let result=await handoffHostLocked({socket,agentDir:options.agentDir??getAgentDir(),hostArgs:options.hostArgs??[],...options.env?{env:options.env}:{},...options.policy?{policy:options.policy}:{},_test:{...options._test?.launch?{launch:options._test.launch}:{},...options._test?.readinessTimeoutMs?{readinessTimeoutMs:options._test.readinessTimeoutMs}:{}}});return result.action==="handoff"?{pid:result.pid,socket,reused:!1,release:await holdEnsured(socket)}:(await appendStderr(paths,`generation handoff refused: ${result.reason}${result.detail?` (${result.detail})`:""}`),{pid:attachedPid,socket,reused:!0,release:await holdEnsured(socket)})}async function holdEnsured(socket){let held=await holdProtocolInfo(socket,EXISTING_HOST_PROBE_TIMEOUT_MS);if(!held)throw new Error(`RPC socket host at ${socket} stopped answering before this ensure could hold it`);return held.hold.release}function ensureClient(options,startedByUs){return{protocolVersion:HOST_PROTOCOL_VERSION,requiredCapabilities:REQUIRED_HOST_CAPABILITIES,identity:engineBuildIdentity(),...options.upgrade==="if-engine-differs"?{launchProfile:hostLaunchProfile(hostChildArgv(options.hostArgs??[]),process.cwd())}:{},startedByUs,platform:process.platform}}function hostChildArgv(hostArgs){return["--mode","rpc","--multi-session",...hostArgs]}function registersSocket(registered,socket){return registered===void 0?!1:registered.socket===void 0||sameEndpoint(registered.socket,socket)}async function startHost(paths,socket,options,generation=0){let testOptions=options._test,instanceId=randomUUID2();process.platform==="win32"&&await createSocketSecret(socketSecretPath(socket)),await writeHostSettings(paths,{socket,capabilities:PINNED_HOST_CLIENT_CAPABILITIES,coldStart:options.policy?.coldStart??"transient",idleExitMs:options.policy?.idleExitMs??9e5,generation,instanceId});let stderr=await open2(paths.stderrLog,generation===0?"w":"a",384),pidFile,child,exitedEarly,childExit;try{let supervisorArgs=["--socket",socket,...options.hostArgs??[]],launch=testOptions?.spawn??testOptions?.launch?.(supervisorArgs)??defaultHostLaunch(supervisorArgs);if(child=spawn2(launch.command,[...launch.args],{detached:!0,windowsHide:!0,env:initialHostEnvironment({agentDir:options.agentDir,env:options.env,paths,instanceId,generation}),stdio:["ignore","ignore",stderr.fd]}),childExit=new Promise(resolveExit=>{child.once("exit",(code,signal)=>{exitedEarly={code,signal},resolveExit(exitedEarly)})}),child.pid===void 0)throw new Error("failed to spawn RPC socket host");let probe=testOptions?.readProcessStartTime??readProcessStartTime,observedStartTime=await Promise.race([waitForStartTime(child.pid,1e4,probe),childExit.then(()=>{throw new Error("RPC socket host exited before its start time could be read")})]),unhurriedProbe=testOptions?.readProcessStartTime?testOptions.readProcessStartTime:pid=>readProcessStartTime(pid,process.platform,15e3),processStartTime=observedStartTime??await unhurriedProbe(child.pid).catch(()=>{});pidFile={pid:child.pid,processStartTime:processStartTime??null},await testOptions?.beforePidFileWrite?.(),await writeHostRegistration(paths,{record:pidFile,socket,instanceId,generation,launchProfileId:hostLaunchProfile(hostChildArgv(options.hostArgs??[]),process.cwd()).profile_id}),child.unref()}catch(error){let exitedBeforeCleanup=exitedEarly;if(!exitedBeforeCleanup&&child&&child.exitCode===null&&child.signalCode===null){try{child.kill("SIGTERM")}catch{}if(childExit&&await Promise.race([childExit,delay3(2e3)]),child.exitCode===null&&child.signalCode===null)try{child.kill("SIGKILL")}catch{}}if(!exitedBeforeCleanup)throw await clearHostRegistration(paths),error;let diagnostic2=await appendStderr(paths,`RPC socket host exited with code ${exitedBeforeCleanup.code??"null"}${exitedBeforeCleanup.signal?` (${exitedBeforeCleanup.signal})`:""} before answering get_protocol_info`);throw await clearHostRegistration(paths),new Error(diagnostic2)}finally{await stderr.close()}let readinessTimeoutMs=testOptions?.readinessTimeoutMs??DEFAULT_READINESS_TIMEOUT_MS,result=await pollProtocolInfo(socket,readinessTimeoutMs,isCompatible,childExit);if(result.ready)return{pid:pidFile.pid,socket,reused:!1,release:result.hold.release};let stopFailure=await stopSpawnedChild(child,childExit,testOptions?.stopTimeoutMs??DEFAULT_STOP_TIMEOUT_MS).then(()=>{},error=>error instanceof Error?error.message:String(error)),message=result.protocol?`RPC socket host answered get_protocol_info with protocolVersion ${result.protocol.protocolVersion}, serverVersion ${result.protocol.serverVersion} and capabilities ${JSON.stringify(result.protocol.capabilities)}, but is incompatible with protocol version ${HOST_PROTOCOL_VERSION} and required capabilities ${JSON.stringify(REQUIRED_HOST_CAPABILITIES)}`:result.exited?`RPC socket host exited with code ${result.exited.code??"null"}${result.exited.signal?` (${result.exited.signal})`:""} before answering get_protocol_info`:`spawned RPC socket host did not answer get_protocol_info within ${readinessTimeoutMs}ms`,diagnostic=await appendStderr(paths,stopFailure===void 0?message:`${message} (teardown also reported: ${stopFailure})`);throw await clearHostRegistration(paths),new Error(diagnostic)}async function matchesPidFileOrUnknown(pidFile,probe){try{return await processMatchesPidFile(pidFile,probe)}catch(error){if(error instanceof ProcessIdentityUnreadableError)return!1;throw error}}async function stopSpawnedChild(child,childExit,termTimeoutMs){let exited=()=>child.exitCode!==null||child.signalCode!==null;if(exited())return;let signal=name=>{try{child.kill(name)}catch(error){if(!isNodeErrorCode(error,"ESRCH"))throw error}},waitFor=ms=>Promise.race([childExit.then(()=>!0),delay3(ms).then(()=>exited())]);if(signal("SIGTERM"),!await waitFor(termTimeoutMs)&&(signal("SIGKILL"),!await waitFor(SIGKILL_GRACE_MS)))throw new Error(`RPC socket host pid ${child.pid??"?"} remained alive after SIGKILL`)}async function stopManagedHost(pidFile,termTimeoutMs,readStartTime=readProcessStartTime){if(await signalValidated(pidFile,"SIGTERM",readStartTime),!await waitForGone(pidFile,termTimeoutMs,readStartTime)&&(await signalValidated(pidFile,"SIGKILL",readStartTime),!await waitForGone(pidFile,SIGKILL_GRACE_MS,readStartTime)))throw new Error(`RPC socket host pid ${pidFile.pid} remained alive after SIGKILL`)}async function resolvePidFileOwnership(pidFile,readStartTime){try{return await processMatchesPidFile(pidFile,readStartTime,processIsLive,{attempts:1})?"owns":"gone"}catch(error){if(error instanceof ProcessIdentityUnreadableError)return"unknown";throw error}}async function signalValidated(pidFile,signal,readStartTime=readProcessStartTime){if(await resolvePidFileOwnership(pidFile,readStartTime)==="owns")try{process.kill(pidFile.pid,signal)}catch(error){if(!isNodeErrorCode(error,"ESRCH"))throw error}}async function waitForGone(pidFile,timeoutMs,readStartTime=readProcessStartTime){let deadline=Date.now()+timeoutMs;for(;Date.now()<=deadline;){if(await resolvePidFileOwnership(pidFile,readStartTime)==="gone")return!0;await delay3(50)}return await resolvePidFileOwnership(pidFile,readStartTime)==="gone"}function isCompatible(protocol){return decideHostAction(ensureClient({socket:""},!1),protocol,"never").action==="reuse"}async function reapOrphanedInternalHostDirs(){try{let entries=await readdir2(tmpdir(),{withFileTypes:!0});await Promise.all(entries.filter(entry=>entry.isDirectory()&&entry.name.startsWith("senpi-rpc-host-internal-")).map(async entry=>{try{let owner=JSON.parse(await readFile3(join4(tmpdir(),entry.name,".owner"),"utf8"));typeof owner.pid=="number"&&typeof owner.processStartTime=="string"&&typeof owner.createdAt=="number"&&owner.processStartTime.length>0&&owner.createdAt<Date.now()-6e4&&(await readdir2(join4(tmpdir(),entry.name))).length===1&&!await processMatchesPidFile({pid:owner.pid,processStartTime:owner.processStartTime}).catch(error=>{if(error instanceof ProcessIdentityUnreadableError)return!0;throw error})&&await rm2(join4(tmpdir(),entry.name),{recursive:!0,force:!0})}catch{}}))}catch{}}async function appendStderr(paths,message){try{let stderr=(await readFile3(paths.stderrLog,"utf8")).trim();return stderr?`${message}
|
|
5
|
-
${stderr}`:message}catch(error){if(isNodeErrorCode(error,"ENOENT"))return message;throw error}}function normalizeSocketPath(value){return value.startsWith("unix://")?value.slice(7):value}function delay3(ms){return new Promise(resolveDelay=>setTimeout(resolveDelay,ms))}function isNodeErrorCode(error,code){return error instanceof Error&&"code"in error&&error.code===code}function assertNever2(value){throw new Error(`unreachable host decision: ${JSON.stringify(value)}`)}var WakeSourceTracker=class{constructor(){this.counts=new Map}observe(data){if(typeof data!="object"||data===null)return;let{source,activeCount}=data;typeof source!="string"||source.length===0||typeof activeCount!="number"||!Number.isFinite(activeCount)||(activeCount>0?this.counts.set(source,activeCount):this.counts.delete(source))}get hasActive(){return this.counts.size>0}get activeSources(){return[...this.counts.keys()]}};function isSessionBusySnapshot(snapshot){return snapshot.isStreaming||snapshot.isBashRunning||snapshot.isCompacting||snapshot.hasSessionWork||snapshot.hasActiveWakeSource}function isHandoffBusy(snapshot){return isSessionBusySnapshot({...snapshot,hasActiveWakeSource:!1})}function selectDrainVerdicts(registry,inFlight,unreadable){let park=[],gone=[];for(let{sessionId,status,sessionPath}of registry.list()){if(status!=="open"||inFlight(sessionId))continue;let entry=registry.peek(sessionId);!entry||handoffBusy(entry,sessionId,unreadable)||(sessionPath!==void 0&&sessionDirectoryRemoved(sessionPath)?gone.push(sessionId):park.push(sessionId))}return{park,gone}}function handoffBusy(entry,sessionId,unreadable){try{let snapshot=entry.runtime?.session.activitySnapshot;return entry.worker?.handoffBusy===!0||snapshot!==void 0&&isHandoffBusy(snapshot)}catch(cause){return unreadable(sessionId,cause),!1}}function readDrainVerdicts(registry,inFlight){try{return selectDrainVerdicts(registry,inFlight,(sessionId,cause)=>{process.stderr.write(`senpi rpc handoff activity of ${sessionId} unreadable, parking: ${String(cause)}
|
|
6
|
-
`)})}catch(cause){process.stderr.write(`senpi rpc handoff drain pass failed, retrying: ${String(cause)}
|
|
7
|
-
`);return}}var BEGIN_HANDOVER_COMMAND="begin_handover",SUCCESSOR_PROBE_TIMEOUT_MS=1e4;function parseBeginHandover(record){let{operation_id,if_instance_id,if_generation,target_runtime_build_id,launch,host_args,env}=record;if(typeof operation_id!="string"||operation_id==="")return"operation_id must be a non-empty string";if(typeof if_instance_id!="string")return"if_instance_id must be a string";if(typeof if_generation!="number"||!Number.isSafeInteger(if_generation))return"if_generation must be an integer";if(typeof target_runtime_build_id!="string")return"target_runtime_build_id must be a string";if(!isRecord4(launch)||typeof launch.command!="string"||!isStringArray(launch.args))return"launch must be { command, args }";if(!isStringArray(host_args))return"host_args must be an array of strings";if(!isRecord4(env)||!Object.values(env).every(value=>typeof value=="string"))return"env must map names to strings";let policy=isRecord4(record.policy)?record.policy:void 0;return{operationId:operation_id,ifInstanceId:if_instance_id,ifGeneration:if_generation,targetRuntimeBuildId:target_runtime_build_id,launch:{command:launch.command,args:launch.args},hostArgs:host_args,env,...policy!==void 0&&{policy}}}function beginHandoverResponse(id,answer2){let data=typeof answer2=="string"?{refused:"invalid_request",detail:answer2}:answer2.kind==="refused"?{refused:answer2.reason,...answer2.detail!==void 0&&{detail:answer2.detail}}:handoverWire(answer2.view);return{...typeof id=="string"&&{id},type:"response",command:BEGIN_HANDOVER_COMMAND,success:!0,data}}function handoverWire(view){return{operation_id:view.operationId,state:view.state,target_runtime_build_id:view.targetRuntimeBuildId,...view.reason!==void 0&&{reason:view.reason},...view.successor!==void 0&&{successor:view.successor}}}function handoverRefusal(command,view){return{...command.id!==void 0&&{id:command.id},type:"response",command:command.type,success:!1,error:`handover_pending: this host is handing over to runtime ${view.targetRuntimeBuildId} (operation ${view.operationId}); new work is admitted by its successor`}}function withHostIdentity(response,runtimeBuildId,handover){return response.command!=="get_protocol_info"||!response.success?response:{...response,data:{...response.data,...runtimeBuildId!==void 0&&{runtimeBuildId},...handover!==void 0&&{handover:handoverWire(handover)}}}}function isHostIdle(registry,inFlight){if((inFlight.get(void 0)??0)>0)return!1;let sessions=registry.list(),verdicts=selectDrainVerdicts(registry,sessionId=>(inFlight.get(sessionId)??0)>0,()=>{});return verdicts.park.length+verdicts.gone.length===sessions.length}async function performIdleHandover(context){let{request,socket,agentDir}=context,env={};for(let name of Object.keys(process.env))env[name]=null;Object.assign(env,request.env);let result=await handoffHost({socket,agentDir,hostArgs:request.hostArgs,env,expectedRuntimeBuildId:request.targetRuntimeBuildId,...request.policy!==void 0&&{policy:request.policy},launch:argv=>({command:request.launch.command,args:[...request.launch.args,...argv]})});if(result.action==="refuse")return{ok:!1,reason:result.detail===void 0?result.reason:`${result.reason}: ${result.detail}`};await writeDaemonEnvKeys(createHostDaemonPaths({socket,agentDir}),Object.keys(request.env).sort()).catch(cause=>{process.stderr.write(`senpi rpc handover: could not record the daemon environment: ${String(cause)}
|
|
8
|
-
`)});let successor=await probeProtocolInfo(socket,SUCCESSOR_PROBE_TIMEOUT_MS);return{ok:!0,successor:{pid:result.pid,instanceId:result.instanceId,generation:result.generation,runtimeBuildId:successor?.runtimeBuildId??null}}}function isRecord4(value){return typeof value=="object"&&value!==null&&!Array.isArray(value)}function isStringArray(value){return Array.isArray(value)&&value.every(entry=>typeof entry=="string")}function parseSessionRows(reply){return!isRecord5(reply)||!Array.isArray(reply.sessions)?[]:reply.sessions.flatMap(entry=>isRecord5(entry)?[{id:typeof entry.sessionId=="string"?entry.sessionId:"",kind:typeof entry.kind=="string"?entry.kind:"interactive",session_path:typeof entry.sessionPath=="string"?entry.sessionPath:null,cwd:typeof entry.cwd=="string"?entry.cwd:null,name:typeof entry.name=="string"?entry.name:null,attachments:typeof entry.attachments=="number"?entry.attachments:0,context:stringRecord(entry.context)}]:[])}async function readClaimRows(paths,generations){let rows=[];for(let claim of await readSessionPathClaims(paths.reservationsDir)){let owner=claim.owner;rows.push({session_path:owner.sessionPath,owner_pid:owner.pid,instance_id:owner.instanceId,generation:generations.find(row=>row.instanceId===owner.instanceId)?.generation??null,attached:owner.attached??null,live:await claimOwnerIsLive(owner)})}return rows.sort((left,right)=>left.session_path.localeCompare(right.session_path))}function stringRecord(value){return isRecord5(value)?Object.fromEntries(Object.entries(value).filter(entry=>typeof entry[1]=="string")):null}function isRecord5(value){return typeof value=="object"&&value!==null&&!Array.isArray(value)}var STATUS_PROBE_TIMEOUT_MS=1e4;async function readHostStatus(options,read={}){return(await probeHostStatus(options,read)).report}async function probeHostStatus(options,read={}){let paths=createHostDaemonPaths({socket:options.socket,...options.agentDir?{agentDir:options.agentDir}:{}}),prune=read.prune!==!1,includeWorkers=options.includeWorkers===!0,timeoutMs=read.timeoutMs??STATUS_PROBE_TIMEOUT_MS,host=await observeProtocolInfo(options.socket,timeoutMs);prune&&await pruneDeadGenerations(paths);let generations=await readGenerationRows(paths,{includeDead:!prune}),current=generations.find(row=>row.current&&row.alive),metrics=current?await readHostProcessMetrics(current.pid):UNOBSERVED_METRICS,listing=host===void 0?[]:await readSessionListing(options.socket,includeWorkers,timeoutMs),claims=await readClaimRows(paths,generations);return{report:{reachable:host!==void 0,socket:options.socket,pid:current?.pid??null,instanceId:host?.instanceId??current?.instanceId??null,generation:host?.generation??current?.generation??null,engineVersion:host?.engineVersion??current?.engineVersion??null,capabilities:host?.capabilities??[],launchProfile:host?.launch_profile??null,runtimeBuildId:host?.runtimeBuildId??null,handover:host?.handover??null,sessions:countSessions(listing),zombies:metrics.zombies,rss_mb:metrics.rss_mb,host_rss_mb:metrics.host_rss_mb,open_fds:metrics.open_fds,memory_pressure:host?.memory_pressure??null,env_keys:await readDaemonEnvKeys(paths),generations,crashes:readHostCrashRecords(paths.dir).length,shard:parseShardSocket(options.socket),session_rows:includeWorkers?listing:[],claims_live:claims.filter(claim=>claim.live).length,claims:includeWorkers?claims:[]},answered:host,listing}}async function readSessionCounts(socket,includeWorkers){return countSessions(await readSessionListing(socket,includeWorkers))}async function readSessionListing(socket,includeWorkers,timeoutMs=STATUS_PROBE_TIMEOUT_MS){let reply=await requestOnSocket(socket,{type:"list_sessions",[OBSERVE_REQUEST_FIELD]:!0,...includeWorkers?{include_workers:!0}:{}},timeoutMs);return parseSessionRows(reply)}function countSessions(rows){let attached=rows.filter(row=>row.attachments>0).length,retained=rows.length-attached;return{total:rows.length,interactive:rows.filter(row=>row.kind!=="worker").length,worker:rows.filter(row=>row.kind==="worker").length,retained,foreign_attached:attached,foreign_retained:retained}}function hostSummary(host){return host===void 0?null:{protocolVersion:host.protocolVersion,serverVersion:host.serverVersion,capabilities:host.capabilities,instanceId:host.instanceId??null,generation:host.generation??null,engineVersion:host.engineVersion??null,launchProfileId:host.launch_profile?.profile_id??null}}var UNOBSERVED_METRICS={rss_mb:null,host_rss_mb:null,open_fds:null,zombies:null};var HOST_EXIT_OK=0,HOST_EXIT_ERROR=1,HOST_EXIT_USAGE=2,HOST_EXIT_REFUSED=3,HOST_EXIT_FALLBACK=4;function identityPayload(socket,pid,host){return{socket,pid,instanceId:host?.instanceId??null,generation:host?.generation??null,engineVersion:host?.engineVersion??null,engineOrdinal:host?.engineOrdinal??null,capabilities:host?.capabilities??[],launchProfileId:host?.launch_profile?.profile_id??null,runtimeBuildId:host?.runtimeBuildId??null,upgradeable:decideHostAction(decisionClient(),host,"upgrade").upgradeable}}async function clientRuntimeBuildId(spec){let profile=socketHostLaunchProfile(spec.hostArgs,process.cwd());return computeRuntimeBuildId({profile:profile.core}).catch(cause=>(process.stderr.write(`senpi host: runtime identity unreadable: ${String(cause)}
|
|
9
|
-
`),null))}function refusal(kind,host,body){return{exitCode:kind==="refuse"?HOST_EXIT_REFUSED:HOST_EXIT_FALLBACK,payload:{action:kind,...body,host:hostSummary(host)}}}function decisionClient(){return{protocolVersion:HOST_PROTOCOL_VERSION,requiredCapabilities:REQUIRED_HOST_CAPABILITIES,identity:engineBuildIdentity(),startedByUs:!1,platform:process.platform}}var PROBE_TIMEOUT_MS=1e4,BEGIN_HANDOVER_TIMEOUT_MS=2*HANDOFF_LOCK_HOLD_MS+2e4;async function idleHandoverOutcome(target,spec,terms){let{socket}=target;if(process.platform==="win32")return refusal("refuse",void 0,{reason:"upgrade_unsupported",socket,operationId:terms.operationId});let before=await probeProtocolInfo(socket,PROBE_TIMEOUT_MS),refuse=(reason,extra={})=>refusal("refuse",before,{reason,socket,operationId:terms.operationId,...extra}),client=await clientRuntimeBuildId(spec);if(client!==terms.targetRuntimeBuildId)return refuse("target_build_mismatch",{detail:`this CLI launches ${client??"a runtime it cannot read"}`,clientRuntimeBuildId:client});if(before===void 0)return refuse("no_host");if(before.runtimeBuildId===terms.targetRuntimeBuildId)return answered(target,"handover_completed",before,terms,null);if(before.instanceId!==terms.ifInstanceId||before.generation!==terms.ifGeneration)return refuse("stale_generation",{detail:`the socket is served by ${before.instanceId??"an unnamed host"} generation ${before.generation??"?"}`});if(!before.capabilities.includes(RUNTIME_IDENTITY_HANDOVER_CAPABILITY))return refuse("handover_unsupported");let reply=await requestOnSocket(socket,beginHandoverCommand(spec,terms),BEGIN_HANDOVER_TIMEOUT_MS);return isRecord6(reply)?typeof reply.refused=="string"?refuse(reply.refused,typeof reply.detail=="string"?{detail:reply.detail}:{}):reply.state==="handover_blocked"?refuse("handover_blocked",{detail:reply.reason??null,handover:reply}):reply.state==="handover_completed"?answered(target,"handover_completed",await probeProtocolInfo(socket,PROBE_TIMEOUT_MS),terms,reply):answered(target,"handover_pending",before,terms,reply):reconcileLostReply(target,terms,before)}function beginHandoverCommand(spec,terms){let env={};for(let[name,value]of Object.entries(daemonEnvironment(process.env,daemonEnvOverrides(process.env,spec.env))))value!==void 0&&(env[name]=value);let launch=defaultHostLaunch([]);return{type:BEGIN_HANDOVER_COMMAND,operation_id:terms.operationId,if_instance_id:terms.ifInstanceId,if_generation:terms.ifGeneration,target_runtime_build_id:terms.targetRuntimeBuildId,launch:{command:launch.command,args:launch.args},host_args:spec.hostArgs,env,...spec.policy!==void 0&&{policy:spec.policy}}}async function reconcileLostReply(target,terms,before){let now=await probeProtocolInfo(target.socket,PROBE_TIMEOUT_MS);return now?.runtimeBuildId===terms.targetRuntimeBuildId?answered(target,"handover_completed",now,terms,null):{exitCode:HOST_EXIT_ERROR,payload:{action:"error",reason:"handover_reply_lost",operationId:terms.operationId,...identityPayload(target.socket,await servingPid(target),now??before)}}}async function answered(target,action,host,terms,handover){return{exitCode:HOST_EXIT_OK,payload:{action,...identityPayload(target.socket,await servingPid(target),host),operationId:typeof handover?.operation_id=="string"?handover.operation_id:terms.operationId,clientRuntimeBuildId:terms.targetRuntimeBuildId,handover}}}async function servingPid(target){return(await readHostRegistration(createHostDaemonPaths(target)))?.record.pid??0}function isRecord6(value){return typeof value=="object"&&value!==null&&!Array.isArray(value)}import{readdir as readdir3}from"node:fs/promises";var TUI_PROBE_TIMEOUT_MS=1500;function endpointProbeTimeoutMs(kind,requestedMs){return kind==="tui"?Math.min(requestedMs??TUI_PROBE_TIMEOUT_MS,TUI_PROBE_TIMEOUT_MS):requestedMs??DEFAULT_PROBE_TIMEOUT_MS}async function classifyEndpointLiveness(entry,options={}){let host=entry.socket===null?void 0:await observeProtocolInfo(entry.socket,endpointProbeTimeoutMs(entry.endpoint_kind,options.timeoutMs));return judgeEndpointLiveness(hostDaemonDirectoryPaths(entry.dir),host===void 0?void 0:host.instanceId??null)}async function judgeEndpointLiveness(paths,answered2){return typeof answered2=="string"&&await recordsInstance(paths,answered2)?"routable":answered2!==void 0||await anyGenerationLive(paths)?"live_unresponsive":"dead"}async function recordsInstance(paths,instanceId){return(await readdir3(paths.generationsDir).catch(()=>[])).includes(instanceId)?await readFileOrUndefined(generationPaths(paths,instanceId).pidFile).catch(()=>{})!==void 0:!1}var STATUS_ALL_MAX_IN_FLIGHT=64;async function readAllHostStatus(options){let endpoints=await listHostEndpoints(options.agentDir),read=options._test?.readEndpoint??(endpoint=>endpointStatus(endpoint,options)),rows=new Array(endpoints.length),next=0,worker=async()=>{for(let index=next++;index<endpoints.length;index=next++){let endpoint=endpoints[index];endpoint!==void 0&&(rows[index]=await read(endpoint))}};return await Promise.all(Array.from({length:Math.min(STATUS_ALL_MAX_IN_FLIGHT,endpoints.length)},worker)),rows}async function endpointStatus(endpoint,options){let located={dir:endpoint.dir,identity:endpoint.identity,endpoint_kind:endpoint.endpoint_kind},paths=hostDaemonDirectoryPaths(endpoint.dir);if(endpoint.socket===null){let verdict2=livenessFields(await judgeEndpointLiveness(paths,void 0)),status=await unaddressableStatus(endpoint.dir),owner2=endpoint.endpoint_kind==="tui"?tuiOwner(status,[]):null;return{...status,socket:null,...located,...verdict2,owner:owner2}}let{report,answered:answered2,listing}=await probeHostStatus({socket:endpoint.socket,agentDir:options.agentDir,includeWorkers:options.includeWorkers},{prune:!1,timeoutMs:endpointProbeTimeoutMs(endpoint.endpoint_kind,options.timeoutMs)}),verdict=await judgeEndpointLiveness(paths,answered2===void 0?void 0:answered2.instanceId??null),owner=endpoint.endpoint_kind==="tui"?tuiOwner(report,listing):null;return{...report,...located,...livenessFields(verdict),owner}}function tuiOwner(status,listing){let row=listing[0];return{pid:status.generations.find(generation=>generation.current)?.pid??null,cwd:row?.cwd??null,session:row===void 0?null:{id:row.id,path:row.session_path,name:row.name}}}function livenessFields(verdict){return verdict==="routable"?{alive:!0,reason:null}:{alive:!1,reason:verdict}}async function unaddressableStatus(dir){let paths=hostDaemonDirectoryPaths(dir),generations=await readGenerationRows(paths,{includeDead:!0}),claims=await readClaimRows(paths,generations);return{reachable:!1,pid:null,instanceId:null,generation:null,engineVersion:null,capabilities:[],launchProfile:null,runtimeBuildId:null,handover:null,sessions:{total:0,interactive:0,worker:0,retained:0,foreign_attached:0,foreign_retained:0},zombies:null,rss_mb:null,host_rss_mb:null,open_fds:null,memory_pressure:null,env_keys:[],generations,crashes:readHostCrashRecords(dir).length,shard:null,session_rows:[],claims_live:claims.filter(claim=>claim.live).length,claims:[]}}var PROBE_TIMEOUT_MS2=1e4;async function runHostRequest(request){if(request.action==="ensure"||request.action==="handoff"||request.action==="stop"){let{socket,agentDir}=request.target;if(await endpointKindOfSocket(socket,agentDir)==="tui")return refusal("refuse",void 0,{reason:"unsupported_endpoint_kind",socket,endpoint_kind:"tui"})}switch(request.action){case"ensure":return ensureOutcome(request.target,request.spec,request.policy);case"status":return request.all===!0?statusAllOutcome(request.target,request.includeWorkers):statusOutcome(request.target,request.includeWorkers);case"stop":return stopOutcome(request.target,request.drain,request.force);case"handoff":return request.handover===void 0?handoffOutcome(request.target,request.spec):idleHandoverOutcome(request.target,request.spec,request.handover);case"shard_path":return shardPathOutcome(request.kind,request.owner,request.root);case"gc":return{exitCode:HOST_EXIT_OK,payload:{...await gcHostEndpoints(request.agentDir)}};default:return assertNever3(request)}}async function ensureOutcome(target,spec,policy){let before=await probeProtocolInfo(target.socket,PROBE_TIMEOUT_MS2);if(policy==="fallback"){let decision=decideHostAction(decisionClient(),before,"fallback");if(decision.action==="fallback")return refusal("fallback",before,{reason:decision.reason,socket:target.socket})}let ensured;try{ensured=await ensureHost({socket:target.socket,agentDir:target.agentDir,hostArgs:spec.hostArgs,env:daemonEnvOverrides(process.env,spec.env),policy:spec.policy,upgrade:policy==="upgrade"?"if-engine-differs":"never"})}catch(error){if(!(error instanceof HostEnsureRefusedError))throw error;return refusal("refuse",before,{reason:error.reason,socket:target.socket,...error.detail!==void 0&&{detail:error.detail}})}let host=await probeProtocolInfo(target.socket,PROBE_TIMEOUT_MS2).finally(ensured.release);return ensured.reused||await recordEnvScope(target,spec),{exitCode:HOST_EXIT_OK,payload:{action:ensureAction(ensured.reused,before,host),...identityPayload(target.socket,ensured.pid,host),clientRuntimeBuildId:await clientRuntimeBuildId(spec),reused:ensured.reused}}}function ensureAction(reused,before,after){return reused?"reuse":before!==void 0&&before.instanceId!==after?.instanceId?"handoff":"start"}async function statusOutcome(target,includeWorkers){let status=await readHostStatus({socket:target.socket,agentDir:target.agentDir,includeWorkers});return{exitCode:status.reachable?HOST_EXIT_OK:HOST_EXIT_REFUSED,payload:{...status}}}async function statusAllOutcome(target,includeWorkers){let endpoints=await readAllHostStatus({agentDir:target.agentDir,includeWorkers});return{exitCode:endpoints.some(endpoint=>endpoint.reachable)?HOST_EXIT_OK:HOST_EXIT_REFUSED,payload:{endpoints}}}function shardPathOutcome(kind,owner,root){let key=shardKey(kind,owner);return{exitCode:HOST_EXIT_OK,payload:{kind,key,socket:shardSocketPathForKey(root,kind,key)}}}async function stopOutcome(target,drain,force){let host=await probeProtocolInfo(target.socket,PROBE_TIMEOUT_MS2),sessions=await readSessionCounts(target.socket,!0),occupied=sessions.foreign_attached+sessions.foreign_retained>0;if(!drain&&!force&&occupied)return refusal("refuse",host,{reason:"sessions_live",socket:target.socket,sessions});let result=await stopHost({socket:target.socket,agentDir:target.agentDir,drain,force});return result.action==="refuse"?refusal("refuse",host,{reason:result.reason,socket:target.socket,sessions}):{exitCode:HOST_EXIT_OK,payload:{action:result.action,socket:target.socket,pid:result.pid,sessions}}}async function handoffOutcome(target,spec){let before=await probeProtocolInfo(target.socket,PROBE_TIMEOUT_MS2),result=await handoffHost({socket:target.socket,agentDir:target.agentDir,hostArgs:spec.hostArgs,env:daemonEnvOverrides(process.env,spec.env),policy:spec.policy});if(result.action==="refuse")return refusal("refuse",before,{reason:upgradeRefusal(result.reason),socket:target.socket,detail:result.reason,upgradeable:result.upgradeable});await recordEnvScope(target,spec);let host=await probeProtocolInfo(target.socket,PROBE_TIMEOUT_MS2);return{exitCode:HOST_EXIT_OK,payload:{action:"handoff",...identityPayload(target.socket,result.pid,host),clientRuntimeBuildId:await clientRuntimeBuildId(spec),reused:!1}}}function upgradeRefusal(reason){return reason==="handoff_unsupported"?"upgrade_unsupported":reason}async function recordEnvScope(target,spec){let paths=createHostDaemonPaths({socket:target.socket,agentDir:target.agentDir});await writeDaemonEnvKeys(paths,daemonEnvKeys(process.env,spec.env))}function assertNever3(value){throw new Error(`unreachable host request: ${JSON.stringify(value)}`)}var SUBCOMMANDS=["ensure","status","stop","handoff","shard-path","gc"],POLICIES=["upgrade","fallback","never"],USAGE=`usage: ${APP_NAME} host <ensure|status|stop|handoff|shard-path|gc> [options]
|
|
10
|
-
|
|
11
|
-
ensure [--launch-spec <file>] [--policy upgrade|fallback|never] [--socket <path>]
|
|
12
|
-
status [--include-workers] [--all] [--socket <path>] (--all: every endpoint, ignores --socket)
|
|
13
|
-
stop [--drain] [--force] [--socket <path>]
|
|
14
|
-
handoff [--launch-spec <file>] [--socket <path>]
|
|
15
|
-
[--when idle --operation <id> --if-instance <id> --if-generation <n> --target-build <sha256:...>]
|
|
16
|
-
(--when idle: the running host hands over to THIS runtime at its next safe idle point)
|
|
17
|
-
shard-path --kind <p|i> --owner <id> [--root <dir>] [--json] (no host contact)
|
|
18
|
-
gc [--agent-dir <dir>] [--json] (removes provably dead endpoints only; ignores --socket)
|
|
19
|
-
|
|
20
|
-
--json the answer is one JSON line on stdout (always, except shard-path's bare path)`;async function runHostCommand(args){let parsed=parseHostArgs(args);if(typeof parsed=="string")return process.stderr.write(`${parsed}
|
|
21
|
-
${USAGE}
|
|
22
|
-
`),HOST_EXIT_USAGE;try{let outcome=await runHostRequest(await hostRequest(parsed));return parsed.subcommand==="shard-path"&&!parsed.json?writeSync(1,`${String(outcome.payload.socket)}
|
|
23
|
-
`):emit(outcome.payload),outcome.exitCode}catch(error){return error instanceof HostLaunchSpecError?(emit({action:"error",reason:error.reason,detail:error.detail}),HOST_EXIT_USAGE):(emit({action:"error",reason:"host_error",detail:error instanceof Error?error.message:String(error)}),HOST_EXIT_ERROR)}}async function hostRequest(parsed){let agentDir=getAgentDir(),target={socket:resolveHostSocket(parsed.socket,agentDir),agentDir};switch(parsed.subcommand){case"ensure":return{action:"ensure",target,spec:await launchSpec(parsed.specPath),policy:parsed.policy};case"status":return{action:"status",target,includeWorkers:parsed.includeWorkers,all:parsed.all};case"stop":return{action:"stop",target,drain:parsed.drain,force:parsed.force};case"handoff":return{action:"handoff",target,spec:await launchSpec(parsed.specPath),...parsed.handover!==void 0&&{handover:parsed.handover}};case"shard-path":return{action:"shard_path",kind:parsed.kind??"p",owner:parsed.owner??"",root:resolve3(parsed.root??join5(agentDir,"rpc","shards"))};case"gc":return{action:"gc",agentDir:resolve3(parsed.agentDir??agentDir)};default:return assertNever4(parsed.subcommand)}}function launchSpec(specPath){return specPath===void 0?DEFAULT_HOST_LAUNCH_SPEC:loadHostLaunchSpec(specPath)}function resolveHostSocket(explicit,agentDir){return explicit??envValue("RPC_SOCKET")??resolve3(agentDir,"rpc","rpc.sock")}function parseHostArgs(args){let[subcommand,...rest]=args;if(subcommand===void 0||!isSubcommand(subcommand))return`Error: unknown host command "${subcommand??""}".`;let specPath,policy="upgrade",socket,includeWorkers=!1,all=!1,drain=!1,force=!1,json=!1,kind,owner,root,agentDir,handover={};for(let index=0;index<rest.length;index++){let flag=rest[index],value=rest[index+1];if(flag==="--json")json=!0;else if(flag==="--include-workers")includeWorkers=!0;else if(flag==="--all"&&subcommand==="status")all=!0;else if(flag==="--kind"&&subcommand==="shard-path"&&(value==="p"||value==="i"))kind=value,index++;else if(flag==="--owner"&&subcommand==="shard-path"&&value!==void 0)owner=value,index++;else if(flag==="--root"&&subcommand==="shard-path"&&value!==void 0)root=value,index++;else if(flag==="--agent-dir"&&subcommand==="gc"&&value!==void 0)agentDir=value,index++;else if(subcommand==="handoff"&&value!==void 0&&HANDOVER_FLAGS.has(flag??""))handover[flag??""]=value,index++;else if(flag==="--drain")drain=!0;else if(flag==="--force")force=!0;else if(flag==="--launch-spec"&&value!==void 0)specPath=value,index++;else if(flag==="--socket"&&value!==void 0)socket=value,index++;else if(flag==="--policy"&&value!==void 0&&isPolicy(value))policy=value,index++;else return`Error: unknown option "${flag}" for "${APP_NAME} host ${subcommand}".`}if(subcommand==="shard-path"&&(kind===void 0||owner===void 0))return`Error: "${APP_NAME} host shard-path" needs --kind <p|i> and --owner <id>.`;let terms=Object.keys(handover).length===0?void 0:handoverTerms(handover);return typeof terms=="string"?terms:{subcommand,...specPath!==void 0&&{specPath},policy,...socket!==void 0&&{socket},includeWorkers,all,drain,force,json,...kind!==void 0&&{kind},...owner!==void 0&&{owner},...root!==void 0&&{root},...agentDir!==void 0&&{agentDir},...terms!==void 0&&{handover:terms}}}var HANDOVER_FLAGS=new Set(["--when","--operation","--if-instance","--if-generation","--target-build"]);function handoverTerms(flags){let when=flags["--when"],operationId=flags["--operation"],ifInstanceId=flags["--if-instance"],generation=flags["--if-generation"],targetRuntimeBuildId=flags["--target-build"];if(when!=="idle")return`Error: "${APP_NAME} host handoff" supports only --when idle.`;if(!operationId||!ifInstanceId||!generation||!targetRuntimeBuildId)return`Error: "${APP_NAME} host handoff --when idle" needs --operation, --if-instance, --if-generation and --target-build.`;let ifGeneration=Number(generation);return!/^\d+$/.test(generation)||!Number.isSafeInteger(ifGeneration)?"Error: --if-generation must be a non-negative integer.":{operationId,ifInstanceId,ifGeneration,targetRuntimeBuildId}}function emit(payload){writeSync(1,`${JSON.stringify(payload)}
|
|
24
|
-
`)}function isSubcommand(value){return SUBCOMMANDS.includes(value)}function isPolicy(value){return POLICIES.includes(value)}function assertNever4(value){throw new Error(`unreachable host subcommand: ${JSON.stringify(value)}`)}export{HostLaunchSpecError,DEFAULT_HOST_LAUNCH_SPEC,loadHostLaunchSpec,parseHostLaunchSpec,RUNTIME_IDENTITY_HANDOVER_CAPABILITY,computeRuntimeBuildId,parseHostProtocolInfo,HOST_PROTOCOL_VERSION,GENERATION_HANDOFF_CAPABILITY,REQUIRED_HOST_CAPABILITIES,HostEnsureRefusedError,decideHostAction,probeHost,PINNED_HOST_CLIENT_CAPABILITIES,EXPECTED_RUNTIME_BUILD_ID_ENV,HostIdleHandover,stopHost,handoffHost,runAsHostGenerationProcess,ensureHost,WakeSourceTracker,isSessionBusySnapshot,isHandoffBusy,readDrainVerdicts,BEGIN_HANDOVER_COMMAND,parseBeginHandover,beginHandoverResponse,handoverRefusal,withHostIdentity,isHostIdle,performIdleHandover,readHostStatus,HOST_EXIT_OK,HOST_EXIT_ERROR,HOST_EXIT_USAGE,HOST_EXIT_REFUSED,HOST_EXIT_FALLBACK,TUI_PROBE_TIMEOUT_MS,endpointProbeTimeoutMs,classifyEndpointLiveness,readAllHostStatus,runHostRequest,runHostCommand,parseHostArgs};
|