@code-yeongyu/senpi 2026.10.5 → 2026.10.7
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 +46 -0
- package/dist/bundle/chunks/{anthropic-messages-G5236QNK.js → anthropic-messages-4MNFPQ66.js} +1 -1
- package/dist/bundle/chunks/{app-server-command-3Q5G4CAF.js → app-server-command-SO5RJ2N7.js} +1 -1
- package/dist/bundle/chunks/{azure-openai-responses-NTA2HGG4.js → azure-openai-responses-A5OEHSJM.js} +1 -1
- package/dist/bundle/chunks/bedrock-converse-stream.js +1 -1
- package/dist/bundle/chunks/{chunk-ECDLITEY.js → chunk-6EH7QXOG.js} +2 -2
- package/dist/bundle/chunks/{chunk-JVSOW4QU.js → chunk-7A3WRAKP.js} +1 -1
- package/dist/bundle/chunks/chunk-7SMFF2QJ.js +14 -0
- package/dist/bundle/chunks/{chunk-WTJU5B2C.js → chunk-BE66QJ3Z.js} +1 -1
- package/dist/bundle/chunks/{chunk-6JXPZACL.js → chunk-CM5QTRC2.js} +1 -1
- package/dist/bundle/chunks/{chunk-UHRXXTFZ.js → chunk-D2W3MXRF.js} +2 -2
- package/dist/bundle/chunks/{chunk-U7CI2JGX.js → chunk-DDEAZX22.js} +1 -1
- package/dist/bundle/chunks/{chunk-UXIQK5NQ.js → chunk-ESSQUSRC.js} +1 -1
- package/dist/bundle/chunks/{chunk-EEOKVSNF.js → chunk-EUU6W5B3.js} +180 -176
- package/dist/bundle/chunks/{chunk-KLBKUBDV.js → chunk-GQ3YLXVZ.js} +1 -1
- package/dist/bundle/chunks/{chunk-HKG7XL4H.js → chunk-HY4AOLON.js} +1 -1
- package/dist/bundle/chunks/{chunk-2WV426OJ.js → chunk-JJNIU2A2.js} +8 -4
- package/dist/bundle/chunks/{chunk-22DAHAHO.js → chunk-LUWWM7GA.js} +2 -2
- package/dist/bundle/chunks/{chunk-CTQU2XNX.js → chunk-MLNJNJFE.js} +1 -1
- package/dist/bundle/chunks/{chunk-BPX6QWPD.js → chunk-PELNNWGO.js} +1 -1
- package/dist/bundle/chunks/{chunk-BBK6SSYT.js → chunk-TAV6QZFM.js} +1 -1
- package/dist/bundle/chunks/{chunk-QUROHJLP.js → chunk-TFO5JGLR.js} +1 -1
- package/dist/bundle/chunks/chunk-VJJ53ORU.js +2 -0
- package/dist/bundle/chunks/{chunk-XYDZ3LIE.js → chunk-YI7SPFDN.js} +2 -2
- package/dist/bundle/chunks/chunk-ZJHRDQCH.js +2 -0
- package/dist/bundle/chunks/cli-main-XBQN73O2.js +3 -0
- package/dist/bundle/chunks/{cloudflare-workers-ai-system-one-5ZUHUKMU.js → cloudflare-workers-ai-system-one-TE3OEJEL.js} +1 -1
- package/dist/bundle/chunks/{google-generative-ai-OTMG6ZXI.js → google-generative-ai-JBC53AMK.js} +1 -1
- package/dist/bundle/chunks/{google-vertex-2LXS3ICC.js → google-vertex-46ODNPM2.js} +1 -1
- package/dist/bundle/chunks/{help-fast-path-SHZJ3QWQ.js → help-fast-path-FUMH5NSA.js} +1 -1
- package/dist/bundle/chunks/host-command-YXQAXNKK.js +2 -0
- package/dist/bundle/chunks/{host-lifecycle-7OVWGVIZ.js → host-lifecycle-L4XQIATP.js} +1 -1
- package/dist/bundle/chunks/host-lifecycle.js +1 -1
- package/dist/bundle/chunks/interactive-mode-ROO5W56P.js +2 -0
- package/dist/bundle/chunks/{llama-cpp-classify-U5XAMKZE.js → llama-cpp-classify-AD6TTKNL.js} +1 -1
- package/dist/bundle/chunks/{multi-session-host-34WOINP4.js → multi-session-host-5WTKAQJM.js} +5 -5
- package/dist/bundle/chunks/{openai-codex-responses-WNTYIPFZ.js → openai-codex-responses-Q4OWIK7Y.js} +1 -1
- package/dist/bundle/chunks/{openai-completions-VSH22FCN.js → openai-completions-6HU7GIGD.js} +1 -1
- package/dist/bundle/chunks/{openai-images-XBTUJXPR.js → openai-images-M73YANTR.js} +1 -1
- package/dist/bundle/chunks/{openai-responses-WLBHCFYL.js → openai-responses-S4QYZXF3.js} +1 -1
- package/dist/bundle/chunks/{openrouter-images-3LVJZY3G.js → openrouter-images-PCJFX2KO.js} +1 -1
- package/dist/bundle/chunks/{package-manager-cli-2QLVGQSC.js → package-manager-cli-NCD5FQGV.js} +1 -1
- package/dist/bundle/chunks/{rotation-stream-JUCEXHWU.js → rotation-stream-7E37KOCX.js} +1 -1
- package/dist/bundle/chunks/rpc-mode-MWURKCP2.js +2 -0
- package/dist/bundle/chunks/{schedule-command-QTYSUYO2.js → schedule-command-UTXB465F.js} +1 -1
- package/dist/bundle/chunks/session-control-endpoint-J6RHZXWB.js +3 -0
- package/dist/bundle/chunks/{session-picker-HX4LOJZ7.js → session-picker-VTTRFGE7.js} +1 -1
- package/dist/bundle/chunks/session-worker.js +376 -368
- package/dist/bundle/chunks/{typesafe-system-one-FNI3UASE.js → typesafe-system-one-TF23RUVG.js} +1 -1
- 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 +12 -0
- package/dist/core/agent-session.d.ts +18 -0
- package/dist/core/agent-session.js +55 -19
- package/dist/core/dynamic-prompt/handoff.d.ts +4 -2
- package/dist/core/dynamic-prompt/handoff.js +1 -1
- package/dist/core/extensions/builtin/diff.js +14 -9
- package/dist/core/extensions/builtin/files.js +6 -2
- package/dist/core/extensions/builtin/gpt-apply-patch/parser.js +5 -1
- package/dist/core/extensions/builtin/gpt-apply-patch/streaming-parser.d.ts +8 -0
- package/dist/core/extensions/builtin/gpt-apply-patch/streaming-parser.js +26 -2
- package/dist/core/extensions/builtin/gpt-apply-patch/streaming-render.js +82 -11
- package/dist/core/extensions/builtin/gpt-apply-patch/types.d.ts +9 -1
- package/dist/core/extensions/builtin/permission-system/auto-credentials.d.ts +2 -0
- package/dist/core/extensions/builtin/permission-system/auto-credentials.js +17 -0
- package/dist/core/extensions/builtin/permission-system/auto-paths.d.ts +14 -0
- package/dist/core/extensions/builtin/permission-system/auto-paths.js +93 -0
- package/dist/core/extensions/builtin/permission-system/auto-policy.d.ts +20 -0
- package/dist/core/extensions/builtin/permission-system/auto-policy.js +126 -0
- package/dist/core/extensions/builtin/permission-system/auto-program-rules.d.ts +6 -0
- package/dist/core/extensions/builtin/permission-system/auto-program-rules.js +139 -0
- package/dist/core/extensions/builtin/permission-system/auto-shell-grammar.d.ts +29 -0
- package/dist/core/extensions/builtin/permission-system/auto-shell-grammar.js +74 -0
- package/dist/core/extensions/builtin/permission-system/auto-shell-segments.d.ts +13 -0
- package/dist/core/extensions/builtin/permission-system/auto-shell-segments.js +23 -0
- package/dist/core/extensions/builtin/permission-system/cli.d.ts +1 -0
- package/dist/core/extensions/builtin/permission-system/cli.js +9 -10
- package/dist/core/extensions/builtin/permission-system/config.d.ts +4 -1
- package/dist/core/extensions/builtin/permission-system/config.js +13 -1
- package/dist/core/extensions/builtin/permission-system/index.js +20 -12
- package/dist/core/extensions/builtin/permission-system/non-interactive.d.ts +11 -6
- package/dist/core/extensions/builtin/permission-system/non-interactive.js +8 -33
- package/dist/core/extensions/builtin/permission-system/service.d.ts +20 -1
- package/dist/core/extensions/builtin/permission-system/service.js +43 -7
- package/dist/core/extensions/builtin/permission-system/settings.d.ts +1 -0
- package/dist/core/extensions/builtin/permission-system/settings.js +4 -3
- package/dist/core/extensions/builtin/permission-system/types.d.ts +1 -1
- package/dist/core/extensions/builtin/prompt-preset/claude-fable-5-1.js +16 -3
- package/dist/core/extensions/eval-handle-host.d.ts +82 -0
- package/dist/core/extensions/eval-handle-host.js +28 -0
- package/dist/core/extensions/loader.js +5 -0
- package/dist/core/extensions/runner.js +4 -0
- package/dist/core/extensions/types.d.ts +15 -0
- package/dist/core/retry-fallback/usage-limit.js +5 -1
- package/dist/core/tools/diff-render.d.ts +4 -0
- package/dist/core/tools/diff-render.js +13 -0
- package/dist/core/tools/renderers/edit.js +6 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/main.js +12 -0
- package/dist/modes/interactive/interactive-mode.d.ts +1 -0
- package/dist/modes/interactive/interactive-mode.js +55 -11
- package/dist/modes/interactive/provider-error-presentation.d.ts +25 -0
- package/dist/modes/interactive/provider-error-presentation.js +42 -0
- package/dist/modes/interactive/session-control-registry.js +2 -0
- package/dist/modes/interactive/theme/dark.json +5 -5
- package/dist/modes/interactive/theme/light.json +5 -5
- package/dist/modes/rpc/custom-capability.d.ts +6 -0
- package/dist/modes/rpc/custom-capability.js +6 -0
- package/dist/modes/rpc/host-daemon-registration.d.ts +9 -0
- package/dist/modes/rpc/host-daemon-registration.js +2 -4
- package/dist/modes/rpc/host-ensure-start.js +2 -0
- package/dist/modes/rpc/host-handoff-unregistered.d.ts +24 -0
- package/dist/modes/rpc/host-handoff-unregistered.js +50 -0
- package/dist/modes/rpc/host-handoff.d.ts +7 -2
- package/dist/modes/rpc/host-handoff.js +7 -3
- package/dist/modes/rpc/host-legacy.d.ts +3 -0
- package/dist/modes/rpc/host-legacy.js +10 -5
- package/dist/modes/rpc/host-probe.d.ts +12 -0
- package/dist/modes/rpc/host-probe.js +53 -5
- package/dist/modes/rpc/host-runner.js +1 -1
- package/dist/modes/rpc/host-successor.d.ts +5 -4
- package/dist/modes/rpc/host-successor.js +11 -4
- package/dist/modes/rpc/rpc-client.d.ts +3 -0
- package/dist/modes/rpc/rpc-input-validation.d.ts +7 -0
- package/dist/modes/rpc/rpc-input-validation.js +37 -0
- package/dist/modes/rpc/rpc-types.d.ts +9 -0
- package/dist/modes/rpc/session-command-activity.d.ts +2 -0
- package/dist/modes/rpc/session-command-activity.js +25 -0
- package/dist/modes/rpc/session-command-router.js +11 -4
- package/dist/modes/rpc/session-registry-attach.js +1 -0
- package/dist/modes/rpc/session-registry-types.d.ts +7 -0
- package/dist/modes/rpc/session-registry-types.js +11 -0
- package/dist/modes/rpc/session-registry.js +6 -3
- package/dist/modes/rpc/session-sweep.d.ts +12 -2
- package/dist/modes/rpc/session-sweep.js +36 -3
- package/dist/modes/rpc/session-teardown.d.ts +2 -0
- package/dist/modes/rpc/session-teardown.js +1 -0
- package/dist/modes/rpc/worker-session-registry.js +5 -1
- package/docs/rpc.md +49 -7
- package/docs/settings.md +3 -1
- package/package.json +6 -6
- package/dist/bundle/chunks/chunk-K3BXFPWZ.js +0 -2
- package/dist/bundle/chunks/chunk-UUAKEDVS.js +0 -14
- package/dist/bundle/chunks/cli-main-VZWSUHFB.js +0 -3
- package/dist/bundle/chunks/host-command-7JIGGOWR.js +0 -2
- package/dist/bundle/chunks/interactive-mode-JA7AHYYL.js +0 -2
- package/dist/bundle/chunks/rpc-mode-QEBSN2OH.js +0 -2
- package/dist/bundle/chunks/session-control-endpoint-UGQIP6ZN.js +0 -3
|
@@ -34,9 +34,9 @@
|
|
|
34
34
|
"customMessageBg": "okhsl(295 42% 24%)",
|
|
35
35
|
"customMessageText": "muted",
|
|
36
36
|
"customMessageLabel": "violet",
|
|
37
|
-
"toolPendingBg": "okhsl(229 5%
|
|
38
|
-
"toolSuccessBg": "okhsl(158
|
|
39
|
-
"toolErrorBg": "okhsl(19
|
|
37
|
+
"toolPendingBg": "okhsl(229 5% 13%)",
|
|
38
|
+
"toolSuccessBg": "okhsl(158 26% 13%)",
|
|
39
|
+
"toolErrorBg": "okhsl(19 28% 14%)",
|
|
40
40
|
"toolTitle": "text",
|
|
41
41
|
"toolOutput": "muted",
|
|
42
42
|
"mdHeading": "yellow",
|
|
@@ -49,8 +49,8 @@
|
|
|
49
49
|
"mdQuoteBorder": "muted",
|
|
50
50
|
"mdHr": "muted",
|
|
51
51
|
"mdListBullet": "violet",
|
|
52
|
-
"toolDiffAdded": "
|
|
53
|
-
"toolDiffRemoved": "
|
|
52
|
+
"toolDiffAdded": "okhsl(159 58% 76%)",
|
|
53
|
+
"toolDiffRemoved": "okhsl(20 70% 77%)",
|
|
54
54
|
"toolDiffContext": "muted",
|
|
55
55
|
"syntaxComment": "muted",
|
|
56
56
|
"syntaxKeyword": "blue",
|
|
@@ -34,9 +34,9 @@
|
|
|
34
34
|
"customMessageBg": "okhsl(295 25% 91%)",
|
|
35
35
|
"customMessageText": "muted",
|
|
36
36
|
"customMessageLabel": "violet",
|
|
37
|
-
"toolPendingBg": "okhsl(248 3%
|
|
38
|
-
"toolSuccessBg": "okhsl(156
|
|
39
|
-
"toolErrorBg": "okhsl(24
|
|
37
|
+
"toolPendingBg": "okhsl(248 3% 93%)",
|
|
38
|
+
"toolSuccessBg": "okhsl(156 25% 91%)",
|
|
39
|
+
"toolErrorBg": "okhsl(24 28% 91%)",
|
|
40
40
|
"toolTitle": "text",
|
|
41
41
|
"toolOutput": "muted",
|
|
42
42
|
"mdHeading": "yellow",
|
|
@@ -49,8 +49,8 @@
|
|
|
49
49
|
"mdQuoteBorder": "muted",
|
|
50
50
|
"mdHr": "muted",
|
|
51
51
|
"mdListBullet": "violet",
|
|
52
|
-
"toolDiffAdded": "
|
|
53
|
-
"toolDiffRemoved": "
|
|
52
|
+
"toolDiffAdded": "okhsl(159 85% 28%)",
|
|
53
|
+
"toolDiffRemoved": "okhsl(20 100% 30%)",
|
|
54
54
|
"toolDiffContext": "muted",
|
|
55
55
|
"syntaxComment": "muted",
|
|
56
56
|
"syntaxKeyword": "blue",
|
|
@@ -74,6 +74,12 @@ export declare const PROMPT_SURFACE_CAPABILITY = "prompt_surface";
|
|
|
74
74
|
* value reaches every consumer, so a client may rely on it the moment it sees this name.
|
|
75
75
|
*/
|
|
76
76
|
export declare const BROWSER_ENGINE_CAPABILITY = "browser_engine";
|
|
77
|
+
/**
|
|
78
|
+
* HOST capability: this host honors `open_session.retryFallback` (`{ modelFallback, fallbackChains }`). The
|
|
79
|
+
* policy is that session's own: it is applied as an in-memory settings override for that session only,
|
|
80
|
+
* never written to a settings file and never seen by another session on the host.
|
|
81
|
+
*/
|
|
82
|
+
export declare const RETRY_FALLBACK_PROFILE_CAPABILITY = "retry_fallback_profile";
|
|
77
83
|
/**
|
|
78
84
|
* HOST capability: `open_session.promptSurface` also accepts `chat` (a chat bridge: no routing line,
|
|
79
85
|
* no handoff block, no todo cues). A host without it refuses `chat` with `invalid_launch_profile`,
|
|
@@ -74,6 +74,12 @@ export const PROMPT_SURFACE_CAPABILITY = "prompt_surface";
|
|
|
74
74
|
* value reaches every consumer, so a client may rely on it the moment it sees this name.
|
|
75
75
|
*/
|
|
76
76
|
export const BROWSER_ENGINE_CAPABILITY = "browser_engine";
|
|
77
|
+
/**
|
|
78
|
+
* HOST capability: this host honors `open_session.retryFallback` (`{ modelFallback, fallbackChains }`). The
|
|
79
|
+
* policy is that session's own: it is applied as an in-memory settings override for that session only,
|
|
80
|
+
* never written to a settings file and never seen by another session on the host.
|
|
81
|
+
*/
|
|
82
|
+
export const RETRY_FALLBACK_PROFILE_CAPABILITY = "retry_fallback_profile";
|
|
77
83
|
/**
|
|
78
84
|
* HOST capability: `open_session.promptSurface` also accepts `chat` (a chat bridge: no routing line,
|
|
79
85
|
* no handoff block, no todo cues). A host without it refuses `chat` with `invalid_launch_profile`,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { EngineOrdinal } from "../../core/engine-build-identity.ts";
|
|
1
2
|
import { type DaemonPidFile } from "../app-server/daemon/process.ts";
|
|
2
3
|
import { type HostDaemonPaths, type HostGenerationPaths } from "./host-daemon-paths.ts";
|
|
3
4
|
/** Who wrote a registration: the process identity a later stop must match to be allowed. */
|
|
@@ -13,6 +14,14 @@ export interface HostRegistration {
|
|
|
13
14
|
readonly generation: number;
|
|
14
15
|
/** The profile the spawned host was launched with, for a client comparing two generations. */
|
|
15
16
|
readonly launchProfileId: string;
|
|
17
|
+
/**
|
|
18
|
+
* The build of the process this record names. Absent while it is not known yet (a handoff's
|
|
19
|
+
* successor before it answers): the record then claims no engine version rather than the writer's.
|
|
20
|
+
*/
|
|
21
|
+
readonly build?: {
|
|
22
|
+
readonly text: string;
|
|
23
|
+
readonly ordinal: EngineOrdinal;
|
|
24
|
+
};
|
|
16
25
|
}
|
|
17
26
|
export interface RegisteredHost {
|
|
18
27
|
readonly record: DaemonPidFile;
|
|
@@ -23,7 +23,6 @@
|
|
|
23
23
|
* this module reads and writes through its primitives and never builds a path of its own.
|
|
24
24
|
*/
|
|
25
25
|
import { rename, rm } from "node:fs/promises";
|
|
26
|
-
import { engineBuildIdentity } from "../../core/engine-build-identity.js";
|
|
27
26
|
import { parseDaemonPidFile, processMatchesPidFile, readProcessStartTime, } from "../app-server/daemon/process.js";
|
|
28
27
|
import { createGenerationDirectory, generationPaths, HOST_DAEMON_LAYOUT, sameEndpoint, } from "./host-daemon-paths.js";
|
|
29
28
|
import { isRecord, parseJson, readFileOrUndefined, writeStateFile } from "./host-daemon-state.js";
|
|
@@ -80,14 +79,13 @@ export async function writeHostRegistration(paths, registration) {
|
|
|
80
79
|
export async function writeGenerationRecord(paths, registration) {
|
|
81
80
|
const generation = generationPaths(paths, registration.instanceId);
|
|
82
81
|
const writer = { pid: process.pid, startTime: await thisProcessStartTime() };
|
|
83
|
-
const build =
|
|
82
|
+
const { build } = registration;
|
|
84
83
|
await createGenerationDirectory(generation);
|
|
85
84
|
await writeStateFile(generation.pidFile, {
|
|
86
85
|
...registration.record,
|
|
87
86
|
instance_id: registration.instanceId,
|
|
88
87
|
generation: registration.generation,
|
|
89
|
-
engineVersion: build.text,
|
|
90
|
-
engineOrdinal: build.ordinal,
|
|
88
|
+
...(build && { engineVersion: build.text, engineOrdinal: build.ordinal }),
|
|
91
89
|
launchProfileId: registration.launchProfileId,
|
|
92
90
|
socket: registration.socket,
|
|
93
91
|
writer,
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import { open, readFile } from "node:fs/promises";
|
|
5
|
+
import { engineBuildIdentity } from "../../core/engine-build-identity.js";
|
|
5
6
|
import { readProcessStartTime, waitForStartTime } from "../app-server/daemon/process.js";
|
|
6
7
|
import { clearHostRegistration, writeHostRegistration } from "./host-daemon-registration.js";
|
|
7
8
|
import { writeHostSettings } from "./host-daemon-state.js";
|
|
@@ -92,6 +93,7 @@ export async function startHost(paths, socket, options, generation = 0) {
|
|
|
92
93
|
instanceId,
|
|
93
94
|
generation,
|
|
94
95
|
launchProfileId: hostLaunchProfile(hostChildArgv(options.hostArgs ?? []), process.cwd()).profile_id,
|
|
96
|
+
build: engineBuildIdentity(),
|
|
95
97
|
});
|
|
96
98
|
child.unref();
|
|
97
99
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A handoff away from a host that no layout-2 registration proves (#2701).
|
|
3
|
+
*
|
|
4
|
+
* After an upgrade the old host is often still serving the very socket the updated client uses, and the
|
|
5
|
+
* only evidence of who it is may be a flat pre-layout-2 record - or nothing provable at all. Refusing
|
|
6
|
+
* such a host with `unknown_owner` left the client unable to replace it until somebody drained it by
|
|
7
|
+
* hand. An IDLE one is replaced instead; one that holds work never is.
|
|
8
|
+
*
|
|
9
|
+
* - The sessions are counted over a connection that stays open, and counted again over that same
|
|
10
|
+
* connection once the successor owns the socket: the recount reaches the old process, and a session
|
|
11
|
+
* it accepted in between is seen there.
|
|
12
|
+
* - A flat record that proves the process is the only licence to signal it, and the signal is a DRAIN
|
|
13
|
+
* (SIGUSR1), sent only when the recount still finds no session. A drain ends no work either way.
|
|
14
|
+
* - Without a provable owner nothing is signalled: the old host notices that it lost the public entry
|
|
15
|
+
* and drains itself (`host-supersession.ts`). That predecessor is named in one warning line, so an
|
|
16
|
+
* old host that outlives the handoff can still be identified.
|
|
17
|
+
*/
|
|
18
|
+
import type { HostDaemonPaths } from "./host-daemon-paths.ts";
|
|
19
|
+
import type { HostProtocolInfo } from "./host-decision.ts";
|
|
20
|
+
import type { HandoffHostOptions, HandoffResult } from "./host-handoff.ts";
|
|
21
|
+
/** One session count; an unregistered host is counted twice (before the swap and over the held connection after). */
|
|
22
|
+
export declare const SESSION_COUNT_TIMEOUT_MS = 10000;
|
|
23
|
+
export declare function handoffUnregisteredHost(options: HandoffHostOptions, paths: HostDaemonPaths, host: HostProtocolInfo): Promise<HandoffResult>;
|
|
24
|
+
//# sourceMappingURL=host-handoff-unregistered.d.ts.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { busyLegacyHostDetail, describeSessions, provenLegacyOwner } from "./host-legacy.js";
|
|
2
|
+
import { holdSessionCount } from "./host-probe.js";
|
|
3
|
+
import { startSuccessor } from "./host-successor.js";
|
|
4
|
+
/** One session count; an unregistered host is counted twice (before the swap and over the held connection after). */
|
|
5
|
+
export const SESSION_COUNT_TIMEOUT_MS = 10_000;
|
|
6
|
+
export async function handoffUnregisteredHost(options, paths, host) {
|
|
7
|
+
const counted = await holdSessionCount(options.socket, SESSION_COUNT_TIMEOUT_MS);
|
|
8
|
+
try {
|
|
9
|
+
const legacy = await provenLegacyOwner(paths, options.socket);
|
|
10
|
+
const sessions = counted?.sessions;
|
|
11
|
+
if (sessions !== 0) {
|
|
12
|
+
if (legacy !== undefined) {
|
|
13
|
+
return {
|
|
14
|
+
action: "refuse",
|
|
15
|
+
reason: "legacy_host",
|
|
16
|
+
upgradeable: true,
|
|
17
|
+
detail: busyLegacyHostDetail(legacy.pid, options.socket, sessions),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
action: "refuse",
|
|
22
|
+
reason: "unknown_owner",
|
|
23
|
+
upgradeable: true,
|
|
24
|
+
detail: `${options.socket} has no provable owner and holds ${describeSessions(sessions)}; it is never signalled - let that work finish, then retry`,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const result = await startSuccessor({
|
|
28
|
+
options,
|
|
29
|
+
paths,
|
|
30
|
+
host,
|
|
31
|
+
owner: legacy,
|
|
32
|
+
drainGate: async () => (await counted?.recount(SESSION_COUNT_TIMEOUT_MS)) === 0,
|
|
33
|
+
});
|
|
34
|
+
if (result.action === "handoff" && legacy === undefined)
|
|
35
|
+
warnUnsignalledPredecessor(options.socket, host);
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
counted?.close();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The one record of a predecessor nobody could prove, on the caller's stderr: the endpoint's log belongs
|
|
44
|
+
* to the generations writing it. Its pid is not known: no record proves it, and a Unix socket's peer
|
|
45
|
+
* credentials are not readable from this runtime.
|
|
46
|
+
*/
|
|
47
|
+
function warnUnsignalledPredecessor(socket, host) {
|
|
48
|
+
process.stderr.write(`generation handoff: no provable owner for ${socket}; the host it replaced (instance ${host.instanceId ?? "unknown"}, engine ${host.engineVersion ?? "unknown"}) listed 0 sessions and was not signalled; it drains itself on losing the public socket\n`);
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=host-handoff-unregistered.js.map
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { HostLifecyclePolicyInput } from "./host-lifecycle.ts";
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* The longest a handoff holds the ensure lock: its probe of the running host, the two session counts
|
|
4
|
+
* an unregistered host gets (before the swap and the recount after it), then the successor's start.
|
|
5
|
+
*/
|
|
3
6
|
export declare const HANDOFF_LOCK_HOLD_MS: number;
|
|
4
7
|
export interface HandoffHostOptions {
|
|
5
8
|
readonly socket: string;
|
|
@@ -43,8 +46,10 @@ export type HandoffRefusal =
|
|
|
43
46
|
| "handoff_unsupported"
|
|
44
47
|
/** win32: a named pipe can be neither renamed nor drained. */
|
|
45
48
|
| "upgrade_unsupported"
|
|
46
|
-
/**
|
|
49
|
+
/** No record proves which process serves this socket, and that host holds sessions (or will not say). */
|
|
47
50
|
| "unknown_owner"
|
|
51
|
+
/** A host from before layout 2, proven by its flat record, holds sessions; `detail` says how to retire it. */
|
|
52
|
+
| "legacy_host"
|
|
48
53
|
/** The public socket stopped being the one this handoff was decided against. */
|
|
49
54
|
| "socket_replaced"
|
|
50
55
|
/** `<public>.next-<gen>` would exceed the platform's socket path limit. */
|
|
@@ -31,11 +31,15 @@ import { createDaemonDirectories, createHostDaemonPaths } from "./host-daemon-pa
|
|
|
31
31
|
import { provenOwner, readHostRegistration } from "./host-daemon-registration.js";
|
|
32
32
|
import { GENERATION_HANDOFF_CAPABILITY } from "./host-decision.js";
|
|
33
33
|
import { acquireHostEnsureLock } from "./host-ensure-lock.js";
|
|
34
|
+
import { handoffUnregisteredHost, SESSION_COUNT_TIMEOUT_MS } from "./host-handoff-unregistered.js";
|
|
34
35
|
import { probeProtocolInfo } from "./host-probe.js";
|
|
35
36
|
import { SUCCESSOR_START_BUDGET_MS, startSuccessor } from "./host-successor.js";
|
|
36
37
|
const HANDOFF_PROBE_TIMEOUT_MS = 10_000;
|
|
37
|
-
/**
|
|
38
|
-
|
|
38
|
+
/**
|
|
39
|
+
* The longest a handoff holds the ensure lock: its probe of the running host, the two session counts
|
|
40
|
+
* an unregistered host gets (before the swap and the recount after it), then the successor's start.
|
|
41
|
+
*/
|
|
42
|
+
export const HANDOFF_LOCK_HOLD_MS = HANDOFF_PROBE_TIMEOUT_MS + 2 * SESSION_COUNT_TIMEOUT_MS + SUCCESSOR_START_BUDGET_MS;
|
|
39
43
|
/**
|
|
40
44
|
* How long a handoff waits for the lock. The longest holder is an ensure that probes the running host
|
|
41
45
|
* and then hands it off itself (its upgrade path); 10 s of headroom for a slow runner.
|
|
@@ -74,7 +78,7 @@ export async function handoffHostLocked(options) {
|
|
|
74
78
|
const registered = await readHostRegistration(paths);
|
|
75
79
|
const owner = await provenOwner(registered, options.socket);
|
|
76
80
|
if (!owner)
|
|
77
|
-
return
|
|
81
|
+
return handoffUnregisteredHost(options, paths, host);
|
|
78
82
|
return startSuccessor({ options, paths, host, owner });
|
|
79
83
|
}
|
|
80
84
|
function handoffUnsupported(options) {
|
|
@@ -15,4 +15,7 @@ export declare function provenLegacyOwner(paths: HostDaemonPaths, socket: string
|
|
|
15
15
|
pid: number;
|
|
16
16
|
processStartTime: string;
|
|
17
17
|
} | undefined>;
|
|
18
|
+
/** Why a proven legacy host that holds work is left alone, and the command that retires it. */
|
|
19
|
+
export declare function busyLegacyHostDetail(pid: number, socket: string, sessions: number | undefined): string;
|
|
20
|
+
export declare function describeSessions(sessions: number | undefined): string;
|
|
18
21
|
//# sourceMappingURL=host-legacy.d.ts.map
|
|
@@ -96,11 +96,16 @@ async function judgeLegacyHost(paths, probe, platform = process.platform) {
|
|
|
96
96
|
const sessions = await probeSessionCount(host.socket, LEGACY_PROBE_TIMEOUT_MS);
|
|
97
97
|
if (sessions === 0)
|
|
98
98
|
return { verdict: "idle", host };
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
};
|
|
99
|
+
return { verdict: "held", detail: busyLegacyHostDetail(pid, host.socket, sessions) };
|
|
100
|
+
}
|
|
101
|
+
/** Why a proven legacy host that holds work is left alone, and the command that retires it. */
|
|
102
|
+
export function busyLegacyHostDetail(pid, socket, sessions) {
|
|
103
|
+
return `pid ${pid} (${socket}) holds ${describeSessions(sessions)}; run \`${APP_NAME} host stop --drain --socket ${socket}\` to let it finish that work and exit, then retry`;
|
|
104
|
+
}
|
|
105
|
+
export function describeSessions(sessions) {
|
|
106
|
+
return sessions === undefined
|
|
107
|
+
? "an unknown number of sessions"
|
|
108
|
+
: `${sessions} open session${sessions === 1 ? "" : "s"}`;
|
|
104
109
|
}
|
|
105
110
|
/**
|
|
106
111
|
* SIGUSR1, then wait for the proven process to leave. A host that exits before the signal lands has
|
|
@@ -32,6 +32,18 @@ export declare function probeSocketReachable(socketPath: string, timeoutMs: numb
|
|
|
32
32
|
* does not answer. A stop decision needs the number the host reports, not the one a client remembers.
|
|
33
33
|
*/
|
|
34
34
|
export declare function probeSessionCount(socketPath: string, timeoutMs: number): Promise<number | undefined>;
|
|
35
|
+
/**
|
|
36
|
+
* The session count together with the connection that answered it, kept open so the SAME host can be
|
|
37
|
+
* asked again after its public path was handed to somebody else: the recount reaches the process that
|
|
38
|
+
* was counted, never whoever serves the path by then. `undefined` when the host does not answer.
|
|
39
|
+
*/
|
|
40
|
+
export interface HeldSessionCount {
|
|
41
|
+
readonly sessions: number;
|
|
42
|
+
/** Asks the counted host again over the held connection; `undefined` when it no longer answers. */
|
|
43
|
+
recount(timeoutMs: number): Promise<number | undefined>;
|
|
44
|
+
close(): void;
|
|
45
|
+
}
|
|
46
|
+
export declare function holdSessionCount(socketPath: string, timeoutMs: number): Promise<HeldSessionCount | undefined>;
|
|
35
47
|
/** Sends one command and returns its `data`, or `undefined` for any failure to get a usable answer. */
|
|
36
48
|
export declare function requestOnSocket(socketPath: string, command: Readonly<Record<string, unknown>>, timeoutMs: number): Promise<unknown>;
|
|
37
49
|
//# sourceMappingURL=host-probe.d.ts.map
|
|
@@ -59,10 +59,58 @@ export async function probeSocketReachable(socketPath, timeoutMs) {
|
|
|
59
59
|
* does not answer. A stop decision needs the number the host reports, not the one a client remembers.
|
|
60
60
|
*/
|
|
61
61
|
export async function probeSessionCount(socketPath, timeoutMs) {
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
return sessionCountOf(await requestOnSocket(socketPath, { type: "list_sessions", include_workers: true }, timeoutMs));
|
|
63
|
+
}
|
|
64
|
+
export async function holdSessionCount(socketPath, timeoutMs) {
|
|
65
|
+
const outcome = await connectAndAsk(socketPath, { id: PROBE_REQUEST_ID, type: "list_sessions", include_workers: true }, timeoutMs, true);
|
|
66
|
+
const kept = outcome.kept;
|
|
67
|
+
if (kept === undefined)
|
|
68
|
+
return undefined;
|
|
69
|
+
const sessions = sessionCountOf(outcome.answer);
|
|
70
|
+
if (sessions === undefined) {
|
|
71
|
+
kept.destroy();
|
|
64
72
|
return undefined;
|
|
65
|
-
|
|
73
|
+
}
|
|
74
|
+
let asked = 0;
|
|
75
|
+
return {
|
|
76
|
+
sessions,
|
|
77
|
+
recount: (recountTimeoutMs) => askAgain(kept, `${PROBE_REQUEST_ID}-recount-${++asked}`, recountTimeoutMs),
|
|
78
|
+
close: () => kept.destroy(),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function askAgain(socket, id, timeoutMs) {
|
|
82
|
+
return new Promise((resolveCount) => {
|
|
83
|
+
let buffer = "";
|
|
84
|
+
const finish = (count) => {
|
|
85
|
+
clearTimeout(timeout);
|
|
86
|
+
socket.off("data", onData);
|
|
87
|
+
socket.off("close", onClose);
|
|
88
|
+
resolveCount(count);
|
|
89
|
+
};
|
|
90
|
+
const onClose = () => finish(undefined);
|
|
91
|
+
const onData = (chunk) => {
|
|
92
|
+
buffer += chunk.toString("utf8");
|
|
93
|
+
for (let newline = buffer.indexOf("\n"); newline !== -1; newline = buffer.indexOf("\n")) {
|
|
94
|
+
const answer = readAnswer(buffer.slice(0, newline), id);
|
|
95
|
+
buffer = buffer.slice(newline + 1);
|
|
96
|
+
if (answer !== undefined) {
|
|
97
|
+
finish(sessionCountOf(answer));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
const timeout = setTimeout(() => finish(undefined), timeoutMs);
|
|
103
|
+
if (socket.destroyed) {
|
|
104
|
+
finish(undefined);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
socket.on("data", onData);
|
|
108
|
+
socket.once("close", onClose);
|
|
109
|
+
socket.write(`${JSON.stringify({ id, type: "list_sessions", include_workers: true })}\n`);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
function sessionCountOf(reply) {
|
|
113
|
+
return isRecord(reply) && Array.isArray(reply.sessions) ? reply.sessions.length : undefined;
|
|
66
114
|
}
|
|
67
115
|
/** Sends one command and returns its `data`, or `undefined` for any failure to get a usable answer. */
|
|
68
116
|
export async function requestOnSocket(socketPath, command, timeoutMs) {
|
|
@@ -125,7 +173,7 @@ async function connectAndAsk(socketPath, request, timeoutMs, keep = false) {
|
|
|
125
173
|
sendSocketHandshake(socket, secret);
|
|
126
174
|
});
|
|
127
175
|
}
|
|
128
|
-
function readAnswer(text) {
|
|
176
|
+
function readAnswer(text, id = PROBE_REQUEST_ID) {
|
|
129
177
|
let parsed;
|
|
130
178
|
try {
|
|
131
179
|
parsed = JSON.parse(text);
|
|
@@ -133,7 +181,7 @@ function readAnswer(text) {
|
|
|
133
181
|
catch {
|
|
134
182
|
return undefined;
|
|
135
183
|
}
|
|
136
|
-
if (!isRecord(parsed) || parsed.id !==
|
|
184
|
+
if (!isRecord(parsed) || parsed.id !== id || parsed.success !== true)
|
|
137
185
|
return undefined;
|
|
138
186
|
return parsed.data ?? {};
|
|
139
187
|
}
|
|
@@ -164,7 +164,7 @@ async function handoffOutcome(target, spec) {
|
|
|
164
164
|
return refusal("refuse", before, {
|
|
165
165
|
reason: upgradeRefusal(result.reason),
|
|
166
166
|
socket: target.socket,
|
|
167
|
-
detail: result.reason,
|
|
167
|
+
detail: result.detail ?? result.reason,
|
|
168
168
|
upgradeable: result.upgradeable,
|
|
169
169
|
});
|
|
170
170
|
}
|
|
@@ -7,10 +7,11 @@ export declare function startSuccessor(context: {
|
|
|
7
7
|
options: HandoffHostOptions;
|
|
8
8
|
paths: HostDaemonPaths;
|
|
9
9
|
host: HostProtocolInfo;
|
|
10
|
+
/** The predecessor's proven identity; `undefined` when nothing proves it, and then it is never signalled. */
|
|
10
11
|
owner: {
|
|
11
|
-
pid: number;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
readonly pid: number;
|
|
13
|
+
} | undefined;
|
|
14
|
+
/** Asked once the successor owns the socket: the drain request is sent only when it answers true. */
|
|
15
|
+
drainGate?: () => Promise<boolean>;
|
|
15
16
|
}): Promise<HandoffResult>;
|
|
16
17
|
//# sourceMappingURL=host-successor.d.ts.map
|
|
@@ -21,7 +21,7 @@ import { randomUUID } from "node:crypto";
|
|
|
21
21
|
import { open, rm, writeFile } from "node:fs/promises";
|
|
22
22
|
import { waitForStartTime } from "../app-server/daemon/process.js";
|
|
23
23
|
import { generationPaths, HOST_STATE_FILE_MODE } from "./host-daemon-paths.js";
|
|
24
|
-
import { releaseGeneration, writeGenerationRecord, writeHostRegistration } from "./host-daemon-registration.js";
|
|
24
|
+
import { releaseGeneration, writeGenerationRecord, writeHostRegistration, } from "./host-daemon-registration.js";
|
|
25
25
|
import { readFileOrUndefined, readHostSettings, writeHostSettings } from "./host-daemon-state.js";
|
|
26
26
|
import { defaultHostLaunch, PINNED_HOST_CLIENT_CAPABILITIES } from "./host-launch.js";
|
|
27
27
|
import { DEFAULT_HOST_IDLE_EXIT_MS } from "./host-lifecycle.js";
|
|
@@ -37,7 +37,7 @@ const SUCCESSOR_EXIT_WAIT_MS = 5_000;
|
|
|
37
37
|
/** The longest `startSuccessor` runs: its start-time read, the readiness window, one last probe and a refusal's exit wait. */
|
|
38
38
|
export const SUCCESSOR_START_BUDGET_MS = 10_000 + DEFAULT_HANDOFF_READINESS_MS + 2_000 + SUCCESSOR_EXIT_WAIT_MS;
|
|
39
39
|
export async function startSuccessor(context) {
|
|
40
|
-
const { options, paths, host, owner } = context;
|
|
40
|
+
const { options, paths, host, owner, drainGate } = context;
|
|
41
41
|
const generation = (host.generation ?? 0) + 1;
|
|
42
42
|
// The successor's identity, chosen here so its generation directory holds its settings before it
|
|
43
43
|
// boots and the pointer can name it the instant it answers on the public socket.
|
|
@@ -119,13 +119,14 @@ export async function startSuccessor(context) {
|
|
|
119
119
|
// The pointer moves to the successor only now: until the rename landed, the generation the
|
|
120
120
|
// clients reach is still the predecessor, and the pointer has to name whoever owns the socket.
|
|
121
121
|
await options._test?.beforeRegistration?.();
|
|
122
|
-
await writeHostRegistration(paths, registration);
|
|
122
|
+
await writeHostRegistration(paths, { ...registration, ...successorBuild(answer) });
|
|
123
123
|
child.unref();
|
|
124
124
|
// The successor owns the socket now: the predecessor may drain. SIGUSR1 is sent only here,
|
|
125
125
|
// to a pid the record proved and a host that advertised it can survive the signal. A
|
|
126
126
|
// predecessor that exited on its own in the meantime is already drained, and the handoff it
|
|
127
127
|
// was being asked to make room for has already happened.
|
|
128
|
-
|
|
128
|
+
if (owner !== undefined && (drainGate === undefined || (await drainGate())))
|
|
129
|
+
signalGeneration(owner.pid, "SIGUSR1");
|
|
129
130
|
return {
|
|
130
131
|
action: "handoff",
|
|
131
132
|
pid: child.pid,
|
|
@@ -185,6 +186,12 @@ async function exitedWithin(exited, ms) {
|
|
|
185
186
|
clearTimeout(timer);
|
|
186
187
|
}
|
|
187
188
|
}
|
|
189
|
+
/** The successor's build as the successor itself reported it on the socket; never this process's build. */
|
|
190
|
+
function successorBuild(answer) {
|
|
191
|
+
if (answer.engineVersion === undefined || answer.engineOrdinal === undefined)
|
|
192
|
+
return {};
|
|
193
|
+
return { build: { text: answer.engineVersion, ordinal: answer.engineOrdinal } };
|
|
194
|
+
}
|
|
188
195
|
/**
|
|
189
196
|
* The successor's own answer on the PUBLIC socket is the only proof the rename landed: it
|
|
190
197
|
* reports a different `instanceId` than the generation being replaced. An exit instead means
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import type { AgentMessage, ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
7
7
|
import type { ImageContent } from "@earendil-works/pi-ai";
|
|
8
8
|
import type { PromptDisposition, QueuedInputDisposition, SessionStats } from "../../core/agent-session.ts";
|
|
9
|
+
import type { SessionRetryFallbackProfile } from "../../core/agent-session-runtime.ts";
|
|
9
10
|
import type { BashResult } from "../../core/bash-executor.ts";
|
|
10
11
|
import type { BrowserEngine } from "../../core/browser-engine.ts";
|
|
11
12
|
import { type ClientMessageIdentity } from "../../core/client-message-identity.ts";
|
|
@@ -136,6 +137,8 @@ export declare class RpcClient {
|
|
|
136
137
|
promptSurface?: PromptSurface;
|
|
137
138
|
/** Which browser this session's skills drive; needs the host's `browser_engine`. */
|
|
138
139
|
browserEngine?: BrowserEngine;
|
|
140
|
+
/** This session's own fallback policy, in memory only; needs the host's `retry_fallback_profile`. */
|
|
141
|
+
retryFallback?: SessionRetryFallbackProfile;
|
|
139
142
|
}): Promise<{
|
|
140
143
|
sessionId: string;
|
|
141
144
|
state: RpcSessionState;
|
|
@@ -40,6 +40,13 @@ export declare function sessionPromptSurfaceError(value: unknown): string | unde
|
|
|
40
40
|
* own browser must not silently run without one.
|
|
41
41
|
*/
|
|
42
42
|
export declare function sessionBrowserEngineError(value: unknown): string | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* Detail for an `open_session.retryFallback` the host refuses, or undefined when it is absent or well
|
|
45
|
+
* formed: `{ modelFallback: boolean, fallbackChains: Record<string, string[]> }`, at most 32 chains of at
|
|
46
|
+
* most 32 non-empty selectors. A malformed profile is refused rather than ignored, so a caller that asked
|
|
47
|
+
* for a chain never runs without one unknowingly.
|
|
48
|
+
*/
|
|
49
|
+
export declare function sessionRetryFallbackError(value: unknown): string | undefined;
|
|
43
50
|
export declare function rpcCommandShapeError(command: unknown): string | undefined;
|
|
44
51
|
export declare function rpcMessageLengthError(command: unknown): string | undefined;
|
|
45
52
|
//# sourceMappingURL=rpc-input-validation.d.ts.map
|
|
@@ -161,6 +161,43 @@ export function sessionBrowserEngineError(value) {
|
|
|
161
161
|
return undefined;
|
|
162
162
|
return `browserEngine must be "connected", "builtin" or "none".`;
|
|
163
163
|
}
|
|
164
|
+
const MAX_RETRY_FALLBACK_CHAINS = 32;
|
|
165
|
+
const MAX_RETRY_FALLBACK_ENTRIES = 32;
|
|
166
|
+
const MAX_RETRY_FALLBACK_SELECTOR_LENGTH = 512;
|
|
167
|
+
/**
|
|
168
|
+
* Detail for an `open_session.retryFallback` the host refuses, or undefined when it is absent or well
|
|
169
|
+
* formed: `{ modelFallback: boolean, fallbackChains: Record<string, string[]> }`, at most 32 chains of at
|
|
170
|
+
* most 32 non-empty selectors. A malformed profile is refused rather than ignored, so a caller that asked
|
|
171
|
+
* for a chain never runs without one unknowingly.
|
|
172
|
+
*/
|
|
173
|
+
export function sessionRetryFallbackError(value) {
|
|
174
|
+
if (value === undefined)
|
|
175
|
+
return undefined;
|
|
176
|
+
const shape = "retryFallback must be { modelFallback: boolean, fallbackChains: Record<string, string[]> }.";
|
|
177
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
178
|
+
return shape;
|
|
179
|
+
const record = value;
|
|
180
|
+
if (typeof record.modelFallback !== "boolean")
|
|
181
|
+
return shape;
|
|
182
|
+
const chains = record.fallbackChains;
|
|
183
|
+
if (typeof chains !== "object" || chains === null || Array.isArray(chains))
|
|
184
|
+
return shape;
|
|
185
|
+
const entries = Object.entries(chains);
|
|
186
|
+
if (entries.length > MAX_RETRY_FALLBACK_CHAINS)
|
|
187
|
+
return `retryFallback allows at most ${MAX_RETRY_FALLBACK_CHAINS} chains.`;
|
|
188
|
+
for (const [key, selectors] of entries) {
|
|
189
|
+
if (!isSelector(key))
|
|
190
|
+
return shape;
|
|
191
|
+
if (!Array.isArray(selectors) || !selectors.every(isSelector))
|
|
192
|
+
return shape;
|
|
193
|
+
if (selectors.length > MAX_RETRY_FALLBACK_ENTRIES)
|
|
194
|
+
return `retryFallback allows at most ${MAX_RETRY_FALLBACK_ENTRIES} entries per chain.`;
|
|
195
|
+
}
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
function isSelector(value) {
|
|
199
|
+
return typeof value === "string" && value.trim().length > 0 && value.length <= MAX_RETRY_FALLBACK_SELECTOR_LENGTH;
|
|
200
|
+
}
|
|
164
201
|
export function rpcCommandShapeError(command) {
|
|
165
202
|
if (typeof command !== "object" || command === null || Array.isArray(command)) {
|
|
166
203
|
return "RPC command must be a JSON object.";
|
|
@@ -9,6 +9,7 @@ import type { ImageContent, Model, ProviderDiagnostic, ThinkingSelection } from
|
|
|
9
9
|
import type { SessionRuntimeKind } from "../../cli/args.ts";
|
|
10
10
|
import type { AgentAbortSource } from "../../core/agent-abort-provenance.ts";
|
|
11
11
|
import type { PromptDisposition, QueuedInput, QueuedInputDisposition, SessionStats } from "../../core/agent-session.ts";
|
|
12
|
+
import type { SessionRetryFallbackProfile } from "../../core/agent-session-runtime.ts";
|
|
12
13
|
import type { BashResult } from "../../core/bash-executor.ts";
|
|
13
14
|
import type { BrowserEngine } from "../../core/browser-engine.ts";
|
|
14
15
|
import type { ClientMessageIdentity } from "../../core/client-message-identity.ts";
|
|
@@ -504,6 +505,14 @@ export type RpcCommand = (RpcSessionCommand & {
|
|
|
504
505
|
* Requires the host capability `browser_engine`. Any other value is refused with `invalid_launch_profile`.
|
|
505
506
|
*/
|
|
506
507
|
browserEngine?: BrowserEngine;
|
|
508
|
+
/**
|
|
509
|
+
* The fallback policy THIS session runs with (e.g. a task child's own chain): applied as an
|
|
510
|
+
* in-memory override of this session's `retry.modelFallback` / `retry.fallbackChains`, never written
|
|
511
|
+
* to a settings file and never seen by another session. Requires the host capability
|
|
512
|
+
* `retry_fallback_profile`; a malformed value is refused with `invalid_launch_profile`. Applied
|
|
513
|
+
* when the open creates the session; an attach keeps the session's existing policy.
|
|
514
|
+
*/
|
|
515
|
+
retryFallback?: SessionRetryFallbackProfile;
|
|
507
516
|
} | {
|
|
508
517
|
id?: string;
|
|
509
518
|
type: "close_session";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Whether the command itself refreshes activity, apart from attached-client keepalives. */
|
|
2
|
+
const observations = new Set([
|
|
3
|
+
"get_state",
|
|
4
|
+
"get_messages",
|
|
5
|
+
"get_entries",
|
|
6
|
+
"get_tree",
|
|
7
|
+
"get_fork_messages",
|
|
8
|
+
"get_last_assistant_text",
|
|
9
|
+
"get_session_stats",
|
|
10
|
+
"get_steering_messages",
|
|
11
|
+
"get_follow_up_messages",
|
|
12
|
+
"get_available_models",
|
|
13
|
+
"get_available_thinking_levels",
|
|
14
|
+
"get_fast_mode",
|
|
15
|
+
"get_commands",
|
|
16
|
+
"get_loaded_surfaces",
|
|
17
|
+
"get_media",
|
|
18
|
+
"get_auth_providers",
|
|
19
|
+
"get_provider_accounts",
|
|
20
|
+
"memory_report",
|
|
21
|
+
]);
|
|
22
|
+
export function refreshesSessionActivity(command) {
|
|
23
|
+
return !observations.has(command);
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=session-command-activity.js.map
|