@code-yeongyu/senpi 2026.10.8 → 2026.10.10-2

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 (111) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/dist/bundle/chunks/{app-server-command-MJOYE5ZS.js → app-server-command-WZG7WYMD.js} +1 -1
  3. package/dist/bundle/chunks/{chunk-MJGHZT4N.js → chunk-3A3C5VVO.js} +1 -1
  4. package/dist/bundle/chunks/{chunk-4GUQVY4Z.js → chunk-3YXB7Z7O.js} +1 -1
  5. package/dist/bundle/chunks/{chunk-CB77NQMN.js → chunk-7GVD54RM.js} +1 -1
  6. package/dist/bundle/chunks/{chunk-6EH7QXOG.js → chunk-CTERKXVZ.js} +2 -2
  7. package/dist/bundle/chunks/{chunk-CM5QTRC2.js → chunk-H67EVDWG.js} +1 -1
  8. package/dist/bundle/chunks/chunk-IZOTKUWG.js +2 -0
  9. package/dist/bundle/chunks/{chunk-K4AZKFRB.js → chunk-J5MUMVWB.js} +2 -2
  10. package/dist/bundle/chunks/{chunk-V2VQ4BHG.js → chunk-M2IHGL3M.js} +1 -1
  11. package/dist/bundle/chunks/{chunk-LUWWM7GA.js → chunk-RADHRQ4K.js} +3 -3
  12. package/dist/bundle/chunks/chunk-RRU5LXMS.js +3 -0
  13. package/dist/bundle/chunks/{chunk-BTYFICUC.js → chunk-SB22X6ZR.js} +1 -1
  14. package/dist/bundle/chunks/chunk-ULYOMGXX.js +14 -0
  15. package/dist/bundle/chunks/{chunk-7Q3EY6HH.js → chunk-UMJL74VN.js} +1 -1
  16. package/dist/bundle/chunks/{chunk-5MG76BCM.js → chunk-W6B7SEAK.js} +180 -178
  17. package/dist/bundle/chunks/{cli-main-GWVLFOIP.js → cli-main-T4T5UNRJ.js} +1 -1
  18. package/dist/bundle/chunks/{help-fast-path-JBQLYO7W.js → help-fast-path-44VUGB7H.js} +1 -1
  19. package/dist/bundle/chunks/{host-command-HTYYQBSG.js → host-command-X64D7PT3.js} +1 -1
  20. package/dist/bundle/chunks/{host-lifecycle-XZF6IKKO.js → host-lifecycle-GC3GIR2N.js} +1 -1
  21. package/dist/bundle/chunks/{interactive-mode-YLM2U7IX.js → interactive-mode-ULFSI46Z.js} +1 -1
  22. package/dist/bundle/chunks/{list-tips-JQDZPB6C.js → list-tips-IIRFXBTO.js} +1 -1
  23. package/dist/bundle/chunks/{multi-session-host-KMCBBPTV.js → multi-session-host-VM7WJBC5.js} +1 -1
  24. package/dist/bundle/chunks/{package-manager-cli-3UIJ4RXY.js → package-manager-cli-OAFUQY7E.js} +1 -1
  25. package/dist/bundle/chunks/{rpc-mode-PUYBZG27.js → rpc-mode-XVRJJG3K.js} +1 -1
  26. package/dist/bundle/chunks/{schedule-command-CVAPK2WQ.js → schedule-command-EQ4PQCRP.js} +1 -1
  27. package/dist/bundle/chunks/{session-control-endpoint-THEF3K7M.js → session-control-endpoint-FEGOYWHN.js} +2 -2
  28. package/dist/bundle/chunks/{session-picker-N463VCDI.js → session-picker-R5N2EKUS.js} +1 -1
  29. package/dist/bundle/chunks/session-worker.js +95 -93
  30. package/dist/bundle/cli.js +1 -1
  31. package/dist/bundle/index.js +1 -1
  32. package/dist/bundle/rpc-entry.js +1 -1
  33. package/dist/bundle/runtime-manifest.json +1 -1
  34. package/dist/core/agent-session-runtime.d.ts +9 -0
  35. package/dist/core/agent-session-runtime.js +22 -0
  36. package/dist/core/agent-session.d.ts +12 -1
  37. package/dist/core/agent-session.js +38 -4
  38. package/dist/core/dynamic-prompt/handoff.js +3 -3
  39. package/dist/core/dynamic-prompt/intent-gate.js +1 -1
  40. package/dist/core/dynamic-prompt/style.js +1 -1
  41. package/dist/core/extensions/builtin/btw/panel.d.ts +3 -1
  42. package/dist/core/extensions/builtin/btw/panel.js +14 -9
  43. package/dist/core/extensions/builtin/compaction/context-pipeline.d.ts +5 -0
  44. package/dist/core/extensions/builtin/compaction/context-pipeline.js +1 -0
  45. package/dist/core/extensions/builtin/compaction/index.js +2 -1
  46. package/dist/core/extensions/builtin/compaction/lane-policy.d.ts +7 -0
  47. package/dist/core/extensions/builtin/compaction/lane-policy.js +28 -15
  48. package/dist/core/extensions/builtin/compaction/log.d.ts +2 -0
  49. package/dist/core/extensions/builtin/compaction/log.js +2 -1
  50. package/dist/core/extensions/builtin/goal/cache-warm-renderer.js +14 -8
  51. package/dist/core/extensions/builtin/goal/cache-warm.d.ts +4 -2
  52. package/dist/core/extensions/builtin/goal/monitor-continuation.js +6 -0
  53. package/dist/core/extensions/builtin/mcp/commands.js +21 -26
  54. package/dist/core/extensions/builtin/mcp/config-edit.d.ts +2 -0
  55. package/dist/core/extensions/builtin/mcp/config-edit.js +5 -2
  56. package/dist/core/extensions/builtin/mcp/manager-view.d.ts +34 -0
  57. package/dist/core/extensions/builtin/mcp/manager-view.js +136 -0
  58. package/dist/core/extensions/builtin/mcp/manager.d.ts +8 -0
  59. package/dist/core/extensions/builtin/mcp/manager.js +193 -0
  60. package/dist/core/extensions/builtin/mcp/service.d.ts +3 -1
  61. package/dist/core/extensions/builtin/mcp/service.js +4 -0
  62. package/dist/core/extensions/builtin/permission-system/index.js +4 -0
  63. package/dist/core/extensions/builtin/permission-system/prompt.js +10 -2
  64. package/dist/core/extensions/builtin/permission-system/types.d.ts +3 -1
  65. package/dist/core/extensions/builtin/prompt-preset/claude-fable-5-1.js +2 -2
  66. package/dist/core/extensions/builtin/prompt-preset/claude-fable-5.js +2 -2
  67. package/dist/core/extensions/builtin/prompt-preset/claude-opus-5-5.js +2 -2
  68. package/dist/core/extensions/builtin/prompt-preset/claude-opus-5.js +2 -2
  69. package/dist/core/extensions/builtin/prompt-preset/claude-sonnet-5-5.js +2 -2
  70. package/dist/core/extensions/builtin/prompt-preset/gpt-5.5.js +3 -3
  71. package/dist/core/extensions/builtin/prompt-preset/gpt-5.6.js +3 -3
  72. package/dist/core/extensions/builtin/prompt-preset/gpt-6-astra.d.ts +2 -2
  73. package/dist/core/extensions/builtin/prompt-preset/gpt-6-astra.js +4 -4
  74. package/dist/core/extensions/builtin/prompt-preset/gpt-surface.js +2 -2
  75. package/dist/core/extensions/builtin/prompt-preset/grok-4.5.js +2 -2
  76. package/dist/core/extensions/builtin/prompt-preset/grok-4.6.js +1 -1
  77. package/dist/core/extensions/builtin/prompt-preset/grok-4.7.js +1 -1
  78. package/dist/core/extensions/builtin/prompt-preset/kimi-k3.js +2 -2
  79. package/dist/core/extensions/builtin/rules/rules/finder.js +8 -3
  80. package/dist/core/extensions/types.d.ts +4 -0
  81. package/dist/core/retry-fallback/expansion.js +9 -3
  82. package/dist/core/sdk.js +7 -3
  83. package/dist/core/thinking-levels.d.ts +23 -1
  84. package/dist/core/thinking-levels.js +37 -0
  85. package/dist/core/trust-manager.js +50 -0
  86. package/dist/main.js +12 -13
  87. package/dist/modes/interactive/interactive-mode.js +8 -0
  88. package/dist/modes/interactive/session-control-endpoint.js +33 -8
  89. package/dist/modes/interactive/session-control-registry.js +3 -1
  90. package/dist/modes/interactive/session-control-wake.d.ts +11 -5
  91. package/dist/modes/interactive/session-control-wake.js +38 -16
  92. package/dist/modes/rpc/connection-handler.d.ts +5 -0
  93. package/dist/modes/rpc/connection-handler.js +41 -5
  94. package/dist/modes/rpc/custom-capability.d.ts +6 -0
  95. package/dist/modes/rpc/custom-capability.js +6 -0
  96. package/dist/modes/rpc/host-daemon-paths.d.ts +5 -4
  97. package/dist/modes/rpc/host-daemon-paths.js +17 -9
  98. package/dist/modes/rpc/host-daemon-registration.d.ts +6 -1
  99. package/dist/modes/rpc/host-daemon-registration.js +6 -2
  100. package/dist/modes/rpc/host-session-control.js +6 -4
  101. package/dist/modes/rpc/rpc-client.d.ts +5 -0
  102. package/dist/modes/rpc/rpc-client.js +7 -0
  103. package/dist/modes/rpc/rpc-mode.js +1 -1
  104. package/dist/modes/rpc/rpc-types.d.ts +14 -0
  105. package/docs/json.md +3 -0
  106. package/docs/mcp.md +16 -1
  107. package/docs/rpc.md +49 -1
  108. package/package.json +6 -6
  109. package/dist/bundle/chunks/chunk-6SMU7ANE.js +0 -3
  110. package/dist/bundle/chunks/chunk-VJJ53ORU.js +0 -2
  111. package/dist/bundle/chunks/chunk-X6UDRO7J.js +0 -14
@@ -80,6 +80,12 @@ export declare const BROWSER_ENGINE_CAPABILITY = "browser_engine";
80
80
  * never written to a settings file and never seen by another session on the host.
81
81
  */
82
82
  export declare const RETRY_FALLBACK_PROFILE_CAPABILITY = "retry_fallback_profile";
83
+ /**
84
+ * SINGLE-SESSION capability: this `--mode rpc` process accepts `set_retry_fallback` (the
85
+ * `open_session.retryFallback` shape) before its first turn, so a caller that spawns one process per
86
+ * session can give it its own fallback chain without a settings file.
87
+ */
88
+ export declare const RETRY_FALLBACK_COMMAND_CAPABILITY = "retry_fallback_command";
83
89
  /**
84
90
  * HOST capability: `open_session.promptSurface` also accepts `chat` (a chat bridge: no routing line,
85
91
  * no handoff block, no todo cues). A host without it refuses `chat` with `invalid_launch_profile`,
@@ -80,6 +80,12 @@ export const BROWSER_ENGINE_CAPABILITY = "browser_engine";
80
80
  * never written to a settings file and never seen by another session on the host.
81
81
  */
82
82
  export const RETRY_FALLBACK_PROFILE_CAPABILITY = "retry_fallback_profile";
83
+ /**
84
+ * SINGLE-SESSION capability: this `--mode rpc` process accepts `set_retry_fallback` (the
85
+ * `open_session.retryFallback` shape) before its first turn, so a caller that spawns one process per
86
+ * session can give it its own fallback chain without a settings file.
87
+ */
88
+ export const RETRY_FALLBACK_COMMAND_CAPABILITY = "retry_fallback_command";
83
89
  /**
84
90
  * HOST capability: `open_session.promptSurface` also accepts `chat` (a chat bridge: no routing line,
85
91
  * no handoff block, no todo cues). A host without it refuses `chat` with `invalid_launch_profile`,
@@ -101,10 +101,11 @@ export declare class HostDaemonStateError extends Error {
101
101
  constructor(path: string, cause: unknown);
102
102
  }
103
103
  /**
104
- * Creates this endpoint's directories and publishes the flat marker. The modes are set explicitly
105
- * rather than left to `mkdir`, because a directory that already exists keeps whatever mode it was
106
- * created with - and this one holds the evidence that decides who may signal the daemon. A `tui`
107
- * registrant passes its kind; everything else is an `rpc_host`.
104
+ * Creates this endpoint's directories and publishes the flat marker. A directory that already exists
105
+ * keeps whatever mode it was created with - and this one holds the evidence that decides who may
106
+ * signal the daemon - so an existing one is re-moded explicitly; one `mkdir` just created already
107
+ * has the mode. The siblings inside the endpoint directory and the marker are written concurrently.
108
+ * A `tui` registrant passes its kind; everything else is an `rpc_host`.
108
109
  */
109
110
  export declare function createDaemonDirectories(paths: HostDaemonPaths, identity?: {
110
111
  readonly kind?: EndpointKind;
@@ -159,27 +159,35 @@ export class HostDaemonStateError extends Error {
159
159
  }
160
160
  }
161
161
  /**
162
- * Creates this endpoint's directories and publishes the flat marker. The modes are set explicitly
163
- * rather than left to `mkdir`, because a directory that already exists keeps whatever mode it was
164
- * created with - and this one holds the evidence that decides who may signal the daemon. A `tui`
165
- * registrant passes its kind; everything else is an `rpc_host`.
162
+ * Creates this endpoint's directories and publishes the flat marker. A directory that already exists
163
+ * keeps whatever mode it was created with - and this one holds the evidence that decides who may
164
+ * signal the daemon - so an existing one is re-moded explicitly; one `mkdir` just created already
165
+ * has the mode. The siblings inside the endpoint directory and the marker are written concurrently.
166
+ * A `tui` registrant passes its kind; everything else is an `rpc_host`.
166
167
  */
167
168
  export async function createDaemonDirectories(paths, identity = {}) {
168
169
  try {
169
170
  // The flat directory may predate this layout and may hold a legacy host's files: it is created
170
171
  // when missing and never re-moded, so a legacy host keeps whatever it set up for itself.
171
172
  await mkdir(paths.flatDir, { recursive: true, mode: DIRECTORY_MODE });
172
- for (const directory of [paths.dir, paths.generationsDir, paths.reservationsDir]) {
173
- await mkdir(directory, { recursive: true, mode: DIRECTORY_MODE });
174
- await chmod(directory, DIRECTORY_MODE);
175
- }
176
- await writeFile(paths.layoutMarker, `${JSON.stringify({ layout: HOST_DAEMON_LAYOUT, dir: basename(paths.dir) })}\n`, { mode: HOST_STATE_FILE_MODE });
173
+ await makePrivateDirectory(paths.dir);
174
+ await Promise.all([
175
+ makePrivateDirectory(paths.generationsDir),
176
+ makePrivateDirectory(paths.reservationsDir),
177
+ writeFile(paths.layoutMarker, `${JSON.stringify({ layout: HOST_DAEMON_LAYOUT, dir: basename(paths.dir) })}\n`, { mode: HOST_STATE_FILE_MODE }),
178
+ ]);
177
179
  }
178
180
  catch (cause) {
179
181
  throw new HostDaemonStateError(paths.dir, cause);
180
182
  }
181
183
  await ensureEndpointIdentity(paths, paths.socket, identity);
182
184
  }
185
+ /** `mkdir` names the directory it created; only one that already existed needs its mode set. */
186
+ async function makePrivateDirectory(directory) {
187
+ const created = await mkdir(directory, { recursive: true, mode: DIRECTORY_MODE });
188
+ if (created === undefined)
189
+ await chmod(directory, DIRECTORY_MODE);
190
+ }
183
191
  /**
184
192
  * Writes `endpoint.json` when it is absent and never rewrites a valid one: the first writer's
185
193
  * `created_at` is the endpoint's birth. The file is written whole to a temporary name and LINKED
@@ -40,8 +40,13 @@ export declare function readHostRegistration(paths: HostDaemonPaths): Promise<Re
40
40
  * Registers a generation and points the daemon directory at it, under this process's writer stamp.
41
41
  * The stamp is what authorizes a later stop: only the process that wrote a record may signal the
42
42
  * host it names, and the recorded start time keeps a recycled pid from inheriting that right.
43
+ *
44
+ * `fresh` says the directory cannot hold an earlier generation (a terminal's endpoint, named by a
45
+ * socket built from a new instance id), so there is nothing to prune.
43
46
  */
44
- export declare function writeHostRegistration(paths: HostDaemonPaths, registration: HostRegistration): Promise<void>;
47
+ export declare function writeHostRegistration(paths: HostDaemonPaths, registration: HostRegistration, options?: {
48
+ readonly fresh?: boolean;
49
+ }): Promise<void>;
45
50
  /**
46
51
  * Records ONE generation's process in its own directory without moving the pointer. A handoff writes
47
52
  * this the moment its successor is spawned: until the rename lands the pointer must keep naming the
@@ -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";
@@ -1319,6 +1328,9 @@ export type RpcExtensionUIRequest = {
1319
1328
  title: string;
1320
1329
  options: string[];
1321
1330
  timeout?: number;
1331
+ /** The tool call this dialog approves, when it is a permission request. */
1332
+ toolCallId?: string;
1333
+ parentToolCallId?: string;
1322
1334
  } | {
1323
1335
  type: "extension_ui_request";
1324
1336
  id: string;
@@ -1333,6 +1345,8 @@ export type RpcExtensionUIRequest = {
1333
1345
  title: string;
1334
1346
  placeholder?: string;
1335
1347
  timeout?: number;
1348
+ toolCallId?: string;
1349
+ parentToolCallId?: string;
1336
1350
  } | {
1337
1351
  type: "extension_ui_request";
1338
1352
  id: string;
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/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
@@ -3562,6 +3597,19 @@ Prompt the user to choose from a list. Dialog methods with a `timeout` field inc
3562
3597
 
3563
3598
  Expected response: `extension_ui_response` with `value` (the selected option string) or `cancelled: true`.
3564
3599
 
3600
+ A permission prompt (title `Permission required: <permission>`) also carries `toolCallId`, the id of the tool call it approves, and `parentToolCallId` when another tool (for example a codemode script) issued that call. Clients bind the prompt to that call by id: the engine raises the prompts for every call of a message before any of them runs, so the order of `tool_execution_start` events does not say which call a prompt belongs to. The `input` that follows a "Deny with feedback" choice carries the same fields.
3601
+
3602
+ ```json
3603
+ {
3604
+ "type": "extension_ui_request",
3605
+ "id": "uuid-3",
3606
+ "method": "select",
3607
+ "title": "Permission required: read\n\nPath: /project/a.txt\n\nPatterns:\n - /project/a.txt",
3608
+ "options": ["Allow once", "Allow always", "Deny", "Deny with feedback"],
3609
+ "toolCallId": "call-a"
3610
+ }
3611
+ ```
3612
+
3565
3613
  #### confirm
3566
3614
 
3567
3615
  Prompt the user for yes/no confirmation.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/senpi",
3
- "version": "2026.10.8",
3
+ "version": "2026.10.10-2",
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.8",
61
- "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.10.8",
62
- "@earendil-works/pi-tui": "npm:@code-yeongyu/senpi-tui@2026.10.8",
60
+ "@earendil-works/pi-agent-core": "npm:@code-yeongyu/senpi-agent-core@2026.10.10-2",
61
+ "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.10.10-2",
62
+ "@earendil-works/pi-tui": "npm:@code-yeongyu/senpi-tui@2026.10.10-2",
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.8",
82
- "@earendil-works/pi-pty": "npm:@code-yeongyu/senpi-pty@2026.10.8",
81
+ "@code-yeongyu/senpi-codemode": "2026.10.10-2",
82
+ "@earendil-works/pi-pty": "npm:@code-yeongyu/senpi-pty@2026.10.10-2",
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};
@@ -1,2 +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{engineBuildIdentity}from"./chunk-ZJHRDQCH.js";import{HOST_GENERATION_ENV,HOST_INSTANCE_ID_ENV}from"./chunk-CIRZTP7W.js";import{parseArgs,resolveSessionRuntime}from"./chunk-MH2BFYGN.js";import*as crypto from"node:crypto";var CUSTOM_UNSUPPORTED_CAPABILITY="custom_unsupported",EXTENSION_EVENTS_CAPABILITY="extension_events",AUTO_TITLE_SESSIONS_CAPABILITY="auto_title_sessions",CONTINUE_FROM_LEAF_CAPABILITY="continue_from_leaf",MEDIA_PLACEHOLDERS_CAPABILITY="media_placeholders",QUESTION_CAPABILITY="question",RETAIN_ON_DISCONNECT_CAPABILITY="retain_on_disconnect",SESSION_CONTEXT_CAPABILITY="session_context",SESSION_KIND_CAPABILITY="session_kind",AUTO_TITLE_PER_SESSION_CAPABILITY="auto_title_per_session",WARM_CAPABILITY="warm",DURABLE_SESSION_ID_CAPABILITY="durable_session_id",PROMPT_SURFACE_CAPABILITY="prompt_surface",BROWSER_ENGINE_CAPABILITY="browser_engine",RETRY_FALLBACK_PROFILE_CAPABILITY="retry_fallback_profile",PROMPT_SURFACE_CHAT_CAPABILITY="prompt_surface_chat",RPC_CLIENT_CAPABILITIES_ENV="SENPI_RPC_CLIENT_CAPABILITIES";function parseClientCapabilities(value){return value?value.split(",").map(part=>part.trim()).filter(part=>part.length>0):[]}var DEFAULT_CUSTOM_EXTENSION_LABEL="custom UI component";function buildCustomUnsupportedRequest(capabilities,extensionName){if(!capabilities?.includes(CUSTOM_UNSUPPORTED_CAPABILITY))return;let name=extensionName.trim()||DEFAULT_CUSTOM_EXTENSION_LABEL;return{type:"extension_ui_request",id:crypto.randomUUID(),method:"custom_unsupported",extensionName:name}}import{createHash,randomUUID as randomUUID2}from"node:crypto";import{resolve}from"node:path";var INSTANCE_ID=resolveInstanceId(process.env[HOST_INSTANCE_ID_ENV]);function resolveInstanceId(value){return value!==void 0&&value.trim()!==""?value:randomUUID2()}function hostInstanceId(){return INSTANCE_ID}var cachedProfile;function hostGeneration(env){let value=env[HOST_GENERATION_ENV];return value!==void 0&&/^\d+$/.test(value)?Number(value):0}function hostLaunchProfile(argv,cwd){let parsed=parseArgs([...argv],{grokNeoEnabled:!1}),core={extensions:[...new Set((parsed.extensions??[]).map(path=>resolve(cwd,path)))].sort(),multi_session:parsed.multiSession===!0,session_runtime:resolveSessionRuntime(parsed)},canonical=JSON.stringify({extensions:core.extensions,multi_session:core.multi_session,session_runtime:core.session_runtime});return{profile_id:createHash("sha256").update(canonical).digest("hex"),core}}function socketHostLaunchProfile(hostArgs,cwd){return hostLaunchProfile(["--mode","rpc","--multi-session","--listen","unix://",...hostArgs],cwd)}function protocolIdentity(){cachedProfile??=hostLaunchProfile(process.argv.slice(2),process.cwd());let build=engineBuildIdentity();return{instanceId:INSTANCE_ID,generation:hostGeneration(process.env),engineVersion:build.text,engineOrdinal:build.ordinal,launch_profile:cachedProfile}}export{CUSTOM_UNSUPPORTED_CAPABILITY,EXTENSION_EVENTS_CAPABILITY,AUTO_TITLE_SESSIONS_CAPABILITY,CONTINUE_FROM_LEAF_CAPABILITY,MEDIA_PLACEHOLDERS_CAPABILITY,QUESTION_CAPABILITY,RETAIN_ON_DISCONNECT_CAPABILITY,SESSION_CONTEXT_CAPABILITY,SESSION_KIND_CAPABILITY,AUTO_TITLE_PER_SESSION_CAPABILITY,WARM_CAPABILITY,DURABLE_SESSION_ID_CAPABILITY,PROMPT_SURFACE_CAPABILITY,BROWSER_ENGINE_CAPABILITY,RETRY_FALLBACK_PROFILE_CAPABILITY,PROMPT_SURFACE_CHAT_CAPABILITY,RPC_CLIENT_CAPABILITIES_ENV,parseClientCapabilities,DEFAULT_CUSTOM_EXTENSION_LABEL,buildCustomUnsupportedRequest,hostInstanceId,hostGeneration,hostLaunchProfile,socketHostLaunchProfile,protocolIdentity};