@code-yeongyu/senpi 2026.10.24-2 → 2026.10.24-4
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 +37 -0
- package/dist/bundle/chunks/{args-6T5EQVY3.js → args-JXULI4EH.js} +1 -1
- package/dist/bundle/chunks/{chunk-VUKNGMDT.js → chunk-3LNSLJ7N.js} +1 -1
- package/dist/bundle/chunks/chunk-3VOG6WFU.js +27 -0
- package/dist/bundle/chunks/{chunk-HHYSXDYP.js → chunk-42OY5SZZ.js} +2 -2
- package/dist/bundle/chunks/chunk-7IRLFYNM.js +23 -0
- package/dist/bundle/chunks/{chunk-FIBIVBQK.js → chunk-7UUOGZKR.js} +1 -1
- package/dist/bundle/chunks/chunk-BMYNMUVZ.js +7 -0
- package/dist/bundle/chunks/{chunk-JAXZOEJE.js → chunk-BWA4FVFW.js} +1 -1
- package/dist/bundle/chunks/{chunk-AGQSRWNN.js → chunk-EHQXLHRB.js} +1 -1
- package/dist/bundle/chunks/chunk-EWOEVVQT.js +4 -0
- package/dist/bundle/chunks/{chunk-YJA2B7FQ.js → chunk-FDL4CF6F.js} +1 -1
- package/dist/bundle/chunks/{chunk-RQHK4QZ5.js → chunk-FWXMMMCV.js} +1 -1
- package/dist/bundle/chunks/{chunk-OA5INWHI.js → chunk-GVJ76VQI.js} +102 -42
- package/dist/bundle/chunks/{chunk-HCF5M6NA.js → chunk-GXY5JEI5.js} +1 -1
- package/dist/bundle/chunks/{chunk-VZAYN5SB.js → chunk-HKUXKTFW.js} +1 -1
- package/dist/bundle/chunks/chunk-II22E3CV.js +3 -0
- package/dist/bundle/chunks/{chunk-ONLNXGO3.js → chunk-IUHU5PEC.js} +1 -1
- package/dist/bundle/chunks/{chunk-LR6Z6V54.js → chunk-NQC7MI2G.js} +1 -1
- package/dist/bundle/chunks/{chunk-IELM5U6I.js → chunk-PXKF2RHD.js} +2 -2
- package/dist/bundle/chunks/{chunk-BRXN5R2F.js → chunk-QH2HSCMX.js} +1 -1
- package/dist/bundle/chunks/{chunk-QQSCUEYJ.js → chunk-RHHBXIRN.js} +2 -5
- package/dist/bundle/chunks/chunk-RVYSJBZ7.js +7 -0
- package/dist/bundle/chunks/chunk-SGDOZT6B.js +8 -0
- package/dist/bundle/chunks/{chunk-BZ75DRZB.js → chunk-U67ML6CQ.js} +4 -2
- package/dist/bundle/chunks/{chunk-3N2PQEQV.js → chunk-UT6AJHPK.js} +1 -1
- package/dist/bundle/chunks/{chunk-RAIZG6HA.js → chunk-V3CK2AVC.js} +1 -1
- package/dist/bundle/chunks/{chunk-CZR7C6CY.js → chunk-VOBD6FQG.js} +1 -1
- package/dist/bundle/chunks/{chunk-HRPMLI7O.js → chunk-VRDU425E.js} +2 -2
- package/dist/bundle/chunks/{chunk-OUPD5LWY.js → chunk-XUFUKDKJ.js} +3 -3
- package/dist/bundle/chunks/{chunk-SGATKHO4.js → chunk-Y5OTHMXO.js} +1 -1
- package/dist/bundle/chunks/{chunk-QEQG42HL.js → chunk-ZGAB7WQA.js} +1 -1
- package/dist/bundle/chunks/cli-main-UH3MAREK.js +3 -0
- package/dist/bundle/chunks/{dispatch-KQEGIXQW.js → dispatch-DEP4PEDZ.js} +3 -2
- package/dist/bundle/chunks/{help-fast-path-6PVN7WI5.js → help-fast-path-ZHZGXAUG.js} +1 -1
- package/dist/bundle/chunks/host-command-OQR4RDTM.js +2 -0
- package/dist/bundle/chunks/host-lifecycle-U36U7JTE.js +3 -0
- package/dist/bundle/chunks/host-lifecycle.js +26 -21
- package/dist/bundle/chunks/host-session-command-IGB27ZPJ.js +20 -0
- package/dist/bundle/chunks/{host-socket-request-KKGH3JHV.js → host-socket-request-ZDJGSSZO.js} +1 -1
- package/dist/bundle/chunks/interactive-mode-R5RCSJ2F.js +2 -0
- package/dist/bundle/chunks/{list-tips-BO5RP34W.js → list-tips-CREX4IXZ.js} +1 -1
- package/dist/bundle/chunks/{multi-session-host-O5GZQSRL.js → multi-session-host-YZEMYUFY.js} +12 -12
- package/dist/bundle/chunks/{package-manager-cli-LC5GAGTW.js → package-manager-cli-5FOXDKUA.js} +1 -1
- package/dist/bundle/chunks/{role-4G43UDOC.js → role-M2MTVACJ.js} +1 -1
- package/dist/bundle/chunks/{rotation-stream-RXMCJJUL.js → rotation-stream-U7YQMC7H.js} +1 -1
- package/dist/bundle/chunks/rpc-mode-6K62CZBW.js +2 -0
- package/dist/bundle/chunks/rpc-monitor-source-OL5KU6NO.js +3 -0
- package/dist/bundle/chunks/{schedule-command-QNCVE4HM.js → schedule-command-XBW4EOG5.js} +1 -1
- package/dist/bundle/chunks/session-control-endpoint-54TMCS7Q.js +3 -0
- package/dist/bundle/chunks/{session-picker-HIC5ZTE6.js → session-picker-P2VUU2MQ.js} +1 -1
- package/dist/bundle/chunks/session-worker.js +437 -368
- package/dist/bundle/cli.js +2 -2
- package/dist/bundle/host-supervisor.js +26 -21
- 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/cli/args.d.ts +2 -0
- package/dist/cli/args.js +27 -0
- package/dist/cli/host-command-args.d.ts +33 -0
- package/dist/cli/host-command-args.js +145 -0
- package/dist/cli/host-command.d.ts +1 -30
- package/dist/cli/host-command.js +5 -137
- package/dist/cli/host-session-command.d.ts +1 -0
- package/dist/cli/host-session-command.js +5 -3
- package/dist/core/agent-session.js +1 -1
- package/dist/core/extensions/builtin/anthropic-subscription/index.js +4 -2
- package/dist/core/extensions/builtin/anthropic-subscription/options.js +2 -1
- package/dist/core/extensions/builtin/anthropic-subscription/sdk-boundary.js +9 -2
- package/dist/core/extensions/builtin/anthropic-subscription/session-cwd.d.ts +3 -0
- package/dist/core/extensions/builtin/anthropic-subscription/session-cwd.js +9 -0
- package/dist/core/extensions/builtin/anthropic-subscription/settings.d.ts +1 -1
- package/dist/core/extensions/builtin/anthropic-subscription/settings.js +3 -1
- package/dist/core/extensions/builtin/anthropic-subscription/stream.js +4 -1
- package/dist/core/extensions/builtin/btw/index.js +4 -1
- package/dist/core/extensions/builtin/compaction/lane-policy.js +3 -1
- package/dist/core/extensions/builtin/look-at/runner.js +1 -0
- package/dist/core/extensions/builtin/mcp/connection-types.d.ts +2 -0
- package/dist/core/extensions/builtin/mcp/connection.js +3 -0
- package/dist/core/extensions/builtin/mcp/elicitation.d.ts +5 -2
- package/dist/core/extensions/builtin/mcp/elicitation.js +57 -11
- package/dist/core/extensions/builtin/mcp/index.js +1 -1
- package/dist/core/extensions/builtin/mcp/modern/dispatch.js +6 -9
- package/dist/core/extensions/builtin/mcp/service-connection.d.ts +1 -0
- package/dist/core/extensions/builtin/mcp/service-connection.js +1 -0
- package/dist/core/extensions/builtin/mcp/service.js +1 -0
- package/dist/core/extensions/builtin/mcp/transport-sdk.d.ts +1 -0
- package/dist/core/extensions/builtin/mcp/transport-sdk.js +5 -5
- package/dist/core/extensions/builtin/mcp/transport.d.ts +1 -0
- package/dist/core/extensions/builtin/mcp/transport.js +1 -0
- package/dist/core/extensions/builtin/prompt-preset/contract.js +6 -1
- package/dist/core/extensions/builtin/prompt-preset/kimi-k3-1.d.ts +3 -0
- package/dist/core/extensions/builtin/prompt-preset/kimi-k3-1.js +93 -0
- package/dist/core/extensions/builtin/prompt-preset/matchers.d.ts +2 -0
- package/dist/core/extensions/builtin/prompt-preset/matchers.js +9 -0
- package/dist/core/extensions/builtin/prompt-preset/presets.js +3 -0
- package/dist/core/extensions/builtin/prompt-preset/settings.d.ts +1 -1
- package/dist/core/extensions/builtin/prompt-preset/settings.js +1 -0
- package/dist/core/extensions/builtin/terminal/rpc-monitor-observer.d.ts +2 -1
- package/dist/core/extensions/builtin/terminal/rpc-monitor-observer.js +12 -4
- package/dist/core/external-admission.d.ts +4 -1
- package/dist/core/external-admission.js +18 -2
- package/dist/core/lifecycle-session-binding.js +20 -0
- package/dist/core/sdk.js +1 -0
- package/dist/core/session-spawned-by.d.ts +4 -0
- package/dist/core/session-spawned-by.js +16 -0
- package/dist/main.js +7 -2
- package/dist/modes/rpc/host-admission-proxy.d.ts +2 -1
- package/dist/modes/rpc/host-admission-proxy.js +10 -9
- package/dist/modes/rpc/host-cli-entry.d.ts +2 -0
- package/dist/modes/rpc/host-cli-entry.js +13 -1
- package/dist/modes/rpc/host-ensure-client.d.ts +5 -1
- package/dist/modes/rpc/host-ensure-client.js +8 -0
- package/dist/modes/rpc/host-ensure-types.d.ts +1 -0
- package/dist/modes/rpc/host-ensure.js +6 -11
- package/dist/modes/rpc/host-gc-pass.js +3 -0
- package/dist/modes/rpc/host-gc.js +3 -0
- package/dist/modes/rpc/host-internal-transport.d.ts +16 -5
- package/dist/modes/rpc/host-internal-transport.js +40 -9
- package/dist/modes/rpc/host-lifecycle-proxy.d.ts +4 -8
- package/dist/modes/rpc/host-lifecycle-proxy.js +9 -21
- package/dist/modes/rpc/host-lifecycle-shutdown.d.ts +2 -0
- package/dist/modes/rpc/host-lifecycle-shutdown.js +3 -0
- package/dist/modes/rpc/host-lifecycle.js +9 -16
- package/dist/modes/rpc/host-observe-request.js +1 -1
- package/dist/modes/rpc/host-probe.d.ts +8 -6
- package/dist/modes/rpc/host-probe.js +13 -13
- package/dist/modes/rpc/host-request-key-root.d.ts +8 -0
- package/dist/modes/rpc/host-request-key-root.js +14 -0
- package/dist/modes/rpc/host-runner.d.ts +3 -1
- package/dist/modes/rpc/host-runner.js +6 -1
- package/dist/modes/rpc/host-socket-bind.d.ts +6 -0
- package/dist/modes/rpc/host-socket-bind.js +42 -0
- package/dist/modes/rpc/host-socket-request.d.ts +3 -2
- package/dist/modes/rpc/host-socket-request.js +55 -10
- package/dist/modes/rpc/host-status.d.ts +3 -2
- package/dist/modes/rpc/host-status.js +5 -0
- package/dist/modes/rpc/host-supervisor-log.d.ts +1 -0
- package/dist/modes/rpc/host-supervisor-log.js +3 -0
- package/dist/modes/rpc/host-watchdog.d.ts +8 -1
- package/dist/modes/rpc/host-watchdog.js +37 -12
- package/dist/modes/rpc/internal-channel.js +14 -14
- package/dist/modes/rpc/multi-session-host.js +18 -46
- package/dist/modes/rpc/rpc-auth-client.d.ts +13 -0
- package/dist/modes/rpc/rpc-auth-client.js +142 -0
- package/dist/modes/rpc/rpc-auth-proof.d.ts +12 -0
- package/dist/modes/rpc/rpc-auth-proof.js +27 -0
- package/dist/modes/rpc/rpc-client-socket.d.ts +5 -0
- package/dist/modes/rpc/rpc-client-socket.js +30 -0
- package/dist/modes/rpc/rpc-client.d.ts +2 -0
- package/dist/modes/rpc/rpc-client.js +2 -24
- package/dist/modes/rpc/rpc-host-command-types.d.ts +4 -0
- package/dist/modes/rpc/rpc-protocol-info-types.d.ts +6 -0
- package/dist/modes/rpc/rpc-public-auth.d.ts +41 -0
- package/dist/modes/rpc/rpc-public-auth.js +237 -0
- package/dist/modes/rpc/rpc-public-key.d.ts +28 -0
- package/dist/modes/rpc/rpc-public-key.js +193 -0
- package/dist/modes/rpc/session-router-open-profile.js +2 -10
- package/dist/modes/rpc/socket-ownership.d.ts +7 -0
- package/dist/modes/rpc/socket-ownership.js +17 -0
- package/docs/rpc.md +108 -2
- package/docs/settings.md +1 -1
- package/docs/usage.md +14 -0
- package/package.json +6 -6
- package/dist/bundle/chunks/chunk-3474DPZK.js +0 -2
- package/dist/bundle/chunks/chunk-64CFJHKT.js +0 -7
- package/dist/bundle/chunks/chunk-B6NJHBLH.js +0 -21
- package/dist/bundle/chunks/chunk-HZTQHE5O.js +0 -26
- package/dist/bundle/chunks/chunk-IBDADH5Z.js +0 -4
- package/dist/bundle/chunks/chunk-OJZD7CO7.js +0 -8
- package/dist/bundle/chunks/cli-main-4JNPG6LB.js +0 -3
- package/dist/bundle/chunks/host-command-I3ZMWQQ3.js +0 -2
- package/dist/bundle/chunks/host-lifecycle-QEO422VW.js +0 -3
- package/dist/bundle/chunks/host-session-command-INULKX6E.js +0 -20
- package/dist/bundle/chunks/interactive-mode-SYWJVPKK.js +0 -2
- package/dist/bundle/chunks/rpc-mode-JMCYSP4P.js +0 -2
- package/dist/bundle/chunks/rpc-monitor-source-34AQDTZ6.js +0 -3
- package/dist/bundle/chunks/session-control-endpoint-IW5X56WN.js +0 -3
|
@@ -16,6 +16,8 @@ export declare function hasKimiK27Signal(value: string): boolean;
|
|
|
16
16
|
export declare function isKimiK27Model(model: ModelWithPromptPresetMetadata): boolean;
|
|
17
17
|
export declare function hasKimiK28Signal(value: string): boolean;
|
|
18
18
|
export declare function isKimiK28Model(model: ModelWithPromptPresetMetadata): boolean;
|
|
19
|
+
export declare function hasKimiK31Signal(value: string): boolean;
|
|
20
|
+
export declare function isKimiK31Model(model: ModelWithPromptPresetMetadata): boolean;
|
|
19
21
|
export declare function hasKimiK3Signal(value: string): boolean;
|
|
20
22
|
export declare function isKimiK3Model(model: ModelWithPromptPresetMetadata): boolean;
|
|
21
23
|
export declare function hasSWE2Signal(value: string): boolean;
|
|
@@ -63,6 +63,15 @@ export function hasKimiK28Signal(value) {
|
|
|
63
63
|
export function isKimiK28Model(model) {
|
|
64
64
|
return hasKimiK28Signal(model.id) || (model.name !== undefined && hasKimiK28Signal(model.name));
|
|
65
65
|
}
|
|
66
|
+
// Kimi K3.1 ids: `k3.1-pro` / `k3.1-pro-highspeed` on Kimi Code, `kimi-k3.1-pro` / `kimi-k3.1-pro-highspeed` on the
|
|
67
|
+
// Kimi API (kimi.com/code/docs models + platform.kimi.com K3.1 Pro quickstart, checked 2026-10-24), plus the
|
|
68
|
+
// dotless `k3p1` spelling OpenCode-style catalogs use. Checked before K3: every K3.1 id also carries the K3 signal.
|
|
69
|
+
export function hasKimiK31Signal(value) {
|
|
70
|
+
return /(?:^|[/@._-])(?:kimi-)?k3(?:[._-]|p)1(?:$|[/@._:-])/.test(normalizeModelId(value));
|
|
71
|
+
}
|
|
72
|
+
export function isKimiK31Model(model) {
|
|
73
|
+
return hasKimiK31Signal(model.id) || (model.name !== undefined && hasKimiK31Signal(model.name));
|
|
74
|
+
}
|
|
66
75
|
export function hasKimiK3Signal(value) {
|
|
67
76
|
const normalized = normalizeModelId(value);
|
|
68
77
|
return normalized === "k3" || /(?:^|[/@._-])kimi-k3(?:$|[/@._:-])/.test(normalized);
|
|
@@ -31,6 +31,7 @@ import { buildKimiK26Prompt } from "./kimi-k2-6.js";
|
|
|
31
31
|
import { buildKimiK27Prompt } from "./kimi-k2-7.js";
|
|
32
32
|
import { buildKimiK28Prompt } from "./kimi-k2-8.js";
|
|
33
33
|
import { buildKimiK3Prompt } from "./kimi-k3.js";
|
|
34
|
+
import { buildKimiK31Prompt } from "./kimi-k3-1.js";
|
|
34
35
|
import { parsePromptPreset } from "./settings.js";
|
|
35
36
|
export function resolvePresetName(model, settings) {
|
|
36
37
|
if (settings.promptPreset !== "auto") {
|
|
@@ -94,6 +95,8 @@ function renderPreset(name, options) {
|
|
|
94
95
|
return { name, prompt: buildGrok45Prompt(options) };
|
|
95
96
|
case "gemini-4-argon":
|
|
96
97
|
return { name, prompt: buildGemini4ArgonPrompt(options) };
|
|
98
|
+
case "kimi-k3-1":
|
|
99
|
+
return { name, prompt: buildKimiK31Prompt(options) };
|
|
97
100
|
case "kimi-k3":
|
|
98
101
|
return { name, prompt: buildKimiK3Prompt(options) };
|
|
99
102
|
case "kimi-k2-8":
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SettingsManager } from "../../../settings-manager.ts";
|
|
2
|
-
export type PromptPresetName = "auto" | "claude-fable-5" | "claude-fable-5-1" | "claude-fable-5-5" | "claude-opus-5-5" | "claude-opus-5" | "claude-sonnet-5-5" | "claude-haiku-5-5" | "claude-opus-4-8" | "claude-opus-4-7" | "claude-opus-4-6" | "claude-opus-4-5" | "deepseek-v4-flash" | "deepseek-v4-flash-0731" | "deepseek-v4-1-flash" | "deepseek-v4-pro" | "glm-5.2" | "glm-5.3" | "gemini-4-argon" | "grok-4.5" | "grok-4.6" | "grok-4.7" | "kimi-k3" | "kimi-k2-8" | "kimi-k2-7" | "kimi-k2-6" | "gpt-5" | "gpt-5.2" | "gpt-5.3-codex" | "gpt-5.4" | "gpt-5.5" | "gpt-5.6" | "gpt-6-astra";
|
|
2
|
+
export type PromptPresetName = "auto" | "claude-fable-5" | "claude-fable-5-1" | "claude-fable-5-5" | "claude-opus-5-5" | "claude-opus-5" | "claude-sonnet-5-5" | "claude-haiku-5-5" | "claude-opus-4-8" | "claude-opus-4-7" | "claude-opus-4-6" | "claude-opus-4-5" | "deepseek-v4-flash" | "deepseek-v4-flash-0731" | "deepseek-v4-1-flash" | "deepseek-v4-pro" | "glm-5.2" | "glm-5.3" | "gemini-4-argon" | "grok-4.5" | "grok-4.6" | "grok-4.7" | "kimi-k3-1" | "kimi-k3" | "kimi-k2-8" | "kimi-k2-7" | "kimi-k2-6" | "gpt-5" | "gpt-5.2" | "gpt-5.3-codex" | "gpt-5.4" | "gpt-5.5" | "gpt-5.6" | "gpt-6-astra";
|
|
3
3
|
export interface PromptPresetSettings {
|
|
4
4
|
promptPreset: PromptPresetName;
|
|
5
5
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type RpcKeyOptions } from "../../../../modes/rpc/rpc-auth-client.ts";
|
|
1
2
|
export type RpcRecord = Readonly<Record<string, unknown>>;
|
|
2
3
|
export interface RpcObserverHandlers {
|
|
3
4
|
/** Every record after the subscribe answer, in arrival order. */
|
|
@@ -10,5 +11,5 @@ export interface RpcObserver {
|
|
|
10
11
|
readonly subscribed: Promise<RpcRecord>;
|
|
11
12
|
close(): void;
|
|
12
13
|
}
|
|
13
|
-
export declare function openRpcObserver(socketPath: string, subscription: RpcRecord, handlers: RpcObserverHandlers): Promise<RpcObserver>;
|
|
14
|
+
export declare function openRpcObserver(socketPath: string, subscription: RpcRecord, handlers: RpcObserverHandlers, options?: RpcKeyOptions): Promise<RpcObserver>;
|
|
14
15
|
//# sourceMappingURL=rpc-monitor-observer.d.ts.map
|
|
@@ -8,12 +8,16 @@
|
|
|
8
8
|
import { createConnection } from "node:net";
|
|
9
9
|
import { OBSERVE_REQUEST_FIELD } from "../../../../modes/rpc/host-observe-request.js";
|
|
10
10
|
import { attachJsonlLineReader, MAX_RPC_LINE_CHARACTERS } from "../../../../modes/rpc/jsonl.js";
|
|
11
|
+
import { connectAuthenticatedRpcSocket } from "../../../../modes/rpc/rpc-auth-client.js";
|
|
11
12
|
import { readSocketSecret, resolveSocketTransportAddress, sendSocketHandshake, socketSecretPath, } from "../../../../modes/rpc/socket-transport.js";
|
|
12
13
|
import { socketNeedsHandshake } from "../../../../modes/rpc/tui-socket.js";
|
|
13
14
|
const SUBSCRIBE_ID = "monitor-subscribe";
|
|
14
|
-
export async function openRpcObserver(socketPath, subscription, handlers) {
|
|
15
|
+
export async function openRpcObserver(socketPath, subscription, handlers, options = {}) {
|
|
15
16
|
const secret = socketNeedsHandshake(socketPath) ? await readSocketSecret(socketSecretPath(socketPath)) : undefined;
|
|
16
|
-
const
|
|
17
|
+
const authenticated = !socketNeedsHandshake(socketPath)
|
|
18
|
+
? await connectAuthenticatedRpcSocket(socketPath, undefined, options)
|
|
19
|
+
: undefined;
|
|
20
|
+
const socket = authenticated?.socket ?? createConnection(resolveSocketTransportAddress(socketPath, process.platform, secret));
|
|
17
21
|
const answer = Promise.withResolvers();
|
|
18
22
|
let ended = false;
|
|
19
23
|
const end = (reason) => {
|
|
@@ -23,14 +27,14 @@ export async function openRpcObserver(socketPath, subscription, handlers) {
|
|
|
23
27
|
answer.reject(new Error(`rpc monitor: ${socketPath}: ${reason}`));
|
|
24
28
|
handlers.closed(reason);
|
|
25
29
|
};
|
|
26
|
-
|
|
30
|
+
const subscribe = () => {
|
|
27
31
|
if (secret !== undefined)
|
|
28
32
|
sendSocketHandshake(socket, secret);
|
|
29
33
|
// Marked observing (senpi#3956): without it the supervisor counts this watch as an attached client, so a
|
|
30
34
|
// watched host never idle-exits and a `children`/`all` watch pins every task shard host on the machine.
|
|
31
35
|
const frame = { ...subscription, [OBSERVE_REQUEST_FIELD]: true, id: SUBSCRIBE_ID, type: "subscribe_sessions" };
|
|
32
36
|
socket.write(`${JSON.stringify(frame)}\n`);
|
|
33
|
-
}
|
|
37
|
+
};
|
|
34
38
|
const detach = attachJsonlLineReader(socket, (line) => {
|
|
35
39
|
let record;
|
|
36
40
|
try {
|
|
@@ -55,6 +59,10 @@ export async function openRpcObserver(socketPath, subscription, handlers) {
|
|
|
55
59
|
detach();
|
|
56
60
|
end("connection closed");
|
|
57
61
|
});
|
|
62
|
+
if (authenticated)
|
|
63
|
+
subscribe();
|
|
64
|
+
else
|
|
65
|
+
socket.once("connect", subscribe);
|
|
58
66
|
return {
|
|
59
67
|
subscribed: answer.promise,
|
|
60
68
|
close: () => socket.destroy(),
|
|
@@ -60,6 +60,8 @@ export declare class ExternalAdmission {
|
|
|
60
60
|
}): InputHold;
|
|
61
61
|
onInputsSettled(listener: () => void): () => void;
|
|
62
62
|
onEmitted(listener: (deliveryId: string) => void): () => void;
|
|
63
|
+
/** Durable custody will replay this delivery: reserve its id before asynchronous recovery or gateway admission. */
|
|
64
|
+
restoreQueued(deliveryId: string, isOwned: () => boolean): void;
|
|
63
65
|
/**
|
|
64
66
|
* From here on every `admit` throws `reason` and changes nothing: the runtime is being handed to
|
|
65
67
|
* another writer, so the delivery stays with its sender and is delivered there instead.
|
|
@@ -82,8 +84,9 @@ export declare class ExternalAdmission {
|
|
|
82
84
|
* deliveries may be admitted again - unless the file's last write was refused too.
|
|
83
85
|
*/
|
|
84
86
|
observeRunSettled(): void;
|
|
85
|
-
/**
|
|
87
|
+
/** Cleared carriers are no longer held; custody not yet enqueued for recovery still owns its delivery. */
|
|
86
88
|
dropQueued(): void;
|
|
89
|
+
private releaseRestored;
|
|
87
90
|
private endHold;
|
|
88
91
|
private isBusy;
|
|
89
92
|
}
|
|
@@ -54,6 +54,13 @@ export class ExternalAdmission {
|
|
|
54
54
|
this.emittedListeners.add(listener);
|
|
55
55
|
return () => this.emittedListeners.delete(listener);
|
|
56
56
|
}
|
|
57
|
+
/** Durable custody will replay this delivery: reserve its id before asynchronous recovery or gateway admission. */
|
|
58
|
+
restoreQueued(deliveryId, isOwned) {
|
|
59
|
+
if (this.pending.has(deliveryId) || this.emitted.has(deliveryId) || this.failed.has(deliveryId))
|
|
60
|
+
return;
|
|
61
|
+
// Recovery chooses the payload's queue lane; this reservation only tracks durable ownership.
|
|
62
|
+
this.pending.set(deliveryId, isOwned);
|
|
63
|
+
}
|
|
57
64
|
/**
|
|
58
65
|
* From here on every `admit` throws `reason` and changes nothing: the runtime is being handed to
|
|
59
66
|
* another writer, so the delivery stays with its sender and is delivered there instead.
|
|
@@ -79,6 +86,7 @@ export class ExternalAdmission {
|
|
|
79
86
|
throw new Error(this.closedReason);
|
|
80
87
|
const turn_epoch = this.turnEpochValue;
|
|
81
88
|
const id = input.delivery_id;
|
|
89
|
+
this.releaseRestored();
|
|
82
90
|
if (this.pending.has(id) || this.emitted.has(id) || this.failed.has(id)) {
|
|
83
91
|
return { kind: "already_admitted", turn_epoch };
|
|
84
92
|
}
|
|
@@ -112,6 +120,7 @@ export class ExternalAdmission {
|
|
|
112
120
|
return { kind: "queued", turn_epoch };
|
|
113
121
|
}
|
|
114
122
|
list() {
|
|
123
|
+
this.releaseRestored();
|
|
115
124
|
const deliveries = { pending: [...this.pending.keys()], emitted: [...this.emitted] };
|
|
116
125
|
if (this.failed.size === 0)
|
|
117
126
|
return deliveries;
|
|
@@ -149,10 +158,17 @@ export class ExternalAdmission {
|
|
|
149
158
|
if (this.fileTakesWrites)
|
|
150
159
|
this.failed.clear();
|
|
151
160
|
}
|
|
152
|
-
/**
|
|
161
|
+
/** Cleared carriers are no longer held; custody not yet enqueued for recovery still owns its delivery. */
|
|
153
162
|
dropQueued() {
|
|
163
|
+
this.releaseRestored();
|
|
164
|
+
for (const [id, lane] of this.pending) {
|
|
165
|
+
if (lane !== "start" && typeof lane !== "function")
|
|
166
|
+
this.pending.delete(id);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
releaseRestored() {
|
|
154
170
|
for (const [id, lane] of this.pending) {
|
|
155
|
-
if (lane
|
|
171
|
+
if (typeof lane === "function" && !lane())
|
|
156
172
|
this.pending.delete(id);
|
|
157
173
|
}
|
|
158
174
|
}
|
|
@@ -2,6 +2,7 @@ import { collectGoalLifecycleFacts, onGoalLifecycleChange } from "./extensions/b
|
|
|
2
2
|
import { goalStoreRef } from "./extensions/builtin/goal/store-ref.js";
|
|
3
3
|
import { collectTurnEndLivenessInput } from "./extensions/builtin/goal/turn-end-liveness-report.js";
|
|
4
4
|
import { StaleExtensionContextError } from "./extensions/stale-context-error.js";
|
|
5
|
+
import { deliveryIdOf } from "./external-admission.js";
|
|
5
6
|
import { consumeLifecycleDelivery, lifecycleDeliveryEntries, messageTicket } from "./lifecycle-delivery.js";
|
|
6
7
|
import { readLifecycleGoal } from "./lifecycle-goal.js";
|
|
7
8
|
import { lifecycleHistory } from "./lifecycle-history.js";
|
|
@@ -41,6 +42,25 @@ export class LifecycleSessionBinding {
|
|
|
41
42
|
},
|
|
42
43
|
readRuntime: () => readLifecycleRuntimeFacts(session),
|
|
43
44
|
});
|
|
45
|
+
// A crashed receiver may have durable custody but no transcript entry yet. Recovery owns that
|
|
46
|
+
// delivery already; register it before session_start can let the gateway enqueue a second carrier.
|
|
47
|
+
const deliveryEntries = session.sessionManager.reduceHistory(lifecycleDeliveryEntries);
|
|
48
|
+
for (const record of this.controller.obligations.active) {
|
|
49
|
+
if (record.state !== "queued" || !record.payload || !record.ticketId)
|
|
50
|
+
continue;
|
|
51
|
+
const ticketId = record.ticketId;
|
|
52
|
+
const message = { role: "custom", ...record.payload.message, timestamp: 0 };
|
|
53
|
+
const deliveryId = deliveryIdOf(message);
|
|
54
|
+
if (deliveryId === undefined)
|
|
55
|
+
continue;
|
|
56
|
+
if (deliveryEntries.has(ticketId))
|
|
57
|
+
session.externalAdmission.observePersisted(message);
|
|
58
|
+
else
|
|
59
|
+
session.externalAdmission.restoreQueued(deliveryId, () =>
|
|
60
|
+
// Consumption writes the entry before message_end observes it in external admission.
|
|
61
|
+
session.sessionManager.reduceHistory(lifecycleDeliveryEntries).has(ticketId) ||
|
|
62
|
+
this.controller.obligations.active.some((current) => current.ticketId === ticketId && current.state === "queued"));
|
|
63
|
+
}
|
|
44
64
|
this.unsubscribeHuman = bindLifecycleHumanInput(session, (event) => this.controller.humanInput(event.at));
|
|
45
65
|
this.unsubscribeAdmission = session.externalAdmission.onInputsSettled(() => this.controller.admissionSettled());
|
|
46
66
|
this.unsubscribeGoal = onGoalLifecycleChange(session.sessionManager, (goal) => {
|
package/dist/core/sdk.js
CHANGED
|
@@ -315,6 +315,7 @@ export async function createAgentSession(options = {}) {
|
|
|
315
315
|
const serviceTier = serviceTierForProvider(model.provider, options?.serviceTier ?? resolveRequestServiceTier(model));
|
|
316
316
|
return modelRuntime.streamSimple(model, context, {
|
|
317
317
|
...options,
|
|
318
|
+
cwd,
|
|
318
319
|
serviceTier,
|
|
319
320
|
timeoutMs,
|
|
320
321
|
websocketConnectTimeoutMs,
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { SpawnedBy } from "./session-lifecycle-types.ts";
|
|
2
|
+
/** The shared CLI / open_session boundary contract; IDs are opaque, non-empty strings. */
|
|
3
|
+
export declare function isSpawnedBy(value: unknown): value is SpawnedBy;
|
|
4
|
+
//# sourceMappingURL=session-spawned-by.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** The shared CLI / open_session boundary contract; IDs are opaque, non-empty strings. */
|
|
2
|
+
export function isSpawnedBy(value) {
|
|
3
|
+
return (typeof value === "object" &&
|
|
4
|
+
value !== null &&
|
|
5
|
+
!Array.isArray(value) &&
|
|
6
|
+
"parentSessionId" in value &&
|
|
7
|
+
typeof value.parentSessionId === "string" &&
|
|
8
|
+
value.parentSessionId.length > 0 &&
|
|
9
|
+
"rootSessionId" in value &&
|
|
10
|
+
typeof value.rootSessionId === "string" &&
|
|
11
|
+
value.rootSessionId.length > 0 &&
|
|
12
|
+
"taskId" in value &&
|
|
13
|
+
typeof value.taskId === "string" &&
|
|
14
|
+
value.taskId.length > 0);
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=session-spawned-by.js.map
|
package/dist/main.js
CHANGED
|
@@ -401,8 +401,11 @@ async function sessionCwdOrUndefined(sessionFile) {
|
|
|
401
401
|
}
|
|
402
402
|
}
|
|
403
403
|
export async function createSessionManager(parsed, cwd, sessionDir, settingsManager, appMode) {
|
|
404
|
+
if (parsed.spawnedBy && (parsed.session || parsed.resume || parsed.continue || parsed.fork || parsed.rebind)) {
|
|
405
|
+
console.error(chalk.yellow("Warning: --spawned-by is ignored when resuming or forking a session."));
|
|
406
|
+
}
|
|
404
407
|
if (parsed.noSession || parsed.help || parsed.listModels !== undefined || parsed.listTips) {
|
|
405
|
-
return SessionManager.inMemory(cwd,
|
|
408
|
+
return SessionManager.inMemory(cwd, { id: parsed.sessionId, spawnedBy: parsed.spawnedBy });
|
|
406
409
|
}
|
|
407
410
|
if (parsed.fork) {
|
|
408
411
|
if (parsed.sessionId) {
|
|
@@ -533,11 +536,13 @@ export async function createSessionManager(parsed, cwd, sessionDir, settingsMana
|
|
|
533
536
|
if (parsed.sessionId) {
|
|
534
537
|
const existingSession = await findLocalSessionByExactId(parsed.sessionId, cwd, sessionDir);
|
|
535
538
|
if (existingSession) {
|
|
539
|
+
if (parsed.spawnedBy)
|
|
540
|
+
console.error(chalk.yellow("Warning: --spawned-by is ignored when resuming an existing session."));
|
|
536
541
|
return SessionManager.open(existingSession.path, sessionDir);
|
|
537
542
|
}
|
|
538
543
|
console.error(chalk.yellow(`Warning: No project session found with id '${parsed.sessionId}'; creating a new session with that id.`));
|
|
539
544
|
}
|
|
540
|
-
return SessionManager.create(cwd, sessionDir, { id: parsed.sessionId });
|
|
545
|
+
return SessionManager.create(cwd, sessionDir, { id: parsed.sessionId, spawnedBy: parsed.spawnedBy });
|
|
541
546
|
}
|
|
542
547
|
function buildSessionOptions(parsed, scopedModels, hasExistingSession, modelRuntime, settingsManager) {
|
|
543
548
|
const options = {};
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { Socket } from "node:net";
|
|
2
2
|
import type { Duplex } from "node:stream";
|
|
3
3
|
import { type HostAdmission } from "./host-admission.ts";
|
|
4
|
+
import type { PublicRpcConnection } from "./rpc-public-auth.ts";
|
|
4
5
|
/**
|
|
5
6
|
* Requests are size-bounded; each direction awaits downstream socket backpressure.
|
|
6
7
|
* Opens remain concurrent with control commands. Stop refuses every pending open,
|
|
7
8
|
* while other commands retain their wire order and child-owned semantics.
|
|
8
9
|
*/
|
|
9
|
-
export declare function proxyWithAdmission(client: Socket, internal: Duplex | undefined, admission: HostAdmission): void;
|
|
10
|
+
export declare function proxyWithAdmission(client: Socket, internal: Duplex | undefined, admission: HostAdmission, auth?: PublicRpcConnection): void;
|
|
10
11
|
//# sourceMappingURL=host-admission-proxy.d.ts.map
|
|
@@ -6,7 +6,8 @@ import { MAX_RPC_LINE_CHARACTERS, serializeJsonLine } from "./jsonl.js";
|
|
|
6
6
|
* Opens remain concurrent with control commands. Stop refuses every pending open,
|
|
7
7
|
* while other commands retain their wire order and child-owned semantics.
|
|
8
8
|
*/
|
|
9
|
-
export function proxyWithAdmission(client, internal, admission) {
|
|
9
|
+
export function proxyWithAdmission(client, internal, admission, auth) {
|
|
10
|
+
const reply = (chunk) => write(client, auth ? auth.response(chunk.toString()) : chunk);
|
|
10
11
|
const pending = [];
|
|
11
12
|
const refused = new Set();
|
|
12
13
|
let protocols = 0;
|
|
@@ -21,7 +22,7 @@ export function proxyWithAdmission(client, internal, admission) {
|
|
|
21
22
|
refused.add(id);
|
|
22
23
|
try {
|
|
23
24
|
for (const id of ids)
|
|
24
|
-
await
|
|
25
|
+
await reply(serializeJsonLine(hostStoppingResponse(id, "open_session", admission)));
|
|
25
26
|
}
|
|
26
27
|
catch {
|
|
27
28
|
fail();
|
|
@@ -50,9 +51,9 @@ export function proxyWithAdmission(client, internal, admission) {
|
|
|
50
51
|
fail();
|
|
51
52
|
});
|
|
52
53
|
async function requests() {
|
|
53
|
-
for await (const line of lines(client, MAX_RPC_LINE_CHARACTERS)) {
|
|
54
|
+
for await (const line of lines(auth?.input ?? client, MAX_RPC_LINE_CHARACTERS)) {
|
|
54
55
|
if (line === undefined) {
|
|
55
|
-
await
|
|
56
|
+
await reply(serializeJsonLine({
|
|
56
57
|
type: "response",
|
|
57
58
|
command: "parse",
|
|
58
59
|
success: false,
|
|
@@ -62,7 +63,7 @@ export function proxyWithAdmission(client, internal, admission) {
|
|
|
62
63
|
}
|
|
63
64
|
const request = record(line);
|
|
64
65
|
if (admission.stopping && request?.type === "get_protocol_info") {
|
|
65
|
-
await
|
|
66
|
+
await reply(serializeJsonLine({
|
|
66
67
|
id: request.id,
|
|
67
68
|
type: "response",
|
|
68
69
|
command: request.type,
|
|
@@ -71,7 +72,7 @@ export function proxyWithAdmission(client, internal, admission) {
|
|
|
71
72
|
}));
|
|
72
73
|
}
|
|
73
74
|
else if (admission.stopping && (request?.type === "open_session" || !internal)) {
|
|
74
|
-
await
|
|
75
|
+
await reply(serializeJsonLine(hostStoppingResponse(request?.id, String(request?.type ?? "parse"), admission)));
|
|
75
76
|
}
|
|
76
77
|
else if (internal) {
|
|
77
78
|
// Record admission before writing: a child response can arrive immediately.
|
|
@@ -95,7 +96,7 @@ export function proxyWithAdmission(client, internal, admission) {
|
|
|
95
96
|
continue;
|
|
96
97
|
}
|
|
97
98
|
const complete = chunk.subarray(0, newline + 1);
|
|
98
|
-
if (pending.length === 0 && protocols === 0 && refused.size === 0) {
|
|
99
|
+
if (!auth && pending.length === 0 && protocols === 0 && refused.size === 0) {
|
|
99
100
|
// Queue the entire record batch synchronously, without copying split buffers.
|
|
100
101
|
client.cork();
|
|
101
102
|
let written;
|
|
@@ -111,12 +112,12 @@ export function proxyWithAdmission(client, internal, admission) {
|
|
|
111
112
|
}
|
|
112
113
|
else {
|
|
113
114
|
const batch = fragments.length > 0 ? Buffer.concat([...fragments, complete]) : complete;
|
|
114
|
-
await
|
|
115
|
+
await reply(filterReplies(batch));
|
|
115
116
|
}
|
|
116
117
|
fragments = newline + 1 < chunk.length ? [chunk.subarray(newline + 1)] : [];
|
|
117
118
|
}
|
|
118
119
|
if (fragments.length > 0)
|
|
119
|
-
await
|
|
120
|
+
await reply(filterReplies(Buffer.concat(fragments)));
|
|
120
121
|
await refusals;
|
|
121
122
|
client.end(() => client.destroy());
|
|
122
123
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/** Whether this source module is the process entry, including a symlinked argv path. */
|
|
2
|
+
export declare function isCliModuleEntry(moduleUrl: string): boolean;
|
|
1
3
|
/**
|
|
2
4
|
* Resolves the committed CLI entry this supervisor wraps (source tree or built dist).
|
|
3
5
|
* Exported for tests, which pass the module path and layout of a bundled install.
|
|
@@ -3,11 +3,23 @@
|
|
|
3
3
|
* runtime snapshot's declared bin). Split out of `host-lifecycle-launch.ts` (senpi#2566); it stays in
|
|
4
4
|
* this directory so `import.meta.url` resolves relative paths exactly as before.
|
|
5
5
|
*/
|
|
6
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
6
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
7
7
|
import { access, readFile } from "node:fs/promises";
|
|
8
8
|
import { dirname, resolve } from "node:path";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
import { isBundledNode } from "../../config.js";
|
|
11
|
+
/** Whether this source module is the process entry, including a symlinked argv path. */
|
|
12
|
+
export function isCliModuleEntry(moduleUrl) {
|
|
13
|
+
const entry = process.argv[1];
|
|
14
|
+
if (!entry)
|
|
15
|
+
return false;
|
|
16
|
+
try {
|
|
17
|
+
return fileURLToPath(moduleUrl) === realpathSync(entry);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
11
23
|
/**
|
|
12
24
|
* Resolves the committed CLI entry this supervisor wraps (source tree or built dist).
|
|
13
25
|
* Exported for tests, which pass the module path and layout of a bundled install.
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import { type HostDecisionClient, type HostProtocolInfo } from "./host-decision.ts";
|
|
1
|
+
import { type HostDecision, type HostDecisionClient, type HostProtocolInfo } from "./host-decision.ts";
|
|
2
2
|
import type { EnsureHostOptions } from "./host-ensure-types.ts";
|
|
3
|
+
/** An ensure must produce a host; an unavailable upgrade falls back to a compatible attachment. */
|
|
4
|
+
export declare function decideEnsureAction(options: EnsureHostOptions, startedByUs: boolean, protocol: HostProtocolInfo | undefined): Exclude<HostDecision, {
|
|
5
|
+
action: "fallback";
|
|
6
|
+
}>;
|
|
3
7
|
/** This build as a client: which protocol it speaks, what it needs from a host, and which build it is. */
|
|
4
8
|
export declare function ensureClient(options: EnsureHostOptions, startedByUs: boolean): HostDecisionClient;
|
|
5
9
|
/**
|
|
@@ -2,7 +2,15 @@
|
|
|
2
2
|
import { isToolRuntimeKind, toolRuntimeCapability } from "../../cli/tool-runtime.js";
|
|
3
3
|
import { engineBuildIdentity } from "../../core/engine-build-identity.js";
|
|
4
4
|
import { decideHostAction, HOST_PROTOCOL_VERSION, REQUIRED_HOST_CAPABILITIES, } from "./host-decision.js";
|
|
5
|
+
import { isHostGenerationProcess } from "./host-process-role.js";
|
|
5
6
|
import { hostLaunchProfile } from "./protocol-identity.js";
|
|
7
|
+
/** An ensure must produce a host; an unavailable upgrade falls back to a compatible attachment. */
|
|
8
|
+
export function decideEnsureAction(options, startedByUs, protocol) {
|
|
9
|
+
if (options.upgrade !== "if-engine-differs" || isHostGenerationProcess())
|
|
10
|
+
return decideHostAction(ensureClient(options, startedByUs), protocol, "never");
|
|
11
|
+
const decision = decideHostAction(ensureClient(options, startedByUs), protocol, "upgrade");
|
|
12
|
+
return decision.action === "fallback" ? { action: "reuse", reason: "compatible", upgradeable: false } : decision;
|
|
13
|
+
}
|
|
6
14
|
/** This build as a client: which protocol it speaks, what it needs from a host, and which build it is. */
|
|
7
15
|
export function ensureClient(options, startedByUs) {
|
|
8
16
|
return {
|
|
@@ -12,6 +12,7 @@ export type HostUpgradePolicy = "never" | "if-engine-differs";
|
|
|
12
12
|
export interface EnsureHostOptions {
|
|
13
13
|
readonly socket: string;
|
|
14
14
|
readonly agentDir?: string;
|
|
15
|
+
readonly keyRoot?: string;
|
|
15
16
|
/** Opt in to this caller process's lifetime. Attach holds remain independently releasable. */
|
|
16
17
|
readonly owner?: "caller";
|
|
17
18
|
/** Host lifecycle policy recorded in settings.json (env overrides win at runtime). */
|
|
@@ -5,8 +5,8 @@ import { processIsLive, readProcessStartTime } from "../../utils/process-identit
|
|
|
5
5
|
import { createDaemonDirectories, createHostDaemonPaths, ensureEndpointIdentity, generationPaths, sameEndpoint, } from "./host-daemon-paths.js";
|
|
6
6
|
import { clearHostRegistration, readHostRegistration, writtenByThisProcess, } from "./host-daemon-registration.js";
|
|
7
7
|
import { claimHostOwner } from "./host-daemon-state.js";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
8
|
+
import { HostEnsureRefusedError } from "./host-decision.js";
|
|
9
|
+
import { decideEnsureAction as decide } from "./host-ensure-client.js";
|
|
10
10
|
import { refuseIfForeignHeld, refuseIfStalled, unregisteredEndpointHeld } from "./host-ensure-liveness.js";
|
|
11
11
|
import { hostEnsureLockOptions, hostEnsureLockTarget } from "./host-ensure-lock.js";
|
|
12
12
|
import { appendStderr, DEFAULT_READINESS_TIMEOUT_MS, DEFAULT_STOP_TIMEOUT_MS, startHost } from "./host-ensure-start.js";
|
|
@@ -16,9 +16,9 @@ import { HANDOFF_LOCK_HOLD_MS, handoffHostLocked } from "./host-handoff.js";
|
|
|
16
16
|
import { reapOrphanedInternalHostDirs } from "./host-internal-dir-reaper.js";
|
|
17
17
|
import { retireIdleLegacyHost } from "./host-legacy.js";
|
|
18
18
|
import { holdProtocolInfo, probeSocketReachable } from "./host-probe.js";
|
|
19
|
-
import { isHostGenerationProcess } from "./host-process-role.js";
|
|
20
19
|
import { hostChildAlive } from "./host-stalled-evidence.js";
|
|
21
20
|
import { acquireOwnershipSafeLock } from "./ownership-safe-lock.js";
|
|
21
|
+
import { rpcKeyRoot, withRpcKeyRoot } from "./rpc-auth-client.js";
|
|
22
22
|
export { createHostDaemonPaths, daemonDirectoryName, HostDaemonStateError, } from "./host-daemon-paths.js";
|
|
23
23
|
export { hostEnsureLockTarget } from "./host-ensure-lock.js";
|
|
24
24
|
export { defaultHostLaunch, PINNED_HOST_CLIENT_CAPABILITIES } from "./host-launch.js";
|
|
@@ -39,6 +39,9 @@ const ENSURE_LOCK_WAIT_MS = EXISTING_HOST_PROBE_TIMEOUT_MS +
|
|
|
39
39
|
10_000;
|
|
40
40
|
const lockOptions = hostEnsureLockOptions(ENSURE_LOCK_WAIT_MS);
|
|
41
41
|
export async function ensureHost(options) {
|
|
42
|
+
return withRpcKeyRoot(rpcKeyRoot(options, options.agentDir), () => ensureHostInKeyRoot(options));
|
|
43
|
+
}
|
|
44
|
+
async function ensureHostInKeyRoot(options) {
|
|
42
45
|
const socket = normalizeSocketPath(options.socket);
|
|
43
46
|
const paths = createHostDaemonPaths({ socket, ...(options.agentDir ? { agentDir: options.agentDir } : {}) });
|
|
44
47
|
await createDaemonDirectories(paths);
|
|
@@ -193,14 +196,6 @@ async function ensureHostLocked(paths, socket, options, stopDeadline = Date.now(
|
|
|
193
196
|
await clearHostRegistration(paths);
|
|
194
197
|
return startHost(paths, socket, options);
|
|
195
198
|
}
|
|
196
|
-
/** `fallback` belongs to clients that can live without a host; an ensure must produce one or fail. */
|
|
197
|
-
function decide(options, startedByUs, protocol) {
|
|
198
|
-
if (options.upgrade !== "if-engine-differs" || isHostGenerationProcess()) {
|
|
199
|
-
return decideHostAction(ensureClient(options, startedByUs), protocol, "never");
|
|
200
|
-
}
|
|
201
|
-
const decision = decideHostAction(ensureClient(options, startedByUs), protocol, "upgrade");
|
|
202
|
-
return decision.action === "fallback" ? { action: "reuse", reason: "compatible", upgradeable: false } : decision;
|
|
203
|
-
}
|
|
204
199
|
/**
|
|
205
200
|
* The upgrade, when the decision allows one: a new generation takes the socket and the running
|
|
206
201
|
* host drains. A refused handoff ATTACHES - an upgrade that cannot happen must never become a stop.
|
|
@@ -25,6 +25,7 @@ import { createHostDaemonPaths, sameEndpoint } from "./host-daemon-paths.js";
|
|
|
25
25
|
import { listHostEndpoints } from "./host-endpoints.js";
|
|
26
26
|
import { gcEndpoint } from "./host-gc.js";
|
|
27
27
|
import { nextSkip, readHostGcPassMarker, writeHostGcPassMarker, } from "./host-gc-pass-marker.js";
|
|
28
|
+
import { gcRpcPublicKeys } from "./rpc-public-key.js";
|
|
28
29
|
export const OPPORTUNISTIC_GC_BUDGET = { budgetMs: 1_500, maxEndpoints: 32, minIntervalMs: 300_000 };
|
|
29
30
|
export async function gcHostEndpointsOpportunistically(options) {
|
|
30
31
|
const now = options.now ?? Date.now;
|
|
@@ -35,6 +36,8 @@ export async function gcHostEndpointsOpportunistically(options) {
|
|
|
35
36
|
const startedAt = now();
|
|
36
37
|
if (previous !== undefined && startedAt - previous.completedAt < minIntervalMs)
|
|
37
38
|
return { ran: false };
|
|
39
|
+
if (process.platform !== "win32")
|
|
40
|
+
await gcRpcPublicKeys(options.agentDir, { deadline: startedAt + budgetMs, maxRecords: maxEndpoints });
|
|
38
41
|
const candidates = await gcCandidates(options);
|
|
39
42
|
const present = new Set(candidates.map((endpoint) => endpoint.name));
|
|
40
43
|
const skip = {};
|
|
@@ -46,9 +46,12 @@ import { acquireHostEnsureLock } from "./host-ensure-lock.js";
|
|
|
46
46
|
import { endpointInUse, socketSiblings } from "./host-gc-evidence.js";
|
|
47
47
|
import { judgeStaleEngine } from "./host-stale-engine.js";
|
|
48
48
|
import { stopHost } from "./host-stop.js";
|
|
49
|
+
import { gcRpcPublicKeys } from "./rpc-public-key.js";
|
|
49
50
|
/** 2 s, the budget a concurrent ensure's critical section gets before gc reports `locked`. */
|
|
50
51
|
const GC_LOCK_WAIT_MS = 2_000;
|
|
51
52
|
export async function gcHostEndpoints(agentDir, options = {}) {
|
|
53
|
+
if (process.platform !== "win32")
|
|
54
|
+
await gcRpcPublicKeys(agentDir);
|
|
52
55
|
const removed = [];
|
|
53
56
|
const stopped = [];
|
|
54
57
|
const kept = [];
|
|
@@ -1,15 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The engine side of the supervisor's private hop (senpi#3795): the stdin-delivered key and lifetime descriptor
|
|
3
|
-
* from the bootstrap, and how each accepted
|
|
3
|
+
* from the bootstrap, and how each accepted connection is admitted. Split out of multi-session-host.ts.
|
|
4
4
|
*/
|
|
5
5
|
import type { Socket } from "node:net";
|
|
6
6
|
import type { Duplex } from "node:stream";
|
|
7
|
-
import type
|
|
7
|
+
import { type HostWatchdogConfig } from "./host-watchdog.ts";
|
|
8
|
+
/**
|
|
9
|
+
* The supervisor binding, read once per process. The watch variables are removed as they are read (senpi#3815 review
|
|
10
|
+
* L4), so no tool, shell or nested host this host starts inherits a binding to a supervisor that is not its own.
|
|
11
|
+
*/
|
|
12
|
+
export declare function hostWatchdogBinding(): HostWatchdogConfig | undefined;
|
|
8
13
|
export interface InternalHostTransport {
|
|
9
|
-
/** The supervisor's watchdog binding, plus the reopened stdin lifetime descriptor when the bootstrap has one. */
|
|
10
14
|
readonly watchdogConfig: HostWatchdogConfig | undefined;
|
|
11
|
-
/** Authenticates and seals (POSIX key), authenticates (Windows secret), or accepts as is
|
|
15
|
+
/** Authenticates and seals (POSIX key), authenticates (Windows secret), filters (public), or accepts as is. */
|
|
12
16
|
admit(socket: Socket, accept: (socket?: Duplex) => void): void;
|
|
17
|
+
/** Publishes the public endpoint's key record after listening; a failure is logged and `onFailure` ends the host. */
|
|
18
|
+
publish(onFailure: () => Promise<void>): Promise<void>;
|
|
19
|
+
dispose(): Promise<void>;
|
|
13
20
|
}
|
|
14
|
-
export declare function internalHostTransport(configured: HostWatchdogConfig | undefined, secret: Uint8Array | undefined
|
|
21
|
+
export declare function internalHostTransport(configured: HostWatchdogConfig | undefined, secret: Uint8Array | undefined, endpoint?: {
|
|
22
|
+
readonly agentDir: string;
|
|
23
|
+
readonly socket: string;
|
|
24
|
+
readonly log: (message: string) => void;
|
|
25
|
+
}): InternalHostTransport;
|
|
15
26
|
//# sourceMappingURL=host-internal-transport.d.ts.map
|
|
@@ -1,18 +1,33 @@
|
|
|
1
1
|
import { VERSION } from "../../config.js";
|
|
2
|
-
import {
|
|
2
|
+
import { consumeHostWatchdogEnv, scrubHostWatchdogEnv } from "./host-watchdog.js";
|
|
3
3
|
import { getInternalHostBootstrap } from "./internal-bootstrap.js";
|
|
4
4
|
import { acceptInternalConnection } from "./internal-channel.js";
|
|
5
|
+
import { PublicRpcConnection } from "./rpc-public-auth.js";
|
|
6
|
+
import { RpcPublicEndpoint } from "./rpc-public-key.js";
|
|
5
7
|
import { authenticateSocket } from "./socket-transport.js";
|
|
6
|
-
|
|
8
|
+
let binding;
|
|
9
|
+
/**
|
|
10
|
+
* The supervisor binding, read once per process. The watch variables are removed as they are read (senpi#3815 review
|
|
11
|
+
* L4), so no tool, shell or nested host this host starts inherits a binding to a supervisor that is not its own.
|
|
12
|
+
*/
|
|
13
|
+
export function hostWatchdogBinding() {
|
|
14
|
+
binding ??= { config: consumeHostWatchdogEnv() };
|
|
15
|
+
return binding.config;
|
|
16
|
+
}
|
|
17
|
+
export function internalHostTransport(configured, secret, endpoint) {
|
|
7
18
|
const bootstrap = getInternalHostBootstrap();
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
19
|
+
scrubHostWatchdogEnv();
|
|
20
|
+
const watchdogConfig = configured && {
|
|
21
|
+
...configured,
|
|
22
|
+
...(bootstrap?.lifetimeFd === undefined ? {} : { lifetimeFd: bootstrap.lifetimeFd }),
|
|
23
|
+
};
|
|
24
|
+
// Supervised means holding the internal key, which only the launching supervisor can hand over and no descendant
|
|
25
|
+
// inherits; a host without it is a public endpoint whatever its environment says (senpi#3815 review L4).
|
|
26
|
+
const publicEndpoint = endpoint !== undefined && process.platform !== "win32" && bootstrap === undefined
|
|
27
|
+
? new RpcPublicEndpoint(endpoint.agentDir, endpoint.socket)
|
|
28
|
+
: undefined;
|
|
11
29
|
return {
|
|
12
|
-
watchdogConfig
|
|
13
|
-
...configured,
|
|
14
|
-
...(bootstrap?.lifetimeFd === undefined ? {} : { lifetimeFd: bootstrap.lifetimeFd }),
|
|
15
|
-
},
|
|
30
|
+
watchdogConfig,
|
|
16
31
|
admit(socket, accept) {
|
|
17
32
|
if (bootstrap)
|
|
18
33
|
void acceptInternalConnection(socket, bootstrap.key, { protocolVersion: 1, serverVersion: VERSION })
|
|
@@ -24,9 +39,25 @@ export function internalHostTransport(configured, secret) {
|
|
|
24
39
|
});
|
|
25
40
|
else if (secret)
|
|
26
41
|
authenticateSocket(socket, secret, () => accept());
|
|
42
|
+
else if (publicEndpoint)
|
|
43
|
+
accept(new PublicRpcConnection(socket, publicEndpoint).stream);
|
|
27
44
|
else
|
|
28
45
|
accept();
|
|
29
46
|
},
|
|
47
|
+
async publish(onFailure) {
|
|
48
|
+
try {
|
|
49
|
+
await publicEndpoint?.publish();
|
|
50
|
+
}
|
|
51
|
+
catch (cause) {
|
|
52
|
+
endpoint?.log(cause instanceof Error ? cause.message : String(cause));
|
|
53
|
+
await onFailure();
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
async dispose() {
|
|
57
|
+
await publicEndpoint?.dispose().catch((cause) => {
|
|
58
|
+
endpoint?.log(`endpoint key cleanup failed: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
59
|
+
});
|
|
60
|
+
},
|
|
30
61
|
};
|
|
31
62
|
}
|
|
32
63
|
//# sourceMappingURL=host-internal-transport.js.map
|