@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.
Files changed (178) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/dist/bundle/chunks/{args-6T5EQVY3.js → args-JXULI4EH.js} +1 -1
  3. package/dist/bundle/chunks/{chunk-VUKNGMDT.js → chunk-3LNSLJ7N.js} +1 -1
  4. package/dist/bundle/chunks/chunk-3VOG6WFU.js +27 -0
  5. package/dist/bundle/chunks/{chunk-HHYSXDYP.js → chunk-42OY5SZZ.js} +2 -2
  6. package/dist/bundle/chunks/chunk-7IRLFYNM.js +23 -0
  7. package/dist/bundle/chunks/{chunk-FIBIVBQK.js → chunk-7UUOGZKR.js} +1 -1
  8. package/dist/bundle/chunks/chunk-BMYNMUVZ.js +7 -0
  9. package/dist/bundle/chunks/{chunk-JAXZOEJE.js → chunk-BWA4FVFW.js} +1 -1
  10. package/dist/bundle/chunks/{chunk-AGQSRWNN.js → chunk-EHQXLHRB.js} +1 -1
  11. package/dist/bundle/chunks/chunk-EWOEVVQT.js +4 -0
  12. package/dist/bundle/chunks/{chunk-YJA2B7FQ.js → chunk-FDL4CF6F.js} +1 -1
  13. package/dist/bundle/chunks/{chunk-RQHK4QZ5.js → chunk-FWXMMMCV.js} +1 -1
  14. package/dist/bundle/chunks/{chunk-OA5INWHI.js → chunk-GVJ76VQI.js} +102 -42
  15. package/dist/bundle/chunks/{chunk-HCF5M6NA.js → chunk-GXY5JEI5.js} +1 -1
  16. package/dist/bundle/chunks/{chunk-VZAYN5SB.js → chunk-HKUXKTFW.js} +1 -1
  17. package/dist/bundle/chunks/chunk-II22E3CV.js +3 -0
  18. package/dist/bundle/chunks/{chunk-ONLNXGO3.js → chunk-IUHU5PEC.js} +1 -1
  19. package/dist/bundle/chunks/{chunk-LR6Z6V54.js → chunk-NQC7MI2G.js} +1 -1
  20. package/dist/bundle/chunks/{chunk-IELM5U6I.js → chunk-PXKF2RHD.js} +2 -2
  21. package/dist/bundle/chunks/{chunk-BRXN5R2F.js → chunk-QH2HSCMX.js} +1 -1
  22. package/dist/bundle/chunks/{chunk-QQSCUEYJ.js → chunk-RHHBXIRN.js} +2 -5
  23. package/dist/bundle/chunks/chunk-RVYSJBZ7.js +7 -0
  24. package/dist/bundle/chunks/chunk-SGDOZT6B.js +8 -0
  25. package/dist/bundle/chunks/{chunk-BZ75DRZB.js → chunk-U67ML6CQ.js} +4 -2
  26. package/dist/bundle/chunks/{chunk-3N2PQEQV.js → chunk-UT6AJHPK.js} +1 -1
  27. package/dist/bundle/chunks/{chunk-RAIZG6HA.js → chunk-V3CK2AVC.js} +1 -1
  28. package/dist/bundle/chunks/{chunk-CZR7C6CY.js → chunk-VOBD6FQG.js} +1 -1
  29. package/dist/bundle/chunks/{chunk-HRPMLI7O.js → chunk-VRDU425E.js} +2 -2
  30. package/dist/bundle/chunks/{chunk-OUPD5LWY.js → chunk-XUFUKDKJ.js} +3 -3
  31. package/dist/bundle/chunks/{chunk-SGATKHO4.js → chunk-Y5OTHMXO.js} +1 -1
  32. package/dist/bundle/chunks/{chunk-QEQG42HL.js → chunk-ZGAB7WQA.js} +1 -1
  33. package/dist/bundle/chunks/cli-main-UH3MAREK.js +3 -0
  34. package/dist/bundle/chunks/{dispatch-KQEGIXQW.js → dispatch-DEP4PEDZ.js} +3 -2
  35. package/dist/bundle/chunks/{help-fast-path-6PVN7WI5.js → help-fast-path-ZHZGXAUG.js} +1 -1
  36. package/dist/bundle/chunks/host-command-OQR4RDTM.js +2 -0
  37. package/dist/bundle/chunks/host-lifecycle-U36U7JTE.js +3 -0
  38. package/dist/bundle/chunks/host-lifecycle.js +26 -21
  39. package/dist/bundle/chunks/host-session-command-IGB27ZPJ.js +20 -0
  40. package/dist/bundle/chunks/{host-socket-request-KKGH3JHV.js → host-socket-request-ZDJGSSZO.js} +1 -1
  41. package/dist/bundle/chunks/interactive-mode-R5RCSJ2F.js +2 -0
  42. package/dist/bundle/chunks/{list-tips-BO5RP34W.js → list-tips-CREX4IXZ.js} +1 -1
  43. package/dist/bundle/chunks/{multi-session-host-O5GZQSRL.js → multi-session-host-YZEMYUFY.js} +12 -12
  44. package/dist/bundle/chunks/{package-manager-cli-LC5GAGTW.js → package-manager-cli-5FOXDKUA.js} +1 -1
  45. package/dist/bundle/chunks/{role-4G43UDOC.js → role-M2MTVACJ.js} +1 -1
  46. package/dist/bundle/chunks/{rotation-stream-RXMCJJUL.js → rotation-stream-U7YQMC7H.js} +1 -1
  47. package/dist/bundle/chunks/rpc-mode-6K62CZBW.js +2 -0
  48. package/dist/bundle/chunks/rpc-monitor-source-OL5KU6NO.js +3 -0
  49. package/dist/bundle/chunks/{schedule-command-QNCVE4HM.js → schedule-command-XBW4EOG5.js} +1 -1
  50. package/dist/bundle/chunks/session-control-endpoint-54TMCS7Q.js +3 -0
  51. package/dist/bundle/chunks/{session-picker-HIC5ZTE6.js → session-picker-P2VUU2MQ.js} +1 -1
  52. package/dist/bundle/chunks/session-worker.js +437 -368
  53. package/dist/bundle/cli.js +2 -2
  54. package/dist/bundle/host-supervisor.js +26 -21
  55. package/dist/bundle/index.js +1 -1
  56. package/dist/bundle/rpc-entry.js +1 -1
  57. package/dist/bundle/runtime-manifest.json +1 -1
  58. package/dist/cli/args.d.ts +2 -0
  59. package/dist/cli/args.js +27 -0
  60. package/dist/cli/host-command-args.d.ts +33 -0
  61. package/dist/cli/host-command-args.js +145 -0
  62. package/dist/cli/host-command.d.ts +1 -30
  63. package/dist/cli/host-command.js +5 -137
  64. package/dist/cli/host-session-command.d.ts +1 -0
  65. package/dist/cli/host-session-command.js +5 -3
  66. package/dist/core/agent-session.js +1 -1
  67. package/dist/core/extensions/builtin/anthropic-subscription/index.js +4 -2
  68. package/dist/core/extensions/builtin/anthropic-subscription/options.js +2 -1
  69. package/dist/core/extensions/builtin/anthropic-subscription/sdk-boundary.js +9 -2
  70. package/dist/core/extensions/builtin/anthropic-subscription/session-cwd.d.ts +3 -0
  71. package/dist/core/extensions/builtin/anthropic-subscription/session-cwd.js +9 -0
  72. package/dist/core/extensions/builtin/anthropic-subscription/settings.d.ts +1 -1
  73. package/dist/core/extensions/builtin/anthropic-subscription/settings.js +3 -1
  74. package/dist/core/extensions/builtin/anthropic-subscription/stream.js +4 -1
  75. package/dist/core/extensions/builtin/btw/index.js +4 -1
  76. package/dist/core/extensions/builtin/compaction/lane-policy.js +3 -1
  77. package/dist/core/extensions/builtin/look-at/runner.js +1 -0
  78. package/dist/core/extensions/builtin/mcp/connection-types.d.ts +2 -0
  79. package/dist/core/extensions/builtin/mcp/connection.js +3 -0
  80. package/dist/core/extensions/builtin/mcp/elicitation.d.ts +5 -2
  81. package/dist/core/extensions/builtin/mcp/elicitation.js +57 -11
  82. package/dist/core/extensions/builtin/mcp/index.js +1 -1
  83. package/dist/core/extensions/builtin/mcp/modern/dispatch.js +6 -9
  84. package/dist/core/extensions/builtin/mcp/service-connection.d.ts +1 -0
  85. package/dist/core/extensions/builtin/mcp/service-connection.js +1 -0
  86. package/dist/core/extensions/builtin/mcp/service.js +1 -0
  87. package/dist/core/extensions/builtin/mcp/transport-sdk.d.ts +1 -0
  88. package/dist/core/extensions/builtin/mcp/transport-sdk.js +5 -5
  89. package/dist/core/extensions/builtin/mcp/transport.d.ts +1 -0
  90. package/dist/core/extensions/builtin/mcp/transport.js +1 -0
  91. package/dist/core/extensions/builtin/prompt-preset/contract.js +6 -1
  92. package/dist/core/extensions/builtin/prompt-preset/kimi-k3-1.d.ts +3 -0
  93. package/dist/core/extensions/builtin/prompt-preset/kimi-k3-1.js +93 -0
  94. package/dist/core/extensions/builtin/prompt-preset/matchers.d.ts +2 -0
  95. package/dist/core/extensions/builtin/prompt-preset/matchers.js +9 -0
  96. package/dist/core/extensions/builtin/prompt-preset/presets.js +3 -0
  97. package/dist/core/extensions/builtin/prompt-preset/settings.d.ts +1 -1
  98. package/dist/core/extensions/builtin/prompt-preset/settings.js +1 -0
  99. package/dist/core/extensions/builtin/terminal/rpc-monitor-observer.d.ts +2 -1
  100. package/dist/core/extensions/builtin/terminal/rpc-monitor-observer.js +12 -4
  101. package/dist/core/external-admission.d.ts +4 -1
  102. package/dist/core/external-admission.js +18 -2
  103. package/dist/core/lifecycle-session-binding.js +20 -0
  104. package/dist/core/sdk.js +1 -0
  105. package/dist/core/session-spawned-by.d.ts +4 -0
  106. package/dist/core/session-spawned-by.js +16 -0
  107. package/dist/main.js +7 -2
  108. package/dist/modes/rpc/host-admission-proxy.d.ts +2 -1
  109. package/dist/modes/rpc/host-admission-proxy.js +10 -9
  110. package/dist/modes/rpc/host-cli-entry.d.ts +2 -0
  111. package/dist/modes/rpc/host-cli-entry.js +13 -1
  112. package/dist/modes/rpc/host-ensure-client.d.ts +5 -1
  113. package/dist/modes/rpc/host-ensure-client.js +8 -0
  114. package/dist/modes/rpc/host-ensure-types.d.ts +1 -0
  115. package/dist/modes/rpc/host-ensure.js +6 -11
  116. package/dist/modes/rpc/host-gc-pass.js +3 -0
  117. package/dist/modes/rpc/host-gc.js +3 -0
  118. package/dist/modes/rpc/host-internal-transport.d.ts +16 -5
  119. package/dist/modes/rpc/host-internal-transport.js +40 -9
  120. package/dist/modes/rpc/host-lifecycle-proxy.d.ts +4 -8
  121. package/dist/modes/rpc/host-lifecycle-proxy.js +9 -21
  122. package/dist/modes/rpc/host-lifecycle-shutdown.d.ts +2 -0
  123. package/dist/modes/rpc/host-lifecycle-shutdown.js +3 -0
  124. package/dist/modes/rpc/host-lifecycle.js +9 -16
  125. package/dist/modes/rpc/host-observe-request.js +1 -1
  126. package/dist/modes/rpc/host-probe.d.ts +8 -6
  127. package/dist/modes/rpc/host-probe.js +13 -13
  128. package/dist/modes/rpc/host-request-key-root.d.ts +8 -0
  129. package/dist/modes/rpc/host-request-key-root.js +14 -0
  130. package/dist/modes/rpc/host-runner.d.ts +3 -1
  131. package/dist/modes/rpc/host-runner.js +6 -1
  132. package/dist/modes/rpc/host-socket-bind.d.ts +6 -0
  133. package/dist/modes/rpc/host-socket-bind.js +42 -0
  134. package/dist/modes/rpc/host-socket-request.d.ts +3 -2
  135. package/dist/modes/rpc/host-socket-request.js +55 -10
  136. package/dist/modes/rpc/host-status.d.ts +3 -2
  137. package/dist/modes/rpc/host-status.js +5 -0
  138. package/dist/modes/rpc/host-supervisor-log.d.ts +1 -0
  139. package/dist/modes/rpc/host-supervisor-log.js +3 -0
  140. package/dist/modes/rpc/host-watchdog.d.ts +8 -1
  141. package/dist/modes/rpc/host-watchdog.js +37 -12
  142. package/dist/modes/rpc/internal-channel.js +14 -14
  143. package/dist/modes/rpc/multi-session-host.js +18 -46
  144. package/dist/modes/rpc/rpc-auth-client.d.ts +13 -0
  145. package/dist/modes/rpc/rpc-auth-client.js +142 -0
  146. package/dist/modes/rpc/rpc-auth-proof.d.ts +12 -0
  147. package/dist/modes/rpc/rpc-auth-proof.js +27 -0
  148. package/dist/modes/rpc/rpc-client-socket.d.ts +5 -0
  149. package/dist/modes/rpc/rpc-client-socket.js +30 -0
  150. package/dist/modes/rpc/rpc-client.d.ts +2 -0
  151. package/dist/modes/rpc/rpc-client.js +2 -24
  152. package/dist/modes/rpc/rpc-host-command-types.d.ts +4 -0
  153. package/dist/modes/rpc/rpc-protocol-info-types.d.ts +6 -0
  154. package/dist/modes/rpc/rpc-public-auth.d.ts +41 -0
  155. package/dist/modes/rpc/rpc-public-auth.js +237 -0
  156. package/dist/modes/rpc/rpc-public-key.d.ts +28 -0
  157. package/dist/modes/rpc/rpc-public-key.js +193 -0
  158. package/dist/modes/rpc/session-router-open-profile.js +2 -10
  159. package/dist/modes/rpc/socket-ownership.d.ts +7 -0
  160. package/dist/modes/rpc/socket-ownership.js +17 -0
  161. package/docs/rpc.md +108 -2
  162. package/docs/settings.md +1 -1
  163. package/docs/usage.md +14 -0
  164. package/package.json +6 -6
  165. package/dist/bundle/chunks/chunk-3474DPZK.js +0 -2
  166. package/dist/bundle/chunks/chunk-64CFJHKT.js +0 -7
  167. package/dist/bundle/chunks/chunk-B6NJHBLH.js +0 -21
  168. package/dist/bundle/chunks/chunk-HZTQHE5O.js +0 -26
  169. package/dist/bundle/chunks/chunk-IBDADH5Z.js +0 -4
  170. package/dist/bundle/chunks/chunk-OJZD7CO7.js +0 -8
  171. package/dist/bundle/chunks/cli-main-4JNPG6LB.js +0 -3
  172. package/dist/bundle/chunks/host-command-I3ZMWQQ3.js +0 -2
  173. package/dist/bundle/chunks/host-lifecycle-QEO422VW.js +0 -3
  174. package/dist/bundle/chunks/host-session-command-INULKX6E.js +0 -20
  175. package/dist/bundle/chunks/interactive-mode-SYWJVPKK.js +0 -2
  176. package/dist/bundle/chunks/rpc-mode-JMCYSP4P.js +0 -2
  177. package/dist/bundle/chunks/rpc-monitor-source-34AQDTZ6.js +0 -3
  178. 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
  }
@@ -21,6 +21,7 @@ export const VALID_PRESETS = new Set([
21
21
  "grok-4.5",
22
22
  "grok-4.6",
23
23
  "grok-4.7",
24
+ "kimi-k3-1",
24
25
  "kimi-k3",
25
26
  "kimi-k2-8",
26
27
  "kimi-k2-7",
@@ -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 socket = createConnection(resolveSocketTransportAddress(socketPath, process.platform, secret));
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
- socket.once("connect", () => {
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
- /** The runtime's queues were cleared: queued deliveries are no longer held. */
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
- /** The runtime's queues were cleared: queued deliveries are no longer held. */
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 !== "start")
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, parsed.sessionId !== undefined ? { id: parsed.sessionId } : undefined);
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 write(client, serializeJsonLine(hostStoppingResponse(id, "open_session", admission)));
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 write(client, serializeJsonLine({
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 write(client, serializeJsonLine({
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 write(client, serializeJsonLine(hostStoppingResponse(request?.id, String(request?.type ?? "parse"), admission)));
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 write(client, filterReplies(batch));
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 write(client, filterReplies(Buffer.concat(fragments)));
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 { decideHostAction, HostEnsureRefusedError } from "./host-decision.js";
9
- import { ensureClient } from "./host-ensure-client.js";
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 internal connection is admitted. Split out of multi-session-host.ts.
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 { HostWatchdogConfig } from "./host-watchdog.ts";
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 (no supervisor). */
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): InternalHostTransport;
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 { HOST_WATCH_LIFETIME_FIFO_ENV } from "./host-lifetime-binding.js";
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
- export function internalHostTransport(configured, secret) {
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
- // The FIFO path is the host's alone: it is in `configured` now, so no tool or shell the host runs inherits it and
9
- // could open the FIFO for writing to hold off shutdown (senpi#3816 review).
10
- delete process.env[HOST_WATCH_LIFETIME_FIFO_ENV];
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: configured && {
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