@code-yeongyu/senpi 2026.10.8 → 2026.10.10-2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +58 -0
- package/dist/bundle/chunks/{app-server-command-MJOYE5ZS.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-4GUQVY4Z.js → chunk-3YXB7Z7O.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-K4AZKFRB.js → chunk-J5MUMVWB.js} +2 -2
- package/dist/bundle/chunks/{chunk-V2VQ4BHG.js → chunk-M2IHGL3M.js} +1 -1
- 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-BTYFICUC.js → chunk-SB22X6ZR.js} +1 -1
- 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-5MG76BCM.js → chunk-W6B7SEAK.js} +180 -178
- package/dist/bundle/chunks/{cli-main-GWVLFOIP.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-YLM2U7IX.js → interactive-mode-ULFSI46Z.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-VM7WJBC5.js} +1 -1
- package/dist/bundle/chunks/{package-manager-cli-3UIJ4RXY.js → package-manager-cli-OAFUQY7E.js} +1 -1
- package/dist/bundle/chunks/{rpc-mode-PUYBZG27.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-picker-N463VCDI.js → session-picker-R5N2EKUS.js} +1 -1
- package/dist/bundle/chunks/session-worker.js +95 -93
- 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/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/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/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/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 +41 -5
- 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/mcp.md +16 -1
- package/docs/rpc.md +49 -1
- package/package.json +6 -6
- package/dist/bundle/chunks/chunk-6SMU7ANE.js +0 -3
- package/dist/bundle/chunks/chunk-VJJ53ORU.js +0 -2
- package/dist/bundle/chunks/chunk-X6UDRO7J.js +0 -14
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import { isValidThinkingLevel } from "../../cli/args.js";
|
|
2
2
|
/**
|
|
3
|
-
* Providers that never receive a bare selector.
|
|
3
|
+
* Providers that never receive a bare selector. Gateways re-publish other
|
|
4
4
|
* vendors' catalogs under namespaced ids, so family expansion would silently
|
|
5
5
|
* route a shipped default through a third-party reseller the user never chose.
|
|
6
|
-
* An explicit
|
|
6
|
+
* An explicit provider-qualified selector the user wrote is unaffected.
|
|
7
7
|
*/
|
|
8
|
-
const BARE_EXPANSION_DENYLIST = new Set([
|
|
8
|
+
const BARE_EXPANSION_DENYLIST = new Set([
|
|
9
|
+
"cursor",
|
|
10
|
+
"openrouter",
|
|
11
|
+
"openrouter-images",
|
|
12
|
+
"opengateway",
|
|
13
|
+
"vercel-ai-gateway",
|
|
14
|
+
]);
|
|
9
15
|
/**
|
|
10
16
|
* Deterministic tie-break inside one auth tier. Earlier wins. Providers absent
|
|
11
17
|
* from the table sort after every listed one, alphabetically.
|
package/dist/core/sdk.js
CHANGED
|
@@ -20,7 +20,7 @@ import { mergeProviderAttributionHeaders } from "./provider-attribution.js";
|
|
|
20
20
|
import { DefaultResourceLoader } from "./resource-loader.js";
|
|
21
21
|
import { getDefaultSessionDir, SessionManager } from "./session-manager.js";
|
|
22
22
|
import { DEFAULT_TOOL_NAMES, SettingsManager } from "./settings-manager.js";
|
|
23
|
-
import { getSupportedThinkingLevels } from "./thinking-levels.js";
|
|
23
|
+
import { clampThinkingSelection, getSupportedThinkingLevels } from "./thinking-levels.js";
|
|
24
24
|
import { time } from "./timings.js";
|
|
25
25
|
import { createBashTool, createCodingTools, createEditTool, createFindTool, createGrepTool, createLsTool, createPowerShellTool, createReadOnlyTools, createReadTool, createWriteTool, withFileMutationQueue, } from "./tools/index.js";
|
|
26
26
|
import { serviceTierForProvider } from "./ultrafast-lanes.js";
|
|
@@ -201,25 +201,29 @@ export async function createAgentSession(options = {}) {
|
|
|
201
201
|
if (thinkingLevel === undefined && model?.defaultThinkingLevel !== undefined) {
|
|
202
202
|
thinkingLevel = model.defaultThinkingLevel;
|
|
203
203
|
}
|
|
204
|
+
let thinkingFromGlobalDefault = false;
|
|
204
205
|
if (thinkingLevel === undefined) {
|
|
205
206
|
const configuredDefault = settingsManager.getDefaultThinkingLevel();
|
|
206
207
|
if (configuredDefault !== undefined) {
|
|
207
208
|
thinkingLevel = configuredDefault;
|
|
208
209
|
thinkingSelection = { level: configuredDefault, source: "explicit" };
|
|
210
|
+
thinkingFromGlobalDefault = true;
|
|
209
211
|
}
|
|
210
212
|
else {
|
|
211
213
|
thinkingLevel = DEFAULT_THINKING_LEVEL;
|
|
212
214
|
}
|
|
213
215
|
}
|
|
214
216
|
// Clamp to model capabilities without inventing provenance for a defaulted level.
|
|
217
|
+
const requestedThinkingLevel = thinkingLevel;
|
|
215
218
|
if (!model) {
|
|
216
219
|
thinkingLevel = "off";
|
|
217
220
|
}
|
|
218
221
|
else {
|
|
219
222
|
thinkingLevel = clampThinkingLevelToModel(thinkingLevel, model);
|
|
220
223
|
}
|
|
221
|
-
|
|
222
|
-
|
|
224
|
+
// senpi#2395: an explicit request the clamp changed keeps the requested level and the reason. The global
|
|
225
|
+
// default is a default, not a request, so its clamp records no requested level and shows no warning.
|
|
226
|
+
thinkingSelection = clampThinkingSelection(thinkingSelection, thinkingFromGlobalDefault ? thinkingLevel : requestedThinkingLevel, thinkingLevel, model);
|
|
223
227
|
const configuredDefaultToolNames = settingsManager.getDefaultTools();
|
|
224
228
|
const sessionDefaultToolNames = options.tools === undefined && options.noTools === undefined ? configuredDefaultToolNames : undefined;
|
|
225
229
|
const allowedToolNames = options.tools ?? (options.noTools === "all" ? [] : undefined);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
2
|
-
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { Api, Model, ModelThinkingLevel, ThinkingSelection } from "@earendil-works/pi-ai";
|
|
3
3
|
/**
|
|
4
4
|
* Tier detection is owned by `packages/ai`, which already treats an explicit `thinkingLevelMap` as
|
|
5
5
|
* authoritative and infers tiers from the model id only when no map is present. These wrappers keep
|
|
@@ -8,6 +8,28 @@ import type { Api, Model } from "@earendil-works/pi-ai";
|
|
|
8
8
|
export declare function supportsXhigh(model: Model<Api>): boolean;
|
|
9
9
|
export declare function supportsMax(model: Model<Api>): boolean;
|
|
10
10
|
export declare function getSupportedThinkingLevels(model: Model<Api>): ThinkingLevel[];
|
|
11
|
+
/** Why an explicit thinking selection runs at a lower level than the one requested (senpi#2395). */
|
|
12
|
+
export type ThinkingClampReason = "model-not-reasoning" | "level-unsupported";
|
|
13
|
+
/** An explicit selection that also records the requested level when a clamp changed it. */
|
|
14
|
+
export interface ClampedThinkingSelection extends ThinkingSelection {
|
|
15
|
+
readonly requested?: ModelThinkingLevel;
|
|
16
|
+
readonly clampReason?: ThinkingClampReason;
|
|
17
|
+
}
|
|
18
|
+
/** One clamp of an explicit request, as shown to the user. */
|
|
19
|
+
export interface ThinkingClampNotice {
|
|
20
|
+
provider: string;
|
|
21
|
+
modelId: string;
|
|
22
|
+
requestedLevel: ModelThinkingLevel;
|
|
23
|
+
appliedLevel: ModelThinkingLevel;
|
|
24
|
+
reason: ThinkingClampReason;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Apply the effective `level` to an explicit selection. When it differs from `requested`, the selection keeps
|
|
28
|
+
* the requested level and the clamp reason instead of reading as if `level` had been chosen.
|
|
29
|
+
*/
|
|
30
|
+
export declare function clampThinkingSelection(selection: ThinkingSelection | undefined, requested: ModelThinkingLevel, level: ModelThinkingLevel, model: Model<Api> | undefined): ThinkingSelection | undefined;
|
|
31
|
+
export declare function getThinkingClampNotice(selection: ThinkingSelection | undefined, model: Model<Api> | undefined): ThinkingClampNotice | undefined;
|
|
32
|
+
export declare function formatThinkingClampWarning(notice: ThinkingClampNotice): string;
|
|
11
33
|
/** Capability classes used by capability-aware reasoning commands. */
|
|
12
34
|
export type ReasoningCapabilityKind = "none" | "always-on" | "on-off" | "graded";
|
|
13
35
|
export interface ReasoningCapability {
|
|
@@ -14,6 +14,43 @@ export function getSupportedThinkingLevels(model) {
|
|
|
14
14
|
const supportedLevels = getSupportedModelThinkingLevels(model);
|
|
15
15
|
return supportedLevels.length > 0 ? supportedLevels : ["off"];
|
|
16
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Apply the effective `level` to an explicit selection. When it differs from `requested`, the selection keeps
|
|
19
|
+
* the requested level and the clamp reason instead of reading as if `level` had been chosen.
|
|
20
|
+
*/
|
|
21
|
+
export function clampThinkingSelection(selection, requested, level, model) {
|
|
22
|
+
if (selection === undefined)
|
|
23
|
+
return undefined;
|
|
24
|
+
const { requested: _staleRequested, clampReason: _staleReason, ...base } = selection;
|
|
25
|
+
if (requested === level || model === undefined)
|
|
26
|
+
return { ...base, level };
|
|
27
|
+
const clamped = {
|
|
28
|
+
...base,
|
|
29
|
+
level,
|
|
30
|
+
requested,
|
|
31
|
+
clampReason: model.reasoning ? "level-unsupported" : "model-not-reasoning",
|
|
32
|
+
};
|
|
33
|
+
return clamped;
|
|
34
|
+
}
|
|
35
|
+
export function getThinkingClampNotice(selection, model) {
|
|
36
|
+
const clamped = selection;
|
|
37
|
+
if (model === undefined || clamped?.requested === undefined || clamped.clampReason === undefined)
|
|
38
|
+
return undefined;
|
|
39
|
+
return {
|
|
40
|
+
provider: model.provider,
|
|
41
|
+
modelId: model.id,
|
|
42
|
+
requestedLevel: clamped.requested,
|
|
43
|
+
appliedLevel: clamped.level,
|
|
44
|
+
reason: clamped.clampReason,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export function formatThinkingClampWarning(notice) {
|
|
48
|
+
const model = `${notice.provider}/${notice.modelId}`;
|
|
49
|
+
if (notice.reason === "model-not-reasoning") {
|
|
50
|
+
return `Thinking level "${notice.requestedLevel}" was requested, but ${model} is not marked as a reasoning model, so thinking is "${notice.appliedLevel}". If the model supports reasoning, set "reasoning": true for it in models.json.`;
|
|
51
|
+
}
|
|
52
|
+
return `Thinking level "${notice.requestedLevel}" is not available on ${model}; using "${notice.appliedLevel}".`;
|
|
53
|
+
}
|
|
17
54
|
/**
|
|
18
55
|
* Classify a model's reasoning capability purely from `model.reasoning` and its supported thinking
|
|
19
56
|
* levels — never from the model id or thinkingFormat/compat config.
|
|
@@ -5,6 +5,7 @@ import lockfile from "proper-lockfile";
|
|
|
5
5
|
import { CONFIG_DIR_NAME } from "../config.js";
|
|
6
6
|
import { canonicalizePath, canonicalizePathStrict, resolvePath } from "../utils/paths.js";
|
|
7
7
|
import { stripBom } from "../utils/text.js";
|
|
8
|
+
import { bundledBuiltinExtensions } from "./bundled-resources.js";
|
|
8
9
|
const TRUST_REQUIRING_PROJECT_CONFIG_RESOURCES = [
|
|
9
10
|
"settings.json",
|
|
10
11
|
"mcp.json",
|
|
@@ -16,6 +17,53 @@ const TRUST_REQUIRING_PROJECT_CONFIG_RESOURCES = [
|
|
|
16
17
|
"APPEND_SYSTEM.md",
|
|
17
18
|
];
|
|
18
19
|
const LEGACY_PROJECT_CONFIG_DIR_NAME = ".pi";
|
|
20
|
+
/**
|
|
21
|
+
* A project codemode file that sets any setting naming an executable (the codemode package's
|
|
22
|
+
* `executable-settings.json` list, today `languages.pyInterpreter`) makes the session run that executable at start,
|
|
23
|
+
* so it asks for a trust decision. A file the check cannot read is treated like a project `mcp.json`, whose mere
|
|
24
|
+
* presence asks; so is any file when the list itself cannot be read.
|
|
25
|
+
*/
|
|
26
|
+
function projectCodemodeNamesExecutable(configDir) {
|
|
27
|
+
const path = join(configDir, "codemode.json");
|
|
28
|
+
if (!existsSync(path))
|
|
29
|
+
return false;
|
|
30
|
+
const executableSettings = codemodeExecutableSettings();
|
|
31
|
+
if (executableSettings === undefined)
|
|
32
|
+
return true;
|
|
33
|
+
let parsed;
|
|
34
|
+
try {
|
|
35
|
+
parsed = JSON.parse(stripBom(readFileSync(path, "utf8")));
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
return executableSettings.some((settingPath) => valueAtPath(parsed, settingPath) !== undefined);
|
|
41
|
+
}
|
|
42
|
+
function codemodeExecutableSettings() {
|
|
43
|
+
const codemode = bundledBuiltinExtensions.find((extension) => extension.id === "codemode");
|
|
44
|
+
try {
|
|
45
|
+
const packageJson = codemode?.resolvePackage();
|
|
46
|
+
if (packageJson === undefined)
|
|
47
|
+
return undefined;
|
|
48
|
+
const list = JSON.parse(readFileSync(join(dirname(packageJson), "src", "config", "executable-settings.json"), "utf8"));
|
|
49
|
+
if (typeof list !== "object" || list === null)
|
|
50
|
+
return undefined;
|
|
51
|
+
const paths = Reflect.get(list, "projectExecutableSettings");
|
|
52
|
+
return Array.isArray(paths) && paths.every((entry) => typeof entry === "string") ? paths : undefined;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function valueAtPath(value, path) {
|
|
59
|
+
let current = value;
|
|
60
|
+
for (const key of path.split(".")) {
|
|
61
|
+
if (typeof current !== "object" || current === null)
|
|
62
|
+
return undefined;
|
|
63
|
+
current = Reflect.get(current, key);
|
|
64
|
+
}
|
|
65
|
+
return current;
|
|
66
|
+
}
|
|
19
67
|
function normalizeCwd(cwd) {
|
|
20
68
|
const resolved = resolvePath(cwd);
|
|
21
69
|
try {
|
|
@@ -169,6 +217,8 @@ export function hasTrustRequiringProjectResources(cwd) {
|
|
|
169
217
|
if (configDirs.some((configDir) => TRUST_REQUIRING_PROJECT_CONFIG_RESOURCES.some((entry) => existsSync(join(configDir, entry))))) {
|
|
170
218
|
return true;
|
|
171
219
|
}
|
|
220
|
+
if (projectCodemodeNamesExecutable(join(currentDir, CONFIG_DIR_NAME)))
|
|
221
|
+
return true;
|
|
172
222
|
while (true) {
|
|
173
223
|
const agentsSkillsDir = join(currentDir, ".agents", "skills");
|
|
174
224
|
if (agentsSkillsDir !== userAgentsSkillsDir && existsSync(agentsSkillsDir)) {
|
package/dist/main.js
CHANGED
|
@@ -29,7 +29,7 @@ import { createProjectTrustContext } from "./cli/project-trust.js";
|
|
|
29
29
|
import { createStartupLoadingIndicator, pauseIndicatorDuringPrompts, shouldShowStartupLoadingIndicator, } from "./cli/startup-loading-indicator.js";
|
|
30
30
|
import { shouldRunFirstTimeSetup, showFirstTimeSetup, showStartupSelector } from "./cli/startup-ui.js";
|
|
31
31
|
import { APP_NAME, DISPLAY_VERSION, ENV_SESSION_DIR, expandTildePath, getAgentDir, getInstallPackageDir, } from "./config.js";
|
|
32
|
-
import { createAgentSessionRuntime } from "./core/agent-session-runtime.js";
|
|
32
|
+
import { applyRetryFallbackProfile, createAgentSessionRuntime, } from "./core/agent-session-runtime.js";
|
|
33
33
|
import { createAgentSessionFromServices, createAgentSessionServices, } from "./core/agent-session-services.js";
|
|
34
34
|
import { formatNoModelsAvailableMessage } from "./core/auth-guidance.js";
|
|
35
35
|
import { AuthStorage, ReadOnlyAuthStorage } from "./core/auth-storage.js";
|
|
@@ -51,6 +51,7 @@ import { assertValidSessionId, SessionManager } from "./core/session-manager.js"
|
|
|
51
51
|
import { classifySessionRepository, readSessionCwd, rebindSessionFile } from "./core/session-rebind.js";
|
|
52
52
|
import { collectSettingsDiagnosticsWithContext } from "./core/settings-diagnostics.js";
|
|
53
53
|
import { SettingsManager } from "./core/settings-manager.js";
|
|
54
|
+
import { formatThinkingClampWarning } from "./core/thinking-levels.js";
|
|
54
55
|
import { printTimings, recordTiming, resetTimings, time } from "./core/timings.js";
|
|
55
56
|
import { hasTrustRequiringProjectResources, ProjectTrustStore } from "./core/trust-manager.js";
|
|
56
57
|
import { builtInExtensions } from "./extensions/index.js";
|
|
@@ -678,17 +679,8 @@ export function createCliRuntimeFactory(configuration, local = {}) {
|
|
|
678
679
|
(!hasTrustRequiringResources || trustStore.get(cwd) === true));
|
|
679
680
|
const runtimeSettingsManager = SettingsManager.create(cwd, agentDir, { projectTrusted });
|
|
680
681
|
// The opener's per-session fallback policy is an in-memory override: never saved, never shared.
|
|
681
|
-
if (launchProfile?.retryFallback)
|
|
682
|
-
runtimeSettingsManager.
|
|
683
|
-
retry: {
|
|
684
|
-
modelFallback: launchProfile.retryFallback.modelFallback,
|
|
685
|
-
fallbackChains: Object.fromEntries(Object.entries(launchProfile.retryFallback.fallbackChains).map(([key, entries]) => [
|
|
686
|
-
key,
|
|
687
|
-
[...entries],
|
|
688
|
-
])),
|
|
689
|
-
},
|
|
690
|
-
});
|
|
691
|
-
}
|
|
682
|
+
if (launchProfile?.retryFallback)
|
|
683
|
+
applyRetryFallbackProfile(runtimeSettingsManager, launchProfile.retryFallback);
|
|
692
684
|
const services = await createAgentSessionServices({
|
|
693
685
|
cwd,
|
|
694
686
|
agentDir,
|
|
@@ -830,7 +822,9 @@ export function createCliRuntimeFactory(configuration, local = {}) {
|
|
|
830
822
|
markSwitch("createSession");
|
|
831
823
|
const cliThinkingOverride = runtimeParsed.thinking !== undefined || cliThinkingFromModel;
|
|
832
824
|
if (created.session.model && cliThinkingOverride) {
|
|
833
|
-
|
|
825
|
+
// senpi#2395: re-apply the level the CLI asked for, so a clamp keeps its requested level and reason.
|
|
826
|
+
const selection = created.session.thinkingSelection;
|
|
827
|
+
created.session.setThinkingLevel(selection?.requested ?? created.session.thinkingLevel);
|
|
834
828
|
}
|
|
835
829
|
return {
|
|
836
830
|
...created,
|
|
@@ -1184,6 +1178,11 @@ export async function main(args, options) {
|
|
|
1184
1178
|
}
|
|
1185
1179
|
time("resolveModelScope");
|
|
1186
1180
|
reportDiagnostics(runtime.diagnostics);
|
|
1181
|
+
// senpi#2395: print/json/RPC runs report a startup thinking clamp on stderr; the TUI shows its own warning.
|
|
1182
|
+
const startupThinkingClamp = runtime.session.startupThinkingClamp;
|
|
1183
|
+
if (appMode !== "interactive" && startupThinkingClamp) {
|
|
1184
|
+
reportDiagnostics([{ type: "warning", message: formatThinkingClampWarning(startupThinkingClamp) }]);
|
|
1185
|
+
}
|
|
1187
1186
|
if (appMode !== "interactive") {
|
|
1188
1187
|
reportDiagnostics(collectAuthDiagnostics(services.authStorage, "runtime creation"));
|
|
1189
1188
|
}
|
|
@@ -39,6 +39,7 @@ import { createSessionLogger } from "../../core/session-log.js";
|
|
|
39
39
|
import { SessionManager, sessionEntryToContextMessages } from "../../core/session-manager.js";
|
|
40
40
|
import { BUILTIN_SLASH_COMMANDS } from "../../core/slash-commands.js";
|
|
41
41
|
import { isInstallTelemetryEnabled } from "../../core/telemetry.js";
|
|
42
|
+
import { formatThinkingClampWarning } from "../../core/thinking-levels.js";
|
|
42
43
|
import { formatTimings, resetTimings, time } from "../../core/timings.js";
|
|
43
44
|
import { withBuiltInRenderers } from "../../core/tools/renderers/index.js";
|
|
44
45
|
import { hasTrustRequiringProjectResources, ProjectTrustStore } from "../../core/trust-manager.js";
|
|
@@ -1354,6 +1355,10 @@ export class InteractiveMode {
|
|
|
1354
1355
|
for (const warning of this.session.fallbackValidationWarnings) {
|
|
1355
1356
|
this.showWarning(warning);
|
|
1356
1357
|
}
|
|
1358
|
+
const startupThinkingClamp = this.session.startupThinkingClamp;
|
|
1359
|
+
if (startupThinkingClamp) {
|
|
1360
|
+
this.showWarning(formatThinkingClampWarning(startupThinkingClamp));
|
|
1361
|
+
}
|
|
1357
1362
|
this.showRiskyMainModelWarning(this.session.model);
|
|
1358
1363
|
void this.maybeWarnAboutAnthropicSubscriptionAuth();
|
|
1359
1364
|
maybeShowRuntimeNotice((spec) => this.showNoticeBox(spec));
|
|
@@ -4207,6 +4212,9 @@ export class InteractiveMode {
|
|
|
4207
4212
|
case "high_reasoning_warning":
|
|
4208
4213
|
this.showHighReasoningWarning(event);
|
|
4209
4214
|
break;
|
|
4215
|
+
case "thinking_level_clamped":
|
|
4216
|
+
this.showWarning(formatThinkingClampWarning(event));
|
|
4217
|
+
break;
|
|
4210
4218
|
case "resume_compaction_required":
|
|
4211
4219
|
this.showWarning(event.notice);
|
|
4212
4220
|
break;
|
|
@@ -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
|
}
|
|
@@ -874,7 +892,7 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
874
892
|
return success(id, "cycle_model", result);
|
|
875
893
|
}
|
|
876
894
|
case "get_available_models": {
|
|
877
|
-
const models = await session.modelRegistry.getAvailable();
|
|
895
|
+
const models = await session.modelRegistry.modelRuntime.getAvailable();
|
|
878
896
|
return success(id, "get_available_models", {
|
|
879
897
|
models: models.map((model) => ({
|
|
880
898
|
...model,
|
|
@@ -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
|
// =================================================================
|