@code-yeongyu/senpi 2026.9.18 → 2026.9.19

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 (195) hide show
  1. package/CHANGELOG.md +103 -0
  2. package/dist/bundle/chunks/{anthropic-messages-ZEJ3DPCP.js → anthropic-messages-WZCFP6BT.js} +1 -1
  3. package/dist/bundle/chunks/{app-server-command-ELSORC4D.js → app-server-command-QK7Q6XK5.js} +1 -1
  4. package/dist/bundle/chunks/bun-extension-importer-JQZRPNJH.js +10 -0
  5. package/dist/bundle/chunks/{chunk-Y6HDGVS5.js → chunk-34GCWLNV.js} +1 -1
  6. package/dist/bundle/chunks/{chunk-XJUVWITG.js → chunk-3FRTRCR6.js} +1 -1
  7. package/dist/bundle/chunks/{chunk-VRAYXSST.js → chunk-FDLVINNY.js} +2 -2
  8. package/dist/bundle/chunks/{chunk-QNZ24YOQ.js → chunk-HGAX6JZ7.js} +3 -1
  9. package/dist/bundle/chunks/{chunk-QGBRSL64.js → chunk-HSH674C6.js} +1 -1
  10. package/dist/bundle/chunks/chunk-IUZUN32Z.js +13 -0
  11. package/dist/bundle/chunks/chunk-IXAUDPOK.js +2 -0
  12. package/dist/bundle/chunks/{chunk-AH72FRZ2.js → chunk-KKDSBABY.js} +11 -11
  13. package/dist/bundle/chunks/{chunk-NLOZDJQX.js → chunk-LF3XTYYF.js} +3 -3
  14. package/dist/bundle/chunks/{chunk-ANTGWIGI.js → chunk-MUC3MZTV.js} +1 -1
  15. package/dist/bundle/chunks/chunk-NARL7XFO.js +8 -0
  16. package/dist/bundle/chunks/chunk-NQAVM5XL.js +2 -0
  17. package/dist/bundle/chunks/chunk-P7PWCGLJ.js +5 -0
  18. package/dist/bundle/chunks/{chunk-4C7LNOHI.js → chunk-PLZK7ABD.js} +1 -1
  19. package/dist/bundle/chunks/{chunk-CKNRCW35.js → chunk-RLRLQRI2.js} +1 -1
  20. package/dist/bundle/chunks/{chunk-7FXIHFZU.js → chunk-UAYBYTFN.js} +1 -1
  21. package/dist/bundle/chunks/{chunk-7A47FVQC.js → chunk-W4SRGDQ4.js} +3 -6
  22. package/dist/bundle/chunks/{chunk-QROWINIH.js → chunk-XK24AFYU.js} +1 -1
  23. package/dist/bundle/chunks/cli-main-7SG3K4K3.js +3 -0
  24. package/dist/bundle/chunks/cursor-agent.js +38 -0
  25. package/dist/bundle/chunks/cursor.js +2 -0
  26. package/dist/bundle/chunks/devin-agent.js +5 -0
  27. package/dist/bundle/chunks/devin.js +75 -0
  28. package/dist/bundle/chunks/{help-fast-path-WQCATMI4.js → help-fast-path-RQDCLDQT.js} +1 -1
  29. package/dist/bundle/chunks/host-command-3LS772D5.js +2 -0
  30. package/dist/bundle/chunks/{host-lifecycle-EAXSS3F2.js → host-lifecycle-TA5XGUW5.js} +1 -1
  31. package/dist/bundle/chunks/host-lifecycle.js +20 -0
  32. package/dist/bundle/chunks/interactive-host-runtime-AITXUDCT.js +4 -0
  33. package/dist/bundle/chunks/interactive-mode-7MN6GGCG.js +2 -0
  34. package/dist/bundle/chunks/{multi-session-host-OK6WMHB4.js → multi-session-host-WKX34SOP.js} +1 -1
  35. package/dist/bundle/chunks/{openai-completions-2RS3VSPV.js → openai-completions-TOWYKX2U.js} +1 -1
  36. package/dist/bundle/chunks/{package-manager-cli-C2K4YRGI.js → package-manager-cli-CB2X57TD.js} +1 -1
  37. package/dist/bundle/chunks/{rotation-stream-536AQTIS.js → rotation-stream-KDLA7K6X.js} +1 -1
  38. package/dist/bundle/chunks/rpc-mode-CI6V2SBW.js +2 -0
  39. package/dist/bundle/chunks/{session-picker-BUHK3FCS.js → session-picker-DXCITGLS.js} +1 -1
  40. package/dist/bundle/chunks/session-worker.js +359 -346
  41. package/dist/bundle/chunks/{state-store-OYDXEIXU.js → state-store-HBBOD7I5.js} +1 -1
  42. package/dist/bundle/cli.js +1 -1
  43. package/dist/bundle/index.js +1 -1
  44. package/dist/bundle/rpc-entry.js +1 -1
  45. package/dist/cli/args.d.ts.map +1 -1
  46. package/dist/cli/args.js +2 -0
  47. package/dist/cli/args.js.map +1 -1
  48. package/dist/cli/deferred-commands.d.ts +7 -0
  49. package/dist/cli/deferred-commands.d.ts.map +1 -1
  50. package/dist/cli/deferred-commands.js +12 -0
  51. package/dist/cli/deferred-commands.js.map +1 -1
  52. package/dist/cli/host-command.d.ts +21 -0
  53. package/dist/cli/host-command.d.ts.map +1 -0
  54. package/dist/cli/host-command.js +148 -0
  55. package/dist/cli/host-command.js.map +1 -0
  56. package/dist/core/extensions/builtin/prompt-preset/kimi-k2-7.d.ts +1 -1
  57. package/dist/core/extensions/builtin/prompt-preset/kimi-k2-7.d.ts.map +1 -1
  58. package/dist/core/extensions/builtin/prompt-preset/kimi-k2-7.js +2 -17
  59. package/dist/core/extensions/builtin/prompt-preset/kimi-k2-7.js.map +1 -1
  60. package/dist/core/extensions/builtin/prompt-preset/kimi-k2-8.d.ts +3 -0
  61. package/dist/core/extensions/builtin/prompt-preset/kimi-k2-8.d.ts.map +1 -0
  62. package/dist/core/extensions/builtin/prompt-preset/kimi-k2-8.js +5 -0
  63. package/dist/core/extensions/builtin/prompt-preset/kimi-k2-8.js.map +1 -0
  64. package/dist/core/extensions/builtin/prompt-preset/kimi-k2-code.d.ts +3 -0
  65. package/dist/core/extensions/builtin/prompt-preset/kimi-k2-code.d.ts.map +1 -0
  66. package/dist/core/extensions/builtin/prompt-preset/kimi-k2-code.js +28 -0
  67. package/dist/core/extensions/builtin/prompt-preset/kimi-k2-code.js.map +1 -0
  68. package/dist/core/extensions/builtin/prompt-preset/presets.d.ts.map +1 -1
  69. package/dist/core/extensions/builtin/prompt-preset/presets.js +21 -1
  70. package/dist/core/extensions/builtin/prompt-preset/presets.js.map +1 -1
  71. package/dist/core/extensions/builtin/prompt-preset/settings.d.ts +1 -1
  72. package/dist/core/extensions/builtin/prompt-preset/settings.d.ts.map +1 -1
  73. package/dist/core/extensions/builtin/prompt-preset/settings.js +1 -0
  74. package/dist/core/extensions/builtin/prompt-preset/settings.js.map +1 -1
  75. package/dist/core/extensions/bun-extension-importer.d.ts.map +1 -1
  76. package/dist/core/extensions/bun-extension-importer.js +31 -5
  77. package/dist/core/extensions/bun-extension-importer.js.map +1 -1
  78. package/dist/core/extensions/bun-extension-registry.d.ts +9 -0
  79. package/dist/core/extensions/bun-extension-registry.d.ts.map +1 -1
  80. package/dist/core/extensions/bun-extension-registry.js +72 -16
  81. package/dist/core/extensions/bun-extension-registry.js.map +1 -1
  82. package/dist/core/extensions/extension-runtime-module.d.ts +5 -0
  83. package/dist/core/extensions/extension-runtime-module.d.ts.map +1 -0
  84. package/dist/core/extensions/extension-runtime-module.js +6 -0
  85. package/dist/core/extensions/extension-runtime-module.js.map +1 -0
  86. package/dist/core/model-resolver.d.ts.map +1 -1
  87. package/dist/core/model-resolver.js +1 -0
  88. package/dist/core/model-resolver.js.map +1 -1
  89. package/dist/index.d.ts +2 -1
  90. package/dist/index.d.ts.map +1 -1
  91. package/dist/index.js +3 -1
  92. package/dist/index.js.map +1 -1
  93. package/dist/main.d.ts.map +1 -1
  94. package/dist/main.js +7 -1
  95. package/dist/main.js.map +1 -1
  96. package/dist/modes/index.d.ts +3 -0
  97. package/dist/modes/index.d.ts.map +1 -1
  98. package/dist/modes/index.js +5 -0
  99. package/dist/modes/index.js.map +1 -1
  100. package/dist/modes/rpc/host-daemon-env.d.ts +18 -0
  101. package/dist/modes/rpc/host-daemon-env.d.ts.map +1 -0
  102. package/dist/modes/rpc/host-daemon-env.js +95 -0
  103. package/dist/modes/rpc/host-daemon-env.js.map +1 -0
  104. package/dist/modes/rpc/host-decision.d.ts +1 -1
  105. package/dist/modes/rpc/host-decision.d.ts.map +1 -1
  106. package/dist/modes/rpc/host-decision.js +2 -0
  107. package/dist/modes/rpc/host-decision.js.map +1 -1
  108. package/dist/modes/rpc/host-ensure.js +8 -1
  109. package/dist/modes/rpc/host-ensure.js.map +1 -1
  110. package/dist/modes/rpc/host-launch-spec.d.ts +43 -0
  111. package/dist/modes/rpc/host-launch-spec.d.ts.map +1 -0
  112. package/dist/modes/rpc/host-launch-spec.js +183 -0
  113. package/dist/modes/rpc/host-launch-spec.js.map +1 -0
  114. package/dist/modes/rpc/host-launch.d.ts +3 -1
  115. package/dist/modes/rpc/host-launch.d.ts.map +1 -1
  116. package/dist/modes/rpc/host-launch.js +24 -5
  117. package/dist/modes/rpc/host-launch.js.map +1 -1
  118. package/dist/modes/rpc/host-lifecycle.d.ts.map +1 -1
  119. package/dist/modes/rpc/host-lifecycle.js +36 -2
  120. package/dist/modes/rpc/host-lifecycle.js.map +1 -1
  121. package/dist/modes/rpc/host-probe.d.ts +7 -0
  122. package/dist/modes/rpc/host-probe.d.ts.map +1 -1
  123. package/dist/modes/rpc/host-probe.js +15 -4
  124. package/dist/modes/rpc/host-probe.js.map +1 -1
  125. package/dist/modes/rpc/host-process-metrics.d.ts +10 -0
  126. package/dist/modes/rpc/host-process-metrics.d.ts.map +1 -0
  127. package/dist/modes/rpc/host-process-metrics.js +83 -0
  128. package/dist/modes/rpc/host-process-metrics.js.map +1 -0
  129. package/dist/modes/rpc/host-runner.d.ts +38 -0
  130. package/dist/modes/rpc/host-runner.d.ts.map +1 -0
  131. package/dist/modes/rpc/host-runner.js +195 -0
  132. package/dist/modes/rpc/host-runner.js.map +1 -0
  133. package/dist/modes/rpc/host-status.d.ts +58 -0
  134. package/dist/modes/rpc/host-status.d.ts.map +1 -0
  135. package/dist/modes/rpc/host-status.js +117 -0
  136. package/dist/modes/rpc/host-status.js.map +1 -0
  137. package/docs/extensions.md +35 -0
  138. package/docs/providers.md +30 -0
  139. package/docs/rpc.md +177 -15
  140. package/docs/settings.md +1 -1
  141. package/node_modules/@code-yeongyu/senpi-codemode/CHANGELOG.md +72 -0
  142. package/node_modules/@code-yeongyu/senpi-codemode/package.json +4 -4
  143. package/node_modules/@earendil-works/pi-agent-core/package.json +3 -3
  144. package/node_modules/@earendil-works/pi-ai/README.md +9 -0
  145. package/node_modules/@earendil-works/pi-ai/dist/env-api-keys.d.ts.map +1 -1
  146. package/node_modules/@earendil-works/pi-ai/dist/env-api-keys.js +1 -0
  147. package/node_modules/@earendil-works/pi-ai/dist/env-api-keys.js.map +1 -1
  148. package/node_modules/@earendil-works/pi-ai/dist/models.generated.d.ts +2 -2
  149. package/node_modules/@earendil-works/pi-ai/dist/models.generated.d.ts.map +1 -1
  150. package/node_modules/@earendil-works/pi-ai/dist/models.generated.js +2 -2
  151. package/node_modules/@earendil-works/pi-ai/dist/models.generated.js.map +1 -1
  152. package/node_modules/@earendil-works/pi-ai/dist/providers/all.d.ts +150 -7
  153. package/node_modules/@earendil-works/pi-ai/dist/providers/all.d.ts.map +1 -1
  154. package/node_modules/@earendil-works/pi-ai/dist/providers/all.js +17 -3
  155. package/node_modules/@earendil-works/pi-ai/dist/providers/all.js.map +1 -1
  156. package/node_modules/@earendil-works/pi-ai/dist/providers/bai-stream.d.ts +13 -0
  157. package/node_modules/@earendil-works/pi-ai/dist/providers/bai-stream.d.ts.map +1 -0
  158. package/node_modules/@earendil-works/pi-ai/dist/providers/bai-stream.js +55 -0
  159. package/node_modules/@earendil-works/pi-ai/dist/providers/bai-stream.js.map +1 -0
  160. package/node_modules/@earendil-works/pi-ai/dist/providers/bai.d.ts +11 -0
  161. package/node_modules/@earendil-works/pi-ai/dist/providers/bai.d.ts.map +1 -0
  162. package/node_modules/@earendil-works/pi-ai/dist/providers/bai.js +94 -0
  163. package/node_modules/@earendil-works/pi-ai/dist/providers/bai.js.map +1 -0
  164. package/node_modules/@earendil-works/pi-ai/dist/providers/bai.models.d.ts +4 -0
  165. package/node_modules/@earendil-works/pi-ai/dist/providers/bai.models.d.ts.map +1 -0
  166. package/node_modules/@earendil-works/pi-ai/dist/providers/bai.models.js +6 -0
  167. package/node_modules/@earendil-works/pi-ai/dist/providers/bai.models.js.map +1 -0
  168. package/node_modules/@earendil-works/pi-ai/dist/providers/data/.manifest.json +1 -1
  169. package/node_modules/@earendil-works/pi-ai/dist/providers/data/bai.json +1 -0
  170. package/node_modules/@earendil-works/pi-ai/dist/providers/data/mistral.json +1 -1
  171. package/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode.json +1 -1
  172. package/node_modules/@earendil-works/pi-ai/dist/providers/data/opengateway.json +1 -1
  173. package/node_modules/@earendil-works/pi-ai/dist/providers/data/openrouter.json +1 -1
  174. package/node_modules/@earendil-works/pi-ai/dist/providers/data/venice.json +1 -1
  175. package/node_modules/@earendil-works/pi-ai/dist/providers/data/vercel-ai-gateway.json +1 -1
  176. package/node_modules/@earendil-works/pi-ai/dist/providers/kimi-coding.models.d.ts +142 -1
  177. package/node_modules/@earendil-works/pi-ai/dist/providers/kimi-coding.models.d.ts.map +1 -1
  178. package/node_modules/@earendil-works/pi-ai/dist/providers/kimi-coding.models.js +156 -3
  179. package/node_modules/@earendil-works/pi-ai/dist/providers/kimi-coding.models.js.map +1 -1
  180. package/node_modules/@earendil-works/pi-ai/dist/types.d.ts +1 -1
  181. package/node_modules/@earendil-works/pi-ai/dist/types.d.ts.map +1 -1
  182. package/node_modules/@earendil-works/pi-ai/dist/types.js.map +1 -1
  183. package/node_modules/@earendil-works/pi-ai/package.json +2 -2
  184. package/node_modules/@earendil-works/pi-pty/package.json +1 -1
  185. package/node_modules/@earendil-works/pi-telemetry/package.json +1 -1
  186. package/node_modules/@earendil-works/pi-tui/package.json +1 -1
  187. package/package.json +7 -7
  188. package/dist/bundle/chunks/bun-extension-importer-SW7XGNSG.js +0 -10
  189. package/dist/bundle/chunks/chunk-AGXZF7RF.js +0 -2
  190. package/dist/bundle/chunks/chunk-F5JZNLDE.js +0 -8
  191. package/dist/bundle/chunks/cli-main-4TWJXJKM.js +0 -3
  192. package/dist/bundle/chunks/interactive-host-runtime-EDXG6A5L.js +0 -4
  193. package/dist/bundle/chunks/interactive-mode-PD7S7KFZ.js +0 -2
  194. package/dist/bundle/chunks/rpc-mode-TIJFUWXS.js +0 -2
  195. package/node_modules/@earendil-works/pi-ai/dist/providers/data/kimi-coding.json +0 -1
@@ -0,0 +1,38 @@
1
+ import { type HostDecisionPolicy } from "./host-decision.ts";
2
+ import type { ResolvedHostLaunchSpec } from "./host-launch-spec.ts";
3
+ export declare const HOST_EXIT_OK = 0;
4
+ export declare const HOST_EXIT_ERROR = 1;
5
+ export declare const HOST_EXIT_USAGE = 2;
6
+ export declare const HOST_EXIT_REFUSED = 3;
7
+ export declare const HOST_EXIT_FALLBACK = 4;
8
+ /** Which daemon a request is about: the endpoint, and the agent directory holding its state. */
9
+ export interface HostTarget {
10
+ readonly socket: string;
11
+ readonly agentDir: string;
12
+ }
13
+ export type HostRequest = {
14
+ readonly action: "ensure";
15
+ readonly target: HostTarget;
16
+ readonly spec: ResolvedHostLaunchSpec;
17
+ readonly policy: HostDecisionPolicy;
18
+ } | {
19
+ readonly action: "status";
20
+ readonly target: HostTarget;
21
+ readonly includeWorkers: boolean;
22
+ } | {
23
+ readonly action: "stop";
24
+ readonly target: HostTarget;
25
+ readonly drain: boolean;
26
+ readonly force: boolean;
27
+ } | {
28
+ readonly action: "handoff";
29
+ readonly target: HostTarget;
30
+ readonly spec: ResolvedHostLaunchSpec;
31
+ };
32
+ /** One JSON line and the exit code it means. */
33
+ export interface HostOutcome {
34
+ readonly exitCode: number;
35
+ readonly payload: Record<string, unknown>;
36
+ }
37
+ export declare function runHostRequest(request: HostRequest): Promise<HostOutcome>;
38
+ //# sourceMappingURL=host-runner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"host-runner.d.ts","sourceRoot":"","sources":["../../../src/modes/rpc/host-runner.ts"],"names":[],"mappings":"AAoBA,OAAO,EAIN,KAAK,kBAAkB,EAIvB,MAAM,oBAAoB,CAAC;AAG5B,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAKpE,eAAO,MAAM,YAAY,IAAI,CAAC;AAC9B,eAAO,MAAM,eAAe,IAAI,CAAC;AACjC,eAAO,MAAM,eAAe,IAAI,CAAC;AACjC,eAAO,MAAM,iBAAiB,IAAI,CAAC;AACnC,eAAO,MAAM,kBAAkB,IAAI,CAAC;AAIpC,gGAAgG;AAChG,MAAM,WAAW,UAAU;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,MAAM,WAAW,GACpB;IACA,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,QAAQ,CAAC,IAAI,EAAE,sBAAsB,CAAC;IACtC,QAAQ,CAAC,MAAM,EAAE,kBAAkB,CAAC;CACnC,GACD;IAAE,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAA;CAAE,GAC5F;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GAC1G;IAAE,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,sBAAsB,CAAA;CAAE,CAAC;AAEtG,gDAAgD;AAChD,MAAM,WAAW,WAAW;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC1C;AAED,wBAAsB,cAAc,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,CAa/E"}
@@ -0,0 +1,195 @@
1
+ /**
2
+ * One host request, performed: ensure a daemon, report one, stop one, or hand one off.
3
+ *
4
+ * This is the whole behaviour behind `senpi host` - and behind the launchers that call the same
5
+ * entry point in-process - separated from the command line that expresses it. Every request answers
6
+ * with a JSON payload plus the exit code that payload means, so a CLI, a desktop and a task runner
7
+ * report the same outcome with the same vocabulary:
8
+ *
9
+ * 0 the request happened 3 refused: a running host this client may not act on,
10
+ * 4 no host is better than this one an unreachable socket, or a stop that would end
11
+ * one (fallback policy) somebody else's work
12
+ *
13
+ * The refusals are the point. One machine-wide daemon holds every client's sessions, so a client
14
+ * that cannot get what it wants must SAY so and leave the running host alone (I1) - never stop it,
15
+ * never bind a second host over its socket. `decideHostAction`, `ensureHost`, `stopHost` and
16
+ * `handoffHost` already enforce that; this module only gives their outcomes a machine-readable shape.
17
+ */
18
+ import { engineBuildIdentity } from "../../core/engine-build-identity.js";
19
+ import { daemonEnvKeys, daemonEnvOverrides, writeDaemonEnvKeys } from "./host-daemon-env.js";
20
+ import { createHostDaemonPaths } from "./host-daemon-paths.js";
21
+ import { decideHostAction, HOST_PROTOCOL_VERSION, HostEnsureRefusedError, REQUIRED_HOST_CAPABILITIES, } from "./host-decision.js";
22
+ import { ensureHost } from "./host-ensure.js";
23
+ import { handoffHost } from "./host-handoff.js";
24
+ import { probeProtocolInfo } from "./host-probe.js";
25
+ import { hostSummary, readHostStatus, readSessionCounts } from "./host-status.js";
26
+ import { stopHost } from "./host-stop.js";
27
+ export const HOST_EXIT_OK = 0;
28
+ export const HOST_EXIT_ERROR = 1;
29
+ export const HOST_EXIT_USAGE = 2;
30
+ export const HOST_EXIT_REFUSED = 3;
31
+ export const HOST_EXIT_FALLBACK = 4;
32
+ const PROBE_TIMEOUT_MS = 10_000;
33
+ export async function runHostRequest(request) {
34
+ switch (request.action) {
35
+ case "ensure":
36
+ return ensureOutcome(request.target, request.spec, request.policy);
37
+ case "status":
38
+ return statusOutcome(request.target, request.includeWorkers);
39
+ case "stop":
40
+ return stopOutcome(request.target, request.drain, request.force);
41
+ case "handoff":
42
+ return handoffOutcome(request.target, request.spec);
43
+ default:
44
+ return assertNever(request);
45
+ }
46
+ }
47
+ async function ensureOutcome(target, spec, policy) {
48
+ const before = await probeProtocolInfo(target.socket, PROBE_TIMEOUT_MS);
49
+ if (policy === "fallback") {
50
+ // `fallback` is the caller saying it can live without a host. The decision is made HERE
51
+ // because an ensure always produces a host or fails: it has no fallback answer of its own.
52
+ const decision = decideHostAction(decisionClient(), before, "fallback");
53
+ if (decision.action === "fallback") {
54
+ return refusal("fallback", before, { reason: decision.reason, socket: target.socket });
55
+ }
56
+ }
57
+ let ensured;
58
+ try {
59
+ ensured = await ensureHost({
60
+ socket: target.socket,
61
+ agentDir: target.agentDir,
62
+ hostArgs: spec.hostArgs,
63
+ env: daemonEnvOverrides(process.env, spec.env),
64
+ policy: spec.policy,
65
+ upgrade: policy === "upgrade" ? "if-engine-differs" : "never",
66
+ });
67
+ }
68
+ catch (error) {
69
+ if (!(error instanceof HostEnsureRefusedError))
70
+ throw error;
71
+ return refusal("refuse", before, { reason: error.reason, socket: target.socket });
72
+ }
73
+ const host = await probeProtocolInfo(target.socket, PROBE_TIMEOUT_MS);
74
+ // Only a spawn grants an environment; a reuse inherited whatever the client that started it did.
75
+ if (!ensured.reused)
76
+ await recordEnvScope(target, spec);
77
+ return {
78
+ exitCode: HOST_EXIT_OK,
79
+ payload: {
80
+ action: ensureAction(ensured.reused, before, host),
81
+ ...identityPayload(target.socket, ensured.pid, host),
82
+ reused: ensured.reused,
83
+ },
84
+ };
85
+ }
86
+ /**
87
+ * What an ensure DID. A handoff and a fresh start both report `reused: false`, so the instance id is
88
+ * what tells them apart: a handoff replaced the process behind a socket somebody was already serving.
89
+ */
90
+ function ensureAction(reused, before, after) {
91
+ if (reused)
92
+ return "reuse";
93
+ return before !== undefined && before.instanceId !== after?.instanceId ? "handoff" : "start";
94
+ }
95
+ async function statusOutcome(target, includeWorkers) {
96
+ const status = await readHostStatus({ socket: target.socket, agentDir: target.agentDir, includeWorkers });
97
+ return { exitCode: status.reachable ? HOST_EXIT_OK : HOST_EXIT_REFUSED, payload: { ...status } };
98
+ }
99
+ /**
100
+ * A hard stop ends every session the daemon holds, including the ones other clients are attached to,
101
+ * so it is gated on the host's own count and refuses with those counts rather than a bare "busy".
102
+ * A DRAIN ends no work - the host stops accepting and leaves when it is empty - so it is never gated.
103
+ */
104
+ async function stopOutcome(target, drain, force) {
105
+ const host = await probeProtocolInfo(target.socket, PROBE_TIMEOUT_MS);
106
+ const sessions = await readSessionCounts(target.socket, true);
107
+ const occupied = sessions.foreign_attached + sessions.foreign_retained > 0;
108
+ if (!drain && !force && occupied) {
109
+ return refusal("refuse", host, { reason: "sessions_live", socket: target.socket, sessions });
110
+ }
111
+ const result = await stopHost({ socket: target.socket, agentDir: target.agentDir, drain, force });
112
+ if (result.action === "refuse") {
113
+ return refusal("refuse", host, { reason: result.reason, socket: target.socket, sessions });
114
+ }
115
+ return {
116
+ exitCode: HOST_EXIT_OK,
117
+ payload: { action: result.action, socket: target.socket, pid: result.pid, sessions },
118
+ };
119
+ }
120
+ async function handoffOutcome(target, spec) {
121
+ const before = await probeProtocolInfo(target.socket, PROBE_TIMEOUT_MS);
122
+ const result = await handoffHost({
123
+ socket: target.socket,
124
+ agentDir: target.agentDir,
125
+ hostArgs: spec.hostArgs,
126
+ env: daemonEnvOverrides(process.env, spec.env),
127
+ policy: spec.policy,
128
+ });
129
+ if (result.action === "refuse") {
130
+ return refusal("refuse", before, {
131
+ reason: upgradeRefusal(result.reason),
132
+ socket: target.socket,
133
+ detail: result.reason,
134
+ upgradeable: result.upgradeable,
135
+ });
136
+ }
137
+ await recordEnvScope(target, spec);
138
+ const host = await probeProtocolInfo(target.socket, PROBE_TIMEOUT_MS);
139
+ return {
140
+ exitCode: HOST_EXIT_OK,
141
+ payload: { action: "handoff", ...identityPayload(target.socket, result.pid, host), reused: false },
142
+ };
143
+ }
144
+ /**
145
+ * A host that cannot drain and a platform that cannot hand off are one answer to the caller: this
146
+ * build may not replace the running generation. The refusal that produced it stays in `detail`.
147
+ */
148
+ function upgradeRefusal(reason) {
149
+ return reason === "handoff_unsupported" ? "upgrade_unsupported" : reason;
150
+ }
151
+ function identityPayload(socket, pid, host) {
152
+ return {
153
+ socket,
154
+ pid,
155
+ instanceId: host?.instanceId ?? null,
156
+ generation: host?.generation ?? null,
157
+ engineVersion: host?.engineVersion ?? null,
158
+ engineOrdinal: host?.engineOrdinal ?? null,
159
+ capabilities: host?.capabilities ?? [],
160
+ launchProfileId: host?.launch_profile?.profile_id ?? null,
161
+ upgradeable: decideHostAction(decisionClient(), host, "upgrade").upgradeable,
162
+ };
163
+ }
164
+ /**
165
+ * The two ways a request ends without acting, and the exit codes they mean: a `refuse` leaves a
166
+ * running host alone (3), a `fallback` says no host is better than this one (4).
167
+ */
168
+ function refusal(kind, host, body) {
169
+ return {
170
+ exitCode: kind === "refuse" ? HOST_EXIT_REFUSED : HOST_EXIT_FALLBACK,
171
+ payload: { action: kind, ...body, host: hostSummary(host) },
172
+ };
173
+ }
174
+ async function recordEnvScope(target, spec) {
175
+ const paths = createHostDaemonPaths({ socket: target.socket, agentDir: target.agentDir });
176
+ await writeDaemonEnvKeys(paths, daemonEnvKeys(process.env, spec.env));
177
+ }
178
+ /**
179
+ * This build as a client, WITHOUT a launch profile: the request's own `ensureHost` decides whether a
180
+ * handoff may happen, from the profile it would launch. What is asked here is only what any client
181
+ * can answer without one - is the running host usable, and could it be handed off from at all.
182
+ */
183
+ function decisionClient() {
184
+ return {
185
+ protocolVersion: HOST_PROTOCOL_VERSION,
186
+ requiredCapabilities: REQUIRED_HOST_CAPABILITIES,
187
+ identity: engineBuildIdentity(),
188
+ startedByUs: false,
189
+ platform: process.platform,
190
+ };
191
+ }
192
+ function assertNever(value) {
193
+ throw new Error(`unreachable host request: ${JSON.stringify(value)}`);
194
+ }
195
+ //# sourceMappingURL=host-runner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"host-runner.js","sourceRoot":"","sources":["../../../src/modes/rpc/host-runner.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * One host request, performed: ensure a daemon, report one, stop one, or hand one off.\n *\n * This is the whole behaviour behind `senpi host` - and behind the launchers that call the same\n * entry point in-process - separated from the command line that expresses it. Every request answers\n * with a JSON payload plus the exit code that payload means, so a CLI, a desktop and a task runner\n * report the same outcome with the same vocabulary:\n *\n * 0 the request happened 3 refused: a running host this client may not act on,\n * 4 no host is better than this one an unreachable socket, or a stop that would end\n * one (fallback policy) somebody else's work\n *\n * The refusals are the point. One machine-wide daemon holds every client's sessions, so a client\n * that cannot get what it wants must SAY so and leave the running host alone (I1) - never stop it,\n * never bind a second host over its socket. `decideHostAction`, `ensureHost`, `stopHost` and\n * `handoffHost` already enforce that; this module only gives their outcomes a machine-readable shape.\n */\nimport { engineBuildIdentity } from \"../../core/engine-build-identity.ts\";\nimport { daemonEnvKeys, daemonEnvOverrides, writeDaemonEnvKeys } from \"./host-daemon-env.ts\";\nimport { createHostDaemonPaths } from \"./host-daemon-paths.ts\";\nimport {\n\tdecideHostAction,\n\tHOST_PROTOCOL_VERSION,\n\ttype HostDecisionClient,\n\ttype HostDecisionPolicy,\n\tHostEnsureRefusedError,\n\ttype HostProtocolInfo,\n\tREQUIRED_HOST_CAPABILITIES,\n} from \"./host-decision.ts\";\nimport { type EnsuredHost, ensureHost } from \"./host-ensure.ts\";\nimport { type HandoffRefusal, handoffHost } from \"./host-handoff.ts\";\nimport type { ResolvedHostLaunchSpec } from \"./host-launch-spec.ts\";\nimport { probeProtocolInfo } from \"./host-probe.ts\";\nimport { hostSummary, readHostStatus, readSessionCounts } from \"./host-status.ts\";\nimport { stopHost } from \"./host-stop.ts\";\n\nexport const HOST_EXIT_OK = 0;\nexport const HOST_EXIT_ERROR = 1;\nexport const HOST_EXIT_USAGE = 2;\nexport const HOST_EXIT_REFUSED = 3;\nexport const HOST_EXIT_FALLBACK = 4;\n\nconst PROBE_TIMEOUT_MS = 10_000;\n\n/** Which daemon a request is about: the endpoint, and the agent directory holding its state. */\nexport interface HostTarget {\n\treadonly socket: string;\n\treadonly agentDir: string;\n}\n\nexport type HostRequest =\n\t| {\n\t\t\treadonly action: \"ensure\";\n\t\t\treadonly target: HostTarget;\n\t\t\treadonly spec: ResolvedHostLaunchSpec;\n\t\t\treadonly policy: HostDecisionPolicy;\n\t }\n\t| { readonly action: \"status\"; readonly target: HostTarget; readonly includeWorkers: boolean }\n\t| { readonly action: \"stop\"; readonly target: HostTarget; readonly drain: boolean; readonly force: boolean }\n\t| { readonly action: \"handoff\"; readonly target: HostTarget; readonly spec: ResolvedHostLaunchSpec };\n\n/** One JSON line and the exit code it means. */\nexport interface HostOutcome {\n\treadonly exitCode: number;\n\treadonly payload: Record<string, unknown>;\n}\n\nexport async function runHostRequest(request: HostRequest): Promise<HostOutcome> {\n\tswitch (request.action) {\n\t\tcase \"ensure\":\n\t\t\treturn ensureOutcome(request.target, request.spec, request.policy);\n\t\tcase \"status\":\n\t\t\treturn statusOutcome(request.target, request.includeWorkers);\n\t\tcase \"stop\":\n\t\t\treturn stopOutcome(request.target, request.drain, request.force);\n\t\tcase \"handoff\":\n\t\t\treturn handoffOutcome(request.target, request.spec);\n\t\tdefault:\n\t\t\treturn assertNever(request);\n\t}\n}\n\nasync function ensureOutcome(\n\ttarget: HostTarget,\n\tspec: ResolvedHostLaunchSpec,\n\tpolicy: HostDecisionPolicy,\n): Promise<HostOutcome> {\n\tconst before = await probeProtocolInfo(target.socket, PROBE_TIMEOUT_MS);\n\tif (policy === \"fallback\") {\n\t\t// `fallback` is the caller saying it can live without a host. The decision is made HERE\n\t\t// because an ensure always produces a host or fails: it has no fallback answer of its own.\n\t\tconst decision = decideHostAction(decisionClient(), before, \"fallback\");\n\t\tif (decision.action === \"fallback\") {\n\t\t\treturn refusal(\"fallback\", before, { reason: decision.reason, socket: target.socket });\n\t\t}\n\t}\n\tlet ensured: EnsuredHost;\n\ttry {\n\t\tensured = await ensureHost({\n\t\t\tsocket: target.socket,\n\t\t\tagentDir: target.agentDir,\n\t\t\thostArgs: spec.hostArgs,\n\t\t\tenv: daemonEnvOverrides(process.env, spec.env),\n\t\t\tpolicy: spec.policy,\n\t\t\tupgrade: policy === \"upgrade\" ? \"if-engine-differs\" : \"never\",\n\t\t});\n\t} catch (error: unknown) {\n\t\tif (!(error instanceof HostEnsureRefusedError)) throw error;\n\t\treturn refusal(\"refuse\", before, { reason: error.reason, socket: target.socket });\n\t}\n\tconst host = await probeProtocolInfo(target.socket, PROBE_TIMEOUT_MS);\n\t// Only a spawn grants an environment; a reuse inherited whatever the client that started it did.\n\tif (!ensured.reused) await recordEnvScope(target, spec);\n\treturn {\n\t\texitCode: HOST_EXIT_OK,\n\t\tpayload: {\n\t\t\taction: ensureAction(ensured.reused, before, host),\n\t\t\t...identityPayload(target.socket, ensured.pid, host),\n\t\t\treused: ensured.reused,\n\t\t},\n\t};\n}\n\n/**\n * What an ensure DID. A handoff and a fresh start both report `reused: false`, so the instance id is\n * what tells them apart: a handoff replaced the process behind a socket somebody was already serving.\n */\nfunction ensureAction(\n\treused: boolean,\n\tbefore: HostProtocolInfo | undefined,\n\tafter: HostProtocolInfo | undefined,\n): string {\n\tif (reused) return \"reuse\";\n\treturn before !== undefined && before.instanceId !== after?.instanceId ? \"handoff\" : \"start\";\n}\n\nasync function statusOutcome(target: HostTarget, includeWorkers: boolean): Promise<HostOutcome> {\n\tconst status = await readHostStatus({ socket: target.socket, agentDir: target.agentDir, includeWorkers });\n\treturn { exitCode: status.reachable ? HOST_EXIT_OK : HOST_EXIT_REFUSED, payload: { ...status } };\n}\n\n/**\n * A hard stop ends every session the daemon holds, including the ones other clients are attached to,\n * so it is gated on the host's own count and refuses with those counts rather than a bare \"busy\".\n * A DRAIN ends no work - the host stops accepting and leaves when it is empty - so it is never gated.\n */\nasync function stopOutcome(target: HostTarget, drain: boolean, force: boolean): Promise<HostOutcome> {\n\tconst host = await probeProtocolInfo(target.socket, PROBE_TIMEOUT_MS);\n\tconst sessions = await readSessionCounts(target.socket, true);\n\tconst occupied = sessions.foreign_attached + sessions.foreign_retained > 0;\n\tif (!drain && !force && occupied) {\n\t\treturn refusal(\"refuse\", host, { reason: \"sessions_live\", socket: target.socket, sessions });\n\t}\n\tconst result = await stopHost({ socket: target.socket, agentDir: target.agentDir, drain, force });\n\tif (result.action === \"refuse\") {\n\t\treturn refusal(\"refuse\", host, { reason: result.reason, socket: target.socket, sessions });\n\t}\n\treturn {\n\t\texitCode: HOST_EXIT_OK,\n\t\tpayload: { action: result.action, socket: target.socket, pid: result.pid, sessions },\n\t};\n}\n\nasync function handoffOutcome(target: HostTarget, spec: ResolvedHostLaunchSpec): Promise<HostOutcome> {\n\tconst before = await probeProtocolInfo(target.socket, PROBE_TIMEOUT_MS);\n\tconst result = await handoffHost({\n\t\tsocket: target.socket,\n\t\tagentDir: target.agentDir,\n\t\thostArgs: spec.hostArgs,\n\t\tenv: daemonEnvOverrides(process.env, spec.env),\n\t\tpolicy: spec.policy,\n\t});\n\tif (result.action === \"refuse\") {\n\t\treturn refusal(\"refuse\", before, {\n\t\t\treason: upgradeRefusal(result.reason),\n\t\t\tsocket: target.socket,\n\t\t\tdetail: result.reason,\n\t\t\tupgradeable: result.upgradeable,\n\t\t});\n\t}\n\tawait recordEnvScope(target, spec);\n\tconst host = await probeProtocolInfo(target.socket, PROBE_TIMEOUT_MS);\n\treturn {\n\t\texitCode: HOST_EXIT_OK,\n\t\tpayload: { action: \"handoff\", ...identityPayload(target.socket, result.pid, host), reused: false },\n\t};\n}\n\n/**\n * A host that cannot drain and a platform that cannot hand off are one answer to the caller: this\n * build may not replace the running generation. The refusal that produced it stays in `detail`.\n */\nfunction upgradeRefusal(reason: HandoffRefusal): string {\n\treturn reason === \"handoff_unsupported\" ? \"upgrade_unsupported\" : reason;\n}\n\nfunction identityPayload(socket: string, pid: number, host: HostProtocolInfo | undefined): Record<string, unknown> {\n\treturn {\n\t\tsocket,\n\t\tpid,\n\t\tinstanceId: host?.instanceId ?? null,\n\t\tgeneration: host?.generation ?? null,\n\t\tengineVersion: host?.engineVersion ?? null,\n\t\tengineOrdinal: host?.engineOrdinal ?? null,\n\t\tcapabilities: host?.capabilities ?? [],\n\t\tlaunchProfileId: host?.launch_profile?.profile_id ?? null,\n\t\tupgradeable: decideHostAction(decisionClient(), host, \"upgrade\").upgradeable,\n\t};\n}\n\n/**\n * The two ways a request ends without acting, and the exit codes they mean: a `refuse` leaves a\n * running host alone (3), a `fallback` says no host is better than this one (4).\n */\nfunction refusal(\n\tkind: \"refuse\" | \"fallback\",\n\thost: HostProtocolInfo | undefined,\n\tbody: { readonly reason: string } & Record<string, unknown>,\n): HostOutcome {\n\treturn {\n\t\texitCode: kind === \"refuse\" ? HOST_EXIT_REFUSED : HOST_EXIT_FALLBACK,\n\t\tpayload: { action: kind, ...body, host: hostSummary(host) },\n\t};\n}\n\nasync function recordEnvScope(target: HostTarget, spec: ResolvedHostLaunchSpec): Promise<void> {\n\tconst paths = createHostDaemonPaths({ socket: target.socket, agentDir: target.agentDir });\n\tawait writeDaemonEnvKeys(paths, daemonEnvKeys(process.env, spec.env));\n}\n\n/**\n * This build as a client, WITHOUT a launch profile: the request's own `ensureHost` decides whether a\n * handoff may happen, from the profile it would launch. What is asked here is only what any client\n * can answer without one - is the running host usable, and could it be handed off from at all.\n */\nfunction decisionClient(): HostDecisionClient {\n\treturn {\n\t\tprotocolVersion: HOST_PROTOCOL_VERSION,\n\t\trequiredCapabilities: REQUIRED_HOST_CAPABILITIES,\n\t\tidentity: engineBuildIdentity(),\n\t\tstartedByUs: false,\n\t\tplatform: process.platform,\n\t};\n}\n\nfunction assertNever(value: never): never {\n\tthrow new Error(`unreachable host request: ${JSON.stringify(value)}`);\n}\n"]}
@@ -0,0 +1,58 @@
1
+ import type { HostProtocolInfo } from "./host-decision.ts";
2
+ import type { RpcLaunchProfile } from "./rpc-types.ts";
3
+ /**
4
+ * Sessions the host reports. `foreign_*` is the same count from the point of view of a client that
5
+ * holds none of them itself: for the CLI every session belongs to somebody else, which is exactly
6
+ * why a hard stop is gated on it.
7
+ */
8
+ export interface HostSessionCounts {
9
+ readonly total: number;
10
+ readonly interactive: number;
11
+ readonly worker: number;
12
+ /** Open at zero attachments: retained across a disconnect, still holding its transcript. */
13
+ readonly retained: number;
14
+ readonly foreign_attached: number;
15
+ readonly foreign_retained: number;
16
+ }
17
+ /** One generation's record in the daemon directory - the current one, and any that outlived it. */
18
+ export interface HostGenerationRow {
19
+ readonly instanceId: string;
20
+ readonly generation: number;
21
+ readonly pid: number;
22
+ readonly engineVersion: string | null;
23
+ /** True for the generation the pointer names: the one serving the socket. */
24
+ readonly current: boolean;
25
+ readonly alive: boolean;
26
+ }
27
+ export interface HostStatusReport {
28
+ readonly reachable: boolean;
29
+ readonly socket: string;
30
+ readonly pid: number | null;
31
+ readonly instanceId: string | null;
32
+ readonly generation: number | null;
33
+ readonly engineVersion: string | null;
34
+ readonly capabilities: readonly string[];
35
+ readonly launchProfile: RpcLaunchProfile | null;
36
+ readonly sessions: HostSessionCounts;
37
+ readonly zombies: number | null;
38
+ readonly rss_mb: number | null;
39
+ readonly open_fds: number | null;
40
+ /** Environment NAMES the daemon was granted, never values. */
41
+ readonly env_keys: readonly string[];
42
+ readonly generations: readonly HostGenerationRow[];
43
+ }
44
+ export interface HostStatusOptions {
45
+ readonly socket: string;
46
+ readonly agentDir?: string;
47
+ /** Ask the host to include `kind: "worker"` rows, exactly as `list_sessions` defines the flag. */
48
+ readonly includeWorkers?: boolean;
49
+ }
50
+ export declare function readHostStatus(options: HostStatusOptions): Promise<HostStatusReport>;
51
+ /**
52
+ * The occupancy a stop decision is made on. A host that does not answer holds nothing a client can
53
+ * see, and an empty count is what lets a stop proceed against a socket nobody serves.
54
+ */
55
+ export declare function readSessionCounts(socket: string, includeWorkers: boolean): Promise<HostSessionCounts>;
56
+ /** Identity fields of a probed host, for the `host` field of a refusal. */
57
+ export declare function hostSummary(host: HostProtocolInfo | undefined): Record<string, unknown> | null;
58
+ //# sourceMappingURL=host-status.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"host-status.d.ts","sourceRoot":"","sources":["../../../src/modes/rpc/host-status.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAG3D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAIvD;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IACjC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,4FAA4F;IAC5F,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;CAClC;AAED,mGAAmG;AACnG,MAAM,WAAW,iBAAiB;IACjC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,6EAA6E;IAC7E,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,gBAAgB;IAChC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,QAAQ,CAAC,aAAa,EAAE,gBAAgB,GAAG,IAAI,CAAC;IAChD,QAAQ,CAAC,QAAQ,EAAE,iBAAiB,CAAC;IACrC,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,8DAA8D;IAC9D,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,QAAQ,CAAC,WAAW,EAAE,SAAS,iBAAiB,EAAE,CAAC;CACnD;AAED,MAAM,WAAW,iBAAiB;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,kGAAkG;IAClG,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC;CAClC;AAED,wBAAsB,cAAc,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAyB1F;AAED;;;GAGG;AACH,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,OAAO,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAiB3G;AAED,2EAA2E;AAC3E,wBAAgB,WAAW,CAAC,IAAI,EAAE,gBAAgB,GAAG,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAW9F"}
@@ -0,0 +1,117 @@
1
+ /**
2
+ * What a client can observe about the daemon on one socket, in one record.
3
+ *
4
+ * Everything that describes the RUNNING host comes from the host's own answers - `get_protocol_info`
5
+ * for identity, `list_sessions` for occupancy - because files describe the past and the socket
6
+ * describes the present. Everything that describes the DAEMON DIRECTORY (which generations exist,
7
+ * which environment scope the last ensure granted) comes from disk, because a generation that no
8
+ * longer answers is exactly what an operator is looking for when they ask.
9
+ *
10
+ * A socket nobody serves is not an error here: `reachable: false` with the same field set is the
11
+ * answer, so a caller parses one shape either way and branches on one boolean.
12
+ */
13
+ import { readdir } from "node:fs/promises";
14
+ import { processIsLive } from "../app-server/daemon/process.js";
15
+ import { readDaemonEnvKeys } from "./host-daemon-env.js";
16
+ import { createHostDaemonPaths, generationPaths } from "./host-daemon-paths.js";
17
+ import { parseJson, readFileOrUndefined } from "./host-daemon-state.js";
18
+ import { probeProtocolInfo, requestOnSocket } from "./host-probe.js";
19
+ import { readHostProcessMetrics } from "./host-process-metrics.js";
20
+ const STATUS_PROBE_TIMEOUT_MS = 10_000;
21
+ export async function readHostStatus(options) {
22
+ const paths = createHostDaemonPaths({
23
+ socket: options.socket,
24
+ ...(options.agentDir ? { agentDir: options.agentDir } : {}),
25
+ });
26
+ const host = await probeProtocolInfo(options.socket, STATUS_PROBE_TIMEOUT_MS);
27
+ const generations = await readGenerations(paths);
28
+ const current = generations.find((row) => row.current);
29
+ const metrics = current ? await readHostProcessMetrics(current.pid) : UNOBSERVED_METRICS;
30
+ return {
31
+ reachable: host !== undefined,
32
+ socket: options.socket,
33
+ pid: current?.pid ?? null,
34
+ instanceId: host?.instanceId ?? current?.instanceId ?? null,
35
+ generation: host?.generation ?? current?.generation ?? null,
36
+ engineVersion: host?.engineVersion ?? current?.engineVersion ?? null,
37
+ capabilities: host?.capabilities ?? [],
38
+ launchProfile: host?.launch_profile ?? null,
39
+ sessions: await readSessionCounts(options.socket, options.includeWorkers === true),
40
+ zombies: metrics.zombies,
41
+ rss_mb: metrics.rss_mb,
42
+ open_fds: metrics.open_fds,
43
+ env_keys: await readDaemonEnvKeys(paths),
44
+ generations,
45
+ };
46
+ }
47
+ /**
48
+ * The occupancy a stop decision is made on. A host that does not answer holds nothing a client can
49
+ * see, and an empty count is what lets a stop proceed against a socket nobody serves.
50
+ */
51
+ export async function readSessionCounts(socket, includeWorkers) {
52
+ const reply = await requestOnSocket(socket, { type: "list_sessions", ...(includeWorkers ? { include_workers: true } : {}) }, STATUS_PROBE_TIMEOUT_MS);
53
+ const rows = sessionRows(reply);
54
+ const attached = rows.filter((row) => row.attachments > 0).length;
55
+ const retained = rows.length - attached;
56
+ return {
57
+ total: rows.length,
58
+ interactive: rows.filter((row) => row.kind !== "worker").length,
59
+ worker: rows.filter((row) => row.kind === "worker").length,
60
+ retained,
61
+ foreign_attached: attached,
62
+ foreign_retained: retained,
63
+ };
64
+ }
65
+ /** Identity fields of a probed host, for the `host` field of a refusal. */
66
+ export function hostSummary(host) {
67
+ if (host === undefined)
68
+ return null;
69
+ return {
70
+ protocolVersion: host.protocolVersion,
71
+ serverVersion: host.serverVersion,
72
+ capabilities: host.capabilities,
73
+ instanceId: host.instanceId ?? null,
74
+ generation: host.generation ?? null,
75
+ engineVersion: host.engineVersion ?? null,
76
+ launchProfileId: host.launch_profile?.profile_id ?? null,
77
+ };
78
+ }
79
+ const UNOBSERVED_METRICS = { rss_mb: null, open_fds: null, zombies: null };
80
+ function sessionRows(reply) {
81
+ if (!isRecord(reply) || !Array.isArray(reply.sessions))
82
+ return [];
83
+ return reply.sessions.flatMap((entry) => {
84
+ if (!isRecord(entry))
85
+ return [];
86
+ return [
87
+ {
88
+ kind: typeof entry.kind === "string" ? entry.kind : "interactive",
89
+ attachments: typeof entry.attachments === "number" ? entry.attachments : 0,
90
+ },
91
+ ];
92
+ });
93
+ }
94
+ async function readGenerations(paths) {
95
+ const pointer = parseJson(await readFileOrUndefined(paths.pointerFile));
96
+ const currentId = typeof pointer?.instance_id === "string" ? pointer.instance_id : undefined;
97
+ const entries = await readdir(paths.generationsDir).catch(() => []);
98
+ const rows = [];
99
+ for (const instanceId of entries) {
100
+ const record = parseJson(await readFileOrUndefined(generationPaths(paths, instanceId).pidFile));
101
+ if (typeof record?.pid !== "number")
102
+ continue;
103
+ rows.push({
104
+ instanceId,
105
+ generation: typeof record.generation === "number" ? record.generation : 0,
106
+ pid: record.pid,
107
+ engineVersion: typeof record.engineVersion === "string" ? record.engineVersion : null,
108
+ current: instanceId === currentId,
109
+ alive: processIsLive(record.pid),
110
+ });
111
+ }
112
+ return rows.sort((left, right) => left.generation - right.generation);
113
+ }
114
+ function isRecord(value) {
115
+ return typeof value === "object" && value !== null && !Array.isArray(value);
116
+ }
117
+ //# sourceMappingURL=host-status.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"host-status.js","sourceRoot":"","sources":["../../../src/modes/rpc/host-status.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What a client can observe about the daemon on one socket, in one record.\n *\n * Everything that describes the RUNNING host comes from the host's own answers - `get_protocol_info`\n * for identity, `list_sessions` for occupancy - because files describe the past and the socket\n * describes the present. Everything that describes the DAEMON DIRECTORY (which generations exist,\n * which environment scope the last ensure granted) comes from disk, because a generation that no\n * longer answers is exactly what an operator is looking for when they ask.\n *\n * A socket nobody serves is not an error here: `reachable: false` with the same field set is the\n * answer, so a caller parses one shape either way and branches on one boolean.\n */\nimport { readdir } from \"node:fs/promises\";\nimport { processIsLive } from \"../app-server/daemon/process.ts\";\nimport { readDaemonEnvKeys } from \"./host-daemon-env.ts\";\nimport { createHostDaemonPaths, generationPaths, type HostDaemonPaths } from \"./host-daemon-paths.ts\";\nimport { parseJson, readFileOrUndefined } from \"./host-daemon-state.ts\";\nimport type { HostProtocolInfo } from \"./host-decision.ts\";\nimport { probeProtocolInfo, requestOnSocket } from \"./host-probe.ts\";\nimport { type HostProcessMetrics, readHostProcessMetrics } from \"./host-process-metrics.ts\";\nimport type { RpcLaunchProfile } from \"./rpc-types.ts\";\n\nconst STATUS_PROBE_TIMEOUT_MS = 10_000;\n\n/**\n * Sessions the host reports. `foreign_*` is the same count from the point of view of a client that\n * holds none of them itself: for the CLI every session belongs to somebody else, which is exactly\n * why a hard stop is gated on it.\n */\nexport interface HostSessionCounts {\n\treadonly total: number;\n\treadonly interactive: number;\n\treadonly worker: number;\n\t/** Open at zero attachments: retained across a disconnect, still holding its transcript. */\n\treadonly retained: number;\n\treadonly foreign_attached: number;\n\treadonly foreign_retained: number;\n}\n\n/** One generation's record in the daemon directory - the current one, and any that outlived it. */\nexport interface HostGenerationRow {\n\treadonly instanceId: string;\n\treadonly generation: number;\n\treadonly pid: number;\n\treadonly engineVersion: string | null;\n\t/** True for the generation the pointer names: the one serving the socket. */\n\treadonly current: boolean;\n\treadonly alive: boolean;\n}\n\nexport interface HostStatusReport {\n\treadonly reachable: boolean;\n\treadonly socket: string;\n\treadonly pid: number | null;\n\treadonly instanceId: string | null;\n\treadonly generation: number | null;\n\treadonly engineVersion: string | null;\n\treadonly capabilities: readonly string[];\n\treadonly launchProfile: RpcLaunchProfile | null;\n\treadonly sessions: HostSessionCounts;\n\treadonly zombies: number | null;\n\treadonly rss_mb: number | null;\n\treadonly open_fds: number | null;\n\t/** Environment NAMES the daemon was granted, never values. */\n\treadonly env_keys: readonly string[];\n\treadonly generations: readonly HostGenerationRow[];\n}\n\nexport interface HostStatusOptions {\n\treadonly socket: string;\n\treadonly agentDir?: string;\n\t/** Ask the host to include `kind: \"worker\"` rows, exactly as `list_sessions` defines the flag. */\n\treadonly includeWorkers?: boolean;\n}\n\nexport async function readHostStatus(options: HostStatusOptions): Promise<HostStatusReport> {\n\tconst paths = createHostDaemonPaths({\n\t\tsocket: options.socket,\n\t\t...(options.agentDir ? { agentDir: options.agentDir } : {}),\n\t});\n\tconst host = await probeProtocolInfo(options.socket, STATUS_PROBE_TIMEOUT_MS);\n\tconst generations = await readGenerations(paths);\n\tconst current = generations.find((row) => row.current);\n\tconst metrics = current ? await readHostProcessMetrics(current.pid) : UNOBSERVED_METRICS;\n\treturn {\n\t\treachable: host !== undefined,\n\t\tsocket: options.socket,\n\t\tpid: current?.pid ?? null,\n\t\tinstanceId: host?.instanceId ?? current?.instanceId ?? null,\n\t\tgeneration: host?.generation ?? current?.generation ?? null,\n\t\tengineVersion: host?.engineVersion ?? current?.engineVersion ?? null,\n\t\tcapabilities: host?.capabilities ?? [],\n\t\tlaunchProfile: host?.launch_profile ?? null,\n\t\tsessions: await readSessionCounts(options.socket, options.includeWorkers === true),\n\t\tzombies: metrics.zombies,\n\t\trss_mb: metrics.rss_mb,\n\t\topen_fds: metrics.open_fds,\n\t\tenv_keys: await readDaemonEnvKeys(paths),\n\t\tgenerations,\n\t};\n}\n\n/**\n * The occupancy a stop decision is made on. A host that does not answer holds nothing a client can\n * see, and an empty count is what lets a stop proceed against a socket nobody serves.\n */\nexport async function readSessionCounts(socket: string, includeWorkers: boolean): Promise<HostSessionCounts> {\n\tconst reply = await requestOnSocket(\n\t\tsocket,\n\t\t{ type: \"list_sessions\", ...(includeWorkers ? { include_workers: true } : {}) },\n\t\tSTATUS_PROBE_TIMEOUT_MS,\n\t);\n\tconst rows = sessionRows(reply);\n\tconst attached = rows.filter((row) => row.attachments > 0).length;\n\tconst retained = rows.length - attached;\n\treturn {\n\t\ttotal: rows.length,\n\t\tinteractive: rows.filter((row) => row.kind !== \"worker\").length,\n\t\tworker: rows.filter((row) => row.kind === \"worker\").length,\n\t\tretained,\n\t\tforeign_attached: attached,\n\t\tforeign_retained: retained,\n\t};\n}\n\n/** Identity fields of a probed host, for the `host` field of a refusal. */\nexport function hostSummary(host: HostProtocolInfo | undefined): Record<string, unknown> | null {\n\tif (host === undefined) return null;\n\treturn {\n\t\tprotocolVersion: host.protocolVersion,\n\t\tserverVersion: host.serverVersion,\n\t\tcapabilities: host.capabilities,\n\t\tinstanceId: host.instanceId ?? null,\n\t\tgeneration: host.generation ?? null,\n\t\tengineVersion: host.engineVersion ?? null,\n\t\tlaunchProfileId: host.launch_profile?.profile_id ?? null,\n\t};\n}\n\nconst UNOBSERVED_METRICS: HostProcessMetrics = { rss_mb: null, open_fds: null, zombies: null };\n\ninterface SessionRow {\n\treadonly kind: string;\n\treadonly attachments: number;\n}\n\nfunction sessionRows(reply: unknown): readonly SessionRow[] {\n\tif (!isRecord(reply) || !Array.isArray(reply.sessions)) return [];\n\treturn reply.sessions.flatMap((entry: unknown) => {\n\t\tif (!isRecord(entry)) return [];\n\t\treturn [\n\t\t\t{\n\t\t\t\tkind: typeof entry.kind === \"string\" ? entry.kind : \"interactive\",\n\t\t\t\tattachments: typeof entry.attachments === \"number\" ? entry.attachments : 0,\n\t\t\t},\n\t\t];\n\t});\n}\n\nasync function readGenerations(paths: HostDaemonPaths): Promise<readonly HostGenerationRow[]> {\n\tconst pointer = parseJson(await readFileOrUndefined(paths.pointerFile));\n\tconst currentId = typeof pointer?.instance_id === \"string\" ? pointer.instance_id : undefined;\n\tconst entries = await readdir(paths.generationsDir).catch(() => []);\n\tconst rows: HostGenerationRow[] = [];\n\tfor (const instanceId of entries) {\n\t\tconst record = parseJson(await readFileOrUndefined(generationPaths(paths, instanceId).pidFile));\n\t\tif (typeof record?.pid !== \"number\") continue;\n\t\trows.push({\n\t\t\tinstanceId,\n\t\t\tgeneration: typeof record.generation === \"number\" ? record.generation : 0,\n\t\t\tpid: record.pid,\n\t\t\tengineVersion: typeof record.engineVersion === \"string\" ? record.engineVersion : null,\n\t\t\tcurrent: instanceId === currentId,\n\t\t\talive: processIsLive(record.pid),\n\t\t});\n\t}\n\treturn rows.sort((left, right) => left.generation - right.generation);\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n"]}
@@ -1510,6 +1510,35 @@ export default function (pi: ExtensionAPI) {
1510
1510
 
1511
1511
  ## ExtensionAPI Methods
1512
1512
 
1513
+ ### pi.sessionKind / pi.sessionContext / pi.sharedHostEnabled
1514
+
1515
+ Read-only facts about the session this extension instance was loaded for, available at factory time so an extension
1516
+ can decide what to register before it registers anything:
1517
+
1518
+ | Property | Type | Value |
1519
+ |---|---|---|
1520
+ | `pi.cwd` | `string` | Absolute working directory of this session |
1521
+ | `pi.sharedHostEnabled` | `boolean` | Whether this session runs on a shared RPC host |
1522
+ | `pi.sessionKind` | `"interactive" \| "worker"` | Visibility class the opener chose (`open_session.kind`); `interactive` for classic launches and any open that omits it |
1523
+ | `pi.sessionContext` | `Readonly<Record<string, string>>` | Opaque labels the opener attached (`open_session.context`), or `{}` |
1524
+
1525
+ ```typescript
1526
+ export default function ({ pi }) {
1527
+ // One extension set serves every session of a shared daemon; gate per session, not per host.
1528
+ if (pi.sessionKind === "worker" && pi.sessionContext.role === "child") {
1529
+ pi.registerTool(reportToParentTool);
1530
+ return;
1531
+ }
1532
+ pi.registerCommand("review", reviewCommand);
1533
+ }
1534
+ ```
1535
+
1536
+ The engine never interprets `sessionContext`: it takes no part in authentication, model selection, resource loading
1537
+ or any other host decision, and it is republished only on `list_sessions { include_workers: true }`. Its caps are
1538
+ enforced at the RPC boundary (at most 32 keys matching `^[a-z][a-z0-9_]*$`, each value at most 16 KiB, at most 32 KiB
1539
+ of JSON in total), so an extension receives an already-validated map. Both values are frozen for the session's life;
1540
+ there is no setter. See [Session kind and context](rpc.md#session-kind-and-context-open_session) for the wire side.
1541
+
1513
1542
  ### pi.on(event, handler)
1514
1543
 
1515
1544
  Subscribe to events. See [Events](#events) for event types and return values.
@@ -2066,6 +2095,12 @@ pi.events.emit("my:event", { ... });
2066
2095
 
2067
2096
  Senpi's default-on `config-reload` builtin watches configured global surfaces and trusted project-local `.senpi` surfaces. A real content change requests the normal full session reload when the agent is idle; busy or compacting sessions defer it until a safe idle edge. When an extension vetoes the reload through `session_before_reload` (for example while subagents it owns are still running), the change also defers quietly: one `Hot-reload deferred: <reason>` notice per distinct veto reason, silent retries on later idle edges plus a periodic veto recheck, and the usual `Hot-reloading:`/`Hot-reloaded:` notifications only once the veto clears and the reload actually runs. Parseable built-in files (`settings.json`, `models.json`, and `keybindings.json`) are validated before reload, so a rejected edit keeps the running configuration active.
2068
2097
 
2098
+ > **Cost on a shared host:** the watcher runs per session. Each session's `config-reload` instance lazily spawns one
2099
+ > `node:worker_threads` Worker for recursive filesystem watching, so a host serving N sessions carries about N extra
2100
+ > OS threads and ~5 MB per session ([senpi#1794](https://github.com/code-yeongyu/senpi/issues/1794)). It is the
2101
+ > dominant per-session cost of a shared RPC daemon ([RPC: session runtime](rpc.md#session-runtime---session-runtime-in-processworker));
2102
+ > a host whose settings disable the builtin adds no thread per session at all.
2103
+
2069
2104
  Configure it in `settings.json` with optional fields; omitted fields use the defaults shown here. Invalid `configReload` fields are ignored individually, so a malformed block falls back to these defaults rather than disabling watching:
2070
2105
 
2071
2106
  ```json
package/docs/providers.md CHANGED
@@ -215,6 +215,35 @@ The provider streams through Ollama's OpenAI-compatible `/v1/chat/completions` e
215
215
  Existing local Ollama configurations remain supported. When an `ollama` provider in `models.json` includes
216
216
  an explicit `models` catalog, that catalog takes precedence and Senpi does not run Ollama Cloud discovery.
217
217
 
218
+ ## B.AI
219
+
220
+ Use `/login bai` to store an API key, or export `BAI_API_KEY`. B.AI model availability is credential-scoped,
221
+ so refresh the catalog after login:
222
+
223
+ ```bash
224
+ export BAI_API_KEY=...
225
+ senpi update --models
226
+ senpi --provider bai --model gpt-5.6-sol
227
+ ```
228
+
229
+ Senpi discovers available IDs from `https://api.b.ai/v1/models` and enriches classified chat models with
230
+ B.AI's documented context, output, input modality, reasoning-level, and standard pricing metadata. Because that
231
+ list is credential-scoped, a model you are not entitled to never appears.
232
+
233
+ B.AI serves one API key over three protocols and documents several models on more than one of them, so the
234
+ endpoint is a client choice rather than a per-model property. Senpi pins one protocol per model: GPT and
235
+ DeepSeek use OpenAI Responses (the two families B.AI names for that endpoint), Claude uses Anthropic Messages,
236
+ and the remaining chat families use OpenAI Chat Completions.
237
+
238
+ B.AI rejects a function tool whose root parameters schema declares no `type`. On the Responses endpoint Senpi
239
+ merges such a union root into a single object schema so the tool keeps its parameters; the Chat Completions and
240
+ Messages paths already do the same normalization for every provider. Image-only IDs such as `gpt-image-2` remain
241
+ on B.AI's image API and are not listed as chat models.
242
+
243
+ The catalog records B.AI's standard reference prices in USD per 1M tokens. They exclude temporary promotions,
244
+ top-up bonuses, DeepSeek idle-period rates, and account benefits, so B.AI's final billing record remains
245
+ authoritative.
246
+
218
247
  ## API Keys
219
248
 
220
249
  ### Environment Variables or Auth File
@@ -232,6 +261,7 @@ senpi
232
261
  | Ant Ling | `ANT_LING_API_KEY` | `ant-ling` |
233
262
  | Azure OpenAI Responses | `AZURE_OPENAI_API_KEY` | `azure-openai-responses` |
234
263
  | OpenAI | `OPENAI_API_KEY` | `openai` |
264
+ | B.AI | `BAI_API_KEY` | `bai` |
235
265
  | Ollama Cloud | `OLLAMA_API_KEY` | `ollama` |
236
266
  | DeepSeek | `DEEPSEEK_API_KEY` | `deepseek` |
237
267
  | NVIDIA NIM | `NVIDIA_API_KEY` | `nvidia` |