@code-yeongyu/senpi 2026.9.29-5 → 2026.9.30

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 (140) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/dist/bundle/chunks/{anthropic-messages-HEVKB3PL.js → anthropic-messages-ADSIWAJ5.js} +1 -1
  3. package/dist/bundle/chunks/{app-server-command-32AFWZEH.js → app-server-command-B3ZYM2WN.js} +1 -1
  4. package/dist/bundle/chunks/{chunk-2WAKOPNA.js → chunk-4P4DUW66.js} +1 -1
  5. package/dist/bundle/chunks/{chunk-7RVWDN5Z.js → chunk-6GV3L3GZ.js} +1 -1
  6. package/dist/bundle/chunks/{chunk-OUG6SDT7.js → chunk-6JZQLUAJ.js} +1 -1
  7. package/dist/bundle/chunks/{chunk-MJQWYN2W.js → chunk-7MKMDWHC.js} +2 -2
  8. package/dist/bundle/chunks/{chunk-XBGO2JZD.js → chunk-A7AQSA6R.js} +1 -1
  9. package/dist/bundle/chunks/{chunk-BJICJY47.js → chunk-DVVROXLZ.js} +1 -1
  10. package/dist/bundle/chunks/chunk-EIG6K5U3.js +6 -0
  11. package/dist/bundle/chunks/chunk-FAWC2EIR.js +2 -0
  12. package/dist/bundle/chunks/{chunk-WCAKUCN3.js → chunk-FNCILR4E.js} +1 -1
  13. package/dist/bundle/chunks/{chunk-XHEMQA2T.js → chunk-HPESFBMB.js} +2 -2
  14. package/dist/bundle/chunks/{chunk-BBNWT72C.js → chunk-HVBFDRP4.js} +1 -1
  15. package/dist/bundle/chunks/{chunk-63QHOG5A.js → chunk-I77ENHCQ.js} +81 -74
  16. package/dist/bundle/chunks/{chunk-UMLU3I2G.js → chunk-ITXF6FAM.js} +1 -1
  17. package/dist/bundle/chunks/{chunk-77AUUH3Z.js → chunk-IXONH7ML.js} +2 -2
  18. package/dist/bundle/chunks/{chunk-K2SHQNAA.js → chunk-JX2CSKLG.js} +1 -1
  19. package/dist/bundle/chunks/{chunk-4VWWEMCJ.js → chunk-JXZ5LDJP.js} +1 -1
  20. package/dist/bundle/chunks/{chunk-VLTMFAWQ.js → chunk-KWI3KUF2.js} +1 -1
  21. package/dist/bundle/chunks/{chunk-7CQX3QFV.js → chunk-L4QGZJKQ.js} +1 -1
  22. package/dist/bundle/chunks/{chunk-AGVPQLIU.js → chunk-MDXUQDGF.js} +1 -1
  23. package/dist/bundle/chunks/{chunk-2N3BZ5DV.js → chunk-OOA6OL42.js} +1 -1
  24. package/dist/bundle/chunks/{chunk-2POUJCWM.js → chunk-OVGH7VP3.js} +1 -1
  25. package/dist/bundle/chunks/{chunk-ZDNR3YVJ.js → chunk-RMVQ5YRX.js} +1 -1
  26. package/dist/bundle/chunks/{chunk-SH6VUPE4.js → chunk-T5MOQDGL.js} +1 -1
  27. package/dist/bundle/chunks/{chunk-CNYWF72S.js → chunk-VKECIIYV.js} +1 -1
  28. package/dist/bundle/chunks/{chunk-U7BNOE23.js → chunk-VNYUGCC7.js} +1 -1
  29. package/dist/bundle/chunks/{chunk-ZPMJIPQC.js → chunk-YSNNL5HU.js} +1 -1
  30. package/dist/bundle/chunks/cli-main-F6WNYPUT.js +3 -0
  31. package/dist/bundle/chunks/{help-fast-path-ZUNFOE53.js → help-fast-path-34Z6I2PH.js} +1 -1
  32. package/dist/bundle/chunks/{host-command-V2LDNNXB.js → host-command-PUOIPEBI.js} +1 -1
  33. package/dist/bundle/chunks/{host-lifecycle-PFLJRXEN.js → host-lifecycle-4VJUHV5U.js} +1 -1
  34. package/dist/bundle/chunks/host-lifecycle.js +1 -1
  35. package/dist/bundle/chunks/{interactive-mode-J6KGDRR7.js → interactive-mode-KM454JPC.js} +1 -1
  36. package/dist/bundle/chunks/{list-tips-ASETMEFS.js → list-tips-GWRRD5UC.js} +1 -1
  37. package/dist/bundle/chunks/{multi-session-host-IBVJ3WN7.js → multi-session-host-ZFT6RM2S.js} +2 -2
  38. package/dist/bundle/chunks/{native-loader-WM7NDPEO.js → native-loader-HW7GQYPY.js} +1 -1
  39. package/dist/bundle/chunks/{openai-completions-SA2UJW7Q.js → openai-completions-32RTG3Q4.js} +1 -1
  40. package/dist/bundle/chunks/{package-manager-cli-JJ5R7UWV.js → package-manager-cli-LNQOWI3A.js} +1 -1
  41. package/dist/bundle/chunks/{rg-engine-G2YTYL2T.js → rg-engine-LWZA7TRH.js} +1 -1
  42. package/dist/bundle/chunks/{rotation-stream-EX7PWQ4S.js → rotation-stream-DIKK7U57.js} +1 -1
  43. package/dist/bundle/chunks/{rpc-mode-ITND2BVC.js → rpc-mode-H7QGQI4P.js} +1 -1
  44. package/dist/bundle/chunks/{schedule-command-D6JNRP3D.js → schedule-command-BUFJOM5L.js} +1 -1
  45. package/dist/bundle/chunks/session-control-endpoint-QIIA6MFR.js +3 -0
  46. package/dist/bundle/chunks/{session-picker-K5FIKV6C.js → session-picker-P53MTWTS.js} +1 -1
  47. package/dist/bundle/chunks/session-worker.js +110 -103
  48. package/dist/bundle/chunks/{state-store-IIQL55GC.js → state-store-QGLITAI4.js} +1 -1
  49. package/dist/bundle/cli.js +2 -2
  50. package/dist/bundle/index.js +1 -1
  51. package/dist/bundle/rpc-entry.js +1 -1
  52. package/dist/bundle/runtime-manifest.json +1 -1
  53. package/dist/cli.js +1 -1
  54. package/dist/core/dynamic-prompt/build.d.ts +4 -3
  55. package/dist/core/dynamic-prompt/build.js +4 -2
  56. package/dist/core/dynamic-prompt/handoff.d.ts +9 -1
  57. package/dist/core/dynamic-prompt/handoff.js +11 -1
  58. package/dist/core/dynamic-prompt/intent-gate.js +1 -1
  59. package/dist/core/dynamic-prompt/style.js +4 -2
  60. package/dist/core/dynamic-prompt/types.d.ts +7 -1
  61. package/dist/core/dynamic-prompt/types.js +3 -1
  62. package/dist/core/dynamic-prompt/verification.d.ts +1 -1
  63. package/dist/core/dynamic-prompt/verification.js +2 -1
  64. package/dist/core/extensions/builtin/anthropic-subscription/stream-protocol.d.ts +1 -1
  65. package/dist/core/extensions/builtin/anthropic-subscription/tools.d.ts +9 -2
  66. package/dist/core/extensions/builtin/anthropic-subscription/tools.js +9 -4
  67. package/dist/core/extensions/builtin/ask-user/resume.js +20 -9
  68. package/dist/core/extensions/builtin/ask-user/tool.d.ts +9 -0
  69. package/dist/core/extensions/builtin/ask-user/tool.js +1 -1
  70. package/dist/core/extensions/builtin/cache-keepalive/index.js +17 -4
  71. package/dist/core/extensions/builtin/chat-reply-scrub/index.d.ts +16 -0
  72. package/dist/core/extensions/builtin/chat-reply-scrub/index.js +39 -0
  73. package/dist/core/extensions/builtin/chat-reply-scrub/scaffold-strip.d.ts +7 -0
  74. package/dist/core/extensions/builtin/chat-reply-scrub/scaffold-strip.js +125 -0
  75. package/dist/core/extensions/builtin/index.js +3 -0
  76. package/dist/core/extensions/builtin/permission-system/cli.js +1 -0
  77. package/dist/core/extensions/builtin/permission-system/config.d.ts +1 -0
  78. package/dist/core/extensions/builtin/permission-system/config.js +10 -0
  79. package/dist/core/extensions/builtin/permission-system/index.js +2 -2
  80. package/dist/core/extensions/builtin/permission-system/settings.js +1 -1
  81. package/dist/core/extensions/builtin/permission-system/types.d.ts +1 -1
  82. package/dist/core/extensions/builtin/prompt-preset/claude-fable-5-1.js +5 -5
  83. package/dist/core/extensions/builtin/prompt-preset/claude-fable-5.js +6 -6
  84. package/dist/core/extensions/builtin/prompt-preset/claude-opus-5-5.js +4 -4
  85. package/dist/core/extensions/builtin/prompt-preset/claude-opus-5.js +4 -4
  86. package/dist/core/extensions/builtin/prompt-preset/claude-sonnet-5-5.js +4 -4
  87. package/dist/core/extensions/builtin/prompt-preset/gpt-5.5.js +9 -6
  88. package/dist/core/extensions/builtin/prompt-preset/gpt-5.6.js +10 -7
  89. package/dist/core/extensions/builtin/prompt-preset/gpt-6-astra.js +15 -6
  90. package/dist/core/extensions/builtin/prompt-preset/gpt-surface.d.ts +2 -2
  91. package/dist/core/extensions/builtin/prompt-preset/grok-4.5.js +4 -4
  92. package/dist/core/extensions/builtin/prompt-preset/grok-4.6.js +4 -4
  93. package/dist/core/extensions/builtin/prompt-preset/grok-4.7.js +3 -3
  94. package/dist/core/extensions/builtin/prompt-preset/kimi-k2-6.js +1 -1
  95. package/dist/core/extensions/builtin/prompt-preset/kimi-k2-code.js +1 -1
  96. package/dist/core/extensions/builtin/prompt-preset/kimi-k3.js +6 -6
  97. package/dist/core/extensions/builtin/recommended-models/index.d.ts +1 -1
  98. package/dist/core/extensions/builtin/recommended-models/index.js +1 -1
  99. package/dist/core/extensions/builtin/todotools/first-turn.d.ts +6 -4
  100. package/dist/core/extensions/builtin/todotools/first-turn.js +17 -5
  101. package/dist/core/extensions/builtin/todotools/todo-ask.d.ts +1 -0
  102. package/dist/core/extensions/builtin/todotools/todo-ask.js +1 -1
  103. package/dist/core/extensions/builtin/todotools/tools/todo.js +4 -1
  104. package/dist/core/resource-loader.js +7 -3
  105. package/dist/modes/interactive/components/ask-user-async-widget.d.ts +2 -0
  106. package/dist/modes/interactive/components/ask-user-async-widget.js +10 -0
  107. package/dist/modes/interactive/components/ask-user-question-state.js +3 -1
  108. package/dist/modes/interactive/interactive-mode.js +7 -1
  109. package/dist/modes/interactive/session-control-commands.d.ts +2 -1
  110. package/dist/modes/interactive/session-control-commands.js +15 -6
  111. package/dist/modes/rpc/connection-question-bridge.js +5 -5
  112. package/dist/modes/rpc/custom-capability.d.ts +6 -0
  113. package/dist/modes/rpc/custom-capability.js +6 -0
  114. package/dist/modes/rpc/extension-ui-response.d.ts +11 -0
  115. package/dist/modes/rpc/extension-ui-response.js +18 -0
  116. package/dist/modes/rpc/host-lifecycle.d.ts +5 -2
  117. package/dist/modes/rpc/host-lifecycle.js +14 -8
  118. package/dist/modes/rpc/rpc-client.d.ts +1 -1
  119. package/dist/modes/rpc/rpc-input-validation.d.ts +1 -1
  120. package/dist/modes/rpc/rpc-input-validation.js +3 -3
  121. package/dist/modes/rpc/rpc-mode.d.ts +3 -2
  122. package/dist/modes/rpc/rpc-mode.js +3 -2
  123. package/dist/modes/rpc/rpc-types.d.ts +4 -3
  124. package/dist/modes/rpc/session-command-router.js +4 -1
  125. package/dist/runtime-snapshot/enter.d.ts +1 -1
  126. package/dist/runtime-snapshot/enter.js +17 -7
  127. package/dist/runtime-snapshot/file-copier.d.ts +24 -0
  128. package/dist/runtime-snapshot/file-copier.js +76 -0
  129. package/dist/runtime-snapshot/layout.d.ts +8 -4
  130. package/dist/runtime-snapshot/layout.js +137 -70
  131. package/dist/runtime-snapshot/registry.d.ts +7 -0
  132. package/dist/runtime-snapshot/registry.js +63 -1
  133. package/dist/senpi +1 -1
  134. package/docs/providers.md +1 -1
  135. package/docs/rpc.md +34 -11
  136. package/package.json +6 -6
  137. package/dist/bundle/chunks/chunk-AWEAVBCR.js +0 -2
  138. package/dist/bundle/chunks/chunk-YLQTKWEC.js +0 -6
  139. package/dist/bundle/chunks/cli-main-ZPMYM3A3.js +0 -3
  140. package/dist/bundle/chunks/session-control-endpoint-UJJGG4WI.js +0 -3
@@ -4,6 +4,13 @@ export declare const UNUSED_SNAPSHOT_GRACE_MS: number;
4
4
  * The lock is a directory, so creating it is atomic on every platform and filesystem.
5
5
  */
6
6
  export declare function withRuntimeLock<T>(root: string, task: () => T): T | undefined;
7
+ /**
8
+ * Runs `build` as the only builder of snapshot `id`; undefined when another live builder keeps it
9
+ * past the wait. Building a snapshot copies thousands of files, so it runs under its own lock, not
10
+ * the runtime root's, and never blocks launches of snapshots that already exist. A lock whose
11
+ * holder died is taken over, and the dead builder's staging directories are removed first.
12
+ */
13
+ export declare function withBuildLock<T>(root: string, id: string, build: () => Promise<T>, isAlive?: (pid: number) => boolean): Promise<T | undefined>;
7
14
  export declare function isProcessAlive(pid: number): boolean;
8
15
  export declare function claimRuntimeSnapshot(snapshotDir: string, pid: number): void;
9
16
  /**
@@ -1,11 +1,17 @@
1
- import { mkdirSync, readdirSync, renameSync, rmSync, statSync, unlinkSync, utimesSync, writeFileSync } from "node:fs";
1
+ import { mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, unlinkSync, utimesSync, writeFileSync, } from "node:fs";
2
2
  import { join } from "node:path";
3
+ import { STAGING_PREFIX } from "./layout.js";
3
4
  import { RUNTIME_SNAPSHOT_MARKER } from "./marker.js";
4
5
  const LOCK_NAME = ".lock";
5
6
  const CLAIMS_DIR = "claims";
6
7
  const STALE_LOCK_MS = 30_000;
7
8
  const LOCK_WAIT_MS = 5_000;
8
9
  const LOCK_RETRY_MS = 25;
10
+ const BUILD_LOCK_PREFIX = ".build-";
11
+ const BUILD_WAIT_MS = 120_000;
12
+ const BUILD_RETRY_MS = 50;
13
+ /** A build lock whose holder never wrote its pid (it died right after creating it). */
14
+ const UNOWNED_BUILD_LOCK_MS = 30_000;
9
15
  export const UNUSED_SNAPSHOT_GRACE_MS = 10 * 60_000;
10
16
  const LEFTOVER_MS = 60 * 60_000;
11
17
  function errorCode(error) {
@@ -58,6 +64,62 @@ export function withRuntimeLock(root, task) {
58
64
  rmSync(lock, { recursive: true, force: true });
59
65
  }
60
66
  }
67
+ function buildLockHolder(lock) {
68
+ try {
69
+ const pid = Number(readFileSync(join(lock, "pid"), "utf8"));
70
+ return Number.isSafeInteger(pid) && pid > 0 ? pid : undefined;
71
+ }
72
+ catch (error) {
73
+ if (errorCode(error) === "ENOENT" || errorCode(error) === "ENOTDIR")
74
+ return undefined;
75
+ throw error;
76
+ }
77
+ }
78
+ function isAbandonedBuildLock(lock, now, isAlive) {
79
+ const holder = buildLockHolder(lock);
80
+ return holder === undefined ? ageMs(lock, now) > UNOWNED_BUILD_LOCK_MS : !isAlive(holder);
81
+ }
82
+ /**
83
+ * Runs `build` as the only builder of snapshot `id`; undefined when another live builder keeps it
84
+ * past the wait. Building a snapshot copies thousands of files, so it runs under its own lock, not
85
+ * the runtime root's, and never blocks launches of snapshots that already exist. A lock whose
86
+ * holder died is taken over, and the dead builder's staging directories are removed first.
87
+ */
88
+ export async function withBuildLock(root, id, build, isAlive = isProcessAlive) {
89
+ mkdirSync(root, { recursive: true });
90
+ const lock = join(root, `${BUILD_LOCK_PREFIX}${id}`);
91
+ const deadline = Date.now() + BUILD_WAIT_MS;
92
+ for (;;) {
93
+ try {
94
+ mkdirSync(lock);
95
+ writeFileSync(join(lock, "pid"), String(process.pid));
96
+ break;
97
+ }
98
+ catch (error) {
99
+ if (errorCode(error) !== "EEXIST")
100
+ throw error;
101
+ }
102
+ if (isAbandonedBuildLock(lock, Date.now(), isAlive)) {
103
+ rmSync(lock, { recursive: true, force: true });
104
+ }
105
+ else if (Date.now() > deadline) {
106
+ return undefined;
107
+ }
108
+ else {
109
+ sleepSync(BUILD_RETRY_MS);
110
+ }
111
+ }
112
+ try {
113
+ for (const name of readdirSync(root)) {
114
+ if (name.startsWith(`${STAGING_PREFIX}${id}-`))
115
+ rmSync(join(root, name), { recursive: true, force: true });
116
+ }
117
+ return await build();
118
+ }
119
+ finally {
120
+ rmSync(lock, { recursive: true, force: true });
121
+ }
122
+ }
61
123
  export function isProcessAlive(pid) {
62
124
  try {
63
125
  process.kill(pid, 0);
package/dist/senpi CHANGED
@@ -168,7 +168,7 @@ else {
168
168
  // an upgrade that rewrites the install cannot remove chunks this session imports later (#2358).
169
169
  const entryPath = fileURLToPath(import.meta.url);
170
170
  const snapshot = isBundledNode
171
- ? prepareRuntimeSnapshot(entryPath, findNodePackageDir(dirname(entryPath)), getAgentDir())
171
+ ? await prepareRuntimeSnapshot(entryPath, findNodePackageDir(dirname(entryPath)), getAgentDir())
172
172
  : undefined;
173
173
  if (snapshot?.kind === "hand-off") {
174
174
  await import(__rewriteRelativeImportExtension(snapshot.entryUrl));
package/docs/providers.md CHANGED
@@ -101,7 +101,7 @@ If your Claude Pro/Max subscription usage through `anthropic-subscription` feels
101
101
 
102
102
  | Lane | Effective TTL | Who controls it | How to override |
103
103
  | --- | --- | --- | --- |
104
- | Anthropic Subscription (subscription, `anthropic-subscription`) | 5 minutes | The Claude SDK owns `cache_control`; senpi cannot add breakpoints. senpi reports 300s for this lane so cache-aware budgets (tool waits, goal timing) size themselves correctly. | Not overridable |
104
+ | Anthropic Subscription (subscription, `anthropic-subscription`) | 1 hour on a Claude subscription; 5 minutes when `ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN`, `ANTHROPIC_BASE_URL` or a Bedrock/Vertex/Foundry switch is set | Claude Code owns `cache_control` and picks the TTL; senpi cannot add breakpoints. senpi reports the same TTL so cache-aware budgets (tool waits, goal timing) size themselves correctly. A subscription past its usage limits drops to 5 minutes, which senpi cannot observe. | `CLAUDE_CODE_PROMPT_CACHE_TTL=5m` or `1h` (also read by Claude Code) |
105
105
  | Direct Anthropic API (`api.anthropic.com`, API key or OAuth token) | 5 minutes | senpi follows Anthropic's default cache retention. Opting into 1h retention makes cache writes cost 2x base input vs 1.25x for 5m ([Anthropic prompt caching](https://docs.claude.com/en/docs/build-with-claude/prompt-caching)). | Set `PI_CACHE_RETENTION=long` or `cacheRetention: "long"` |
106
106
  | Anthropic-compatible providers (kimi-coding, fireworks, gateways) | 5 minutes | The 1h TTL is gated on the native `api.anthropic.com` base URL, so these lanes stay short. | `cacheRetention` |
107
107
 
package/docs/rpc.md CHANGED
@@ -753,15 +753,21 @@ Where a session's replies render decides its system prompt. `terminal` is the cl
753
753
  shows replies as chat, such as the OmO Desktop) keeps the same intent routing and stop-condition rules but asks for no
754
754
  visible `I read this as ...` routing line, and tells the agent that tool and hook feedback (comment-checker findings,
755
755
  language-server availability, internal notices) is for it to act on, not to report unless it changes the result.
756
+ `chat` (a chat bridge that posts each reply to people in a conversation) takes every `app` rule and also drops the
757
+ handoff block and todo/ledger lines: the reply is the answer itself, and the todo tool emits no "Handoff due" cue.
758
+ As a backstop, the `chat-reply-scrub` builtin removes a leading routing line, a handoff block and todo-ledger lines
759
+ from each finalized assistant message on `chat` before it is emitted or persisted; streamed `message_update` deltas are
760
+ not rewritten.
756
761
 
757
- A multi-session host decides the surface per session: `open_session.promptSurface: "terminal" | "app"` builds that
762
+ A multi-session host decides the surface per session: `open_session.promptSurface: "terminal" | "app" | "chat"` builds that
758
763
  session's prompt for the named surface, so one host serves a terminal client and an app client at once. An omitted field
759
- uses the host process's `SENPI_PROMPT_SURFACE` (`app` selects the app surface; unset or any other value is `terminal`),
764
+ uses the host process's `SENPI_PROMPT_SURFACE` (`app` or `chat` selects that surface; unset or any other value is `terminal`),
760
765
  which remains the only switch for a single-process `--mode rpc` client. A later `open_session` that attaches to a live
761
766
  session with another `promptSurface` rebuilds that session's prompt; an attach without the field keeps the current
762
767
  surface, and `new_session` / `switch_session` / `fork` inside the session keep it too. Probe `prompt_surface` in
763
- `get_protocol_info` before sending the field; an older host ignores it. Any value other than `terminal` or `app` is
764
- refused with `invalid_launch_profile`.
768
+ `get_protocol_info` before sending the field; an older host ignores it. Probe `prompt_surface_chat` before sending
769
+ `chat`: a host without it refuses `chat` with `invalid_launch_profile`, so a gateway falls back to `app`. Any value other
770
+ than `terminal`, `app` or `chat` is refused with `invalid_launch_profile`.
765
771
 
766
772
  ### Session auto-titling
767
773
 
@@ -998,9 +1004,10 @@ registrant opens no socket and writes no registry directory.
998
1004
  still holds after it.
999
1005
  - `wake { delivery_ids? }`: one drain pass, answered `{ admitted: [{ delivery_id, kind }] }`.
1000
1006
  - `extension_ui_response`: answers only a question this session asked and still waits on
1001
- (`unknown_request` / `invalid_response` otherwise). `uiRequestId` names the question (a pending
1002
- question id from the `question` feed); without it, `id` does. The reply carries the frame's `id`,
1003
- as on a host (see "Extension UI Responses").
1007
+ (`unknown_request` / `question_incomplete` / `invalid_response` otherwise). `uiRequestId` names the
1008
+ question (a pending question id from the `question` feed); without it, `id` does. The answer settles by
1009
+ the host's rule (see `question`) and the reply carries the frame's `id`, as on a host (see "Extension
1010
+ UI Responses").
1004
1011
  - `prompt`, `steer` and `follow_up` are `unsupported`.
1005
1012
  - **Admission.** A message from another session enters only through the registrant's drain, which calls
1006
1013
  `pi.session.admitExternalMessage({ delivery_id, text, deliverAs, expected_turn_id? })`. One synchronous call
@@ -1283,9 +1290,15 @@ containment, or containment of arbitrary native code. They are not an extension
1283
1290
 
1284
1291
  ### D1 normative table (multi-session mode)
1285
1292
 
1293
+ Hosts that support `open_session.permissionPreset: "accept-edits"` advertise
1294
+ `permission_preset_accept_edits`. This preset allows project read/list/grep/edit
1295
+ and asks for bash, external_directory and other tools. Clients without that
1296
+ capability must use `ask`, never `workspace`, for an edit-only approval promise.
1297
+ Explicit permission rules and remembered approvals retain their existing precedence.
1298
+
1286
1299
  | Command | Params | Success data | Notes |
1287
1300
  | --- | --- | --- | --- |
1288
- | `get_protocol_info` | - | `{ protocolVersion: 1, serverVersion: string, capabilities: string[], mode: "classic"\|"multi", instanceId: string, generation: number, engineVersion: string, engineOrdinal: [y, m, d, n, epoch], launch_profile: { profile_id, core }, memory_pressure?: boolean }` | Answered in BOTH modes; side-effect-free; the capability probe. Multi-session hosts include `multi_session`, `retain_on_disconnect`, `session_kind`, `session_context`, `auto_title_per_session`, `durable_session_id` and `prompt_surface` plus the negotiated launch capabilities. Those are HOST capabilities (a client never sends them) and are advertised only in multi-session mode, where the host owns the attachment refcount and the per-session launch profile. The identity fields are described under "Host identity" above; compatibility is decided from `protocolVersion`, `capabilities` and `engineOrdinal`, NEVER from `serverVersion`. |
1301
+ | `get_protocol_info` | - | `{ protocolVersion: 1, serverVersion: string, capabilities: string[], mode: "classic"\|"multi", instanceId: string, generation: number, engineVersion: string, engineOrdinal: [y, m, d, n, epoch], launch_profile: { profile_id, core }, memory_pressure?: boolean }` | Answered in BOTH modes; side-effect-free; the capability probe. Multi-session hosts include `multi_session`, `retain_on_disconnect`, `session_kind`, `session_context`, `auto_title_per_session`, `durable_session_id`, `prompt_surface` and `prompt_surface_chat` plus the negotiated launch capabilities. Those are HOST capabilities (a client never sends them) and are advertised only in multi-session mode, where the host owns the attachment refcount and the per-session launch profile. The identity fields are described under "Host identity" above; compatibility is decided from `protocolVersion`, `capabilities` and `engineOrdinal`, NEVER from `serverVersion`. |
1289
1302
  | `open_session` | `sessionPath?`, `cwd?`, `provider?`, `modelId?`, `thinkingLevel?`, `permissionPreset?`, `retain_on_disconnect?`, `kind?`, `context?`, `auto_title?`, `durableSessionId?`, `promptSurface?` (all optional; paths MUST be absolute) | `{ sessionId, state: RpcSessionState, attached?: true }` | `sessionPath` = today's `--session` semantics (open-if-exists else create persisting there, `session-manager.ts:926-940`); `provider`/`modelId` applied only on create (resume restores the session's model — mirrors `SenpiSessionRuntime.ts:198-200`); params form the immutable launch profile (D8). When the path is already held by a fully-open session, the open ATTACHES to it: same routing handle, `attached: true`, one more attachment counted; the runtime is torn down only when the last attachment closes. Idle sessions past the eviction window are closed by the host itself. `retain_on_disconnect: true` (default false) makes a dropped connection DETACH from this session instead of closing it — see "Retained sessions" below. `kind` (default `interactive`) and the opaque `context` map are described under "Session kind and context" above; both are stored frozen for the session's life and never influence auth, model or resource resolution. |
1290
1303
  | `close_session` | `sessionId` | `{}` | Refused with `unknown_session` when the requesting connection never attached to that handle (a close releases the CALLER's attachment, and `list_sessions` publishes every handle). Otherwise aborts active work, awaits agent idle + settled persistence for up to the host grace window (default 10s), then quarantines any worker that has not exited without releasing its path reservation; its response is the LAST record tagged with that handle for the first closer — no events after (test-pinned). An admitted concurrent close joins the same teardown and receives its own successful response; output saturation rejects admission with the bounded close-overflow/resync notice described above. |
1291
1304
  | `list_sessions` | `include_workers?` (default false) | `{ sessions: [{ sessionId, durableSessionId, sessionPath, cwd, name, status, attachments, kind, context? }] }` | Includes `opening`/`closing` entries. Internally quarantined workers remain externally `closing` until exit. `attachments` is the session's live client attachment count; `0` on an `open` row is a retained session with no client attached. Every row carries `kind`. Rows with `kind: "worker"` are omitted unless `include_workers: true`, and `context` is published ONLY on that listing — a default listing carries no `context` at all. |
@@ -1319,7 +1332,7 @@ In the response `error` field, machine-matchable:
1319
1332
  - `open_failed: <detail>`
1320
1333
  - `invalid_session_context: <detail>` (`open_session.context` past a documented cap: more than 32 keys, a key that does not match `^[a-z][a-z0-9_]*$`, a non-string or >16 KiB value, or more than 32 KiB of JSON in total; the detail names the cap and its byte budget)
1321
1334
  - `invalid_session_kind: <detail>` (`open_session.kind` other than `interactive` or `worker`)
1322
- - `invalid_launch_profile: <detail>` (`open_session.auto_title` present but not a boolean, or `open_session.promptSurface` other than `terminal` or `app`)
1335
+ - `invalid_launch_profile: <detail>` (`open_session.auto_title` present but not a boolean, or `open_session.promptSurface` other than `terminal`, `app` or `chat`)
1323
1336
  - `host_memory_pressure` (sent only by hosts released before #2207, which declined to CREATE a `kind: "worker"` session above `SENPI_RPC_HOST_RSS_REFUSE_MB`; `errorData { rssMb, retry_after_ms }` says when to ask again. Current hosts never refuse an open for memory; a client talking to an older generation waits and retries, it never starts a second host or a per-child process)
1324
1337
  - `warm_failed: <detail>` (`warm` could not load its profile, for example a `cwd` that does not exist; not remembered, so a retry loads again)
1325
1338
  - `media_not_found` (`get_media` for an unknown `toolCallId`, or a `contentIndex` that does not point at an image block)
@@ -3407,7 +3420,17 @@ Present one or more questions to the user. Requires the `question` client capabi
3407
3420
  }
3408
3421
  ```
3409
3422
 
3410
- Expected response: `extension_ui_response` with `answers` (a map of question id to `{ selected: string[], text?: string }`) and an optional `comment`. Partial answers are allowed: unanswered question ids are reported back to the model. Send `cancelled: true` to dismiss.
3423
+ Expected response: `extension_ui_response` with `answers` (a map of question id to `{ selected: string[], text?: string }`) and an optional `comment`. Partial answers are allowed: the model is told which questions went unanswered, by header. Send `cancelled: true` to dismiss.
3424
+
3425
+ A multi-session host and a terminal control endpoint settle a `question` answer by one rule, so the same frame reaches the model as the same message on either surface:
3426
+
3427
+ - `cancelled: true` dismisses the question.
3428
+ - Otherwise `answers` and `comment` are read; a `value` or `confirmed` in the same frame belongs to the other dialog methods and is ignored.
3429
+ - A non-blank `comment` settles it `comment-submitted`: the model receives `The user responded: <comment>`, then any answered questions and the headers of the unanswered ones (`Unanswered: <header>, ...`). A frame that carries its text only as `comment` with `answers: {}` is a complete answer.
3430
+ - Without a comment, any entry in `answers` settles it `answered`.
3431
+ - Neither a non-blank comment nor an answer is refused `question_incomplete`, and the question stays pending.
3432
+
3433
+ A terminal refuses a frame whose `answers` is missing or malformed with `invalid_response`.
3411
3434
 
3412
3435
  While the question is open, the client may send `extension_ui_progress` frames with draft `answers` and `comment`. Each progress frame resets the idle timer; the host emits `question_updated` with the refreshed `deadlineAtMs` and `remainingMs`.
3413
3436
 
@@ -3454,7 +3477,7 @@ carrying the frame's `id`:
3454
3477
  `success: true` means the answer resolved a pending request. A refusal carries the same `id` and an
3455
3478
  `error`: on a host `question_incomplete` (a `question` answer with neither answers nor a comment),
3456
3479
  `question_already_resolved` (a late answer) or `unknown_extension_ui_request` (no request of that
3457
- session has that id); on a terminal `unknown_request` or `invalid_response`. A single-session stdio
3480
+ session has that id); on a terminal `unknown_request`, `question_incomplete` or `invalid_response`. A single-session stdio
3458
3481
  connection answers every response it resolves the same way, and ignores one that matches none of its
3459
3482
  requests. A client may still fire and forget: the reply is an ordinary `response` record.
3460
3483
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/senpi",
3
- "version": "2026.9.29-5",
3
+ "version": "2026.9.30",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "piConfig": {
@@ -56,9 +56,9 @@
56
56
  },
57
57
  "dependencies": {
58
58
  "@earendil-works/chord": "0.85.1",
59
- "@earendil-works/pi-agent-core": "npm:@code-yeongyu/senpi-agent-core@2026.9.29-5",
60
- "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.9.29-5",
61
- "@earendil-works/pi-tui": "npm:@code-yeongyu/senpi-tui@2026.9.29-5",
59
+ "@earendil-works/pi-agent-core": "npm:@code-yeongyu/senpi-agent-core@2026.9.30",
60
+ "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.9.30",
61
+ "@earendil-works/pi-tui": "npm:@code-yeongyu/senpi-tui@2026.9.30",
62
62
  "@silvia-odwyer/photon-node": "0.3.4",
63
63
  "chalk": "6.0.0",
64
64
  "cross-spawn": "7.0.6",
@@ -77,8 +77,8 @@
77
77
  "yaml": "2.9.1",
78
78
  "@anthropic-ai/claude-agent-sdk": "0.3.284",
79
79
  "@anthropic-ai/sdk": "0.127.0",
80
- "@code-yeongyu/senpi-codemode": "2026.9.29-5",
81
- "@earendil-works/pi-pty": "npm:@code-yeongyu/senpi-pty@2026.9.29-5",
80
+ "@code-yeongyu/senpi-codemode": "2026.9.30",
81
+ "@earendil-works/pi-pty": "npm:@code-yeongyu/senpi-pty@2026.9.30",
82
82
  "@modelcontextprotocol/sdk": "1.30.0",
83
83
  "@mozilla/readability": "0.6.0",
84
84
  "linkedom": "0.18.13",
@@ -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{HOST_GENERATION_ENV,HOST_INSTANCE_ID_ENV}from"./chunk-CIRZTP7W.js";import{engineBuildIdentity}from"./chunk-AGVPQLIU.js";import{parseArgs,resolveSessionRuntime}from"./chunk-7CQX3QFV.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",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",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 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,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,RPC_CLIENT_CAPABILITIES_ENV,parseClientCapabilities,DEFAULT_CUSTOM_EXTENSION_LABEL,buildCustomUnsupportedRequest,hostInstanceId,hostGeneration,hostLaunchProfile,protocolIdentity};