@code-yeongyu/senpi 2026.10.11 → 2026.10.12

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 (188) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/dist/bundle/chunks/anthropic-messages-GYG6YZY5.js +10 -0
  3. package/dist/bundle/chunks/{app-server-command-NHDOL4HQ.js → app-server-command-OL6PPUOA.js} +1 -1
  4. package/dist/bundle/chunks/{azure-openai-responses-EJF2XJ7Q.js → azure-openai-responses-GU6FOSJR.js} +1 -1
  5. package/dist/bundle/chunks/{chunk-ZABLYCHT.js → chunk-46JEQXD7.js} +1 -1
  6. package/dist/bundle/chunks/{chunk-VE3D74HN.js → chunk-55VKYA5V.js} +1 -1
  7. package/dist/bundle/chunks/{chunk-7UNN3YHW.js → chunk-5AVFHHDF.js} +230 -221
  8. package/dist/bundle/chunks/{chunk-2JE2DOIF.js → chunk-5DPTM5E2.js} +1 -1
  9. package/dist/bundle/chunks/chunk-5UPUSGP5.js +2 -0
  10. package/dist/bundle/chunks/chunk-5VMRMY6N.js +16 -0
  11. package/dist/bundle/chunks/{chunk-7X3VRAM3.js → chunk-7P3QPVTF.js} +1 -1
  12. package/dist/bundle/chunks/{chunk-T6IYKWIH.js → chunk-BUGZZVQW.js} +1 -1
  13. package/dist/bundle/chunks/chunk-DL7EFVT2.js +12 -0
  14. package/dist/bundle/chunks/chunk-E5YUVGLQ.js +11 -0
  15. package/dist/bundle/chunks/{chunk-RLFQICEY.js → chunk-EQJY2B5F.js} +1 -1
  16. package/dist/bundle/chunks/{chunk-VYKBN4MR.js → chunk-GC2RU5TS.js} +1 -1
  17. package/dist/bundle/chunks/chunk-GPKEJOTQ.js +202 -0
  18. package/dist/bundle/chunks/{chunk-KCVSSFOV.js → chunk-GQ5PWY6P.js} +4 -3
  19. package/dist/bundle/chunks/{chunk-4GPJOGXZ.js → chunk-I4AEC6MH.js} +1 -1
  20. package/dist/bundle/chunks/chunk-JXSFGRBE.js +8 -0
  21. package/dist/bundle/chunks/{chunk-YGCMN2CX.js → chunk-NAY5LB53.js} +1 -1
  22. package/dist/bundle/chunks/{chunk-EBOD66AS.js → chunk-PBN2CATL.js} +2 -2
  23. package/dist/bundle/chunks/chunk-PFNBY43W.js +2 -0
  24. package/dist/bundle/chunks/{chunk-CY2B4QRR.js → chunk-PUWEQK2W.js} +1 -1
  25. package/dist/bundle/chunks/{chunk-5DBPUQSX.js → chunk-SF6AUNQZ.js} +1 -1
  26. package/dist/bundle/chunks/{chunk-G2XAU4BX.js → chunk-TMTT5CLA.js} +1 -1
  27. package/dist/bundle/chunks/{chunk-2JKDZXIY.js → chunk-VWFKLW73.js} +2 -2
  28. package/dist/bundle/chunks/chunk-WZMSBRTW.js +2 -0
  29. package/dist/bundle/chunks/{chunk-55HLKZSB.js → chunk-XVJRRLGN.js} +1 -1
  30. package/dist/bundle/chunks/chunk-Z2PWWBBD.js +31 -0
  31. package/dist/bundle/chunks/cli-main-RSIYAR4J.js +3 -0
  32. package/dist/bundle/chunks/{cloudflare-workers-ai-system-one-SWVU2AXJ.js → cloudflare-workers-ai-system-one-LVWRKOUD.js} +1 -1
  33. package/dist/bundle/chunks/{google-generative-ai-XG4YX6FQ.js → google-generative-ai-JMGYPTXL.js} +1 -1
  34. package/dist/bundle/chunks/{google-vertex-BBOZ6OUB.js → google-vertex-CI4FPIU7.js} +1 -1
  35. package/dist/bundle/chunks/{help-fast-path-EZ3KEW6V.js → help-fast-path-M7KMBM6K.js} +1 -1
  36. package/dist/bundle/chunks/host-command-6UR3DSFR.js +2 -0
  37. package/dist/bundle/chunks/host-lifecycle-2BKCCK5X.js +3 -0
  38. package/dist/bundle/chunks/host-lifecycle.js +10 -7
  39. package/dist/bundle/chunks/host-session-command-A2MZHEBP.js +19 -0
  40. package/dist/bundle/chunks/interactive-mode-EJBMTJOH.js +2 -0
  41. package/dist/bundle/chunks/{list-tips-WE2YGQM6.js → list-tips-BEQAOA4N.js} +1 -1
  42. package/dist/bundle/chunks/{mistral-conversations-RB3ZBCGE.js → mistral-conversations-5OD7662T.js} +1 -1
  43. package/dist/bundle/chunks/{multi-session-host-V4CUQLV7.js → multi-session-host-YYMLHCUA.js} +4 -4
  44. package/dist/bundle/chunks/{openai-codex-responses-DBTPUXRW.js → openai-codex-responses-FN3UPAFI.js} +1 -1
  45. package/dist/bundle/chunks/{openai-completions-QAJQC4BY.js → openai-completions-KMUQ3T4M.js} +1 -1
  46. package/dist/bundle/chunks/{openai-responses-UBJWCEHG.js → openai-responses-V4Q5GVLO.js} +1 -1
  47. package/dist/bundle/chunks/{package-manager-cli-WTRDT4ZH.js → package-manager-cli-6A5ZP26E.js} +1 -1
  48. package/dist/bundle/chunks/rotation-stream-PT6LLBMJ.js +2 -0
  49. package/dist/bundle/chunks/rpc-mode-XNYJPYUM.js +2 -0
  50. package/dist/bundle/chunks/runtime-session-ZBHFMT65.js +2 -0
  51. package/dist/bundle/chunks/{schedule-command-PJVRKAKD.js → schedule-command-FYQG2MFX.js} +1 -1
  52. package/dist/bundle/chunks/session-control-endpoint-7TX4XHN4.js +3 -0
  53. package/dist/bundle/chunks/session-picker-4RNB27SM.js +2 -0
  54. package/dist/bundle/chunks/session-worker.js +418 -412
  55. package/dist/bundle/chunks/{typesafe-system-one-B5TW3FHM.js → typesafe-system-one-WUHW7HR4.js} +1 -1
  56. package/dist/bundle/cli.js +1 -1
  57. package/dist/bundle/index.js +1 -1
  58. package/dist/bundle/rpc-entry.js +1 -1
  59. package/dist/bundle/runtime-manifest.json +1 -1
  60. package/dist/core/agent-session.d.ts +11 -2
  61. package/dist/core/agent-session.js +152 -43
  62. package/dist/core/compaction/compaction.js +2 -0
  63. package/dist/core/extensions/builtin/anthropic-web-search/index.js +22 -14
  64. package/dist/core/extensions/builtin/cache-keepalive/index.js +8 -4
  65. package/dist/core/extensions/builtin/cache-keepalive/session-prewarm.d.ts +2 -1
  66. package/dist/core/extensions/builtin/cache-keepalive/session-prewarm.js +55 -7
  67. package/dist/core/extensions/builtin/cache-keepalive/startup-activity-gate.d.ts +8 -0
  68. package/dist/core/extensions/builtin/cache-keepalive/startup-activity-gate.js +25 -0
  69. package/dist/core/extensions/builtin/compaction/index.js +13 -5
  70. package/dist/core/extensions/builtin/compaction/speculative.d.ts +1 -0
  71. package/dist/core/extensions/builtin/compaction/speculative.js +8 -4
  72. package/dist/core/extensions/builtin/moved-path-guard/tool-classes.js +1 -0
  73. package/dist/core/extensions/builtin/openai-web-search/index.d.ts +4 -1
  74. package/dist/core/extensions/builtin/openai-web-search/index.js +54 -17
  75. package/dist/core/extensions/builtin/server-search-fallback.d.ts +12 -0
  76. package/dist/core/extensions/builtin/server-search-fallback.js +50 -0
  77. package/dist/core/extensions/builtin/terminal/runtime-session.d.ts +10 -1
  78. package/dist/core/extensions/builtin/terminal/runtime-session.js +48 -4
  79. package/dist/core/extensions/builtin/websearch/index.d.ts +1 -1
  80. package/dist/core/extensions/builtin/websearch/index.js +119 -18
  81. package/dist/core/extensions/builtin/websearch/server-search.d.ts +11 -0
  82. package/dist/core/extensions/builtin/websearch/server-search.js +230 -0
  83. package/dist/core/extensions/builtin/websearch/websearch/native.d.ts +2 -0
  84. package/dist/core/extensions/builtin/websearch/websearch/search-model.js +15 -0
  85. package/dist/core/extensions/runner.d.ts +8 -1
  86. package/dist/core/extensions/runner.js +56 -7
  87. package/dist/core/extensions/types.d.ts +12 -2
  88. package/dist/core/extensions/user-activity.d.ts +10 -0
  89. package/dist/core/extensions/user-activity.js +20 -0
  90. package/dist/core/model-config-schema.d.ts +34 -0
  91. package/dist/core/model-config-schema.js +2 -0
  92. package/dist/core/model-config.js +1 -0
  93. package/dist/core/reserved-keywords.d.ts +28 -0
  94. package/dist/core/reserved-keywords.js +81 -0
  95. package/dist/core/retry-fallback/candidates.d.ts +1 -1
  96. package/dist/core/retry-fallback/candidates.js +1 -12
  97. package/dist/core/retry-fallback/controller.d.ts +3 -1
  98. package/dist/core/retry-fallback/controller.js +11 -5
  99. package/dist/core/retry-fallback/cooldown.d.ts +2 -1
  100. package/dist/core/retry-fallback/cooldown.js +21 -12
  101. package/dist/core/retry-fallback/unusable.d.ts +2 -0
  102. package/dist/core/retry-fallback/unusable.js +14 -2
  103. package/dist/core/retry-fallback/usage-limit.d.ts +10 -1
  104. package/dist/core/retry-fallback/usage-limit.js +39 -2
  105. package/dist/core/session-title-generator.js +2 -0
  106. package/dist/index.d.ts +2 -1
  107. package/dist/index.js +1 -0
  108. package/dist/modes/index.d.ts +1 -1
  109. package/dist/modes/interactive/components/assistant-render-descriptors.js +7 -1
  110. package/dist/modes/interactive/composer-user-activity.d.ts +6 -0
  111. package/dist/modes/interactive/composer-user-activity.js +30 -0
  112. package/dist/modes/interactive/grok/chrome.js +2 -1
  113. package/dist/modes/interactive/interactive-mode.js +15 -6
  114. package/dist/modes/interactive/session-control-registry.js +8 -1
  115. package/dist/modes/interactive/theme/reserved-keyword-rainbow.d.ts +11 -0
  116. package/dist/modes/interactive/theme/reserved-keyword-rainbow.js +50 -0
  117. package/dist/modes/interactive/theme/theme.d.ts +3 -1
  118. package/dist/modes/interactive/theme/theme.js +9 -0
  119. package/dist/modes/print-mode.js +7 -2
  120. package/dist/modes/rpc/connection-handler.js +43 -11
  121. package/dist/modes/rpc/custom-capability.d.ts +2 -0
  122. package/dist/modes/rpc/custom-capability.js +2 -0
  123. package/dist/modes/rpc/host-admission-proxy.d.ts +9 -0
  124. package/dist/modes/rpc/host-admission-proxy.js +211 -0
  125. package/dist/modes/rpc/host-admission.d.ts +21 -0
  126. package/dist/modes/rpc/host-admission.js +59 -0
  127. package/dist/modes/rpc/host-capabilities.js +2 -1
  128. package/dist/modes/rpc/host-client-occupancy.d.ts +2 -1
  129. package/dist/modes/rpc/host-client-occupancy.js +7 -0
  130. package/dist/modes/rpc/host-decision.d.ts +2 -2
  131. package/dist/modes/rpc/host-decision.js +4 -0
  132. package/dist/modes/rpc/host-ensure-types.d.ts +2 -0
  133. package/dist/modes/rpc/host-ensure.js +31 -3
  134. package/dist/modes/rpc/host-lifecycle-drain.d.ts +2 -1
  135. package/dist/modes/rpc/host-lifecycle-drain.js +3 -1
  136. package/dist/modes/rpc/host-lifecycle-proxy.d.ts +3 -2
  137. package/dist/modes/rpc/host-lifecycle-proxy.js +10 -19
  138. package/dist/modes/rpc/host-lifecycle-shutdown.d.ts +2 -0
  139. package/dist/modes/rpc/host-lifecycle-shutdown.js +3 -1
  140. package/dist/modes/rpc/host-lifecycle-ticker.d.ts +11 -0
  141. package/dist/modes/rpc/host-lifecycle-ticker.js +27 -0
  142. package/dist/modes/rpc/host-lifecycle.js +15 -23
  143. package/dist/modes/rpc/host-outcome.js +1 -0
  144. package/dist/modes/rpc/host-protocol-info.d.ts +2 -0
  145. package/dist/modes/rpc/host-protocol-info.js +1 -0
  146. package/dist/modes/rpc/host-session-runner.js +22 -1
  147. package/dist/modes/rpc/host-status-all.js +1 -0
  148. package/dist/modes/rpc/host-status.d.ts +1 -0
  149. package/dist/modes/rpc/host-status.js +5 -1
  150. package/dist/modes/rpc/rpc-client.d.ts +7 -1
  151. package/dist/modes/rpc/rpc-client.js +12 -0
  152. package/dist/modes/rpc/rpc-types.d.ts +49 -1
  153. package/dist/modes/rpc/rpc-types.js +5 -0
  154. package/dist/modes/rpc/session-command-activity.js +2 -0
  155. package/dist/modes/rpc/session-held.js +3 -0
  156. package/docs/extensions.md +13 -1
  157. package/docs/providers.md +88 -0
  158. package/docs/rpc.md +150 -5
  159. package/docs/web-search.md +3 -1
  160. package/package.json +7 -7
  161. package/dist/bundle/chunks/anthropic-messages-5QQJVQPZ.js +0 -9
  162. package/dist/bundle/chunks/chunk-2A3MS2Z7.js +0 -8
  163. package/dist/bundle/chunks/chunk-6SBY4ZWC.js +0 -2
  164. package/dist/bundle/chunks/chunk-6WCOSBI7.js +0 -2
  165. package/dist/bundle/chunks/chunk-E75RO54O.js +0 -14
  166. package/dist/bundle/chunks/chunk-JMYO34RK.js +0 -2
  167. package/dist/bundle/chunks/chunk-KRNXUTZJ.js +0 -2
  168. package/dist/bundle/chunks/chunk-L4HVJWIF.js +0 -11
  169. package/dist/bundle/chunks/chunk-N5WLLHUE.js +0 -2
  170. package/dist/bundle/chunks/chunk-NEFNNQSV.js +0 -9
  171. package/dist/bundle/chunks/chunk-PWIYVLIA.js +0 -9
  172. package/dist/bundle/chunks/chunk-Q3R44CGT.js +0 -5
  173. package/dist/bundle/chunks/chunk-QIARZN7G.js +0 -31
  174. package/dist/bundle/chunks/chunk-QKU6JOA4.js +0 -2
  175. package/dist/bundle/chunks/chunk-QYWXDE47.js +0 -2
  176. package/dist/bundle/chunks/chunk-XHELSLN3.js +0 -188
  177. package/dist/bundle/chunks/chunk-YDJ4HGWO.js +0 -16
  178. package/dist/bundle/chunks/chunk-ZJHRDQCH.js +0 -2
  179. package/dist/bundle/chunks/cli-main-TUBNBZNK.js +0 -3
  180. package/dist/bundle/chunks/host-command-WO7VKJBJ.js +0 -2
  181. package/dist/bundle/chunks/host-lifecycle-X2J5FTJL.js +0 -3
  182. package/dist/bundle/chunks/host-session-command-ICBOH5RP.js +0 -19
  183. package/dist/bundle/chunks/interactive-mode-UGDNCGQ5.js +0 -2
  184. package/dist/bundle/chunks/rotation-stream-OODURUGZ.js +0 -2
  185. package/dist/bundle/chunks/rpc-mode-22MCT32O.js +0 -2
  186. package/dist/bundle/chunks/runtime-session-I4NDNGC5.js +0 -2
  187. package/dist/bundle/chunks/session-control-endpoint-SFPRQBDO.js +0 -3
  188. package/dist/bundle/chunks/session-picker-3ZQPV7Z3.js +0 -2
@@ -0,0 +1,27 @@
1
+ /** Idle and lifetime-owner checks share one ticker and never overlap owner probes. */
2
+ export function startSupervisorTicker(options) {
3
+ const { activity, owner, draining, shuttingDown, shutdown } = options;
4
+ const tickIntervalMs = Math.max(20, Math.min(1_000, options.idleExitMs / 4));
5
+ let ownerCheck = false;
6
+ const ticker = setInterval(() => {
7
+ if (!draining() && activity.refresh() === "exit" && activity.clients.unclassifiedCount === 0)
8
+ void shutdown("idle", 0);
9
+ if (!ownerCheck && !shuttingDown() && !draining()) {
10
+ ownerCheck = true;
11
+ void owner
12
+ .shouldExit(activity)
13
+ .then((exit) => {
14
+ if (exit && !shuttingDown() && !draining())
15
+ void shutdown("owner_gone", 0);
16
+ })
17
+ .finally(() => {
18
+ ownerCheck = false;
19
+ });
20
+ }
21
+ }, tickIntervalMs);
22
+ return () => {
23
+ clearInterval(ticker);
24
+ owner.stop();
25
+ };
26
+ }
27
+ //# sourceMappingURL=host-lifecycle-ticker.js.map
@@ -3,7 +3,8 @@ import { randomUUID } from "node:crypto";
3
3
  import { realpathSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
- import { getAgentDir, isBundledNode } from "../../config.js";
6
+ import { getAgentDir, isBundledNode, VERSION } from "../../config.js";
7
+ import { HostAdmission } from "./host-admission.js";
7
8
  import { classifyChildExit, noteChildExit } from "./host-child-exit.js";
8
9
  import { hostCrashCleanupPaths } from "./host-cleanup-paths.js";
9
10
  import { createHostDaemonPaths, generationPaths } from "./host-daemon-paths.js";
@@ -15,6 +16,7 @@ import { resolveHostPolicy } from "./host-lifecycle-policy.js";
15
16
  import { adoptPublicSocket, createPublicProxy, ensurePublicSocketSecret, listen, prepareSocketPath, waitForListener, } from "./host-lifecycle-proxy.js";
16
17
  import { createInternalSocketPath, readSettingsFile, recordChildPid } from "./host-lifecycle-scratch.js";
17
18
  import { performShutdown, registerSupervisorSignals, supervisorSender, } from "./host-lifecycle-shutdown.js";
19
+ import { startSupervisorTicker } from "./host-lifecycle-ticker.js";
18
20
  import { errorMessage, writeStderrLine } from "./host-supervisor-log.js";
19
21
  import { PUBLIC_SOCKET_IDENTITY_FILE, statSocketIdentity, writeSocketIdentityFile } from "./socket-ownership.js";
20
22
  import { createSocketSecret, socketSecretPath } from "./socket-transport.js";
@@ -110,13 +112,14 @@ export async function runHostSupervisor(launch) {
110
112
  const verdict = classifyChildExit(code, signal);
111
113
  void state.childExitRecorded.then(() => shutdown(verdict.reason, verdict.exitCode));
112
114
  });
113
- const drain = new SupervisorDrain(child, () => state.shuttingDown);
115
+ const admission = new HostAdmission({ protocolVersion: 1, serverVersion: VERSION, capabilities: [], instanceId });
116
+ const drain = new SupervisorDrain(child, () => state.shuttingDown, () => admission.stop());
114
117
  const server = createPublicProxy({
115
118
  internalSocket,
116
119
  ...(internalSecret ? { internalSecret } : {}),
117
120
  ...(publicSecret ? { publicSecret } : {}),
118
121
  clients: activity.clients,
119
- refusing: () => state.shuttingDown || drain.active,
122
+ admission,
120
123
  onDetach: () => activity.refresh(),
121
124
  onAdmit: () => owner.activity(),
122
125
  });
@@ -124,26 +127,14 @@ export async function runHostSupervisor(launch) {
124
127
  if (!state.shuttingDown)
125
128
  void shutdown(`public socket listener failed: ${errorMessage(cause)}`, 1);
126
129
  });
127
- const tickIntervalMs = Math.max(20, Math.min(1_000, policy.idleExitMs / 4));
128
- let ownerCheck = false;
129
- const ticker = setInterval(() => {
130
- if (!drain.active && activity.refresh() === "exit" && activity.clients.unclassifiedCount === 0)
131
- void shutdown("idle", 0);
132
- if (!ownerCheck && !state.shuttingDown && !drain.active) {
133
- ownerCheck = true;
134
- void owner
135
- .shouldExit(activity)
136
- .then((exit) => {
137
- if (exit && !state.shuttingDown && !drain.active)
138
- void shutdown("owner_gone", 0);
139
- })
140
- .finally(() => {
141
- ownerCheck = false;
142
- });
143
- }
144
- }, tickIntervalMs);
145
- watchers.push(() => clearInterval(ticker));
146
- watchers.push(() => owner.stop());
130
+ watchers.push(startSupervisorTicker({
131
+ idleExitMs: policy.idleExitMs,
132
+ activity,
133
+ owner,
134
+ draining: () => drain.active,
135
+ shuttingDown: () => state.shuttingDown,
136
+ shutdown,
137
+ }));
147
138
  const teardown = {
148
139
  paths,
149
140
  generation,
@@ -154,6 +145,7 @@ export async function runHostSupervisor(launch) {
154
145
  child,
155
146
  activity,
156
147
  drain,
148
+ admission,
157
149
  state,
158
150
  stopWatchers: () => {
159
151
  for (const stop of watchers.splice(0))
@@ -17,6 +17,7 @@ export function identityPayload(socket, pid, host) {
17
17
  return {
18
18
  socket,
19
19
  pid,
20
+ lifecycle: host?.lifecycle ?? null,
20
21
  instanceId: host?.instanceId ?? null,
21
22
  generation: host?.generation ?? null,
22
23
  engineVersion: host?.engineVersion ?? null,
@@ -16,6 +16,8 @@ import type { RpcLaunchProfile } from "./rpc-types.ts";
16
16
  * host from an older release answers without them; absence is read as "uncomparable", never as zero.
17
17
  */
18
18
  export interface HostProtocolInfo {
19
+ /** Absent on older hosts. A stopping generation never admits a new session. */
20
+ readonly lifecycle?: "ready" | "stopping";
19
21
  readonly protocolVersion: number;
20
22
  /** Informational only. Nothing in this module compares it. */
21
23
  readonly serverVersion: string;
@@ -14,6 +14,7 @@ export function parseHostProtocolInfo(data) {
14
14
  protocolVersion: typeof data.protocolVersion === "number" ? data.protocolVersion : 0,
15
15
  serverVersion: data.serverVersion,
16
16
  capabilities: data.capabilities,
17
+ ...((data.lifecycle === "ready" || data.lifecycle === "stopping") && { lifecycle: data.lifecycle }),
17
18
  ...(typeof data.instanceId === "string" && { instanceId: data.instanceId }),
18
19
  ...(typeof data.generation === "number" &&
19
20
  Number.isSafeInteger(data.generation) && { generation: data.generation }),
@@ -4,13 +4,30 @@ import { createHostDaemonPaths } from "./host-daemon-paths.js";
4
4
  import { HostEnsureRefusedError } from "./host-decision.js";
5
5
  import { endpointKindOfSocket } from "./host-endpoints.js";
6
6
  import { ensureHost } from "./host-ensure.js";
7
+ import { STOP_WAIT_BUDGET_MS } from "./host-ensure-stop.js";
7
8
  import { HOST_EXIT_OK, refusal } from "./host-outcome.js";
8
9
  import { mapError } from "./host-session-errors.js";
9
10
  import { openHostSession } from "./host-session-open.js";
10
11
  import { resolveSessionRef } from "./host-session-ref.js";
11
12
  import { waitForHostSession } from "./host-session-wait.js";
12
- import { RpcClient } from "./rpc-client.js";
13
+ import { RpcClient, RpcCommandError } from "./rpc-client.js";
13
14
  export async function runHostSessionRequest(request) {
15
+ const deadline = Date.now() + STOP_WAIT_BUDGET_MS;
16
+ for (;;) {
17
+ try {
18
+ return await runHostSessionRequestOnce(request, Math.max(0, deadline - Date.now()));
19
+ }
20
+ catch (error) {
21
+ if (request.action === "open" &&
22
+ error instanceof RpcCommandError &&
23
+ error.errorCode === "host_stopping" &&
24
+ Date.now() < deadline)
25
+ continue;
26
+ return mapError(error, request.target.socket);
27
+ }
28
+ }
29
+ }
30
+ async function runHostSessionRequestOnce(request, stoppingTimeoutMs) {
14
31
  const { socket, agentDir } = request.target;
15
32
  let client;
16
33
  let ensured;
@@ -27,6 +44,7 @@ export async function runHostSessionRequest(request) {
27
44
  env: daemonEnvOverrides(process.env, request.spec.env),
28
45
  policy: request.spec.policy,
29
46
  upgrade: "never",
47
+ stoppingTimeoutMs,
30
48
  });
31
49
  if (!ensured.reused) {
32
50
  await writeDaemonEnvKeys(createHostDaemonPaths({ socket, agentDir }), daemonEnvKeys(process.env, request.spec.env));
@@ -160,6 +178,9 @@ export async function runHostSessionRequest(request) {
160
178
  }
161
179
  }
162
180
  catch (error) {
181
+ // Only an explicit open owns a launch spec. No other operation may invent one.
182
+ if (request.action === "open" && error instanceof RpcCommandError && error.errorCode === "host_stopping")
183
+ throw error;
163
184
  if (error instanceof HostEnsureRefusedError) {
164
185
  return refusal("refuse", undefined, { reason: error.reason, socket, detail: error.detail });
165
186
  }
@@ -83,6 +83,7 @@ async function unaddressableStatus(dir) {
83
83
  const claims = await readClaimRows(paths, generations);
84
84
  return {
85
85
  reachable: false,
86
+ lifecycle: null,
86
87
  pid: null,
87
88
  instanceId: null,
88
89
  generation: null,
@@ -21,6 +21,7 @@ export type { HostGenerationRow } from "./host-generations.ts";
21
21
  export type { HostPathClaimRow, HostSessionRow } from "./host-status-rows.ts";
22
22
  export interface HostStatusReport {
23
23
  readonly reachable: boolean;
24
+ readonly lifecycle: "ready" | "stopping" | null;
24
25
  readonly socket: string;
25
26
  readonly pid: number | null;
26
27
  readonly instanceId: string | null;
@@ -47,10 +47,13 @@ export async function probeHostStatus(options, read = {}) {
47
47
  const metrics = current ? await readHostProcessMetrics(current.pid) : UNOBSERVED_METRICS;
48
48
  // A socket that did not answer who it is will not answer what it holds: asking again would only
49
49
  // spend a second budget on a hung endpoint.
50
- const listing = host === undefined ? [] : await readSessionListing(options.socket, includeWorkers, timeoutMs);
50
+ const listing = host === undefined || host.lifecycle === "stopping"
51
+ ? []
52
+ : await readSessionListing(options.socket, includeWorkers, timeoutMs);
51
53
  const claims = await readClaimRows(paths, generations);
52
54
  const report = {
53
55
  reachable: host !== undefined,
56
+ lifecycle: host?.lifecycle ?? null,
54
57
  socket: options.socket,
55
58
  pid: current?.pid ?? null,
56
59
  instanceId: host?.instanceId ?? current?.instanceId ?? null,
@@ -106,6 +109,7 @@ export function hostSummary(host) {
106
109
  return {
107
110
  protocolVersion: host.protocolVersion,
108
111
  serverVersion: host.serverVersion,
112
+ lifecycle: host.lifecycle ?? null,
109
113
  capabilities: host.capabilities,
110
114
  instanceId: host.instanceId ?? null,
111
115
  generation: host.generation ?? null,
@@ -21,7 +21,7 @@ export declare class RpcCommandError extends Error {
21
21
  }
22
22
  import type { SessionEntry, SessionTreeNode } from "../../core/session-manager.ts";
23
23
  import type { JsonAgentSessionEvent } from "../json-event.ts";
24
- import type { EditAssistantMessageResult, EditUserMessageResult, RpcAccountFailoverEvent, RpcAuthAccountsChangedEvent, RpcExtensionEvent, RpcExtensionUIProgress, RpcExtensionUIRequest, RpcExtensionUIResponse, RpcOpenQueuedEvent, RpcProviderAccount, RpcSessionModelEntry, RpcSessionParkedEvent, RpcSessionReplacedEvent, RpcSessionState, RpcSlashCommand } from "./rpc-types.ts";
24
+ import type { EditAssistantMessageResult, EditUserMessageResult, RpcAccountFailoverEvent, RpcAuthAccountsChangedEvent, RpcEvalCell, RpcEvalCellSnapshot, RpcExtensionEvent, RpcExtensionUIProgress, RpcExtensionUIRequest, RpcExtensionUIResponse, RpcOpenQueuedEvent, RpcProviderAccount, RpcSessionModelEntry, RpcSessionParkedEvent, RpcSessionReplacedEvent, RpcSessionState, RpcSlashCommand } from "./rpc-types.ts";
25
25
  export interface RpcClientOptions {
26
26
  /** Connect to an existing multi-session Unix socket instead of spawning a child. */
27
27
  socketPath?: string;
@@ -476,6 +476,12 @@ export declare class RpcClient {
476
476
  getCommands(): Promise<RpcSlashCommand[]>;
477
477
  /** Invoke one extension-owned RPC request handler and return its structured result. */
478
478
  requestExtension<T = unknown>(name: string, data?: unknown): Promise<T>;
479
+ /** Read live and recently settled eval cells without a model tool call. */
480
+ listEvalCells(): Promise<RpcEvalCell[]>;
481
+ /** Read a bounded UTF-8 output tail; the host caps tailBytes at 64 KiB. */
482
+ peekEvalCell(cellId: string, tailBytes?: number): Promise<RpcEvalCellSnapshot>;
483
+ /** Stop a session-owned cell and return its confirmed terminal snapshot. */
484
+ stopEvalCell(cellId: string): Promise<RpcEvalCellSnapshot>;
479
485
  /** List safe metadata for the named provider's configured account slots. */
480
486
  getProviderAccounts(provider: string): Promise<RpcProviderAccount[]>;
481
487
  /** Pin a provider account for future session-affine requests, or clear the pin. */
@@ -782,6 +782,18 @@ export class RpcClient {
782
782
  });
783
783
  return this.getData(response);
784
784
  }
785
+ /** Read live and recently settled eval cells without a model tool call. */
786
+ async listEvalCells() {
787
+ return this.getData(await this.send({ type: "eval_cells.list" }));
788
+ }
789
+ /** Read a bounded UTF-8 output tail; the host caps tailBytes at 64 KiB. */
790
+ async peekEvalCell(cellId, tailBytes) {
791
+ return this.getData(await this.send({ type: "eval_cells.peek", cellId, ...(tailBytes === undefined ? {} : { tailBytes }) }));
792
+ }
793
+ /** Stop a session-owned cell and return its confirmed terminal snapshot. */
794
+ async stopEvalCell(cellId) {
795
+ return this.getData(await this.send({ type: "eval_cells.stop", cellId }));
796
+ }
785
797
  /** List safe metadata for the named provider's configured account slots. */
786
798
  async getProviderAccounts(provider) {
787
799
  const response = await this.send({ type: "get_provider_accounts", provider });
@@ -331,6 +331,18 @@ type RpcSessionCommand = {
331
331
  type: "extension_request";
332
332
  name: string;
333
333
  data?: unknown;
334
+ } | {
335
+ id?: string;
336
+ type: "eval_cells.list";
337
+ } | {
338
+ id?: string;
339
+ type: "eval_cells.peek";
340
+ cellId: string;
341
+ tailBytes?: number;
342
+ } | {
343
+ id?: string;
344
+ type: "eval_cells.stop";
345
+ cellId: string;
334
346
  } | {
335
347
  id?: string;
336
348
  type: "get_auth_providers";
@@ -409,6 +421,9 @@ export declare const RPC_ERROR_STREAMING = "streaming";
409
421
  export declare const RPC_ERROR_ENTRY_NOT_FOUND = "not_found";
410
422
  /** `set_model` named a provider/model pair the session's registry does not offer. */
411
423
  export declare const RPC_ERROR_MODEL_NOT_FOUND = "model_not_found";
424
+ export declare const RPC_ERROR_EVAL_CELL_NOT_FOUND = "eval_cell_not_found";
425
+ export declare const RPC_ERROR_INVALID_EVAL_CELL_REQUEST = "invalid_eval_cell_request";
426
+ export declare const RPC_ERROR_EVAL_CELLS_UNAVAILABLE = "eval_cells_unavailable";
412
427
  export declare const RPC_ERROR_NOT_ASSISTANT = "not_assistant";
413
428
  export declare const RPC_ERROR_NOT_USER = "not_user";
414
429
  export declare const RPC_ERROR_EMPTY_TEXT = "empty";
@@ -437,7 +452,9 @@ export declare const RPC_ERROR_RELEASE_UNSUPPORTED = "release_unsupported";
437
452
  export declare const RPC_ERROR_RELEASE_FAILED = "release_failed";
438
453
  /** A draining host parks every session itself; `release_session` and `open_session` are refused meanwhile. */
439
454
  export declare const RPC_ERROR_HOST_DRAINING = "host_draining";
440
- export type RpcErrorCode = typeof RPC_ERROR_UNKNOWN_SESSION | typeof RPC_ERROR_SESSION_CLOSING | typeof RPC_ERROR_SESSION_PATH_IN_USE | typeof RPC_ERROR_SESSION_HELD | typeof RPC_ERROR_SESSION_RESERVATION_LIMIT | typeof RPC_ERROR_MISSING_SESSION_ID | typeof RPC_ERROR_MULTI_SESSION_DISABLED | typeof RPC_ERROR_INVALID_PATH | typeof RPC_ERROR_OPEN_FAILED | typeof RPC_ERROR_MEDIA_NOT_FOUND | typeof RPC_ERROR_INVALID_SESSION_CONTEXT | typeof RPC_ERROR_INVALID_SESSION_KIND | typeof RPC_ERROR_INVALID_LAUNCH_PROFILE | typeof RPC_ERROR_INVALID_SESSION_ID | typeof RPC_ERROR_SESSION_ID_IN_USE | typeof RPC_ERROR_HOST_MEMORY_PRESSURE | typeof RPC_ERROR_WARM_FAILED | typeof RPC_ERROR_STREAMING | typeof RPC_ERROR_ENTRY_NOT_FOUND | typeof RPC_ERROR_MODEL_NOT_FOUND | typeof RPC_ERROR_NOT_ASSISTANT | typeof RPC_ERROR_NOT_USER | typeof RPC_ERROR_EMPTY_TEXT | typeof RPC_ERROR_UNKNOWN_COMMAND | typeof RPC_ERROR_STALE_LEAF | typeof RPC_ERROR_TURN_ACTIVE | typeof RPC_ERROR_SESSION_BUSY | typeof RPC_ERROR_ATTACHED | typeof RPC_ERROR_INVALID_RELEASE_REASON | typeof RPC_ERROR_RELEASE_UNSUPPORTED | typeof RPC_ERROR_RELEASE_FAILED | typeof RPC_ERROR_HOST_DRAINING;
455
+ /** The supervisor has closed admission; a launch-owning caller may ensure a successor. */
456
+ export declare const RPC_ERROR_HOST_STOPPING = "host_stopping";
457
+ export type RpcErrorCode = typeof RPC_ERROR_HOST_STOPPING | typeof RPC_ERROR_UNKNOWN_SESSION | typeof RPC_ERROR_SESSION_CLOSING | typeof RPC_ERROR_SESSION_PATH_IN_USE | typeof RPC_ERROR_SESSION_HELD | typeof RPC_ERROR_SESSION_RESERVATION_LIMIT | typeof RPC_ERROR_MISSING_SESSION_ID | typeof RPC_ERROR_MULTI_SESSION_DISABLED | typeof RPC_ERROR_INVALID_PATH | typeof RPC_ERROR_OPEN_FAILED | typeof RPC_ERROR_MEDIA_NOT_FOUND | typeof RPC_ERROR_INVALID_SESSION_CONTEXT | typeof RPC_ERROR_INVALID_SESSION_KIND | typeof RPC_ERROR_INVALID_LAUNCH_PROFILE | typeof RPC_ERROR_INVALID_SESSION_ID | typeof RPC_ERROR_SESSION_ID_IN_USE | typeof RPC_ERROR_HOST_MEMORY_PRESSURE | typeof RPC_ERROR_WARM_FAILED | typeof RPC_ERROR_STREAMING | typeof RPC_ERROR_ENTRY_NOT_FOUND | typeof RPC_ERROR_MODEL_NOT_FOUND | typeof RPC_ERROR_EVAL_CELL_NOT_FOUND | typeof RPC_ERROR_INVALID_EVAL_CELL_REQUEST | typeof RPC_ERROR_EVAL_CELLS_UNAVAILABLE | typeof RPC_ERROR_NOT_ASSISTANT | typeof RPC_ERROR_NOT_USER | typeof RPC_ERROR_EMPTY_TEXT | typeof RPC_ERROR_UNKNOWN_COMMAND | typeof RPC_ERROR_STALE_LEAF | typeof RPC_ERROR_TURN_ACTIVE | typeof RPC_ERROR_SESSION_BUSY | typeof RPC_ERROR_ATTACHED | typeof RPC_ERROR_INVALID_RELEASE_REASON | typeof RPC_ERROR_RELEASE_UNSUPPORTED | typeof RPC_ERROR_RELEASE_FAILED | typeof RPC_ERROR_HOST_DRAINING;
441
458
  /** Every established command accepts an additive routing envelope. */
442
459
  export type RpcCommand = (RpcSessionCommand & {
443
460
  sessionId?: string;
@@ -707,6 +724,8 @@ export interface RpcProtocolIdentity {
707
724
  readonly launch_profile: RpcLaunchProfile;
708
725
  }
709
726
  export interface RpcProtocolInfo extends RpcProtocolIdentity {
727
+ /** Supervised socket hosts expose admission state independently of child responsiveness. */
728
+ readonly lifecycle?: "ready" | "stopping";
710
729
  readonly protocolVersion: 1;
711
730
  /** Informational engine version. Never compare it for compatibility - use the fields above. */
712
731
  readonly serverVersion: string;
@@ -1204,6 +1223,18 @@ export type RpcResponse = {
1204
1223
  command: "extension_request";
1205
1224
  success: true;
1206
1225
  data: unknown;
1226
+ } | {
1227
+ id?: string;
1228
+ type: "response";
1229
+ command: "eval_cells.list";
1230
+ success: true;
1231
+ data: RpcEvalCell[];
1232
+ } | {
1233
+ id?: string;
1234
+ type: "response";
1235
+ command: "eval_cells.peek" | "eval_cells.stop";
1236
+ success: true;
1237
+ data: RpcEvalCellSnapshot;
1207
1238
  } | {
1208
1239
  id?: string;
1209
1240
  type: "response";
@@ -1259,6 +1290,23 @@ export type RpcResponse = {
1259
1290
  errorCode?: string;
1260
1291
  errorData?: unknown;
1261
1292
  };
1293
+ /** Session-owned cell metadata; live and recently settled cells share the same list shape. */
1294
+ export interface RpcEvalCell {
1295
+ readonly cellId: string;
1296
+ readonly language: "js" | "py" | "rb" | "jl";
1297
+ readonly state: "queued" | "running" | "detached" | "completed" | "failed" | "cancelled";
1298
+ readonly startedAtMs: number;
1299
+ readonly summary?: string;
1300
+ readonly queuedBehind?: readonly string[];
1301
+ }
1302
+ /** Bounded text-only snapshot: no duplicated result body, code, or inline images. */
1303
+ export interface RpcEvalCellSnapshot extends RpcEvalCell {
1304
+ readonly outputTail: string;
1305
+ readonly stateRetained?: boolean;
1306
+ readonly interruptNote?: string;
1307
+ readonly hardLimitSeconds?: number;
1308
+ readonly runBudgetSeconds?: number;
1309
+ }
1262
1310
  /** Success payload of `edit_assistant_message`. `leafId` is the session leaf after the call. */
1263
1311
  export type EditAssistantMessageResult = {
1264
1312
  outcome: "edited";
@@ -46,6 +46,9 @@ export const RPC_ERROR_STREAMING = "streaming";
46
46
  export const RPC_ERROR_ENTRY_NOT_FOUND = "not_found";
47
47
  /** `set_model` named a provider/model pair the session's registry does not offer. */
48
48
  export const RPC_ERROR_MODEL_NOT_FOUND = "model_not_found";
49
+ export const RPC_ERROR_EVAL_CELL_NOT_FOUND = "eval_cell_not_found";
50
+ export const RPC_ERROR_INVALID_EVAL_CELL_REQUEST = "invalid_eval_cell_request";
51
+ export const RPC_ERROR_EVAL_CELLS_UNAVAILABLE = "eval_cells_unavailable";
49
52
  export const RPC_ERROR_NOT_ASSISTANT = "not_assistant";
50
53
  export const RPC_ERROR_NOT_USER = "not_user";
51
54
  export const RPC_ERROR_EMPTY_TEXT = "empty";
@@ -74,4 +77,6 @@ export const RPC_ERROR_RELEASE_UNSUPPORTED = "release_unsupported";
74
77
  export const RPC_ERROR_RELEASE_FAILED = "release_failed";
75
78
  /** A draining host parks every session itself; `release_session` and `open_session` are refused meanwhile. */
76
79
  export const RPC_ERROR_HOST_DRAINING = "host_draining";
80
+ /** The supervisor has closed admission; a launch-owning caller may ensure a successor. */
81
+ export const RPC_ERROR_HOST_STOPPING = "host_stopping";
77
82
  //# sourceMappingURL=rpc-types.js.map
@@ -18,6 +18,8 @@ const observations = new Set([
18
18
  "get_auth_providers",
19
19
  "get_provider_accounts",
20
20
  "memory_report",
21
+ "eval_cells.list",
22
+ "eval_cells.peek",
21
23
  ]);
22
24
  export function refreshesSessionActivity(command) {
23
25
  return !observations.has(command);
@@ -25,6 +25,9 @@ const READ_ONLY_OR_CONTROL = new Set([
25
25
  "get_auth_providers",
26
26
  "get_provider_accounts",
27
27
  "memory_report",
28
+ "eval_cells.list",
29
+ "eval_cells.peek",
30
+ "eval_cells.stop",
28
31
  "export_html",
29
32
  "export_jsonl",
30
33
  "abort",
@@ -709,7 +709,7 @@ The `systemPromptOptions` field gives extensions access to the same structured d
709
709
 
710
710
  Inside `before_agent_start`, `event.systemPrompt` and `ctx.getSystemPrompt()` both reflect the chained system prompt as of the current handler. Later `before_agent_start` handlers can still modify it again.
711
711
 
712
- `event.preview` is `true` when senpi composes the next turn's system prompt before any user prompt exists: the session-start prompt-cache prewarm sends the first turn's prefix ahead of time, and `ctx.getPromptCachePrefixRequest()` returns that prefix to extensions. In a preview `event.prompt` is empty and no turn follows.
712
+ `event.preview` is `true` when senpi composes the next turn's system prompt before any user prompt exists. Prompt-cache prewarm sends that prefix only after the first real composer activity and only when `promptCache.keepAlive.enabled` is `true` (default `false`, so prewarm is off by default). Session start, resume, fork, reload, and extension-injected text never trigger prewarm. `ctx.getPromptCachePrefixRequest()` also returns this prefix to extensions. In a preview `event.prompt` is empty and no turn follows.
713
713
 
714
714
  A preview only reaches handlers that opt in with `{ previewSafe: true }`:
715
715
 
@@ -870,6 +870,8 @@ Fired before each LLM call. Modify messages non-destructively. See [Session Form
870
870
 
871
871
  `event.source` is `"agent"` for the live agent request and `"projection"` for `prepareProviderRequest` calls, including summarizer inputs. Projections must not advance state belonging to the live request, even when their messages match the branch. The session journal can lag a live request, so transcript equality is not a reliable substitute for this marker.
872
872
 
873
+ `ctx.prepareProviderRequest(messages, excludeExtensionPath?, options?)` returns the projected context and its payload transform. In this release the transform excludes server-search tools by default, while retaining client function tools such as `web_search`. Call `ctx.prepareProviderRequest(messages, undefined, { allowServerSearch: true })` when a request must mirror the main turn's native tools and include fields, as keepalive does. Cache-friendly compaction retains the original main-turn payload hook separately; standalone summaries and session titles exclude server search.
874
+
873
875
  ```typescript
874
876
  pi.on("context", async (event, ctx) => {
875
877
  // event.messages - deep copy, safe to modify
@@ -1259,6 +1261,12 @@ Optional read-only getter for the absolute path to the current session's authori
1259
1261
 
1260
1262
  Shell tools and eval kernels expose this value as `PI_GOAL_STORE_FILE` and `ctx.cwd` as `PI_SESSION_CWD`. Hand-built contexts and older hosts may omit the getter, in which case `PI_GOAL_STORE_FILE` is unset.
1261
1263
 
1264
+ ### ctx.onUserActivity()
1265
+
1266
+ Optional subscription-only hook for host-authenticated activity. `ctx.onUserActivity(listener)` calls a synchronous listener with `"draft"` for actual terminal composer edits or `"prompt"` for user-originated prompt, steer, or follow-up admission, including RPC. It returns an unsubscribe function and is also removed when the extension generation retires. The host owns the emitter; `pi.events.emit("user_activity", ...)` cannot invoke this hook. Extension `pasteToEditor`, programmatic draft restoration, and lifecycle events do not publish activity.
1267
+
1268
+ Cache prewarm consumes the first activity per session generation. It is opt-in via `promptCache.keepAlive.enabled` (default `false`): a first draft edit may prewarm, while prompt admission uses the real request instead of issuing an extra warm call. This one-shot policy does not gate the existing idle keep-alive loop after completed turns.
1269
+
1262
1270
  ### ctx.modelRegistry / ctx.model / ctx.thinkingLevel / ctx.scopedModels
1263
1271
 
1264
1272
  Access to models, providers, and resolved authentication. `ctx.modelRegistry.getProvider(id)` returns the effective pi-ai provider, while `getProviderAuth(id)` resolves its current API key, headers, base URL, and provider-scoped environment without requiring a loaded model. `ctx.model` is the active model, and `ctx.thinkingLevel` is its current effective thinking level.
@@ -1329,6 +1337,10 @@ if (usage && usage.tokens > 100_000) {
1329
1337
  }
1330
1338
  ```
1331
1339
 
1340
+ ### ctx.getCompactionModel()
1341
+
1342
+ Optional on hosts that expose session-owned summarization routing. Returns the current session model while its account is usable, preserving builtin compaction behavior even when core compaction has a model override. For a spent account, it tries the held switch target, the configured summary override, then an authenticated alternate with enough context capacity for the active transcript and reserved output. It does not change the active session selection and throws an account-limit error when no usable summary model is available.
1343
+
1332
1344
  ### ctx.compact()
1333
1345
 
1334
1346
  Trigger compaction without awaiting completion. Use `onComplete` and `onError` for follow-up actions.
package/docs/providers.md CHANGED
@@ -4,6 +4,7 @@ Senpi supports subscription-based providers via OAuth and API key providers via
4
4
 
5
5
  ## Table of Contents
6
6
 
7
+ - [In-Turn Web Search](#in-turn-web-search)
7
8
  - [Subscriptions](#subscriptions)
8
9
  - [API Keys](#api-keys)
9
10
  - [Auth File](#auth-file)
@@ -13,6 +14,93 @@ Senpi supports subscription-based providers via OAuth and API key providers via
13
14
  - [Custom Providers](#custom-providers)
14
15
  - [Resolution Order](#resolution-order)
15
16
 
17
+ ## In-Turn Web Search
18
+
19
+ The session model uses its provider's server search before the standalone
20
+ `web_search` tool. A successful native search does not issue an auxiliary
21
+ request on another model. A search-specific request error or native result
22
+ error records one `websearch-native-fallback` entry and enables the standalone
23
+ tool for that session/model, including later prompts and switches back to it.
24
+ Starting a new session clears the failed-model set. Existing core retries own their retry
25
+ when one is already pending; otherwise the extension continues once after
26
+ the agent settles. User cancellation never schedules that continuation.
27
+ A provider that recovers from a failed search and completes its answer does
28
+ not receive a duplicate continuation. Sonar's inherent retrieval has no
29
+ independent server-tool error contract; its model errors remain with core
30
+ retry rather than receiving unsupported function-tool definitions.
31
+ Standalone summaries, session titles and default projections exclude only server-search tools, not client function declarations. Tool-less requests do not gain empty tool arrays. Cache-friendly compaction and keepalive retain the exact main-turn payload transform, tools and include fields.
32
+
33
+ Anthropic Messages supports `web_search_20250305`. The direct Anthropic API and
34
+ Claude OAuth retain main's default-on behavior. Anthropic-compatible gateways
35
+ must explicitly declare search and replay support. For example, merge this override into your existing provider entry,
36
+ keeping its current credential configuration:
37
+
38
+ ```json
39
+ {
40
+ "providers": {
41
+ "anthropic": {
42
+ "baseUrl": "https://your-proxy.example/anthropic",
43
+ "modelOverrides": {
44
+ "claude-opus-5-5": {
45
+ "compat": { "supportsWebSearch": true }
46
+ }
47
+ }
48
+ }
49
+ }
50
+ }
51
+ ```
52
+
53
+ This opt-in was verified with a real ccapi search and exact encrypted-result
54
+ replay. Senpi does not infer support from a proxy hostname. Anthropic requires
55
+ the assistant blocks and their `encrypted_content` to be replayed unchanged.
56
+
57
+ | Route | In-turn tool / API | Capability and replay contract |
58
+ | --- | --- | --- |
59
+ | Direct Anthropic API / Claude OAuth | `web_search_20250305`, Messages | Default-on (unchanged from main); `compat.supportsWebSearch: false` opts out. No new direct-endpoint probe is claimed here. |
60
+ | Anthropic-compatible gateways, including ccapi | `web_search_20250305`, Messages | Opt-in `compat.supportsWebSearch: true`. ccapi search and encrypted-result replay verified live. |
61
+ | OpenAI API / Azure | `web_search_preview`, Responses | Default-on (unchanged from main). Custom Responses endpoints remain opt-in `compat.supportsWebSearchPreview: true`. The current `web_search` name is also supported. |
62
+ | ChatGPT/Codex subscription | `web_search`, Codex Responses | First-party subscription default, except Spark; custom endpoints require opt-in. Real search and output-item replay verified. |
63
+ | xAI | `web_search`, Responses | Opt-in `compat.supportsWebSearchPreview: true`. Catalogs select the tool shape but do not enable it. Held OAuth probe: HTTP 403, not live-verified. |
64
+ | Google Gemini / Vertex | `googleSearch`, GenerateContent | Opt-in `compat.supportsWebSearch: true` on Gemini 2/3. Held probe: HTTP 400 `API_KEY_INVALID`, not live-verified. Explicit mixed-tool rejection enables the session/model fallback. |
65
+ | OpenRouter | `openrouter:web_search`, Chat Completions or Responses | Opt-in `compat.supportsWebSearch: true`; doc only, not live-verified. Uses OpenRouter's own tool shape. |
66
+ | Moonshot PAYG | `$web_search` / `builtin_function`, Chat Completions | Opt-in `compat.supportsWebSearch: true`; doc only, not live-verified. Echoes server-generated arguments without another search request. Retires October 20, 2026. Cannot be discovered or lazily activated through tool_search, or declared on another route. |
67
+ | Kimi for Coding | Anthropic-compatible Messages | Not live-verified (no personal credential). Coding-plan integration docs do not specify a server-search name/shape or replay contract. Not default-on; generic Messages opt-in is not verification. Follow-up [#3134](https://github.com/code-yeongyu/senpi/issues/3134). |
68
+ | DeepSeek | Claude Code search on its Anthropic-compatible endpoint | Not live-verified (no personal credential). Claude Code docs claim API search but do not publish its raw shape/replay contract; a manually configured Messages route remains opt-in, not default-on. Built-in Chat Completions remains unsupported, and Responses docs list `web_search` as ignored. Follow-up [#3134](https://github.com/code-yeongyu/senpi/issues/3134). |
69
+ | Z.AI PAYG chat | `web_search`, Chat Completions | Explicit `compat.supportsWebSearch: true` on that route; never enabled automatically on the coding-plan endpoint. |
70
+ | GLM coding plan, z.ai Anthropic endpoint | `web_search_20250305`, Messages | Supported, opt-in `compat.supportsWebSearch: true`. Live response `msg_202610120132474ee314e9947c4ea2` returned server search with a `tool_result` block, which now remains visible and replays with its same-model server-tool pair even when the search usage counter is zero. OpenAI-compatible coding probe: not verified (length limit, no search payload); bigmodel.cn: not verified (HTTP 401, expired key). Remote MCP `webSearchPrime` remains a separate option. |
71
+ | Perplexity Sonar | Search inherent in its main completion | No extra tool injection. Native-bypass routing is opt-in `compat.supportsWebSearch: true`; doc only, not live-verified. |
72
+ | Exa, Tavily, Brave, Kagi, Serper, Serpdive, Keenable, Google CSE, SearXNG and result-page engines | Dedicated retrieval APIs/MCP | Standalone search fallbacks, not session-model server tools. |
73
+
74
+ Official references:
75
+
76
+ Custom xAI-compatible Responses entries declare both
77
+ `supportsWebSearchPreview: true` and `webSearchToolType: "web_search"`.
78
+ The type selection belongs to the model compatibility entry, not a provider-id
79
+ or hostname guess. OpenAI-compatible endpoints that only support the legacy
80
+ preview type can retain `webSearchToolType: "web_search_preview"`.
81
+
82
+ - [Anthropic tool and replay](https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-search-tool)
83
+ - [OpenAI Responses search](https://developers.openai.com/api/docs/guides/tools-web-search)
84
+ - [Codex cached/live search](https://developers.openai.com/codex/config-basic/)
85
+ - [xAI Responses search](https://docs.x.ai/developers/tools/web-search)
86
+ - [Google grounding](https://ai.google.dev/gemini-api/docs/google-search)
87
+ - [Google SDK examples](https://github.com/googleapis/js-genai/blob/main/codegen_instructions.md)
88
+ - [OpenRouter server search](https://openrouter.ai/docs/guides/features/server-tools/web-search)
89
+ - [Moonshot built-in flow](https://platform.kimi.ai/docs/guide/use-web-search)
90
+ - [Kimi coding endpoint](https://www.kimi.com/code/docs/en/third-party-tools/claude-code)
91
+ - [DeepSeek Claude Code integration](https://api-docs.deepseek.com/quick_start/agent_integrations/claude_code)
92
+ - [DeepSeek Responses compatibility: built-in search ignored](https://api-docs.deepseek.com/guides/responses_api#tools)
93
+ - [Z.AI PAYG chat search](https://docs.z.ai/guides/tools/web-search)
94
+ - [GLM coding-plan search MCP](https://docs.z.ai/devpack/mcp/search-mcp-server)
95
+ - [GLM Claude Code / Anthropic integration](https://docs.z.ai/devpack/tool/claude)
96
+ - [Perplexity migration and conversation contract](https://docs.perplexity.ai/docs/sonar/quickstart)
97
+
98
+ The `nativeModel` setting and cheaper same-provider defaults apply only to
99
+ auxiliary search on routes without an in-turn server tool. They cannot
100
+ preempt server search: a different `nativeModel` setting is ignored on an enabled
101
+ server-search route. If an eligible route needs the standalone fallback,
102
+ that retry uses the session model rather than swapping to a cheaper model.
103
+
16
104
  ## Subscriptions
17
105
 
18
106
  Use `/login` in interactive mode, then select a provider: