@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.
- package/CHANGELOG.md +32 -0
- package/dist/bundle/chunks/{anthropic-messages-HEVKB3PL.js → anthropic-messages-ADSIWAJ5.js} +1 -1
- package/dist/bundle/chunks/{app-server-command-32AFWZEH.js → app-server-command-B3ZYM2WN.js} +1 -1
- package/dist/bundle/chunks/{chunk-2WAKOPNA.js → chunk-4P4DUW66.js} +1 -1
- package/dist/bundle/chunks/{chunk-7RVWDN5Z.js → chunk-6GV3L3GZ.js} +1 -1
- package/dist/bundle/chunks/{chunk-OUG6SDT7.js → chunk-6JZQLUAJ.js} +1 -1
- package/dist/bundle/chunks/{chunk-MJQWYN2W.js → chunk-7MKMDWHC.js} +2 -2
- package/dist/bundle/chunks/{chunk-XBGO2JZD.js → chunk-A7AQSA6R.js} +1 -1
- package/dist/bundle/chunks/{chunk-BJICJY47.js → chunk-DVVROXLZ.js} +1 -1
- package/dist/bundle/chunks/chunk-EIG6K5U3.js +6 -0
- package/dist/bundle/chunks/chunk-FAWC2EIR.js +2 -0
- package/dist/bundle/chunks/{chunk-WCAKUCN3.js → chunk-FNCILR4E.js} +1 -1
- package/dist/bundle/chunks/{chunk-XHEMQA2T.js → chunk-HPESFBMB.js} +2 -2
- package/dist/bundle/chunks/{chunk-BBNWT72C.js → chunk-HVBFDRP4.js} +1 -1
- package/dist/bundle/chunks/{chunk-63QHOG5A.js → chunk-I77ENHCQ.js} +81 -74
- package/dist/bundle/chunks/{chunk-UMLU3I2G.js → chunk-ITXF6FAM.js} +1 -1
- package/dist/bundle/chunks/{chunk-77AUUH3Z.js → chunk-IXONH7ML.js} +2 -2
- package/dist/bundle/chunks/{chunk-K2SHQNAA.js → chunk-JX2CSKLG.js} +1 -1
- package/dist/bundle/chunks/{chunk-4VWWEMCJ.js → chunk-JXZ5LDJP.js} +1 -1
- package/dist/bundle/chunks/{chunk-VLTMFAWQ.js → chunk-KWI3KUF2.js} +1 -1
- package/dist/bundle/chunks/{chunk-7CQX3QFV.js → chunk-L4QGZJKQ.js} +1 -1
- package/dist/bundle/chunks/{chunk-AGVPQLIU.js → chunk-MDXUQDGF.js} +1 -1
- package/dist/bundle/chunks/{chunk-2N3BZ5DV.js → chunk-OOA6OL42.js} +1 -1
- package/dist/bundle/chunks/{chunk-2POUJCWM.js → chunk-OVGH7VP3.js} +1 -1
- package/dist/bundle/chunks/{chunk-ZDNR3YVJ.js → chunk-RMVQ5YRX.js} +1 -1
- package/dist/bundle/chunks/{chunk-SH6VUPE4.js → chunk-T5MOQDGL.js} +1 -1
- package/dist/bundle/chunks/{chunk-CNYWF72S.js → chunk-VKECIIYV.js} +1 -1
- package/dist/bundle/chunks/{chunk-U7BNOE23.js → chunk-VNYUGCC7.js} +1 -1
- package/dist/bundle/chunks/{chunk-ZPMJIPQC.js → chunk-YSNNL5HU.js} +1 -1
- package/dist/bundle/chunks/cli-main-F6WNYPUT.js +3 -0
- package/dist/bundle/chunks/{help-fast-path-ZUNFOE53.js → help-fast-path-34Z6I2PH.js} +1 -1
- package/dist/bundle/chunks/{host-command-V2LDNNXB.js → host-command-PUOIPEBI.js} +1 -1
- package/dist/bundle/chunks/{host-lifecycle-PFLJRXEN.js → host-lifecycle-4VJUHV5U.js} +1 -1
- package/dist/bundle/chunks/host-lifecycle.js +1 -1
- package/dist/bundle/chunks/{interactive-mode-J6KGDRR7.js → interactive-mode-KM454JPC.js} +1 -1
- package/dist/bundle/chunks/{list-tips-ASETMEFS.js → list-tips-GWRRD5UC.js} +1 -1
- package/dist/bundle/chunks/{multi-session-host-IBVJ3WN7.js → multi-session-host-ZFT6RM2S.js} +2 -2
- package/dist/bundle/chunks/{native-loader-WM7NDPEO.js → native-loader-HW7GQYPY.js} +1 -1
- package/dist/bundle/chunks/{openai-completions-SA2UJW7Q.js → openai-completions-32RTG3Q4.js} +1 -1
- package/dist/bundle/chunks/{package-manager-cli-JJ5R7UWV.js → package-manager-cli-LNQOWI3A.js} +1 -1
- package/dist/bundle/chunks/{rg-engine-G2YTYL2T.js → rg-engine-LWZA7TRH.js} +1 -1
- package/dist/bundle/chunks/{rotation-stream-EX7PWQ4S.js → rotation-stream-DIKK7U57.js} +1 -1
- package/dist/bundle/chunks/{rpc-mode-ITND2BVC.js → rpc-mode-H7QGQI4P.js} +1 -1
- package/dist/bundle/chunks/{schedule-command-D6JNRP3D.js → schedule-command-BUFJOM5L.js} +1 -1
- package/dist/bundle/chunks/session-control-endpoint-QIIA6MFR.js +3 -0
- package/dist/bundle/chunks/{session-picker-K5FIKV6C.js → session-picker-P53MTWTS.js} +1 -1
- package/dist/bundle/chunks/session-worker.js +110 -103
- package/dist/bundle/chunks/{state-store-IIQL55GC.js → state-store-QGLITAI4.js} +1 -1
- package/dist/bundle/cli.js +2 -2
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/bundle/runtime-manifest.json +1 -1
- package/dist/cli.js +1 -1
- package/dist/core/dynamic-prompt/build.d.ts +4 -3
- package/dist/core/dynamic-prompt/build.js +4 -2
- package/dist/core/dynamic-prompt/handoff.d.ts +9 -1
- package/dist/core/dynamic-prompt/handoff.js +11 -1
- package/dist/core/dynamic-prompt/intent-gate.js +1 -1
- package/dist/core/dynamic-prompt/style.js +4 -2
- package/dist/core/dynamic-prompt/types.d.ts +7 -1
- package/dist/core/dynamic-prompt/types.js +3 -1
- package/dist/core/dynamic-prompt/verification.d.ts +1 -1
- package/dist/core/dynamic-prompt/verification.js +2 -1
- package/dist/core/extensions/builtin/anthropic-subscription/stream-protocol.d.ts +1 -1
- package/dist/core/extensions/builtin/anthropic-subscription/tools.d.ts +9 -2
- package/dist/core/extensions/builtin/anthropic-subscription/tools.js +9 -4
- package/dist/core/extensions/builtin/ask-user/resume.js +20 -9
- package/dist/core/extensions/builtin/ask-user/tool.d.ts +9 -0
- package/dist/core/extensions/builtin/ask-user/tool.js +1 -1
- package/dist/core/extensions/builtin/cache-keepalive/index.js +17 -4
- package/dist/core/extensions/builtin/chat-reply-scrub/index.d.ts +16 -0
- package/dist/core/extensions/builtin/chat-reply-scrub/index.js +39 -0
- package/dist/core/extensions/builtin/chat-reply-scrub/scaffold-strip.d.ts +7 -0
- package/dist/core/extensions/builtin/chat-reply-scrub/scaffold-strip.js +125 -0
- package/dist/core/extensions/builtin/index.js +3 -0
- package/dist/core/extensions/builtin/permission-system/cli.js +1 -0
- package/dist/core/extensions/builtin/permission-system/config.d.ts +1 -0
- package/dist/core/extensions/builtin/permission-system/config.js +10 -0
- package/dist/core/extensions/builtin/permission-system/index.js +2 -2
- package/dist/core/extensions/builtin/permission-system/settings.js +1 -1
- package/dist/core/extensions/builtin/permission-system/types.d.ts +1 -1
- package/dist/core/extensions/builtin/prompt-preset/claude-fable-5-1.js +5 -5
- package/dist/core/extensions/builtin/prompt-preset/claude-fable-5.js +6 -6
- package/dist/core/extensions/builtin/prompt-preset/claude-opus-5-5.js +4 -4
- package/dist/core/extensions/builtin/prompt-preset/claude-opus-5.js +4 -4
- package/dist/core/extensions/builtin/prompt-preset/claude-sonnet-5-5.js +4 -4
- package/dist/core/extensions/builtin/prompt-preset/gpt-5.5.js +9 -6
- package/dist/core/extensions/builtin/prompt-preset/gpt-5.6.js +10 -7
- package/dist/core/extensions/builtin/prompt-preset/gpt-6-astra.js +15 -6
- package/dist/core/extensions/builtin/prompt-preset/gpt-surface.d.ts +2 -2
- package/dist/core/extensions/builtin/prompt-preset/grok-4.5.js +4 -4
- package/dist/core/extensions/builtin/prompt-preset/grok-4.6.js +4 -4
- package/dist/core/extensions/builtin/prompt-preset/grok-4.7.js +3 -3
- package/dist/core/extensions/builtin/prompt-preset/kimi-k2-6.js +1 -1
- package/dist/core/extensions/builtin/prompt-preset/kimi-k2-code.js +1 -1
- package/dist/core/extensions/builtin/prompt-preset/kimi-k3.js +6 -6
- package/dist/core/extensions/builtin/recommended-models/index.d.ts +1 -1
- package/dist/core/extensions/builtin/recommended-models/index.js +1 -1
- package/dist/core/extensions/builtin/todotools/first-turn.d.ts +6 -4
- package/dist/core/extensions/builtin/todotools/first-turn.js +17 -5
- package/dist/core/extensions/builtin/todotools/todo-ask.d.ts +1 -0
- package/dist/core/extensions/builtin/todotools/todo-ask.js +1 -1
- package/dist/core/extensions/builtin/todotools/tools/todo.js +4 -1
- package/dist/core/resource-loader.js +7 -3
- package/dist/modes/interactive/components/ask-user-async-widget.d.ts +2 -0
- package/dist/modes/interactive/components/ask-user-async-widget.js +10 -0
- package/dist/modes/interactive/components/ask-user-question-state.js +3 -1
- package/dist/modes/interactive/interactive-mode.js +7 -1
- package/dist/modes/interactive/session-control-commands.d.ts +2 -1
- package/dist/modes/interactive/session-control-commands.js +15 -6
- package/dist/modes/rpc/connection-question-bridge.js +5 -5
- package/dist/modes/rpc/custom-capability.d.ts +6 -0
- package/dist/modes/rpc/custom-capability.js +6 -0
- package/dist/modes/rpc/extension-ui-response.d.ts +11 -0
- package/dist/modes/rpc/extension-ui-response.js +18 -0
- package/dist/modes/rpc/host-lifecycle.d.ts +5 -2
- package/dist/modes/rpc/host-lifecycle.js +14 -8
- package/dist/modes/rpc/rpc-client.d.ts +1 -1
- package/dist/modes/rpc/rpc-input-validation.d.ts +1 -1
- package/dist/modes/rpc/rpc-input-validation.js +3 -3
- package/dist/modes/rpc/rpc-mode.d.ts +3 -2
- package/dist/modes/rpc/rpc-mode.js +3 -2
- package/dist/modes/rpc/rpc-types.d.ts +4 -3
- package/dist/modes/rpc/session-command-router.js +4 -1
- package/dist/runtime-snapshot/enter.d.ts +1 -1
- package/dist/runtime-snapshot/enter.js +17 -7
- package/dist/runtime-snapshot/file-copier.d.ts +24 -0
- package/dist/runtime-snapshot/file-copier.js +76 -0
- package/dist/runtime-snapshot/layout.d.ts +8 -4
- package/dist/runtime-snapshot/layout.js +137 -70
- package/dist/runtime-snapshot/registry.d.ts +7 -0
- package/dist/runtime-snapshot/registry.js +63 -1
- package/dist/senpi +1 -1
- package/docs/providers.md +1 -1
- package/docs/rpc.md +34 -11
- package/package.json +6 -6
- package/dist/bundle/chunks/chunk-AWEAVBCR.js +0 -2
- package/dist/bundle/chunks/chunk-YLQTKWEC.js +0 -6
- package/dist/bundle/chunks/cli-main-ZPMYM3A3.js +0 -3
- 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 |
|
|
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
|
|
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.
|
|
764
|
-
|
|
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
|
|
1002
|
-
question id from the `question` feed); without it, `id` does. The
|
|
1003
|
-
as on a host (see "Extension
|
|
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 `
|
|
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 `
|
|
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:
|
|
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.
|
|
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.
|
|
60
|
-
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.9.
|
|
61
|
-
"@earendil-works/pi-tui": "npm:@code-yeongyu/senpi-tui@2026.9.
|
|
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.
|
|
81
|
-
"@earendil-works/pi-pty": "npm:@code-yeongyu/senpi-pty@2026.9.
|
|
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};
|