@code-yeongyu/senpi 2026.10.9 → 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.
- package/CHANGELOG.md +41 -0
- package/dist/bundle/chunks/{app-server-command-2HNDZGTR.js → app-server-command-WZG7WYMD.js} +1 -1
- package/dist/bundle/chunks/{chunk-MJGHZT4N.js → chunk-3A3C5VVO.js} +1 -1
- package/dist/bundle/chunks/{chunk-CB77NQMN.js → chunk-7GVD54RM.js} +1 -1
- package/dist/bundle/chunks/{chunk-6EH7QXOG.js → chunk-CTERKXVZ.js} +2 -2
- package/dist/bundle/chunks/{chunk-CM5QTRC2.js → chunk-H67EVDWG.js} +1 -1
- package/dist/bundle/chunks/chunk-IZOTKUWG.js +2 -0
- package/dist/bundle/chunks/{chunk-LUWWM7GA.js → chunk-RADHRQ4K.js} +3 -3
- package/dist/bundle/chunks/chunk-RRU5LXMS.js +3 -0
- package/dist/bundle/chunks/chunk-ULYOMGXX.js +14 -0
- package/dist/bundle/chunks/{chunk-7Q3EY6HH.js → chunk-UMJL74VN.js} +1 -1
- package/dist/bundle/chunks/{chunk-DERNLWDS.js → chunk-W6B7SEAK.js} +146 -144
- package/dist/bundle/chunks/{cli-main-VMDAGZCA.js → cli-main-T4T5UNRJ.js} +1 -1
- package/dist/bundle/chunks/{help-fast-path-JBQLYO7W.js → help-fast-path-44VUGB7H.js} +1 -1
- package/dist/bundle/chunks/{host-command-HTYYQBSG.js → host-command-X64D7PT3.js} +1 -1
- package/dist/bundle/chunks/{host-lifecycle-XZF6IKKO.js → host-lifecycle-GC3GIR2N.js} +1 -1
- package/dist/bundle/chunks/{interactive-mode-U7IH4VMJ.js → interactive-mode-ULFSI46Z.js} +1 -1
- package/dist/bundle/chunks/{multi-session-host-HHIC6N7C.js → multi-session-host-VM7WJBC5.js} +1 -1
- package/dist/bundle/chunks/{package-manager-cli-2FXWPM2U.js → package-manager-cli-OAFUQY7E.js} +1 -1
- package/dist/bundle/chunks/{rpc-mode-K5YGBGQO.js → rpc-mode-XVRJJG3K.js} +1 -1
- package/dist/bundle/chunks/{schedule-command-CVAPK2WQ.js → schedule-command-EQ4PQCRP.js} +1 -1
- package/dist/bundle/chunks/{session-control-endpoint-THEF3K7M.js → session-control-endpoint-FEGOYWHN.js} +2 -2
- package/dist/bundle/chunks/session-worker.js +59 -57
- package/dist/bundle/cli.js +1 -1
- 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/core/agent-session-runtime.d.ts +9 -0
- package/dist/core/agent-session-runtime.js +22 -0
- package/dist/core/agent-session.d.ts +12 -1
- package/dist/core/agent-session.js +38 -4
- package/dist/core/extensions/builtin/btw/panel.d.ts +3 -1
- package/dist/core/extensions/builtin/btw/panel.js +14 -9
- package/dist/core/extensions/builtin/compaction/context-pipeline.d.ts +5 -0
- package/dist/core/extensions/builtin/compaction/context-pipeline.js +1 -0
- package/dist/core/extensions/builtin/compaction/index.js +2 -1
- package/dist/core/extensions/builtin/compaction/lane-policy.d.ts +7 -0
- package/dist/core/extensions/builtin/compaction/lane-policy.js +28 -15
- package/dist/core/extensions/builtin/compaction/log.d.ts +2 -0
- package/dist/core/extensions/builtin/compaction/log.js +2 -1
- package/dist/core/extensions/builtin/goal/cache-warm-renderer.js +14 -8
- package/dist/core/extensions/builtin/goal/cache-warm.d.ts +4 -2
- package/dist/core/extensions/builtin/goal/monitor-continuation.js +6 -0
- package/dist/core/extensions/builtin/mcp/commands.js +21 -26
- package/dist/core/extensions/builtin/mcp/config-edit.d.ts +2 -0
- package/dist/core/extensions/builtin/mcp/config-edit.js +5 -2
- package/dist/core/extensions/builtin/mcp/manager-view.d.ts +34 -0
- package/dist/core/extensions/builtin/mcp/manager-view.js +136 -0
- package/dist/core/extensions/builtin/mcp/manager.d.ts +8 -0
- package/dist/core/extensions/builtin/mcp/manager.js +193 -0
- package/dist/core/extensions/builtin/mcp/service.d.ts +3 -1
- package/dist/core/extensions/builtin/mcp/service.js +4 -0
- package/dist/core/extensions/builtin/rules/rules/finder.js +8 -3
- package/dist/core/retry-fallback/expansion.js +9 -3
- package/dist/core/sdk.js +7 -3
- package/dist/core/thinking-levels.d.ts +23 -1
- package/dist/core/thinking-levels.js +37 -0
- package/dist/core/trust-manager.js +50 -0
- package/dist/main.js +12 -13
- package/dist/modes/interactive/interactive-mode.js +8 -0
- package/dist/modes/interactive/session-control-endpoint.js +33 -8
- package/dist/modes/interactive/session-control-registry.js +3 -1
- package/dist/modes/interactive/session-control-wake.d.ts +11 -5
- package/dist/modes/interactive/session-control-wake.js +38 -16
- package/dist/modes/rpc/connection-handler.d.ts +5 -0
- package/dist/modes/rpc/connection-handler.js +30 -3
- package/dist/modes/rpc/custom-capability.d.ts +6 -0
- package/dist/modes/rpc/custom-capability.js +6 -0
- package/dist/modes/rpc/host-daemon-paths.d.ts +5 -4
- package/dist/modes/rpc/host-daemon-paths.js +17 -9
- package/dist/modes/rpc/host-daemon-registration.d.ts +6 -1
- package/dist/modes/rpc/host-daemon-registration.js +6 -2
- package/dist/modes/rpc/host-session-control.js +6 -4
- package/dist/modes/rpc/rpc-client.d.ts +5 -0
- package/dist/modes/rpc/rpc-client.js +7 -0
- package/dist/modes/rpc/rpc-mode.js +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +9 -0
- package/docs/json.md +3 -0
- package/docs/mcp.md +16 -1
- package/docs/rpc.md +36 -1
- package/package.json +6 -6
- package/dist/bundle/chunks/chunk-6SMU7ANE.js +0 -3
- package/dist/bundle/chunks/chunk-M3BQ2PAQ.js +0 -14
- package/dist/bundle/chunks/chunk-VJJ53ORU.js +0 -2
|
@@ -34,7 +34,7 @@ import { ClientAdmissions } from "./client-admissions.js";
|
|
|
34
34
|
import { handleClientInput } from "./client-input-handler.js";
|
|
35
35
|
import { ClientMessageEvents } from "./client-message-events.js";
|
|
36
36
|
import { ConnectionQuestionBridge, degradeQuestion, sessionQuestionBridges } from "./connection-question-bridge.js";
|
|
37
|
-
import { AUTO_TITLE_SESSIONS_CAPABILITY, buildCustomUnsupportedRequest, CONTINUE_FROM_LEAF_CAPABILITY, DEFAULT_CUSTOM_EXTENSION_LABEL, EXTENSION_EVENTS_CAPABILITY, MEDIA_PLACEHOLDERS_CAPABILITY, QUESTION_CAPABILITY, } from "./custom-capability.js";
|
|
37
|
+
import { AUTO_TITLE_SESSIONS_CAPABILITY, buildCustomUnsupportedRequest, CONTINUE_FROM_LEAF_CAPABILITY, DEFAULT_CUSTOM_EXTENSION_LABEL, EXTENSION_EVENTS_CAPABILITY, MEDIA_PLACEHOLDERS_CAPABILITY, QUESTION_CAPABILITY, RETRY_FALLBACK_COMMAND_CAPABILITY, } from "./custom-capability.js";
|
|
38
38
|
import { createRpcEventOutputBuffer } from "./event-output-buffer.js";
|
|
39
39
|
import { settleExtensionUiResponse } from "./extension-ui-response.js";
|
|
40
40
|
import { HostSessionControl } from "./host-session-control.js";
|
|
@@ -42,7 +42,7 @@ import { createRpcLoginPromptCallbacks } from "./login-prompts.js";
|
|
|
42
42
|
import { answerMemoryReport } from "./memory-report-command.js";
|
|
43
43
|
import { protocolIdentity } from "./protocol-identity.js";
|
|
44
44
|
import { buildRpcCommandsForSession, createCommandsChangedEvent, rpcCommandListDigest } from "./rpc-command-surface.js";
|
|
45
|
-
import { rpcCommandPayloadError, rpcCommandShapeError, rpcMessageLengthError } from "./rpc-input-validation.js";
|
|
45
|
+
import { rpcCommandPayloadError, rpcCommandShapeError, rpcMessageLengthError, sessionRetryFallbackError, } from "./rpc-input-validation.js";
|
|
46
46
|
import { buildRpcSessionState } from "./rpc-session-state.js";
|
|
47
47
|
import { RPC_ERROR_MEDIA_NOT_FOUND } from "./rpc-types.js";
|
|
48
48
|
import { SessionExtensionUiRequests } from "./session-extension-ui-requests.js";
|
|
@@ -158,6 +158,8 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
158
158
|
let session = runtimeHost.session;
|
|
159
159
|
let sessionControl;
|
|
160
160
|
const promptCalls = new Set();
|
|
161
|
+
// Set by the first command that asks this session for a turn; `set_retry_fallback` is refused after it.
|
|
162
|
+
let turnRequested = false;
|
|
161
163
|
let unsubscribe;
|
|
162
164
|
let unsubscribeBackpressure;
|
|
163
165
|
let unsubscribeLoadedSurfaces;
|
|
@@ -737,6 +739,7 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
737
739
|
MEDIA_PLACEHOLDERS_CAPABILITY,
|
|
738
740
|
DURABLE_CLIENT_MESSAGE_ID_CAPABILITY,
|
|
739
741
|
CONTINUE_FROM_LEAF_CAPABILITY,
|
|
742
|
+
...(options.retryFallbackCommand ? [RETRY_FALLBACK_COMMAND_CAPABILITY] : []),
|
|
740
743
|
...(options.capabilities ?? []),
|
|
741
744
|
]),
|
|
742
745
|
],
|
|
@@ -750,6 +753,7 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
750
753
|
// Prompting
|
|
751
754
|
// =================================================================
|
|
752
755
|
case "prompt": {
|
|
756
|
+
turnRequested = true;
|
|
753
757
|
const admission = handleClientInput(session, command, { output, promptCalls });
|
|
754
758
|
promptCalls.add(admission);
|
|
755
759
|
void admission.finally(() => promptCalls.delete(admission));
|
|
@@ -769,6 +773,7 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
769
773
|
return success(id, "append_session_entry");
|
|
770
774
|
}
|
|
771
775
|
case "continue_from_leaf": {
|
|
776
|
+
turnRequested = true;
|
|
772
777
|
try {
|
|
773
778
|
await session.continueFromLeaf();
|
|
774
779
|
return success(id, "continue_from_leaf");
|
|
@@ -780,6 +785,9 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
780
785
|
}
|
|
781
786
|
}
|
|
782
787
|
case "send_custom_message": {
|
|
788
|
+
// A context message that asks for no turn does not block a launch-time chain, like an extension's.
|
|
789
|
+
if (command.triggerTurn === true)
|
|
790
|
+
turnRequested = true;
|
|
783
791
|
await session.sendCustomMessage({
|
|
784
792
|
customType: command.customType,
|
|
785
793
|
content: command.content,
|
|
@@ -790,6 +798,7 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
790
798
|
}
|
|
791
799
|
case "steer":
|
|
792
800
|
case "follow_up": {
|
|
801
|
+
turnRequested = true;
|
|
793
802
|
await handleClientInput(session, command, { output, promptCalls });
|
|
794
803
|
return undefined;
|
|
795
804
|
}
|
|
@@ -883,7 +892,7 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
883
892
|
return success(id, "cycle_model", result);
|
|
884
893
|
}
|
|
885
894
|
case "get_available_models": {
|
|
886
|
-
const models = await session.modelRegistry.getAvailable();
|
|
895
|
+
const models = await session.modelRegistry.modelRuntime.getAvailable();
|
|
887
896
|
return success(id, "get_available_models", {
|
|
888
897
|
models: models.map((model) => ({
|
|
889
898
|
...model,
|
|
@@ -989,6 +998,24 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
989
998
|
session.abortRetry();
|
|
990
999
|
return success(id, "abort_retry");
|
|
991
1000
|
}
|
|
1001
|
+
case "set_retry_fallback": {
|
|
1002
|
+
if (!options.retryFallbackCommand) {
|
|
1003
|
+
return error(id, "set_retry_fallback", "set_retry_fallback is for a single-session rpc process; a host session takes open_session.retryFallback.");
|
|
1004
|
+
}
|
|
1005
|
+
const profileError = sessionRetryFallbackError(command.retryFallback);
|
|
1006
|
+
if (profileError !== undefined || command.retryFallback === undefined) {
|
|
1007
|
+
return error(id, "set_retry_fallback", profileError ?? "set_retry_fallback needs retryFallback.");
|
|
1008
|
+
}
|
|
1009
|
+
// A launch-time setting: refused once this connection asked for a turn (even one not started yet), while
|
|
1010
|
+
// any turn runs, or once the session holds turn history, so a chain never changes under a turn or a
|
|
1011
|
+
// retry already in flight. Extension context messages (`custom`, e.g. one a component adds on
|
|
1012
|
+
// session_start) are not a turn and do not count.
|
|
1013
|
+
if (turnRequested || session.isStreaming || session.messages.some((message) => message.role !== "custom")) {
|
|
1014
|
+
return error(id, "set_retry_fallback", "set_retry_fallback must arrive before the session's first turn.");
|
|
1015
|
+
}
|
|
1016
|
+
runtimeHost.setRetryFallback(command.retryFallback);
|
|
1017
|
+
return success(id, "set_retry_fallback");
|
|
1018
|
+
}
|
|
992
1019
|
// =================================================================
|
|
993
1020
|
// Bash
|
|
994
1021
|
// =================================================================
|
|
@@ -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.
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
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.
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
24
|
+
let inbox;
|
|
25
25
|
const registration = {
|
|
26
26
|
scheduler,
|
|
27
27
|
release: () => {
|
|
28
28
|
unsubscribeIdle();
|
|
29
29
|
unsubscribeEmitted();
|
|
30
|
-
|
|
30
|
+
inbox?.stop();
|
|
31
31
|
scheduler.dispose();
|
|
32
32
|
},
|
|
33
33
|
};
|
|
34
34
|
try {
|
|
35
|
-
|
|
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
|
|
48
|
+
// Anything that reached the inbox before this point is picked up by this first pass, and anything
|
|
49
|
+
// written after it but before the watch was armed (no event for it) by the pass once arming settled.
|
|
49
50
|
wake("inbox");
|
|
51
|
+
void inbox.armed.then(() => wake("inbox"));
|
|
50
52
|
return {
|
|
51
53
|
status: "registered",
|
|
52
54
|
socket: this.socket,
|
|
@@ -330,6 +330,11 @@ export declare class RpcClient {
|
|
|
330
330
|
* Abort in-progress retry.
|
|
331
331
|
*/
|
|
332
332
|
abortRetry(): Promise<void>;
|
|
333
|
+
/**
|
|
334
|
+
* Give this single-session process its fallback chain before the first turn (`set_retry_fallback`).
|
|
335
|
+
* Requires the `retry_fallback_command` capability.
|
|
336
|
+
*/
|
|
337
|
+
setRetryFallback(retryFallback: SessionRetryFallbackProfile): Promise<void>;
|
|
333
338
|
/**
|
|
334
339
|
* Execute a bash command.
|
|
335
340
|
*/
|
|
@@ -599,6 +599,13 @@ export class RpcClient {
|
|
|
599
599
|
async abortRetry() {
|
|
600
600
|
await this.send({ type: "abort_retry" });
|
|
601
601
|
}
|
|
602
|
+
/**
|
|
603
|
+
* Give this single-session process its fallback chain before the first turn (`set_retry_fallback`).
|
|
604
|
+
* Requires the `retry_fallback_command` capability.
|
|
605
|
+
*/
|
|
606
|
+
async setRetryFallback(retryFallback) {
|
|
607
|
+
await this.send({ type: "set_retry_fallback", retryFallback });
|
|
608
|
+
}
|
|
602
609
|
/**
|
|
603
610
|
* Execute a bash command.
|
|
604
611
|
*/
|
|
@@ -128,7 +128,7 @@ export async function runRpcMode(runtimeHost) {
|
|
|
128
128
|
// it from a client handshake; a plain stdio client that sets nothing gets
|
|
129
129
|
// byte-identical default behavior.
|
|
130
130
|
const capabilities = parseClientCapabilities(envValue("RPC_CLIENT_CAPABILITIES"));
|
|
131
|
-
const handler = createRpcConnectionHandler(runtimeHost, sink, { capabilities });
|
|
131
|
+
const handler = createRpcConnectionHandler(runtimeHost, sink, { capabilities, retryFallbackCommand: true });
|
|
132
132
|
const signalCleanupHandlers = [];
|
|
133
133
|
const registerSignalHandlers = () => {
|
|
134
134
|
const signals = ["SIGTERM"];
|
|
@@ -173,6 +173,10 @@ type RpcSessionCommand = {
|
|
|
173
173
|
} | {
|
|
174
174
|
id?: string;
|
|
175
175
|
type: "abort_retry";
|
|
176
|
+
} | {
|
|
177
|
+
id?: string;
|
|
178
|
+
type: "set_retry_fallback";
|
|
179
|
+
retryFallback: SessionRetryFallbackProfile;
|
|
176
180
|
} | {
|
|
177
181
|
id?: string;
|
|
178
182
|
type: "bash";
|
|
@@ -991,6 +995,11 @@ export type RpcResponse = {
|
|
|
991
995
|
type: "response";
|
|
992
996
|
command: "abort_retry";
|
|
993
997
|
success: true;
|
|
998
|
+
} | {
|
|
999
|
+
id?: string;
|
|
1000
|
+
type: "response";
|
|
1001
|
+
command: "set_retry_fallback";
|
|
1002
|
+
success: true;
|
|
994
1003
|
} | {
|
|
995
1004
|
id?: string;
|
|
996
1005
|
type: "response";
|
package/docs/json.md
CHANGED
|
@@ -22,6 +22,7 @@ type AgentSessionEvent =
|
|
|
22
22
|
| { type: "compaction_end"; reason: "manual" | "threshold" | "overflow"; result: CompactionResult | undefined; aborted: boolean; willRetry: boolean; requestId?: string; accepted?: boolean; rejectionCause?: "cancelled-by-extension" | "external-owner" | "would-overflow" | "circuit-breaker" | "per-turn-cap"; errorMessage?: string }
|
|
23
23
|
| { type: "session_info_changed"; name: string | undefined }
|
|
24
24
|
| { type: "thinking_level_changed"; level: "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max" }
|
|
25
|
+
| { type: "thinking_level_clamped"; provider: string; modelId: string; requestedLevel: "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"; appliedLevel: "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"; reason: "model-not-reasoning" | "level-unsupported" }
|
|
25
26
|
| SystemPromptChangeEvent // type: "system_prompt_change"
|
|
26
27
|
| ExtensionToolHookLifecycleEvent // type: "tool_hook_status"
|
|
27
28
|
| { type: "auto_retry_start"; attempt: number; maxAttempts: number; delayMs: number; errorMessage: string }
|
|
@@ -55,6 +56,8 @@ type JsonAgentSessionEvent =
|
|
|
55
56
|
|
|
56
57
|
`queue_update` emits the full pending steering and follow-up queues whenever they change. `compaction_start`, `compaction_progress`, and `compaction_end` cover both manual and automatic compaction. `session_info_changed` fires when the session display name changes, `thinking_level_changed` when the thinking level changes, `system_prompt_change` (see `SystemPromptChangeEvent` in [`extensions/types.ts`](../src/core/extensions/types.ts)) when a model switch changes the active system prompt, and `tool_hook_status` (see `ExtensionToolHookLifecycleEvent` in [`extensions/runner.ts`](../src/core/extensions/runner.ts)) for extension tool hook start/end phases.
|
|
57
58
|
|
|
59
|
+
`thinking_level_clamped` fires once per model and requested level when an explicit thinking request (such as `--thinking`, `/thinking`, or a remembered per-model level) runs at a lower level than asked. A level that only comes from the global `defaultThinkingLevel` does not count as a request. In RPC mode, the `thinkingSelection` carried by `thinking_level_changed` and `get_state` then also has `requested` and `clampReason`.
|
|
60
|
+
|
|
58
61
|
Other base events come from
|
|
59
62
|
[`AgentEvent`](../../agent/src/types.ts):
|
|
60
63
|
|
package/docs/mcp.md
CHANGED
|
@@ -22,12 +22,27 @@ catalog costs almost nothing until the model actually needs it.
|
|
|
22
22
|
}
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
2. Start senpi. Run `/mcp` for the
|
|
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
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@code-yeongyu/senpi",
|
|
3
|
-
"version": "2026.10.
|
|
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.
|
|
61
|
-
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.10.
|
|
62
|
-
"@earendil-works/pi-tui": "npm:@code-yeongyu/senpi-tui@2026.10.
|
|
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.
|
|
82
|
-
"@earendil-works/pi-pty": "npm:@code-yeongyu/senpi-pty@2026.10.
|
|
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};
|