@code-yeongyu/senpi 2026.10.9 → 2026.10.10-3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/dist/bundle/chunks/{anthropic-messages-CVXI6CLT.js → anthropic-messages-OE3QBX4P.js} +1 -1
  3. package/dist/bundle/chunks/{app-server-command-2HNDZGTR.js → app-server-command-DVWM37GF.js} +1 -1
  4. package/dist/bundle/chunks/{chunk-MJGHZT4N.js → chunk-3A3C5VVO.js} +1 -1
  5. package/dist/bundle/chunks/{chunk-Q2ZTPMWD.js → chunk-4TK7VCZZ.js} +1 -1
  6. package/dist/bundle/chunks/{chunk-CB77NQMN.js → chunk-7GVD54RM.js} +1 -1
  7. package/dist/bundle/chunks/{chunk-6EH7QXOG.js → chunk-CTERKXVZ.js} +2 -2
  8. package/dist/bundle/chunks/{chunk-SB22X6ZR.js → chunk-H4UIUFU5.js} +1 -1
  9. package/dist/bundle/chunks/{chunk-CM5QTRC2.js → chunk-H67EVDWG.js} +1 -1
  10. package/dist/bundle/chunks/chunk-I5HLTDT4.js +2 -0
  11. package/dist/bundle/chunks/chunk-IZOTKUWG.js +2 -0
  12. package/dist/bundle/chunks/{chunk-DERNLWDS.js → chunk-MT2C4I5R.js} +151 -148
  13. package/dist/bundle/chunks/{chunk-LSE7KE2C.js → chunk-ODYQK5XJ.js} +1 -1
  14. package/dist/bundle/chunks/chunk-PDSG6VER.js +14 -0
  15. package/dist/bundle/chunks/{chunk-LUWWM7GA.js → chunk-RADHRQ4K.js} +3 -3
  16. package/dist/bundle/chunks/chunk-RRU5LXMS.js +3 -0
  17. package/dist/bundle/chunks/{chunk-7Q3EY6HH.js → chunk-UMJL74VN.js} +1 -1
  18. package/dist/bundle/chunks/{chunk-3YXB7Z7O.js → chunk-Z7MMKKIY.js} +1 -1
  19. package/dist/bundle/chunks/{cli-main-VMDAGZCA.js → cli-main-B4S4DGBX.js} +1 -1
  20. package/dist/bundle/chunks/{help-fast-path-JBQLYO7W.js → help-fast-path-EHRZEBRO.js} +1 -1
  21. package/dist/bundle/chunks/{host-command-HTYYQBSG.js → host-command-VATE7HRG.js} +1 -1
  22. package/dist/bundle/chunks/{host-lifecycle-XZF6IKKO.js → host-lifecycle-GC3GIR2N.js} +1 -1
  23. package/dist/bundle/chunks/{interactive-mode-U7IH4VMJ.js → interactive-mode-QOFA64JL.js} +1 -1
  24. package/dist/bundle/chunks/{multi-session-host-HHIC6N7C.js → multi-session-host-E5RDPWQN.js} +1 -1
  25. package/dist/bundle/chunks/{openai-completions-6HU7GIGD.js → openai-completions-Q2LEWXEC.js} +1 -1
  26. package/dist/bundle/chunks/{openai-responses-S4QYZXF3.js → openai-responses-DHGWSTZM.js} +1 -1
  27. package/dist/bundle/chunks/{package-manager-cli-2FXWPM2U.js → package-manager-cli-YGTWXRG5.js} +1 -1
  28. package/dist/bundle/chunks/{rotation-stream-ZJQ6I2RZ.js → rotation-stream-C5BM7TMM.js} +1 -1
  29. package/dist/bundle/chunks/{rpc-mode-K5YGBGQO.js → rpc-mode-HDAGQ4X3.js} +1 -1
  30. package/dist/bundle/chunks/{schedule-command-CVAPK2WQ.js → schedule-command-EQ4PQCRP.js} +1 -1
  31. package/dist/bundle/chunks/{session-control-endpoint-THEF3K7M.js → session-control-endpoint-NABTZHCZ.js} +2 -2
  32. package/dist/bundle/chunks/{session-picker-R5N2EKUS.js → session-picker-O7OESJWU.js} +1 -1
  33. package/dist/bundle/chunks/session-worker.js +70 -67
  34. package/dist/bundle/cli.js +1 -1
  35. package/dist/bundle/index.js +1 -1
  36. package/dist/bundle/rpc-entry.js +1 -1
  37. package/dist/bundle/runtime-manifest.json +1 -1
  38. package/dist/core/agent-session-runtime.d.ts +9 -0
  39. package/dist/core/agent-session-runtime.js +22 -0
  40. package/dist/core/agent-session.d.ts +14 -1
  41. package/dist/core/agent-session.js +56 -8
  42. package/dist/core/extensions/builtin/btw/panel.d.ts +3 -1
  43. package/dist/core/extensions/builtin/btw/panel.js +14 -9
  44. package/dist/core/extensions/builtin/compaction/context-pipeline.d.ts +5 -0
  45. package/dist/core/extensions/builtin/compaction/context-pipeline.js +1 -0
  46. package/dist/core/extensions/builtin/compaction/index.js +2 -1
  47. package/dist/core/extensions/builtin/compaction/lane-policy.d.ts +7 -0
  48. package/dist/core/extensions/builtin/compaction/lane-policy.js +28 -15
  49. package/dist/core/extensions/builtin/compaction/log.d.ts +2 -0
  50. package/dist/core/extensions/builtin/compaction/log.js +2 -1
  51. package/dist/core/extensions/builtin/goal/cache-warm-renderer.js +14 -8
  52. package/dist/core/extensions/builtin/goal/cache-warm.d.ts +4 -2
  53. package/dist/core/extensions/builtin/goal/lifecycle-helpers.js +1 -1
  54. package/dist/core/extensions/builtin/goal/monitor-continuation.js +9 -1
  55. package/dist/core/extensions/builtin/goal/prompt.d.ts +6 -1
  56. package/dist/core/extensions/builtin/goal/prompt.js +32 -1
  57. package/dist/core/extensions/builtin/mcp/commands.js +21 -26
  58. package/dist/core/extensions/builtin/mcp/config-edit.d.ts +2 -0
  59. package/dist/core/extensions/builtin/mcp/config-edit.js +5 -2
  60. package/dist/core/extensions/builtin/mcp/manager-view.d.ts +34 -0
  61. package/dist/core/extensions/builtin/mcp/manager-view.js +136 -0
  62. package/dist/core/extensions/builtin/mcp/manager.d.ts +8 -0
  63. package/dist/core/extensions/builtin/mcp/manager.js +193 -0
  64. package/dist/core/extensions/builtin/mcp/service.d.ts +3 -1
  65. package/dist/core/extensions/builtin/mcp/service.js +4 -0
  66. package/dist/core/extensions/builtin/rules/rules/finder.js +8 -3
  67. package/dist/core/extensions/builtin/websearch/websearch/config.js +2 -0
  68. package/dist/core/extensions/builtin/websearch/websearch/provider-endpoints.js +1 -0
  69. package/dist/core/extensions/builtin/websearch/websearch/providers/keenable.d.ts +3 -0
  70. package/dist/core/extensions/builtin/websearch/websearch/providers/keenable.js +55 -0
  71. package/dist/core/extensions/builtin/websearch/websearch/providers.js +2 -0
  72. package/dist/core/extensions/builtin/websearch/websearch/types.d.ts +1 -1
  73. package/dist/core/model-config-schema.d.ts +5 -0
  74. package/dist/core/model-config-schema.js +1 -0
  75. package/dist/core/provider-composer.js +5 -1
  76. package/dist/core/provider-timeout-retry.d.ts +7 -1
  77. package/dist/core/provider-timeout-retry.js +23 -12
  78. package/dist/core/reload-veto.d.ts +1 -1
  79. package/dist/core/reload-veto.js +7 -1
  80. package/dist/core/retry-fallback/expansion.js +9 -3
  81. package/dist/core/sdk.js +7 -3
  82. package/dist/core/thinking-levels.d.ts +23 -1
  83. package/dist/core/thinking-levels.js +37 -0
  84. package/dist/core/trust-manager.js +50 -0
  85. package/dist/main.js +12 -13
  86. package/dist/modes/interactive/interactive-mode.js +8 -0
  87. package/dist/modes/interactive/session-control-endpoint.js +33 -8
  88. package/dist/modes/interactive/session-control-registry.js +3 -1
  89. package/dist/modes/interactive/session-control-wake.d.ts +11 -5
  90. package/dist/modes/interactive/session-control-wake.js +38 -16
  91. package/dist/modes/rpc/connection-handler.d.ts +5 -0
  92. package/dist/modes/rpc/connection-handler.js +30 -3
  93. package/dist/modes/rpc/custom-capability.d.ts +6 -0
  94. package/dist/modes/rpc/custom-capability.js +6 -0
  95. package/dist/modes/rpc/host-daemon-paths.d.ts +5 -4
  96. package/dist/modes/rpc/host-daemon-paths.js +17 -9
  97. package/dist/modes/rpc/host-daemon-registration.d.ts +6 -1
  98. package/dist/modes/rpc/host-daemon-registration.js +6 -2
  99. package/dist/modes/rpc/host-session-control.js +6 -4
  100. package/dist/modes/rpc/rpc-client.d.ts +5 -0
  101. package/dist/modes/rpc/rpc-client.js +7 -0
  102. package/dist/modes/rpc/rpc-mode.js +1 -1
  103. package/dist/modes/rpc/rpc-types.d.ts +9 -0
  104. package/docs/json.md +3 -0
  105. package/docs/mcp.md +16 -1
  106. package/docs/models.md +34 -0
  107. package/docs/rpc.md +36 -1
  108. package/docs/web-search.md +3 -1
  109. package/package.json +6 -6
  110. package/dist/bundle/chunks/chunk-6SMU7ANE.js +0 -3
  111. package/dist/bundle/chunks/chunk-M3BQ2PAQ.js +0 -14
  112. package/dist/bundle/chunks/chunk-VJJ53ORU.js +0 -2
  113. package/dist/bundle/chunks/chunk-YSPHACIN.js +0 -2
@@ -53,12 +53,16 @@ export async function readHostRegistration(paths) {
53
53
  * Registers a generation and points the daemon directory at it, under this process's writer stamp.
54
54
  * The stamp is what authorizes a later stop: only the process that wrote a record may signal the
55
55
  * host it names, and the recorded start time keeps a recycled pid from inheriting that right.
56
+ *
57
+ * `fresh` says the directory cannot hold an earlier generation (a terminal's endpoint, named by a
58
+ * socket built from a new instance id), so there is nothing to prune.
56
59
  */
57
- export async function writeHostRegistration(paths, registration) {
60
+ export async function writeHostRegistration(paths, registration, options = {}) {
58
61
  // Every write is also the moment to drop what is no longer running: records of dead generations
59
62
  // and their session-path claims otherwise accumulate for the life of the agent directory, and a
60
63
  // stale pointer among them reads as "a daemon serves this endpoint" (#1893).
61
- await pruneDeadGenerations(paths);
64
+ if (options.fresh !== true)
65
+ await pruneDeadGenerations(paths);
62
66
  const { generation, writer } = await writeGenerationRecord(paths, registration);
63
67
  // The pointer is replaced by rename: a reader either sees the generation that owned the socket
64
68
  // before this call or the one that owns it now, never a half-written pointer.
@@ -21,18 +21,18 @@ export class HostSessionControl {
21
21
  wake("idle");
22
22
  });
23
23
  const unsubscribeEmitted = session.externalAdmission.onEmitted(() => wake("emitted"));
24
- let stopInbox;
24
+ let inbox;
25
25
  const registration = {
26
26
  scheduler,
27
27
  release: () => {
28
28
  unsubscribeIdle();
29
29
  unsubscribeEmitted();
30
- stopInbox?.();
30
+ inbox?.stop();
31
31
  scheduler.dispose();
32
32
  },
33
33
  };
34
34
  try {
35
- stopInbox = await watchInbox(options.inboxDir, () => wake("inbox"), (error) => this.report(`control inbox watch failed: ${errorText(error)}`));
35
+ inbox = await watchInbox(options.inboxDir, () => wake("inbox"), (error) => this.report(`control inbox watch failed: ${errorText(error)}`));
36
36
  }
37
37
  catch (error) {
38
38
  registration.release();
@@ -45,8 +45,10 @@ export class HostSessionControl {
45
45
  }
46
46
  this.releaseActive();
47
47
  this.active = registration;
48
- // Anything that reached the inbox before the watch was armed is picked up by this first pass.
48
+ // Anything that reached the inbox before this point is picked up by this first pass, and anything
49
+ // written after it but before the watch was armed (no event for it) by the pass once arming settled.
49
50
  wake("inbox");
51
+ void inbox.armed.then(() => wake("inbox"));
50
52
  return {
51
53
  status: "registered",
52
54
  socket: this.socket,
@@ -330,6 +330,11 @@ export declare class RpcClient {
330
330
  * Abort in-progress retry.
331
331
  */
332
332
  abortRetry(): Promise<void>;
333
+ /**
334
+ * Give this single-session process its fallback chain before the first turn (`set_retry_fallback`).
335
+ * Requires the `retry_fallback_command` capability.
336
+ */
337
+ setRetryFallback(retryFallback: SessionRetryFallbackProfile): Promise<void>;
333
338
  /**
334
339
  * Execute a bash command.
335
340
  */
@@ -599,6 +599,13 @@ export class RpcClient {
599
599
  async abortRetry() {
600
600
  await this.send({ type: "abort_retry" });
601
601
  }
602
+ /**
603
+ * Give this single-session process its fallback chain before the first turn (`set_retry_fallback`).
604
+ * Requires the `retry_fallback_command` capability.
605
+ */
606
+ async setRetryFallback(retryFallback) {
607
+ await this.send({ type: "set_retry_fallback", retryFallback });
608
+ }
602
609
  /**
603
610
  * Execute a bash command.
604
611
  */
@@ -128,7 +128,7 @@ export async function runRpcMode(runtimeHost) {
128
128
  // it from a client handshake; a plain stdio client that sets nothing gets
129
129
  // byte-identical default behavior.
130
130
  const capabilities = parseClientCapabilities(envValue("RPC_CLIENT_CAPABILITIES"));
131
- const handler = createRpcConnectionHandler(runtimeHost, sink, { capabilities });
131
+ const handler = createRpcConnectionHandler(runtimeHost, sink, { capabilities, retryFallbackCommand: true });
132
132
  const signalCleanupHandlers = [];
133
133
  const registerSignalHandlers = () => {
134
134
  const signals = ["SIGTERM"];
@@ -173,6 +173,10 @@ type RpcSessionCommand = {
173
173
  } | {
174
174
  id?: string;
175
175
  type: "abort_retry";
176
+ } | {
177
+ id?: string;
178
+ type: "set_retry_fallback";
179
+ retryFallback: SessionRetryFallbackProfile;
176
180
  } | {
177
181
  id?: string;
178
182
  type: "bash";
@@ -991,6 +995,11 @@ export type RpcResponse = {
991
995
  type: "response";
992
996
  command: "abort_retry";
993
997
  success: true;
998
+ } | {
999
+ id?: string;
1000
+ type: "response";
1001
+ command: "set_retry_fallback";
1002
+ success: true;
994
1003
  } | {
995
1004
  id?: string;
996
1005
  type: "response";
package/docs/json.md CHANGED
@@ -22,6 +22,7 @@ type AgentSessionEvent =
22
22
  | { type: "compaction_end"; reason: "manual" | "threshold" | "overflow"; result: CompactionResult | undefined; aborted: boolean; willRetry: boolean; requestId?: string; accepted?: boolean; rejectionCause?: "cancelled-by-extension" | "external-owner" | "would-overflow" | "circuit-breaker" | "per-turn-cap"; errorMessage?: string }
23
23
  | { type: "session_info_changed"; name: string | undefined }
24
24
  | { type: "thinking_level_changed"; level: "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max" }
25
+ | { type: "thinking_level_clamped"; provider: string; modelId: string; requestedLevel: "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"; appliedLevel: "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"; reason: "model-not-reasoning" | "level-unsupported" }
25
26
  | SystemPromptChangeEvent // type: "system_prompt_change"
26
27
  | ExtensionToolHookLifecycleEvent // type: "tool_hook_status"
27
28
  | { type: "auto_retry_start"; attempt: number; maxAttempts: number; delayMs: number; errorMessage: string }
@@ -55,6 +56,8 @@ type JsonAgentSessionEvent =
55
56
 
56
57
  `queue_update` emits the full pending steering and follow-up queues whenever they change. `compaction_start`, `compaction_progress`, and `compaction_end` cover both manual and automatic compaction. `session_info_changed` fires when the session display name changes, `thinking_level_changed` when the thinking level changes, `system_prompt_change` (see `SystemPromptChangeEvent` in [`extensions/types.ts`](../src/core/extensions/types.ts)) when a model switch changes the active system prompt, and `tool_hook_status` (see `ExtensionToolHookLifecycleEvent` in [`extensions/runner.ts`](../src/core/extensions/runner.ts)) for extension tool hook start/end phases.
57
58
 
59
+ `thinking_level_clamped` fires once per model and requested level when an explicit thinking request (such as `--thinking`, `/thinking`, or a remembered per-model level) runs at a lower level than asked. A level that only comes from the global `defaultThinkingLevel` does not count as a request. In RPC mode, the `thinkingSelection` carried by `thinking_level_changed` and `get_state` then also has `requested` and `clampReason`.
60
+
58
61
  Other base events come from
59
62
  [`AgentEvent`](../../agent/src/types.ts):
60
63
 
package/docs/mcp.md CHANGED
@@ -22,12 +22,27 @@ catalog costs almost nothing until the model actually needs it.
22
22
  }
23
23
  ```
24
24
 
25
- 2. Start senpi. Run `/mcp` for the status panel, `/mcp status` for a one-line
25
+ 2. Start senpi. Run `/mcp` for the interactive server manager, `/mcp status` for a one-line
26
26
  summary, `/mcp add <name> <command...>` to add servers interactively.
27
27
  3. Servers needing OAuth: `/mcp auth <name>` (see [Auth](#auth)).
28
28
  4. Use it: small catalogs register directly; big ones surface through
29
29
  `tool_search` (see [Exposure tiers](#exposure-tiers)).
30
30
 
31
+ ## Interactive manager
32
+
33
+ `/mcp` lists servers with their connection state, tool and available resource
34
+ counts, exposure mode, and configuration source. Select a server to inspect its
35
+ tools, details, or logs; test or reconnect it; or sign in/out when OAuth applies.
36
+ The list updates when connections and catalogs change and preserves selection.
37
+ Navigation, confirmation, and cancellation use your configured keybindings.
38
+
39
+ Enable/disable and exposure changes are saved to the selected server's global
40
+ or trusted project `mcp.json`. Imported, skill-owned, extension-owned, and
41
+ untrusted definitions are read-only in this manager. OAuth actions close the
42
+ manager before running the existing authorization flow, keeping its notices
43
+ visible. Outside the TUI, `/mcp` reports status through the existing notification
44
+ channel; existing subcommands remain available.
45
+
31
46
  ## Configuration reference
32
47
 
33
48
  Top-level shape: `{ "settings": { ... }, "mcpServers": { "<name>": { ... } } }`.
package/docs/models.md CHANGED
@@ -142,6 +142,10 @@ Set `api` at provider level (default for all models) or model level (override pe
142
142
  | `authHeader` | Set `true` to add `Authorization: Bearer <apiKey>` automatically |
143
143
  | `models` | Array of model configurations |
144
144
  | `modelOverrides` | Per-model overrides for built-in or extension-registered models on this provider |
145
+ | `whitelist` | Model ids to keep; every other model of the provider is removed (see [Hiding Providers and Models](#hiding-providers-and-models)) |
146
+ | `blacklist` | Model ids to remove from the provider |
147
+ | `hideFreeModels` | Set `true` to remove every model of the provider whose input and output cost are both `0` |
148
+ | `disabled` | Set `true` to remove the whole provider |
145
149
 
146
150
  For providers with `models`, non-built-in provider configs need `baseUrl` and an `api` value at either provider or model level. `apiKey` is not required to load the file: models become available when auth is configured through `/login`/`auth.json`, CLI `--api-key`, or provider `apiKey`. If no auth is configured, the models load but stay unavailable in `/model` and `--list-models`.
147
151
 
@@ -510,6 +514,36 @@ Merge semantics:
510
514
  - If a custom model `id` matches a built-in model `id`, the custom model replaces that built-in model.
511
515
  - If a custom model `id` is new, it is added alongside built-in models.
512
516
 
517
+ ## Hiding Providers and Models
518
+
519
+ Hidden models and providers disappear from the catalog everywhere it is read: `--list-models`, `/model`, Ctrl+P cycling, startup selection, and `enabledModels` / `favoriteModels` resolution.
520
+
521
+ ```json
522
+ {
523
+ "disabledProviders": ["openrouter"],
524
+ "providers": {
525
+ "anthropic": { "whitelist": ["claude-sonnet-4-5"] },
526
+ "openai": { "blacklist": ["gpt-5.4"] },
527
+ "opencode": { "hideFreeModels": true }
528
+ }
529
+ }
530
+ ```
531
+
532
+ | Key | Where | Effect |
533
+ |-----|-------|--------|
534
+ | `disabledProviders` | Top level, array of provider ids | Removes each listed provider and all of its models. `providers.<id>.disabled: true` does the same for one provider. |
535
+ | `whitelist` | Provider block, array of model ids | Keeps only the listed ids. |
536
+ | `blacklist` | Provider block, array of model ids | Removes the listed ids. |
537
+ | `hideFreeModels` | Provider block, boolean | Removes every model of that provider whose `cost.input` and `cost.output` are both `0`. |
538
+
539
+ Behavior notes:
540
+ - `whitelist` and `blacklist` match whole model ids exactly and case-sensitively; there are no globs. When both are set, a model must be in `whitelist` and not in `blacklist`.
541
+ - All three provider filters run after custom `models` are merged in, and before `modelOverrides`. They also apply to the provider's custom `models` entries.
542
+ - `hideFreeModels` is per provider. Other providers are unaffected, so a local provider whose models cost `0` (Ollama, LM Studio, vLLM) stays listed unless that provider sets the flag itself.
543
+ - `hideFreeModels` also hides a custom `models` entry that omits `cost`, because an omitted `cost` is `0`. Give such a model a nonzero `cost`, or leave the flag off for that provider.
544
+ - `hideFreeModels` reads the catalog `cost` (or the custom model's own `cost`). A `cost` set in `modelOverrides` is not considered.
545
+ - A provider block that sets only `whitelist`, `blacklist`, or `hideFreeModels` is valid; no `baseUrl` or `models` is needed.
546
+
513
547
  ## Per-model Overrides
514
548
 
515
549
  Use `modelOverrides` to customize built-in models and matching extension-registered models without replacing the provider's full model list.
package/docs/rpc.md CHANGED
@@ -1924,7 +1924,7 @@ The `model` field is a full [Model](#model) object.
1924
1924
 
1925
1925
  #### get_available_models
1926
1926
 
1927
- List all configured models.
1927
+ List all configured models, refreshing credential availability on each request so credentials added or removed by another session take effect without reopening this session.
1928
1928
 
1929
1929
  ```json
1930
1930
  {"type": "get_available_models"}
@@ -2191,6 +2191,41 @@ Response:
2191
2191
  {"type": "response", "command": "abort_retry", "success": true}
2192
2192
  ```
2193
2193
 
2194
+ #### set_retry_fallback
2195
+
2196
+ A single-session `--mode rpc` process has no `open_session`. A caller that spawns one process per session (e.g. a task
2197
+ child that runs as its own process) gives it its fallback policy with `set_retry_fallback`:
2198
+
2199
+ ```json
2200
+ {"type": "set_retry_fallback", "retryFallback": {"modelFallback": true, "fallbackChains": {"provider/model": ["provider/spare"]}}}
2201
+ ```
2202
+
2203
+ Response:
2204
+ ```json
2205
+ {"type": "response", "command": "set_retry_fallback", "success": true}
2206
+ ```
2207
+
2208
+ The profile is the same shape, with the same limits, as `open_session.retryFallback`, and it has the same effect. It
2209
+ overrides the session's `retry.modelFallback` and `retry.fallbackChains` in memory only, it is never written to a
2210
+ settings file, and the process's later sessions (`new_session`, `switch_session`, `fork`) keep it.
2211
+
2212
+ It is a launch-time setting:
2213
+ - It is accepted only before the session's first turn. It is refused once this connection has asked for a turn
2214
+ (`prompt`, `steer`, `follow_up`, `continue_from_leaf`, `send_custom_message` with `triggerTurn`), even one that has
2215
+ not started yet, while a turn streams (an extension's included), and once the session holds turn history (a resumed
2216
+ session's). So a chain never changes under a provider request or a retry already in flight. Context messages added
2217
+ before the first turn (role `custom`, from an extension on `session_start` or a `send_custom_message` without
2218
+ `triggerTurn`) are not a turn and do not block it.
2219
+ - Send it on its own and await its response: a `set_retry_fallback` pipelined while a `new_session`, `switch_session`
2220
+ or `fork` is still being created can answer success while the session being created runs without it.
2221
+ - A malformed profile is refused with the `open_session` shape message, and nothing is applied.
2222
+ - A multi-session host refuses the command on its session connections; a host session takes its policy from
2223
+ `open_session.retryFallback`.
2224
+
2225
+ Probe `retry_fallback_command` in `get_protocol_info` before sending it. Only a single-session process advertises it.
2226
+ Unlike an environment variable, the profile reaches this process only, never what its tools spawn. Unlike argv, it has
2227
+ no command-line length limit and is not visible in a process listing.
2228
+
2194
2229
  ### Bash
2195
2230
 
2196
2231
  #### bash
@@ -75,7 +75,9 @@ A config file replaces the free default completely: senpi uses only the provider
75
75
  | `nativeModel` | The model native search runs on; see [Choosing the model native search runs on](#choosing-the-model-native-search-runs-on). |
76
76
  | `providers[]` | The providers to use. Each entry takes `provider`, and optionally `id`, `apiKey`, `baseUrl`, `maxResults`, `timeoutMs`, `priority`, `weight`, `allowedDomains` or `blockedDomains`. |
77
77
 
78
- Free engines you can list without a key: `duckduckgo-html`, `exa-mcp`, `startpage`, `mojeek`, `ecosia`, `google-html`, and `searxng`. Providers that need `apiKey`: `exa`, `tavily`, `brave`, `serper`, `serpdive`, `kagi`, `perplexity`, `z-ai`, `xai`, `kimi`, `deepseek`, `anthropic`, `openai` (plus `searchEngineId` for `google-cse`). `chatgpt-subscription` and `google` use your senpi login when `apiKey` is omitted; see [Listing login-based routes](#listing-login-based-routes).
78
+ Free engines you can list without a key: `duckduckgo-html`, `exa-mcp`, `startpage`, `mojeek`, `ecosia`, `google-html`, `searxng`, and `keenable`. Providers that need `apiKey`: `exa`, `tavily`, `brave`, `serper`, `serpdive`, `kagi`, `perplexity`, `z-ai`, `xai`, `kimi`, `deepseek`, `anthropic`, `openai` (plus `searchEngineId` for `google-cse`). `chatgpt-subscription` and `google` use your senpi login when `apiKey` is omitted; see [Listing login-based routes](#listing-login-based-routes).
79
+
80
+ `keenable` ([Keenable](https://keenable.ai)) takes an `apiKey` but also works without one: a keyed entry posts to `https://api.keenable.ai/v1/search` with `X-API-Key`, while a keyless entry uses the shared public tier at `/v1/search/public` and identifies itself with an `X-Keenable-Title` app header instead of a credential. Because the public tier is a per-IP pool, `keenable` is never in the no-config default chain — list it explicitly in `websearch.json`. A single `allowedDomains` entry maps to Keenable's native `site` filter; multiple or blocked domains stay `site:`/`-site:` terms in the query.
79
81
 
80
82
  ## Limit or turn off the free engines
81
83
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/senpi",
3
- "version": "2026.10.9",
3
+ "version": "2026.10.10-3",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "piConfig": {
@@ -57,9 +57,9 @@
57
57
  },
58
58
  "dependencies": {
59
59
  "@earendil-works/chord": "0.99.1",
60
- "@earendil-works/pi-agent-core": "npm:@code-yeongyu/senpi-agent-core@2026.10.9",
61
- "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.10.9",
62
- "@earendil-works/pi-tui": "npm:@code-yeongyu/senpi-tui@2026.10.9",
60
+ "@earendil-works/pi-agent-core": "npm:@code-yeongyu/senpi-agent-core@2026.10.10-3",
61
+ "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.10.10-3",
62
+ "@earendil-works/pi-tui": "npm:@code-yeongyu/senpi-tui@2026.10.10-3",
63
63
  "@silvia-odwyer/photon-node": "0.3.4",
64
64
  "chalk": "6.0.0",
65
65
  "cross-spawn": "7.0.6",
@@ -78,8 +78,8 @@
78
78
  "yaml": "2.9.1",
79
79
  "@anthropic-ai/claude-agent-sdk": "0.3.289",
80
80
  "@anthropic-ai/sdk": "0.127.0",
81
- "@code-yeongyu/senpi-codemode": "2026.10.9",
82
- "@earendil-works/pi-pty": "npm:@code-yeongyu/senpi-pty@2026.10.9",
81
+ "@code-yeongyu/senpi-codemode": "2026.10.10-3",
82
+ "@earendil-works/pi-pty": "npm:@code-yeongyu/senpi-pty@2026.10.10-3",
83
83
  "@modelcontextprotocol/sdk": "1.30.0",
84
84
  "@mozilla/readability": "0.6.0",
85
85
  "linkedom": "0.18.13",
@@ -1,3 +0,0 @@
1
- import { createRequire as __piCreateRequire } from "node:module"; const require = __piCreateRequire(import.meta.url); { const __piWorkerThreads = require("node:worker_threads"); if (typeof __piWorkerThreads.markAsUncloneable !== "function") { __piWorkerThreads.markAsUncloneable = () => {}; } }
2
- import{require_proper_lockfile}from"./chunk-GZVCRV7I.js";import{CONFIG_DIR_NAME,canonicalizePath,canonicalizePathStrict,resolvePath,stripBom}from"./chunk-463OK2Y4.js";import{__toESM}from"./chunk-IKOFCODM.js";var import_proper_lockfile=__toESM(require_proper_lockfile(),1);import{existsSync,mkdirSync,readFileSync,writeFileSync}from"node:fs";import{homedir}from"node:os";import{dirname,join}from"node:path";var TRUST_REQUIRING_PROJECT_CONFIG_RESOURCES=["settings.json","mcp.json","extensions","skills","prompts","themes","SYSTEM.md","APPEND_SYSTEM.md"],LEGACY_PROJECT_CONFIG_DIR_NAME=".pi";function normalizeCwd(cwd){let resolved=resolvePath(cwd);try{return canonicalizePathStrict(resolved)}catch{return resolved}}function findNearestTrustEntry(data,cwd){let currentDir=normalizeCwd(cwd);for(;;){let value=data[currentDir];if(value===!0||value===!1)return{path:currentDir,decision:value};let parentDir=dirname(currentDir);if(parentDir===currentDir)return null;currentDir=parentDir}}function getProjectTrustParentPath(cwd){let trustPath=normalizeCwd(cwd),parentDir=dirname(trustPath);return parentDir===trustPath?void 0:parentDir}function getProjectTrustOptions(cwd,options){let trustPath=normalizeCwd(cwd),trustOptions=[{label:"Trust",trusted:!0,updates:[{path:trustPath,decision:!0}],savedPath:trustPath}],parentPath=getProjectTrustParentPath(cwd);return parentPath!==void 0&&trustOptions.push({label:`Trust parent folder (${parentPath})`,trusted:!0,updates:[{path:parentPath,decision:!0},{path:trustPath,decision:null}],savedPath:parentPath}),options?.includeSessionOnly&&trustOptions.push({label:"Trust (this session only)",trusted:!0,updates:[]}),trustOptions.push({label:"Do not trust",trusted:!1,updates:[{path:trustPath,decision:!1}],savedPath:trustPath}),options?.includeSessionOnly&&trustOptions.push({label:"Do not trust (this session only)",trusted:!1,updates:[]}),trustOptions}function readTrustFile(path){if(!existsSync(path))return{};let parsed;try{parsed=JSON.parse(stripBom(readFileSync(path,"utf-8")))}catch(error){let message=error instanceof Error?error.message:String(error);throw new Error(`Failed to read trust store ${path}: ${message}`)}if(typeof parsed!="object"||parsed===null||Array.isArray(parsed))throw new Error(`Invalid trust store ${path}: expected an object`);let data={};for(let[key,value]of Object.entries(parsed)){if(value!==!0&&value!==!1&&value!==null)throw new Error(`Invalid trust store ${path}: value for ${JSON.stringify(key)} must be true, false, or null`);data[key]=value}return data}function writeTrustFile(path,data){let sorted={};for(let key of Object.keys(data).sort()){let value=data[key];(value===!0||value===!1||value===null)&&(sorted[key]=value)}mkdirSync(dirname(path),{recursive:!0}),writeFileSync(path,`${JSON.stringify(sorted,null,2)}
3
- `,"utf-8")}function acquireTrustLockSync(path){let trustDir=dirname(path);mkdirSync(trustDir,{recursive:!0});let maxAttempts=10,delayMs=20,lastError;for(let attempt=1;attempt<=maxAttempts;attempt++)try{return import_proper_lockfile.default.lockSync(trustDir,{realpath:!1,lockfilePath:`${path}.lock`})}catch(error){if((typeof error=="object"&&error!==null&&"code"in error?String(error.code):void 0)!=="ELOCKED"||attempt===maxAttempts)throw error;lastError=error;let start=Date.now();for(;Date.now()-start<delayMs;);}throw lastError instanceof Error?lastError:new Error("Failed to acquire trust store lock")}function withTrustFileLock(path,fn){let release=acquireTrustLockSync(path);try{return fn()}finally{release()}}function hasTrustRequiringProjectResources(cwd){let homeDir=canonicalizePath(resolvePath(process.env.HOME||homedir())),userAgentsSkillsDir=join(homeDir,".agents","skills"),currentDir=canonicalizePath(resolvePath(cwd));if([join(currentDir,CONFIG_DIR_NAME),join(currentDir,LEGACY_PROJECT_CONFIG_DIR_NAME)].some(configDir=>TRUST_REQUIRING_PROJECT_CONFIG_RESOURCES.some(entry=>existsSync(join(configDir,entry)))))return!0;for(;;){let agentsSkillsDir=join(currentDir,".agents","skills");if(agentsSkillsDir!==userAgentsSkillsDir&&existsSync(agentsSkillsDir))return!0;let parentDir=dirname(currentDir);if(parentDir===currentDir)return!1;currentDir=parentDir}}var ProjectTrustStore=class{constructor(agentDir){this.trustPath=join(resolvePath(agentDir),"trust.json")}get(cwd){return this.getEntry(cwd)?.decision??null}getEntry(cwd){return withTrustFileLock(this.trustPath,()=>{let data=readTrustFile(this.trustPath);return findNearestTrustEntry(data,cwd)})}set(cwd,decision){this.setMany([{path:cwd,decision}])}setMany(decisions){withTrustFileLock(this.trustPath,()=>{let data=readTrustFile(this.trustPath);for(let{path,decision}of decisions){let key=normalizeCwd(path);decision===null?delete data[key]:data[key]=decision}writeTrustFile(this.trustPath,data)})}};export{getProjectTrustOptions,hasTrustRequiringProjectResources,ProjectTrustStore};