@code-yeongyu/senpi 2026.10.8 → 2026.10.10
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 +37 -0
- package/dist/bundle/chunks/{app-server-command-MJOYE5ZS.js → app-server-command-SZWKEC36.js} +1 -1
- package/dist/bundle/chunks/{chunk-MJGHZT4N.js → chunk-3A3C5VVO.js} +1 -1
- package/dist/bundle/chunks/{chunk-4GUQVY4Z.js → chunk-3YXB7Z7O.js} +1 -1
- package/dist/bundle/chunks/{chunk-6EH7QXOG.js → chunk-6VVV27W2.js} +1 -1
- package/dist/bundle/chunks/{chunk-CB77NQMN.js → chunk-7GVD54RM.js} +1 -1
- 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-K4AZKFRB.js → chunk-J5MUMVWB.js} +2 -2
- package/dist/bundle/chunks/{chunk-V2VQ4BHG.js → chunk-M2IHGL3M.js} +1 -1
- package/dist/bundle/chunks/chunk-N3QEG537.js +14 -0
- package/dist/bundle/chunks/{chunk-LUWWM7GA.js → chunk-RADHRQ4K.js} +3 -3
- package/dist/bundle/chunks/{chunk-BTYFICUC.js → chunk-SB22X6ZR.js} +1 -1
- package/dist/bundle/chunks/{chunk-5MG76BCM.js → chunk-WQ77WBQW.js} +60 -60
- package/dist/bundle/chunks/{cli-main-GWVLFOIP.js → cli-main-C3AS73Q4.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-YLM2U7IX.js → interactive-mode-KUP43WXL.js} +1 -1
- package/dist/bundle/chunks/{list-tips-JQDZPB6C.js → list-tips-IIRFXBTO.js} +1 -1
- package/dist/bundle/chunks/{multi-session-host-KMCBBPTV.js → multi-session-host-EM24ZBGK.js} +1 -1
- package/dist/bundle/chunks/{package-manager-cli-3UIJ4RXY.js → package-manager-cli-MGFHGI5O.js} +1 -1
- package/dist/bundle/chunks/{rpc-mode-PUYBZG27.js → rpc-mode-2GT75YBZ.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-DOIW7IBF.js} +2 -2
- package/dist/bundle/chunks/{session-picker-N463VCDI.js → session-picker-R5N2EKUS.js} +1 -1
- package/dist/bundle/chunks/session-worker.js +73 -73
- 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 +36 -4
- package/dist/core/dynamic-prompt/handoff.js +3 -3
- package/dist/core/dynamic-prompt/intent-gate.js +1 -1
- package/dist/core/dynamic-prompt/style.js +1 -1
- 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 +1 -0
- 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/permission-system/index.js +4 -0
- package/dist/core/extensions/builtin/permission-system/prompt.js +10 -2
- package/dist/core/extensions/builtin/permission-system/types.d.ts +3 -1
- package/dist/core/extensions/builtin/prompt-preset/claude-fable-5-1.js +2 -2
- package/dist/core/extensions/builtin/prompt-preset/claude-fable-5.js +2 -2
- package/dist/core/extensions/builtin/prompt-preset/claude-opus-5-5.js +2 -2
- package/dist/core/extensions/builtin/prompt-preset/claude-opus-5.js +2 -2
- package/dist/core/extensions/builtin/prompt-preset/claude-sonnet-5-5.js +2 -2
- package/dist/core/extensions/builtin/prompt-preset/gpt-5.5.js +3 -3
- package/dist/core/extensions/builtin/prompt-preset/gpt-5.6.js +3 -3
- package/dist/core/extensions/builtin/prompt-preset/gpt-6-astra.d.ts +2 -2
- package/dist/core/extensions/builtin/prompt-preset/gpt-6-astra.js +4 -4
- package/dist/core/extensions/builtin/prompt-preset/gpt-surface.js +2 -2
- package/dist/core/extensions/builtin/prompt-preset/grok-4.5.js +2 -2
- package/dist/core/extensions/builtin/prompt-preset/grok-4.6.js +1 -1
- package/dist/core/extensions/builtin/prompt-preset/grok-4.7.js +1 -1
- package/dist/core/extensions/builtin/prompt-preset/kimi-k3.js +2 -2
- package/dist/core/extensions/builtin/rules/rules/finder.js +8 -3
- package/dist/core/extensions/types.d.ts +4 -0
- 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/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 +40 -4
- 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 +14 -0
- package/docs/json.md +3 -0
- package/docs/rpc.md +48 -0
- package/package.json +6 -6
- package/dist/bundle/chunks/chunk-VJJ53ORU.js +0 -2
- package/dist/bundle/chunks/chunk-X6UDRO7J.js +0 -14
|
@@ -3,12 +3,16 @@
|
|
|
3
3
|
* registers one (the interactive host imports it lazily), so a plain TUI pays nothing.
|
|
4
4
|
*
|
|
5
5
|
* Order matters and each step is undone on failure, so a failed bind leaves no half-registered
|
|
6
|
-
* directory: the
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* directory: the socket is bound (secret 0600, directory 0700) while the session header is made
|
|
7
|
+
* durable, and only once both are done is the endpoint registered - a visible endpoint is one that
|
|
8
|
+
* answers, and its session id is already on disk. Nothing a sender needs waits on work it does not
|
|
9
|
+
* need: the writer stamp's process lookup starts at entry, the inbox watch arms after registration
|
|
10
|
+
* returned, and dead `tui` endpoints of other terminals are reaped after activation. Wakes come
|
|
11
|
+
* from edges only (see `session-control-wake.ts`).
|
|
9
12
|
*/
|
|
10
13
|
import { randomUUID } from "node:crypto";
|
|
11
14
|
import { rm } from "node:fs/promises";
|
|
15
|
+
import { thisProcessStartTime } from "../rpc/host-daemon-registration.js";
|
|
12
16
|
import { gcHostEndpoints } from "../rpc/host-gc.js";
|
|
13
17
|
import { createSocketSecret, socketSecretPath } from "../rpc/socket-transport.js";
|
|
14
18
|
import { runControlCommand } from "./session-control-commands.js";
|
|
@@ -33,8 +37,12 @@ export async function registerSessionControlEndpoint(context, options) {
|
|
|
33
37
|
}
|
|
34
38
|
async function openEndpoint(context, options, cleanups) {
|
|
35
39
|
const { session, agentDir, surface } = context;
|
|
36
|
-
|
|
37
|
-
|
|
40
|
+
// The writer stamp's `ps` lookup is a process constant: started here, it overlaps the header write
|
|
41
|
+
// and the bind instead of running inside the registry lock.
|
|
42
|
+
void thisProcessStartTime();
|
|
43
|
+
// Bound while the header is written, awaited before the endpoint is registered.
|
|
44
|
+
const headerDurable = session.sessionManager.persistHeaderNow();
|
|
45
|
+
headerDurable.catch(() => undefined);
|
|
38
46
|
const instanceId = randomUUID();
|
|
39
47
|
const socket = await resolveTuiSocket(agentDir, instanceId);
|
|
40
48
|
cleanups.push(() => rm(socket, { force: true }), () => rm(socketSecretPath(socket), { force: true }));
|
|
@@ -59,12 +67,29 @@ async function openEndpoint(context, options, cleanups) {
|
|
|
59
67
|
listenerError: (error) => surface.notice(`control endpoint listener failed: ${error.message}`),
|
|
60
68
|
});
|
|
61
69
|
cleanups.push(() => server.close());
|
|
70
|
+
await headerDurable;
|
|
62
71
|
const entry = await registerTuiEndpoint({ agentDir, socket, instanceId });
|
|
63
72
|
cleanups.push(() => unregisterTuiEndpoint(entry));
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
73
|
+
const inbox = await watchInbox(options.inboxDir, () => void wake("inbox"), (error) => surface.notice(`control endpoint inbox watch failed: ${errorText(error)}`));
|
|
74
|
+
cleanups.push(() => inbox.stop());
|
|
75
|
+
const endpoint = activateControlEndpoint({
|
|
76
|
+
context,
|
|
77
|
+
options,
|
|
78
|
+
entry,
|
|
79
|
+
server,
|
|
80
|
+
scheduler,
|
|
81
|
+
feed,
|
|
82
|
+
stopInbox: inbox.stop,
|
|
83
|
+
wake,
|
|
84
|
+
});
|
|
85
|
+
// Anything that reached the inbox before this point is picked up by this first pass, and anything
|
|
86
|
+
// written after it but before the watch was armed (no event for it) by the pass once arming settled.
|
|
67
87
|
endpoint.wake("inbox");
|
|
88
|
+
void inbox.armed.then(() => endpoint.wake("inbox"));
|
|
89
|
+
// Other terminals' dead records are nothing a sender reads: reaped after activation, off the path.
|
|
90
|
+
setImmediate(() => {
|
|
91
|
+
gcHostEndpoints(agentDir, { kinds: ["tui"] }).catch((error) => surface.notice(`control endpoint gc of dead terminals failed: ${errorText(error)}`));
|
|
92
|
+
});
|
|
68
93
|
return endpoint;
|
|
69
94
|
}
|
|
70
95
|
function errorText(error) {
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* Registration runs under the endpoint's ensure lock (the lock `host gc` takes) and writes the
|
|
6
6
|
* generation record BEFORE `endpoint.json`: a reader that can see the endpoint can always see a
|
|
7
7
|
* live generation behind it, so the endpoint is never judged `dead` while it is being registered.
|
|
8
|
+
* The directory is named by a socket built from a fresh instance id, so it never holds an earlier
|
|
9
|
+
* generation and the registration skips the dead-generation prune a host's directory needs.
|
|
8
10
|
* A TUI endpoint has nothing to reattach to, so a clean exit removes its whole directory.
|
|
9
11
|
*/
|
|
10
12
|
import { createHash } from "node:crypto";
|
|
@@ -66,7 +68,7 @@ export async function registerTuiEndpoint(options) {
|
|
|
66
68
|
generation: 0,
|
|
67
69
|
launchProfileId: "tui",
|
|
68
70
|
build: engineBuildIdentity(),
|
|
69
|
-
});
|
|
71
|
+
}, { fresh: true });
|
|
70
72
|
await createDaemonDirectories(paths, { kind: "tui" });
|
|
71
73
|
}
|
|
72
74
|
finally {
|
|
@@ -14,17 +14,23 @@ export declare class WakeScheduler {
|
|
|
14
14
|
}
|
|
15
15
|
/** Prefix of the engine's own entries in an inbox; a registrant never treats them as deliveries. */
|
|
16
16
|
export declare const INBOX_ENGINE_ENTRY_PREFIX = ".senpi-";
|
|
17
|
+
/** A live inbox watch. `armed` settles once arming did: the watch confirmed, the bounded retry ran out, or it was stopped. */
|
|
18
|
+
export interface InboxWatch {
|
|
19
|
+
readonly armed: Promise<void>;
|
|
20
|
+
stop(): void;
|
|
21
|
+
}
|
|
17
22
|
/**
|
|
18
23
|
* One watch on the inbox directory. Creation and teardown run on the shared watch worker (FSEvents
|
|
19
24
|
* and inotify setup/teardown block the calling thread), so neither ever stalls the terminal.
|
|
20
25
|
*
|
|
21
26
|
* The worker arms the watch asynchronously, and an entry written before it is armed produces no
|
|
22
|
-
* event.
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
27
|
+
* event. Arming is confirmed by a sentinel entry's own event coming back - the sentinel is re-touched
|
|
28
|
+
* until then, bounded, which is the only timer here and runs only while arming (an unconfirmed arm is
|
|
29
|
+
* reported through `onError`). Nothing waits on that confirmation: the watch is returned at once and
|
|
30
|
+
* `armed` settles when arming did, so the caller can request the pass that picks up anything created
|
|
31
|
+
* before the watch could see it.
|
|
26
32
|
*/
|
|
27
|
-
export declare function watchInbox(inboxDir: string, onChange: () => void, onError: (error: unknown) => void): Promise<
|
|
33
|
+
export declare function watchInbox(inboxDir: string, onChange: () => void, onError: (error: unknown) => void): Promise<InboxWatch>;
|
|
28
34
|
/**
|
|
29
35
|
* A continue after any stop (`^Z` or an external SIGSTOP) wakes the drain once the terminal has been
|
|
30
36
|
* restored: the listener is persistent, so every stop/continue cycle is covered, and it defers past
|
|
@@ -87,33 +87,55 @@ const INBOX_ARM_ATTEMPT_MS = 200;
|
|
|
87
87
|
* and inotify setup/teardown block the calling thread), so neither ever stalls the terminal.
|
|
88
88
|
*
|
|
89
89
|
* The worker arms the watch asynchronously, and an entry written before it is armed produces no
|
|
90
|
-
* event.
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
90
|
+
* event. Arming is confirmed by a sentinel entry's own event coming back - the sentinel is re-touched
|
|
91
|
+
* until then, bounded, which is the only timer here and runs only while arming (an unconfirmed arm is
|
|
92
|
+
* reported through `onError`). Nothing waits on that confirmation: the watch is returned at once and
|
|
93
|
+
* `armed` settles when arming did, so the caller can request the pass that picks up anything created
|
|
94
|
+
* before the watch could see it.
|
|
94
95
|
*/
|
|
95
96
|
export async function watchInbox(inboxDir, onChange, onError) {
|
|
96
97
|
await mkdir(inboxDir, { recursive: true, mode: 0o700 });
|
|
97
98
|
const sentinel = `${INBOX_ENGINE_ENTRY_PREFIX}armed-${randomUUID()}`;
|
|
98
|
-
const
|
|
99
|
+
const confirmation = Promise.withResolvers();
|
|
100
|
+
let stopped = false;
|
|
99
101
|
const unsubscribe = createFsWatchEventSource((error) => onError(error))(inboxDir, (_eventType, filename) => {
|
|
100
102
|
if (filename === sentinel)
|
|
101
|
-
|
|
103
|
+
confirmation.resolve(true);
|
|
102
104
|
else if (!filename?.startsWith(INBOX_ENGINE_ENTRY_PREFIX))
|
|
103
105
|
onChange();
|
|
104
106
|
}, { recursive: false });
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
107
|
+
const arming = confirmArm(join(inboxDir, sentinel), confirmation.promise, () => stopped).then((confirmed) => {
|
|
108
|
+
if (!confirmed && !stopped)
|
|
109
|
+
onError(new Error(`inbox watch on ${inboxDir} was not confirmed`));
|
|
110
|
+
}, (error) => {
|
|
111
|
+
if (!stopped)
|
|
112
|
+
onError(error);
|
|
113
|
+
});
|
|
114
|
+
// Settles on the sentinel's own event, before any later event of the same watch is delivered, so a
|
|
115
|
+
// pass requested on it runs ahead of the passes those events cause; otherwise when arming ended.
|
|
116
|
+
const armed = Promise.race([confirmation.promise.then((confirmed) => (confirmed ? undefined : arming)), arming]);
|
|
117
|
+
return {
|
|
118
|
+
armed,
|
|
119
|
+
stop: () => {
|
|
120
|
+
stopped = true;
|
|
121
|
+
confirmation.resolve(false);
|
|
122
|
+
void Promise.resolve(unsubscribe()).catch(onError);
|
|
123
|
+
},
|
|
115
124
|
};
|
|
116
125
|
}
|
|
126
|
+
async function confirmArm(sentinelPath, confirmed, stopped) {
|
|
127
|
+
let seen = false;
|
|
128
|
+
try {
|
|
129
|
+
for (let attempt = 0; attempt < INBOX_ARM_ATTEMPTS && !seen && !stopped(); attempt++) {
|
|
130
|
+
await writeFile(sentinelPath, String(attempt), { mode: 0o600 });
|
|
131
|
+
seen = await Promise.race([confirmed, delay(INBOX_ARM_ATTEMPT_MS, false)]);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
await rm(sentinelPath, { force: true });
|
|
136
|
+
}
|
|
137
|
+
return seen;
|
|
138
|
+
}
|
|
117
139
|
/**
|
|
118
140
|
* A continue after any stop (`^Z` or an external SIGSTOP) wakes the drain once the terminal has been
|
|
119
141
|
* restored: the listener is persistent, so every stop/continue cycle is covered, and it defers past
|
|
@@ -28,6 +28,11 @@ export interface RpcConnectionOptions {
|
|
|
28
28
|
eventFlushScheduler?: (flush: () => void) => void;
|
|
29
29
|
/** Multi-session routing handle. Absent preserves classic wire output exactly. */
|
|
30
30
|
sessionId?: string;
|
|
31
|
+
/**
|
|
32
|
+
* A single-session `--mode rpc` process: it accepts `set_retry_fallback` and advertises it. A host's
|
|
33
|
+
* session connection leaves it off; a host session takes its chain from `open_session.retryFallback`.
|
|
34
|
+
*/
|
|
35
|
+
retryFallbackCommand?: boolean;
|
|
31
36
|
/**
|
|
32
37
|
* Shared-session capability registry. A `set_client_info` carrying `capabilities`
|
|
33
38
|
* registers them for the connection that sent it; absent on a classic connection.
|
|
@@ -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,11 +42,20 @@ 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";
|
|
49
49
|
export { buildRpcSessionState } from "./rpc-session-state.js";
|
|
50
|
+
/** The tool call a dialog is about, as wire fields (absent when the dialog is not a permission request). */
|
|
51
|
+
function dialogCall(opts) {
|
|
52
|
+
if (opts?.toolCallId === undefined)
|
|
53
|
+
return {};
|
|
54
|
+
return {
|
|
55
|
+
toolCallId: opts.toolCallId,
|
|
56
|
+
...(opts.parentToolCallId === undefined ? {} : { parentToolCallId: opts.parentToolCallId }),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
50
59
|
function loadedExtensionName(path) {
|
|
51
60
|
const synthetic = /^<(?:builtin|inline):([^>]+)>$/.exec(path);
|
|
52
61
|
if (synthetic)
|
|
@@ -149,6 +158,8 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
149
158
|
let session = runtimeHost.session;
|
|
150
159
|
let sessionControl;
|
|
151
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;
|
|
152
163
|
let unsubscribe;
|
|
153
164
|
let unsubscribeBackpressure;
|
|
154
165
|
let unsubscribeLoadedSurfaces;
|
|
@@ -292,9 +303,9 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
292
303
|
question: (request, opts) => clientCapabilities?.includes(QUESTION_CAPABILITY)
|
|
293
304
|
? questions.ask(request, opts)
|
|
294
305
|
: degradeQuestion(createExtensionUIContext(), request, opts),
|
|
295
|
-
select: (title, options, opts) => createDialogPromise(opts, undefined, { method: "select", title, options, timeout: opts?.timeout }, (r) => "cancelled" in r && r.cancelled ? undefined : "value" in r ? r.value : undefined),
|
|
306
|
+
select: (title, options, opts) => createDialogPromise(opts, undefined, { method: "select", title, options, timeout: opts?.timeout, ...dialogCall(opts) }, (r) => ("cancelled" in r && r.cancelled ? undefined : "value" in r ? r.value : undefined)),
|
|
296
307
|
confirm: (title, message, opts) => createDialogPromise(opts, false, { method: "confirm", title, message, timeout: opts?.timeout }, (r) => "cancelled" in r && r.cancelled ? false : "confirmed" in r ? r.confirmed : false),
|
|
297
|
-
input: (title, placeholder, opts) => createDialogPromise(opts, undefined, { method: "input", title, placeholder, timeout: opts?.timeout }, (r) => "cancelled" in r && r.cancelled ? undefined : "value" in r ? r.value : undefined),
|
|
308
|
+
input: (title, placeholder, opts) => createDialogPromise(opts, undefined, { method: "input", title, placeholder, timeout: opts?.timeout, ...dialogCall(opts) }, (r) => ("cancelled" in r && r.cancelled ? undefined : "value" in r ? r.value : undefined)),
|
|
298
309
|
notify(message, type) {
|
|
299
310
|
// Fire and forget - no response needed
|
|
300
311
|
output({
|
|
@@ -728,6 +739,7 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
728
739
|
MEDIA_PLACEHOLDERS_CAPABILITY,
|
|
729
740
|
DURABLE_CLIENT_MESSAGE_ID_CAPABILITY,
|
|
730
741
|
CONTINUE_FROM_LEAF_CAPABILITY,
|
|
742
|
+
...(options.retryFallbackCommand ? [RETRY_FALLBACK_COMMAND_CAPABILITY] : []),
|
|
731
743
|
...(options.capabilities ?? []),
|
|
732
744
|
]),
|
|
733
745
|
],
|
|
@@ -741,6 +753,7 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
741
753
|
// Prompting
|
|
742
754
|
// =================================================================
|
|
743
755
|
case "prompt": {
|
|
756
|
+
turnRequested = true;
|
|
744
757
|
const admission = handleClientInput(session, command, { output, promptCalls });
|
|
745
758
|
promptCalls.add(admission);
|
|
746
759
|
void admission.finally(() => promptCalls.delete(admission));
|
|
@@ -760,6 +773,7 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
760
773
|
return success(id, "append_session_entry");
|
|
761
774
|
}
|
|
762
775
|
case "continue_from_leaf": {
|
|
776
|
+
turnRequested = true;
|
|
763
777
|
try {
|
|
764
778
|
await session.continueFromLeaf();
|
|
765
779
|
return success(id, "continue_from_leaf");
|
|
@@ -771,6 +785,9 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
771
785
|
}
|
|
772
786
|
}
|
|
773
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;
|
|
774
791
|
await session.sendCustomMessage({
|
|
775
792
|
customType: command.customType,
|
|
776
793
|
content: command.content,
|
|
@@ -781,6 +798,7 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
781
798
|
}
|
|
782
799
|
case "steer":
|
|
783
800
|
case "follow_up": {
|
|
801
|
+
turnRequested = true;
|
|
784
802
|
await handleClientInput(session, command, { output, promptCalls });
|
|
785
803
|
return undefined;
|
|
786
804
|
}
|
|
@@ -980,6 +998,24 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
980
998
|
session.abortRetry();
|
|
981
999
|
return success(id, "abort_retry");
|
|
982
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
|
+
}
|
|
983
1019
|
// =================================================================
|
|
984
1020
|
// Bash
|
|
985
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";
|
|
@@ -1319,6 +1328,9 @@ export type RpcExtensionUIRequest = {
|
|
|
1319
1328
|
title: string;
|
|
1320
1329
|
options: string[];
|
|
1321
1330
|
timeout?: number;
|
|
1331
|
+
/** The tool call this dialog approves, when it is a permission request. */
|
|
1332
|
+
toolCallId?: string;
|
|
1333
|
+
parentToolCallId?: string;
|
|
1322
1334
|
} | {
|
|
1323
1335
|
type: "extension_ui_request";
|
|
1324
1336
|
id: string;
|
|
@@ -1333,6 +1345,8 @@ export type RpcExtensionUIRequest = {
|
|
|
1333
1345
|
title: string;
|
|
1334
1346
|
placeholder?: string;
|
|
1335
1347
|
timeout?: number;
|
|
1348
|
+
toolCallId?: string;
|
|
1349
|
+
parentToolCallId?: string;
|
|
1336
1350
|
} | {
|
|
1337
1351
|
type: "extension_ui_request";
|
|
1338
1352
|
id: string;
|
package/docs/json.md
CHANGED
|
@@ -22,6 +22,7 @@ type AgentSessionEvent =
|
|
|
22
22
|
| { type: "compaction_end"; reason: "manual" | "threshold" | "overflow"; result: CompactionResult | undefined; aborted: boolean; willRetry: boolean; requestId?: string; accepted?: boolean; rejectionCause?: "cancelled-by-extension" | "external-owner" | "would-overflow" | "circuit-breaker" | "per-turn-cap"; errorMessage?: string }
|
|
23
23
|
| { type: "session_info_changed"; name: string | undefined }
|
|
24
24
|
| { type: "thinking_level_changed"; level: "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max" }
|
|
25
|
+
| { type: "thinking_level_clamped"; provider: string; modelId: string; requestedLevel: "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"; appliedLevel: "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"; reason: "model-not-reasoning" | "level-unsupported" }
|
|
25
26
|
| SystemPromptChangeEvent // type: "system_prompt_change"
|
|
26
27
|
| ExtensionToolHookLifecycleEvent // type: "tool_hook_status"
|
|
27
28
|
| { type: "auto_retry_start"; attempt: number; maxAttempts: number; delayMs: number; errorMessage: string }
|
|
@@ -55,6 +56,8 @@ type JsonAgentSessionEvent =
|
|
|
55
56
|
|
|
56
57
|
`queue_update` emits the full pending steering and follow-up queues whenever they change. `compaction_start`, `compaction_progress`, and `compaction_end` cover both manual and automatic compaction. `session_info_changed` fires when the session display name changes, `thinking_level_changed` when the thinking level changes, `system_prompt_change` (see `SystemPromptChangeEvent` in [`extensions/types.ts`](../src/core/extensions/types.ts)) when a model switch changes the active system prompt, and `tool_hook_status` (see `ExtensionToolHookLifecycleEvent` in [`extensions/runner.ts`](../src/core/extensions/runner.ts)) for extension tool hook start/end phases.
|
|
57
58
|
|
|
59
|
+
`thinking_level_clamped` fires once per model and requested level when an explicit thinking request (such as `--thinking`, `/thinking`, or a remembered per-model level) runs at a lower level than asked. A level that only comes from the global `defaultThinkingLevel` does not count as a request. In RPC mode, the `thinkingSelection` carried by `thinking_level_changed` and `get_state` then also has `requested` and `clampReason`.
|
|
60
|
+
|
|
58
61
|
Other base events come from
|
|
59
62
|
[`AgentEvent`](../../agent/src/types.ts):
|
|
60
63
|
|