@code-yeongyu/senpi 2026.10.4 → 2026.10.5
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 +20 -0
- package/dist/bundle/chunks/{anthropic-messages-GE4OCV55.js → anthropic-messages-G5236QNK.js} +1 -1
- package/dist/bundle/chunks/{app-server-command-I2AKSC6I.js → app-server-command-3Q5G4CAF.js} +1 -1
- package/dist/bundle/chunks/{chunk-2CS2GZBE.js → chunk-2WV426OJ.js} +2 -2
- package/dist/bundle/chunks/{chunk-INGR4HFB.js → chunk-ECDLITEY.js} +2 -2
- package/dist/bundle/chunks/{chunk-6O26XO2C.js → chunk-EEOKVSNF.js} +220 -252
- package/dist/bundle/chunks/{chunk-MNLBL232.js → chunk-JVSOW4QU.js} +1 -1
- package/dist/bundle/chunks/chunk-K3BXFPWZ.js +2 -0
- package/dist/bundle/chunks/{chunk-QOT4HXY2.js → chunk-UHRXXTFZ.js} +3 -3
- package/dist/bundle/chunks/chunk-UUAKEDVS.js +14 -0
- package/dist/bundle/chunks/{chunk-ZKLQIAG5.js → chunk-UXIQK5NQ.js} +1 -1
- package/dist/bundle/chunks/chunk-WTJU5B2C.js +51 -0
- package/dist/bundle/chunks/chunk-YSPHACIN.js +2 -0
- package/dist/bundle/chunks/{cli-main-BLXUYHLQ.js → cli-main-VZWSUHFB.js} +1 -1
- package/dist/bundle/chunks/{help-fast-path-TQWU6DPG.js → help-fast-path-SHZJ3QWQ.js} +1 -1
- package/dist/bundle/chunks/{host-command-JXNOYPAC.js → host-command-7JIGGOWR.js} +1 -1
- package/dist/bundle/chunks/{interactive-mode-CSVSUAS7.js → interactive-mode-JA7AHYYL.js} +1 -1
- package/dist/bundle/chunks/multi-session-host-34WOINP4.js +23 -0
- package/dist/bundle/chunks/{openai-completions-2DUKDEVN.js → openai-completions-VSH22FCN.js} +1 -1
- package/dist/bundle/chunks/{openai-responses-WCB4HIPI.js → openai-responses-WLBHCFYL.js} +1 -1
- package/dist/bundle/chunks/{package-manager-cli-Q5EQZL7I.js → package-manager-cli-2QLVGQSC.js} +1 -1
- package/dist/bundle/chunks/{rotation-stream-VZLJIEP5.js → rotation-stream-JUCEXHWU.js} +1 -1
- package/dist/bundle/chunks/{rpc-mode-6VMXY46C.js → rpc-mode-QEBSN2OH.js} +1 -1
- package/dist/bundle/chunks/{session-control-endpoint-K3RWX652.js → session-control-endpoint-UGQIP6ZN.js} +1 -1
- package/dist/bundle/chunks/{session-picker-IPG3MKRC.js → session-picker-HX4LOJZ7.js} +1 -1
- package/dist/bundle/chunks/session-worker.js +251 -242
- 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 +5 -0
- package/dist/core/agent-session-runtime.js +5 -0
- package/dist/core/agent-session-services.d.ts +1 -0
- package/dist/core/agent-session-services.js +1 -0
- package/dist/core/agent-session.d.ts +14 -0
- package/dist/core/agent-session.js +31 -0
- package/dist/core/browser-engine.d.ts +7 -0
- package/dist/core/browser-engine.js +7 -0
- package/dist/core/continue-from-leaf.d.ts +18 -0
- package/dist/core/continue-from-leaf.js +30 -0
- package/dist/core/extensions/builtin/gpt-apply-patch/line-endings.d.ts +28 -0
- package/dist/core/extensions/builtin/gpt-apply-patch/line-endings.js +76 -0
- package/dist/core/extensions/builtin/gpt-apply-patch/parser.js +21 -23
- package/dist/core/extensions/builtin/gpt-apply-patch/patch-replace.js +5 -16
- package/dist/core/extensions/builtin/gpt-apply-patch/streaming-parser.js +3 -1
- package/dist/core/extensions/builtin/gpt-apply-patch/text.d.ts +8 -0
- package/dist/core/extensions/builtin/gpt-apply-patch/text.js +26 -3
- package/dist/core/extensions/builtin/gpt-apply-patch/types.d.ts +2 -0
- package/dist/core/extensions/builtin/terminal/tools/bash.d.ts +1 -0
- package/dist/core/extensions/builtin/terminal/tools/bash.js +8 -1
- package/dist/core/extensions/runner.d.ts +1 -0
- package/dist/core/extensions/runner.js +7 -0
- package/dist/core/extensions/types.d.ts +4 -0
- package/dist/core/model-resolver.js +1 -1
- package/dist/core/sdk.d.ts +3 -0
- package/dist/core/sdk.js +1 -0
- package/dist/core/tools/bash.js +3 -0
- package/dist/main.js +1 -0
- package/dist/modes/interactive/components/session-selector.d.ts +11 -0
- package/dist/modes/interactive/components/session-selector.js +30 -5
- package/dist/modes/rpc/connection-handler.js +18 -1
- package/dist/modes/rpc/custom-capability.d.ts +9 -0
- package/dist/modes/rpc/custom-capability.js +9 -0
- package/dist/modes/rpc/host-daemon-env.js +2 -0
- package/dist/modes/rpc/host-idle-handover.js +1 -0
- package/dist/modes/rpc/media-placeholders.d.ts +23 -1
- package/dist/modes/rpc/media-placeholders.js +16 -13
- package/dist/modes/rpc/rpc-client.d.ts +8 -0
- package/dist/modes/rpc/rpc-client.js +8 -0
- package/dist/modes/rpc/rpc-input-validation.d.ts +6 -0
- package/dist/modes/rpc/rpc-input-validation.js +11 -0
- package/dist/modes/rpc/rpc-types.d.ts +19 -0
- package/dist/modes/rpc/session-command-router.js +11 -2
- package/dist/modes/rpc/session-event-writer.d.ts +4 -0
- package/dist/modes/rpc/session-event-writer.js +12 -1
- package/dist/modes/rpc/session-registry-attach.js +4 -0
- package/dist/modes/rpc/session-worker-client.d.ts +3 -0
- package/dist/modes/rpc/session-worker-client.js +6 -0
- package/dist/modes/rpc/session-worker-protocol.d.ts +5 -0
- package/dist/modes/rpc/session-worker.js +6 -0
- package/dist/modes/rpc/tool-media-store.d.ts +59 -0
- package/dist/modes/rpc/tool-media-store.js +190 -0
- package/dist/modes/rpc/worker-session-registry.js +8 -3
- package/docs/environment-variables.md +1 -0
- package/docs/rpc.md +62 -3
- package/package.json +7 -6
- package/dist/bundle/chunks/chunk-KMMECBM2.js +0 -15
- package/dist/bundle/chunks/chunk-LHYG7TK7.js +0 -2
- package/dist/bundle/chunks/chunk-LJWC5IVX.js +0 -2
- package/dist/bundle/chunks/chunk-PC3E2YS6.js +0 -9
- package/dist/bundle/chunks/multi-session-host-5AFJKSSM.js +0 -23
|
@@ -7,6 +7,7 @@ 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
9
|
import type { BashResult } from "../../core/bash-executor.ts";
|
|
10
|
+
import type { BrowserEngine } from "../../core/browser-engine.ts";
|
|
10
11
|
import { type ClientMessageIdentity } from "../../core/client-message-identity.ts";
|
|
11
12
|
import type { CompactionResult } from "../../core/compaction/index.ts";
|
|
12
13
|
import type { PromptSurface } from "../../core/dynamic-prompt/types.ts";
|
|
@@ -133,6 +134,8 @@ export declare class RpcClient {
|
|
|
133
134
|
auto_title?: boolean;
|
|
134
135
|
/** Where this session's replies render; needs the host's `prompt_surface` (`prompt_surface_chat` for `chat`). */
|
|
135
136
|
promptSurface?: PromptSurface;
|
|
137
|
+
/** Which browser this session's skills drive; needs the host's `browser_engine`. */
|
|
138
|
+
browserEngine?: BrowserEngine;
|
|
136
139
|
}): Promise<{
|
|
137
140
|
sessionId: string;
|
|
138
141
|
state: RpcSessionState;
|
|
@@ -176,6 +179,11 @@ export declare class RpcClient {
|
|
|
176
179
|
triggerTurn?: boolean;
|
|
177
180
|
deliverAs?: "steer" | "followUp" | "nextTurn";
|
|
178
181
|
}): Promise<void>;
|
|
182
|
+
/**
|
|
183
|
+
* Start a turn from the current leaf with no new prompt (#1930), for example after an
|
|
184
|
+
* edited assistant response. Refused with `streaming` or `nothing_to_continue`.
|
|
185
|
+
*/
|
|
186
|
+
continueFromLeaf(): Promise<void>;
|
|
179
187
|
/**
|
|
180
188
|
* Queue a steering message to interrupt the agent mid-run.
|
|
181
189
|
*/
|
|
@@ -393,6 +393,14 @@ export class RpcClient {
|
|
|
393
393
|
async sendCustomMessage(message, options) {
|
|
394
394
|
await this.send({ type: "send_custom_message", ...message, ...options });
|
|
395
395
|
}
|
|
396
|
+
/**
|
|
397
|
+
* Start a turn from the current leaf with no new prompt (#1930), for example after an
|
|
398
|
+
* edited assistant response. Refused with `streaming` or `nothing_to_continue`.
|
|
399
|
+
*/
|
|
400
|
+
async continueFromLeaf() {
|
|
401
|
+
const response = await this.send({ type: "continue_from_leaf" });
|
|
402
|
+
this.getData(response);
|
|
403
|
+
}
|
|
396
404
|
/**
|
|
397
405
|
* Queue a steering message to interrupt the agent mid-run.
|
|
398
406
|
*/
|
|
@@ -34,6 +34,12 @@ export declare function sessionAutoTitleError(value: unknown): string | undefine
|
|
|
34
34
|
* for the app or chat prompt must not silently get the routing line.
|
|
35
35
|
*/
|
|
36
36
|
export declare function sessionPromptSurfaceError(value: unknown): string | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* Detail for an `open_session.browserEngine` the host refuses, or undefined when the value is absent
|
|
39
|
+
* or a known engine. An unknown value is never read as `none`: a client that asked for the user's
|
|
40
|
+
* own browser must not silently run without one.
|
|
41
|
+
*/
|
|
42
|
+
export declare function sessionBrowserEngineError(value: unknown): string | undefined;
|
|
37
43
|
export declare function rpcCommandShapeError(command: unknown): string | undefined;
|
|
38
44
|
export declare function rpcMessageLengthError(command: unknown): string | undefined;
|
|
39
45
|
//# sourceMappingURL=rpc-input-validation.d.ts.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isBrowserEngine } from "../../core/browser-engine.js";
|
|
1
2
|
import { clientMessageIdentitySchema } from "../../core/client-message-identity.js";
|
|
2
3
|
export const MAX_RPC_MESSAGE_CHARACTERS = 1_000_000;
|
|
3
4
|
/**
|
|
@@ -150,6 +151,16 @@ export function sessionPromptSurfaceError(value) {
|
|
|
150
151
|
return undefined;
|
|
151
152
|
return `promptSurface must be "terminal", "app" or "chat".`;
|
|
152
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* Detail for an `open_session.browserEngine` the host refuses, or undefined when the value is absent
|
|
156
|
+
* or a known engine. An unknown value is never read as `none`: a client that asked for the user's
|
|
157
|
+
* own browser must not silently run without one.
|
|
158
|
+
*/
|
|
159
|
+
export function sessionBrowserEngineError(value) {
|
|
160
|
+
if (value === undefined || isBrowserEngine(value))
|
|
161
|
+
return undefined;
|
|
162
|
+
return `browserEngine must be "connected", "builtin" or "none".`;
|
|
163
|
+
}
|
|
153
164
|
export function rpcCommandShapeError(command) {
|
|
154
165
|
if (typeof command !== "object" || command === null || Array.isArray(command)) {
|
|
155
166
|
return "RPC command must be a JSON object.";
|
|
@@ -10,6 +10,7 @@ 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
12
|
import type { BashResult } from "../../core/bash-executor.ts";
|
|
13
|
+
import type { BrowserEngine } from "../../core/browser-engine.ts";
|
|
13
14
|
import type { ClientMessageIdentity } from "../../core/client-message-identity.ts";
|
|
14
15
|
import type { CompactionResult } from "../../core/compaction/index.ts";
|
|
15
16
|
import type { PromptSurface } from "../../core/dynamic-prompt/types.ts";
|
|
@@ -45,6 +46,9 @@ type RpcSessionCommand = {
|
|
|
45
46
|
details?: unknown;
|
|
46
47
|
triggerTurn?: boolean;
|
|
47
48
|
deliverAs?: "steer" | "followUp" | "nextTurn";
|
|
49
|
+
} | {
|
|
50
|
+
id?: string;
|
|
51
|
+
type: "continue_from_leaf";
|
|
48
52
|
} | {
|
|
49
53
|
id?: string;
|
|
50
54
|
type: "append_user_message";
|
|
@@ -491,6 +495,15 @@ export type RpcCommand = (RpcSessionCommand & {
|
|
|
491
495
|
* Requires the host capability `prompt_surface`. Any other value is refused with `invalid_launch_profile`.
|
|
492
496
|
*/
|
|
493
497
|
promptSurface?: PromptSurface;
|
|
498
|
+
/**
|
|
499
|
+
* Which browser THIS session's skills drive: `connected` (the user's own browser), `builtin` (the
|
|
500
|
+
* app's in-app browser) or `none`. Session-scoped, never process-wide: the session's tool
|
|
501
|
+
* subprocesses and eval kernel see `OMO_BROWSER_ENGINE=<value>`, other sessions on the host do
|
|
502
|
+
* not, and a session opened without it sees no such variable. A later open that attaches with
|
|
503
|
+
* another value moves the live session to it; an attach without it keeps the current engine.
|
|
504
|
+
* Requires the host capability `browser_engine`. Any other value is refused with `invalid_launch_profile`.
|
|
505
|
+
*/
|
|
506
|
+
browserEngine?: BrowserEngine;
|
|
494
507
|
} | {
|
|
495
508
|
id?: string;
|
|
496
509
|
type: "close_session";
|
|
@@ -881,6 +894,11 @@ export type RpcResponse = {
|
|
|
881
894
|
thinkingLevel: ThinkingLevel;
|
|
882
895
|
isScoped: boolean;
|
|
883
896
|
} | null;
|
|
897
|
+
} | {
|
|
898
|
+
id?: string;
|
|
899
|
+
type: "response";
|
|
900
|
+
command: "continue_from_leaf";
|
|
901
|
+
success: true;
|
|
884
902
|
} | {
|
|
885
903
|
id?: string;
|
|
886
904
|
type: "response";
|
|
@@ -889,6 +907,7 @@ export type RpcResponse = {
|
|
|
889
907
|
data: {
|
|
890
908
|
models: Array<Model<any> & {
|
|
891
909
|
supportedThinkingLevels: ThinkingLevel[];
|
|
910
|
+
supportsAssistantPrefill: boolean;
|
|
892
911
|
}>;
|
|
893
912
|
};
|
|
894
913
|
} | {
|
|
@@ -2,10 +2,10 @@ import { VERSION } from "../../config.js";
|
|
|
2
2
|
import { ACCEPT_EDITS_PERMISSION_PRESET_CAPABILITY } from "../../core/extensions/builtin/permission-system/config.js";
|
|
3
3
|
import { DURABLE_CLIENT_MESSAGE_ID_CAPABILITY } from "./client-admission-record.js";
|
|
4
4
|
import { buildRpcSessionState } from "./connection-handler.js";
|
|
5
|
-
import { AUTO_TITLE_PER_SESSION_CAPABILITY, AUTO_TITLE_SESSIONS_CAPABILITY, DURABLE_SESSION_ID_CAPABILITY, MEDIA_PLACEHOLDERS_CAPABILITY, PROMPT_SURFACE_CAPABILITY, PROMPT_SURFACE_CHAT_CAPABILITY, RETAIN_ON_DISCONNECT_CAPABILITY, SESSION_CONTEXT_CAPABILITY, SESSION_KIND_CAPABILITY, WARM_CAPABILITY, } from "./custom-capability.js";
|
|
5
|
+
import { AUTO_TITLE_PER_SESSION_CAPABILITY, AUTO_TITLE_SESSIONS_CAPABILITY, BROWSER_ENGINE_CAPABILITY, CONTINUE_FROM_LEAF_CAPABILITY, DURABLE_SESSION_ID_CAPABILITY, MEDIA_PLACEHOLDERS_CAPABILITY, PROMPT_SURFACE_CAPABILITY, PROMPT_SURFACE_CHAT_CAPABILITY, RETAIN_ON_DISCONNECT_CAPABILITY, SESSION_CONTEXT_CAPABILITY, SESSION_KIND_CAPABILITY, WARM_CAPABILITY, } from "./custom-capability.js";
|
|
6
6
|
import { answerWarm } from "./host-warm.js";
|
|
7
7
|
import { protocolIdentity } from "./protocol-identity.js";
|
|
8
|
-
import { sessionAutoTitleError, sessionContextError, sessionKindError, sessionPromptSurfaceError, } from "./rpc-input-validation.js";
|
|
8
|
+
import { sessionAutoTitleError, sessionBrowserEngineError, sessionContextError, sessionKindError, sessionPromptSurfaceError, } from "./rpc-input-validation.js";
|
|
9
9
|
import { RPC_ERROR_INVALID_LAUNCH_PROFILE, RPC_ERROR_INVALID_SESSION_CONTEXT, RPC_ERROR_INVALID_SESSION_KIND, RPC_ERROR_MISSING_SESSION_ID, RPC_ERROR_OPEN_FAILED, RPC_ERROR_SESSION_CLOSING, RPC_ERROR_UNKNOWN_SESSION, } from "./rpc-types.js";
|
|
10
10
|
import { runWithSessionAttribution } from "./session-attribution.js";
|
|
11
11
|
import { createRpcSessionBinding } from "./session-binding.js";
|
|
@@ -13,6 +13,7 @@ import { readDrainVerdicts } from "./session-drain.js";
|
|
|
13
13
|
import { RpcSessionRegistryError } from "./session-registry.js";
|
|
14
14
|
import { releaseSession } from "./session-release.js";
|
|
15
15
|
import { selectSweepEvictions } from "./session-sweep.js";
|
|
16
|
+
import { liveToolMediaScope, toolMediaPersister } from "./tool-media-store.js";
|
|
16
17
|
/** How often a draining host re-checks whether the work it is waiting for has settled. */
|
|
17
18
|
const DRAIN_SWEEP_MS = 50;
|
|
18
19
|
function error(id, command, code, data) {
|
|
@@ -195,6 +196,7 @@ export class SessionCommandRouter {
|
|
|
195
196
|
AUTO_TITLE_SESSIONS_CAPABILITY,
|
|
196
197
|
MEDIA_PLACEHOLDERS_CAPABILITY,
|
|
197
198
|
DURABLE_CLIENT_MESSAGE_ID_CAPABILITY,
|
|
199
|
+
CONTINUE_FROM_LEAF_CAPABILITY,
|
|
198
200
|
// Host capabilities, not client opt-ins: only a multi-session host owns the
|
|
199
201
|
// attachment refcount `open_session.retain_on_disconnect` detaches from, the
|
|
200
202
|
// per-session launch profile `context`/`auto_title` travel on, and the session
|
|
@@ -209,6 +211,8 @@ export class SessionCommandRouter {
|
|
|
209
211
|
// Every session's prompt is built from its own launch profile, so one host serves both surfaces.
|
|
210
212
|
PROMPT_SURFACE_CAPABILITY,
|
|
211
213
|
PROMPT_SURFACE_CHAT_CAPABILITY,
|
|
214
|
+
// Each session's tool subprocesses and eval kernel get its own OMO_BROWSER_ENGINE from its launch profile.
|
|
215
|
+
BROWSER_ENGINE_CAPABILITY,
|
|
212
216
|
ACCEPT_EDITS_PERMISSION_PRESET_CAPABILITY,
|
|
213
217
|
// Only an in-process runtime shares the loop a warm loads into (senpi#2314).
|
|
214
218
|
...(this.registry.warm ? [WARM_CAPABILITY] : []),
|
|
@@ -479,6 +483,9 @@ export class SessionCommandRouter {
|
|
|
479
483
|
const promptSurfaceError = sessionPromptSurfaceError(command.promptSurface);
|
|
480
484
|
if (promptSurfaceError)
|
|
481
485
|
return error(command.id, "open_session", `${RPC_ERROR_INVALID_LAUNCH_PROFILE}: ${promptSurfaceError}`);
|
|
486
|
+
const browserEngineError = sessionBrowserEngineError(command.browserEngine);
|
|
487
|
+
if (browserEngineError)
|
|
488
|
+
return error(command.id, "open_session", `${RPC_ERROR_INVALID_LAUNCH_PROFILE}: ${browserEngineError}`);
|
|
482
489
|
let opened;
|
|
483
490
|
try {
|
|
484
491
|
opened = await this.registry.openSession({
|
|
@@ -496,12 +503,14 @@ export class SessionCommandRouter {
|
|
|
496
503
|
...(command.durableSessionId !== undefined ? { durableSessionId: command.durableSessionId } : {}),
|
|
497
504
|
...(typeof command.auto_title === "boolean" ? { autoTitle: command.auto_title } : {}),
|
|
498
505
|
...(command.promptSurface !== undefined ? { promptSurface: command.promptSurface } : {}),
|
|
506
|
+
...(command.browserEngine !== undefined ? { browserEngine: command.browserEngine } : {}),
|
|
499
507
|
},
|
|
500
508
|
// Host lifecycle policy, deliberately outside the immutable launch profile.
|
|
501
509
|
{ retainOnDisconnect: command.retain_on_disconnect === true });
|
|
502
510
|
const openedSession = opened;
|
|
503
511
|
const entry = this.registry.getForCommand(openedSession.sessionId, "open_session");
|
|
504
512
|
this.writer.setSessionKind(openedSession.sessionId, entry.kind);
|
|
513
|
+
this.writer.setSessionMedia(openedSession.sessionId, toolMediaPersister(() => liveToolMediaScope(entry)));
|
|
505
514
|
if (owner !== undefined) {
|
|
506
515
|
if (!this.writer.hasRegisteredConnectionCapabilities(owner))
|
|
507
516
|
this.writer.setConnectionCapabilities(owner, this.pendingCapabilities.get(owner) ?? this.connectionOptions?.capabilities ?? []);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { SessionKind } from "../../core/extensions/types.ts";
|
|
2
|
+
import { type MediaPersister } from "./media-placeholders.ts";
|
|
2
3
|
import type { RpcHostLifecycleEvent, RpcOpenQueuedEvent, RpcSessionClosedReason } from "./rpc-types.ts";
|
|
3
4
|
import { type SessionEventWriterConnection } from "./session-event-fanout.ts";
|
|
4
5
|
export type { SessionEventWriterConnection } from "./session-event-fanout.ts";
|
|
@@ -26,6 +27,8 @@ export declare class SessionEventWriter {
|
|
|
26
27
|
/** Sessions whose lifecycle records stay on their attached connections (`kind: "worker"`). */
|
|
27
28
|
private readonly workerSessions;
|
|
28
29
|
private readonly openTurns;
|
|
30
|
+
/** Per-session image persisters: bytes reach disk before the placeholder that names them is emitted. */
|
|
31
|
+
private readonly mediaPersisters;
|
|
29
32
|
private readonly writeRaw;
|
|
30
33
|
private readonly waitForBackpressure?;
|
|
31
34
|
private readonly scheduleFlush;
|
|
@@ -61,6 +64,7 @@ export declare class SessionEventWriter {
|
|
|
61
64
|
* instead of broadcast; an interactive session keeps the broadcast every client (the
|
|
62
65
|
* desktop mirror, the supervisor's idle observer) relies on.
|
|
63
66
|
*/
|
|
67
|
+
setSessionMedia(sessionId: string, persister: MediaPersister | undefined): void;
|
|
64
68
|
setSessionKind(sessionId: string, kind: SessionKind): void;
|
|
65
69
|
/** Drain just this connection before closing it after its last handoff park. */
|
|
66
70
|
flushConnection(id: string): Promise<void>;
|
|
@@ -67,6 +67,8 @@ export class SessionEventWriter {
|
|
|
67
67
|
/** Sessions whose lifecycle records stay on their attached connections (`kind: "worker"`). */
|
|
68
68
|
this.workerSessions = new Set();
|
|
69
69
|
this.openTurns = new SessionOpenTurns();
|
|
70
|
+
/** Per-session image persisters: bytes reach disk before the placeholder that names them is emitted. */
|
|
71
|
+
this.mediaPersisters = new Map();
|
|
70
72
|
this.flushScheduled = false;
|
|
71
73
|
this.controlOverflowReported = false;
|
|
72
74
|
this.closeOverflowReported = false;
|
|
@@ -129,6 +131,12 @@ export class SessionEventWriter {
|
|
|
129
131
|
* instead of broadcast; an interactive session keeps the broadcast every client (the
|
|
130
132
|
* desktop mirror, the supervisor's idle observer) relies on.
|
|
131
133
|
*/
|
|
134
|
+
setSessionMedia(sessionId, persister) {
|
|
135
|
+
if (persister === undefined)
|
|
136
|
+
this.mediaPersisters.delete(sessionId);
|
|
137
|
+
else
|
|
138
|
+
this.mediaPersisters.set(sessionId, persister);
|
|
139
|
+
}
|
|
132
140
|
setSessionKind(sessionId, kind) {
|
|
133
141
|
if (kind === "worker")
|
|
134
142
|
this.workerSessions.add(sessionId);
|
|
@@ -175,7 +183,7 @@ export class SessionEventWriter {
|
|
|
175
183
|
// A record is only walked and re-serialized when a target asked for placeholders;
|
|
176
184
|
// otherwise this is byte-for-byte today's path, with serializeJsonLine called once.
|
|
177
185
|
const hasPlaceholderTarget = targets.some((target) => this.fanout.connectionHas(target, MEDIA_PLACEHOLDERS_CAPABILITY));
|
|
178
|
-
const redacted = hasPlaceholderTarget ? omitInlineMedia(tagged) : tagged;
|
|
186
|
+
const redacted = hasPlaceholderTarget ? omitInlineMedia(tagged, this.mediaPersisters.get(sessionId)) : tagged;
|
|
179
187
|
const placeholderLine = redacted === tagged ? undefined : serializeJsonLine(redacted);
|
|
180
188
|
if (!isTargeted)
|
|
181
189
|
this.fanout.rememberSnapshot(sessionId, tagged, line, placeholderLine, tagged);
|
|
@@ -337,6 +345,7 @@ export class SessionEventWriter {
|
|
|
337
345
|
return;
|
|
338
346
|
this.sealedSessions.add(sessionId);
|
|
339
347
|
this.fanout.forgetSession(sessionId);
|
|
348
|
+
this.mediaPersisters.delete(sessionId);
|
|
340
349
|
const targetId = this.connectionContext.getStore();
|
|
341
350
|
// `reason` tells an attached client WHY the handle ended, so a park it can reopen by path is
|
|
342
351
|
// not read as a session that is gone. Absent unless the caller names one; clients tolerate that.
|
|
@@ -388,6 +397,7 @@ export class SessionEventWriter {
|
|
|
388
397
|
return;
|
|
389
398
|
this.sealedSessions.add(sessionId);
|
|
390
399
|
this.fanout.forgetSession(sessionId);
|
|
400
|
+
this.mediaPersisters.delete(sessionId);
|
|
391
401
|
if (this.fanout.isEmpty())
|
|
392
402
|
this.appendSessionRecord(sessionId, lifecycle);
|
|
393
403
|
else if (this.workerSessions.has(sessionId))
|
|
@@ -491,6 +501,7 @@ export class SessionEventWriter {
|
|
|
491
501
|
this.openTurns.take(sessionId);
|
|
492
502
|
this.workerSessions.delete(sessionId);
|
|
493
503
|
this.fanout.forgetSession(sessionId);
|
|
504
|
+
this.mediaPersisters.delete(sessionId);
|
|
494
505
|
}
|
|
495
506
|
/** Drain every retained lane and the current in-flight record. */
|
|
496
507
|
flush() {
|
|
@@ -21,6 +21,10 @@ export async function attachToOpenSession(entries, sessionPath, profile, options
|
|
|
21
21
|
entry.profile = frozenProfile({ ...entry.profile, promptSurface: profile.promptSurface });
|
|
22
22
|
entry.runtime?.setPromptSurface(profile.promptSurface);
|
|
23
23
|
}
|
|
24
|
+
if (profile.browserEngine !== undefined && profile.browserEngine !== entry.profile.browserEngine) {
|
|
25
|
+
entry.profile = frozenProfile({ ...entry.profile, browserEngine: profile.browserEngine });
|
|
26
|
+
entry.runtime?.setBrowserEngine(profile.browserEngine);
|
|
27
|
+
}
|
|
24
28
|
entry.lastCommandAt = now;
|
|
25
29
|
if (wasParked) {
|
|
26
30
|
entry.lifecycleMutex = entry.lifecycleMutex.then(() => entry.runtime?.emitAttachmentEvent("session_resumed"));
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Worker } from "node:worker_threads";
|
|
2
|
+
import type { BrowserEngine } from "../../core/browser-engine.ts";
|
|
2
3
|
import type { PromptSurface } from "../../core/dynamic-prompt/types.ts";
|
|
3
4
|
import type { CliRuntimeConfiguration } from "../../main.ts";
|
|
4
5
|
import type { RpcConnectionOptions } from "./connection-handler.ts";
|
|
@@ -37,6 +38,8 @@ export declare class SessionWorkerClient {
|
|
|
37
38
|
prepare(configuration: CliRuntimeConfiguration, profile: RpcSessionLaunchProfile): Promise<string>;
|
|
38
39
|
/** Moves the worker's live session to another prompt surface (a later `open_session.promptSurface`). */
|
|
39
40
|
setPromptSurface(surface: PromptSurface): Promise<void>;
|
|
41
|
+
/** Moves the worker's live session to another browser engine (a later `open_session.browserEngine`). */
|
|
42
|
+
setBrowserEngine(engine: BrowserEngine): Promise<void>;
|
|
40
43
|
commit(): Promise<WorkerSnapshot>;
|
|
41
44
|
bind(sessionId: string, writer: SessionEventWriter, requestClose: () => void, options: Pick<RpcConnectionOptions, "capabilities" | "clientInfo">): Promise<RpcSessionBinding>;
|
|
42
45
|
get busy(): boolean;
|
|
@@ -54,6 +54,12 @@ export class SessionWorkerClient {
|
|
|
54
54
|
if (result.type !== "result")
|
|
55
55
|
throw new Error("Invalid worker prompt_surface response");
|
|
56
56
|
}
|
|
57
|
+
/** Moves the worker's live session to another browser engine (a later `open_session.browserEngine`). */
|
|
58
|
+
async setBrowserEngine(engine) {
|
|
59
|
+
const result = await this.request({ type: "browser_engine", engine });
|
|
60
|
+
if (result.type !== "result")
|
|
61
|
+
throw new Error("Invalid worker browser_engine response");
|
|
62
|
+
}
|
|
57
63
|
async commit() {
|
|
58
64
|
const result = await this.request({ type: "commit" });
|
|
59
65
|
if (result.type !== "ready")
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { BrowserEngine } from "../../core/browser-engine.ts";
|
|
1
2
|
import type { PromptSurface } from "../../core/dynamic-prompt/types.ts";
|
|
2
3
|
import type { CliRuntimeConfiguration } from "../../main.ts";
|
|
3
4
|
import type { RpcSessionState } from "./rpc-types.ts";
|
|
@@ -55,6 +56,10 @@ export type HostToSessionWorker = {
|
|
|
55
56
|
type: "prompt_surface";
|
|
56
57
|
request: number;
|
|
57
58
|
surface: PromptSurface;
|
|
59
|
+
} | {
|
|
60
|
+
type: "browser_engine";
|
|
61
|
+
request: number;
|
|
62
|
+
engine: BrowserEngine;
|
|
58
63
|
} | {
|
|
59
64
|
type: "cancel_ui";
|
|
60
65
|
} | {
|
|
@@ -177,6 +177,12 @@ async function handle(message) {
|
|
|
177
177
|
entry.runtime.setPromptSurface(message.surface);
|
|
178
178
|
send({ type: "result", request: message.request });
|
|
179
179
|
return;
|
|
180
|
+
case "browser_engine":
|
|
181
|
+
if (!entry?.runtime)
|
|
182
|
+
throw new Error("session_closing");
|
|
183
|
+
entry.runtime.setBrowserEngine(message.engine);
|
|
184
|
+
send({ type: "result", request: message.request });
|
|
185
|
+
return;
|
|
180
186
|
case "cancel_ui":
|
|
181
187
|
binding?.cancelPendingExtensionUiRequests?.();
|
|
182
188
|
send({ type: "control_done", control: "cancel_ui" });
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { type MediaPersister, type PersistedMedia } from "./media-placeholders.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Where a session keeps the images its tools produced.
|
|
4
|
+
*
|
|
5
|
+
* A `media_placeholders` client never receives the bytes on the socket. The host writes them
|
|
6
|
+
* here, once, BEFORE the placeholder that names the file is emitted, so a client can render the
|
|
7
|
+
* picture from the path whenever it likes: after a disconnect, an idle shutdown or a generation
|
|
8
|
+
* handover, because the files outlive every connection. The directory sits next to the session
|
|
9
|
+
* files, never under the user's project, and is removed with the durable session.
|
|
10
|
+
*/
|
|
11
|
+
export interface ToolMediaScope {
|
|
12
|
+
readonly sessionDir: string;
|
|
13
|
+
readonly durableSessionId: string;
|
|
14
|
+
}
|
|
15
|
+
export declare const MAX_TOOL_IMAGE_BYTES: number;
|
|
16
|
+
export declare const MAX_SESSION_MEDIA_BYTES: number;
|
|
17
|
+
export interface ToolMediaLimits {
|
|
18
|
+
readonly perImage: number;
|
|
19
|
+
readonly perSession: number;
|
|
20
|
+
}
|
|
21
|
+
export interface LiveSessionSource {
|
|
22
|
+
readonly runtime?: {
|
|
23
|
+
readonly session: {
|
|
24
|
+
readonly sessionManager: {
|
|
25
|
+
getSessionFile(): string | undefined;
|
|
26
|
+
getSessionId(): string;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
readonly worker?: {
|
|
31
|
+
readonly snapshot?: {
|
|
32
|
+
readonly state: {
|
|
33
|
+
readonly sessionFile?: string;
|
|
34
|
+
readonly sessionId: string;
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export declare function liveToolMediaScope(entry: LiveSessionSource): ToolMediaScope | undefined;
|
|
40
|
+
export declare function toolMediaRoot(scope: ToolMediaScope): string;
|
|
41
|
+
/**
|
|
42
|
+
* Persist one tool-result image and report where it lives, or why it does not.
|
|
43
|
+
*
|
|
44
|
+
* Layout: `<sessionDir>/media/<durableSessionId>/<sha256(toolCallId)>/<contentIndex>-<sha256(bytes)>.<ext>`.
|
|
45
|
+
* The content hash makes a file immutable and a repeat a no-op; the per-image and per-session
|
|
46
|
+
* limits reject new storage and never evict a file a transcript may still reference.
|
|
47
|
+
*/
|
|
48
|
+
export declare function persistToolImage(scope: ToolMediaScope, ref: {
|
|
49
|
+
readonly toolCallId: string;
|
|
50
|
+
readonly contentIndex: number;
|
|
51
|
+
}, block: {
|
|
52
|
+
readonly data: string;
|
|
53
|
+
readonly mimeType?: string;
|
|
54
|
+
}, limits?: ToolMediaLimits): PersistedMedia;
|
|
55
|
+
export declare function toolMediaPersister(scope: () => ToolMediaScope | undefined): MediaPersister;
|
|
56
|
+
export declare function removeToolMedia(scope: ToolMediaScope): void;
|
|
57
|
+
/** The media scope of the session stored in `sessionFile`; a file with no readable header has none. */
|
|
58
|
+
export declare function toolMediaScopeOfSessionFile(sessionFile: string): Promise<ToolMediaScope | undefined>;
|
|
59
|
+
//# sourceMappingURL=tool-media-store.d.ts.map
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { chmodSync, existsSync, lstatSync, mkdirSync, readdirSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import { open } from "node:fs/promises";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { assertValidSessionId } from "../../core/session-manager.js";
|
|
6
|
+
import { base64ByteLength } from "./media-placeholders.js";
|
|
7
|
+
export const MAX_TOOL_IMAGE_BYTES = 20 * 1024 * 1024;
|
|
8
|
+
export const MAX_SESSION_MEDIA_BYTES = 256 * 1024 * 1024;
|
|
9
|
+
const DEFAULT_LIMITS = { perImage: MAX_TOOL_IMAGE_BYTES, perSession: MAX_SESSION_MEDIA_BYTES };
|
|
10
|
+
const MEDIA_DIRECTORY = "media";
|
|
11
|
+
const EXTENSIONS = {
|
|
12
|
+
"image/png": "png",
|
|
13
|
+
"image/jpeg": "jpg",
|
|
14
|
+
"image/gif": "gif",
|
|
15
|
+
"image/webp": "webp",
|
|
16
|
+
};
|
|
17
|
+
/** Bytes already stored per media root: the reservation every concurrent write is checked against. */
|
|
18
|
+
const reserved = new Map();
|
|
19
|
+
const SIGNATURES = {
|
|
20
|
+
"image/png": (bytes) => bytes.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])),
|
|
21
|
+
"image/jpeg": (bytes) => bytes.subarray(0, 3).equals(Buffer.from([0xff, 0xd8, 0xff])),
|
|
22
|
+
"image/gif": (bytes) => bytes.subarray(0, 4).toString("latin1") === "GIF8",
|
|
23
|
+
"image/webp": (bytes) => bytes.subarray(0, 4).toString("latin1") === "RIFF" && bytes.subarray(8, 12).toString("latin1") === "WEBP",
|
|
24
|
+
};
|
|
25
|
+
function assertRealDirectoryChain(root, callDirectory) {
|
|
26
|
+
for (const path of [dirname(root), root, callDirectory]) {
|
|
27
|
+
let stat;
|
|
28
|
+
try {
|
|
29
|
+
stat = lstatSync(path);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (!stat.isDirectory())
|
|
35
|
+
throw new Error(`not a real directory: ${path}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** Outcomes per live image block, so re-emitting a record (`get_messages`, `turn_end`) never rehashes it. */
|
|
39
|
+
const outcomes = new WeakMap();
|
|
40
|
+
export function liveToolMediaScope(entry) {
|
|
41
|
+
const manager = entry.runtime?.session.sessionManager;
|
|
42
|
+
const sessionFile = manager?.getSessionFile() ?? entry.worker?.snapshot?.state.sessionFile;
|
|
43
|
+
const durableSessionId = manager?.getSessionId() ?? entry.worker?.snapshot?.state.sessionId;
|
|
44
|
+
return sessionFile === undefined || durableSessionId === undefined
|
|
45
|
+
? undefined
|
|
46
|
+
: { sessionDir: dirname(sessionFile), durableSessionId };
|
|
47
|
+
}
|
|
48
|
+
export function toolMediaRoot(scope) {
|
|
49
|
+
assertValidSessionId(scope.durableSessionId);
|
|
50
|
+
return join(scope.sessionDir, MEDIA_DIRECTORY, scope.durableSessionId);
|
|
51
|
+
}
|
|
52
|
+
function directoryBytes(directory) {
|
|
53
|
+
let total = 0;
|
|
54
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
55
|
+
const path = join(directory, entry.name);
|
|
56
|
+
total += entry.isDirectory() ? directoryBytes(path) : statSync(path).size;
|
|
57
|
+
}
|
|
58
|
+
return total;
|
|
59
|
+
}
|
|
60
|
+
function storedBytes(root) {
|
|
61
|
+
const known = reserved.get(root);
|
|
62
|
+
if (known !== undefined)
|
|
63
|
+
return known;
|
|
64
|
+
const scanned = existsSync(root) ? directoryBytes(root) : 0;
|
|
65
|
+
reserved.set(root, scanned);
|
|
66
|
+
return scanned;
|
|
67
|
+
}
|
|
68
|
+
const unavailable = (reason) => ({
|
|
69
|
+
unavailableReason: reason,
|
|
70
|
+
});
|
|
71
|
+
function writeImage(root, path, bytes) {
|
|
72
|
+
const callDirectory = dirname(path);
|
|
73
|
+
mkdirSync(callDirectory, { recursive: true, mode: 0o700 });
|
|
74
|
+
assertRealDirectoryChain(root, callDirectory);
|
|
75
|
+
const staging = `${path}.${randomBytes(6).toString("hex")}.tmp`;
|
|
76
|
+
try {
|
|
77
|
+
writeFileSync(staging, bytes, { mode: 0o600, flag: "wx" });
|
|
78
|
+
renameSync(staging, path);
|
|
79
|
+
chmodSync(path, 0o400);
|
|
80
|
+
}
|
|
81
|
+
catch (cause) {
|
|
82
|
+
rmSync(staging, { force: true });
|
|
83
|
+
throw cause;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Persist one tool-result image and report where it lives, or why it does not.
|
|
88
|
+
*
|
|
89
|
+
* Layout: `<sessionDir>/media/<durableSessionId>/<sha256(toolCallId)>/<contentIndex>-<sha256(bytes)>.<ext>`.
|
|
90
|
+
* The content hash makes a file immutable and a repeat a no-op; the per-image and per-session
|
|
91
|
+
* limits reject new storage and never evict a file a transcript may still reference.
|
|
92
|
+
*/
|
|
93
|
+
export function persistToolImage(scope, ref, block, limits = DEFAULT_LIMITS) {
|
|
94
|
+
const known = outcomes.get(block);
|
|
95
|
+
if (known !== undefined)
|
|
96
|
+
return known;
|
|
97
|
+
const outcome = store(scope, ref, block, limits);
|
|
98
|
+
if (!("unavailableReason" in outcome) || outcome.unavailableReason !== "storage_error")
|
|
99
|
+
outcomes.set(block, outcome);
|
|
100
|
+
return outcome;
|
|
101
|
+
}
|
|
102
|
+
function store(scope, ref, block, limits) {
|
|
103
|
+
const extension = EXTENSIONS[(block.mimeType ?? "").toLowerCase()];
|
|
104
|
+
if (extension === undefined)
|
|
105
|
+
return unavailable("storage_error");
|
|
106
|
+
if (base64ByteLength(block.data) > limits.perImage)
|
|
107
|
+
return unavailable("image_too_large");
|
|
108
|
+
const bytes = Buffer.from(block.data, "base64");
|
|
109
|
+
if (bytes.length > limits.perImage)
|
|
110
|
+
return unavailable("image_too_large");
|
|
111
|
+
if (SIGNATURES[(block.mimeType ?? "").toLowerCase()]?.(bytes) !== true)
|
|
112
|
+
return unavailable("storage_error");
|
|
113
|
+
try {
|
|
114
|
+
const root = toolMediaRoot(scope);
|
|
115
|
+
const callDirectory = createHash("sha256").update(ref.toolCallId).digest("hex");
|
|
116
|
+
const digest = createHash("sha256").update(bytes).digest("hex");
|
|
117
|
+
const path = resolve(root, callDirectory, `${ref.contentIndex}-${digest}.${extension}`);
|
|
118
|
+
assertRealDirectoryChain(root, dirname(path));
|
|
119
|
+
if (existsSync(path))
|
|
120
|
+
return { path };
|
|
121
|
+
const used = storedBytes(root);
|
|
122
|
+
if (used + bytes.length > limits.perSession)
|
|
123
|
+
return unavailable("session_limit");
|
|
124
|
+
reserved.set(root, used + bytes.length);
|
|
125
|
+
try {
|
|
126
|
+
writeImage(root, path, bytes);
|
|
127
|
+
}
|
|
128
|
+
catch (cause) {
|
|
129
|
+
reserved.set(root, storedBytes(root) - bytes.length);
|
|
130
|
+
throw cause;
|
|
131
|
+
}
|
|
132
|
+
return { path };
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return unavailable("storage_error");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
export function toolMediaPersister(scope) {
|
|
139
|
+
return (ref, block) => {
|
|
140
|
+
const current = scope();
|
|
141
|
+
return current === undefined ? undefined : persistToolImage(current, ref, block);
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
export function removeToolMedia(scope) {
|
|
145
|
+
const root = toolMediaRoot(scope);
|
|
146
|
+
reserved.delete(root);
|
|
147
|
+
makeTreeWritable(root);
|
|
148
|
+
rmSync(root, { recursive: true, force: true });
|
|
149
|
+
}
|
|
150
|
+
function makeTreeWritable(path) {
|
|
151
|
+
let entries;
|
|
152
|
+
try {
|
|
153
|
+
if (!lstatSync(path).isDirectory())
|
|
154
|
+
return;
|
|
155
|
+
chmodSync(path, 0o700);
|
|
156
|
+
entries = readdirSync(path, { withFileTypes: true });
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
for (const entry of entries) {
|
|
162
|
+
const child = join(path, entry.name);
|
|
163
|
+
if (entry.isDirectory())
|
|
164
|
+
makeTreeWritable(child);
|
|
165
|
+
else if (entry.isFile())
|
|
166
|
+
try {
|
|
167
|
+
chmodSync(child, 0o600);
|
|
168
|
+
}
|
|
169
|
+
catch { }
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/** The media scope of the session stored in `sessionFile`; a file with no readable header has none. */
|
|
173
|
+
export async function toolMediaScopeOfSessionFile(sessionFile) {
|
|
174
|
+
try {
|
|
175
|
+
const handle = await open(sessionFile, "r");
|
|
176
|
+
try {
|
|
177
|
+
const { buffer, bytesRead } = await handle.read(Buffer.alloc(4096), 0, 4096, 0);
|
|
178
|
+
const firstLine = buffer.subarray(0, bytesRead).toString("utf8").split("\n", 1)[0] ?? "";
|
|
179
|
+
const id = JSON.parse(firstLine).id;
|
|
180
|
+
return typeof id === "string" ? { sessionDir: dirname(sessionFile), durableSessionId: id } : undefined;
|
|
181
|
+
}
|
|
182
|
+
finally {
|
|
183
|
+
await handle.close();
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
//# sourceMappingURL=tool-media-store.js.map
|
|
@@ -51,7 +51,7 @@ export class WorkerSessionRegistry {
|
|
|
51
51
|
const key = this.knownReservationKey(profile.sessionPath);
|
|
52
52
|
const owner = key ? this.reservations.owner(key) : undefined;
|
|
53
53
|
if (key && owner)
|
|
54
|
-
return this.attach(owner, key, options, profile
|
|
54
|
+
return this.attach(owner, key, options, profile);
|
|
55
55
|
}
|
|
56
56
|
if (this.size >= SESSION_WORKER_LIMITS.workers)
|
|
57
57
|
throw new Error("too_many_sessions");
|
|
@@ -99,7 +99,7 @@ export class WorkerSessionRegistry {
|
|
|
99
99
|
const path = await worker.prepare(this.options.configuration, profile);
|
|
100
100
|
const owner = this.reservations.owner(path);
|
|
101
101
|
if (owner) {
|
|
102
|
-
const attached = await this.attach(owner, path, options, profile
|
|
102
|
+
const attached = await this.attach(owner, path, options, profile);
|
|
103
103
|
entry.state = "quarantined";
|
|
104
104
|
worker.quarantine();
|
|
105
105
|
return attached;
|
|
@@ -228,7 +228,8 @@ export class WorkerSessionRegistry {
|
|
|
228
228
|
}
|
|
229
229
|
return undefined;
|
|
230
230
|
}
|
|
231
|
-
async attach(owner, path, options,
|
|
231
|
+
async attach(owner, path, options, requested = { cwd: "" }) {
|
|
232
|
+
const { promptSurface, browserEngine } = requested;
|
|
232
233
|
const entry = this.entries.get(owner);
|
|
233
234
|
if (entry?.state !== "open" || !entry.worker?.bindingReady || entry.worker.snapshot?.sessionPath !== path)
|
|
234
235
|
throw new RpcSessionRegistryError("session_path_in_use");
|
|
@@ -237,6 +238,10 @@ export class WorkerSessionRegistry {
|
|
|
237
238
|
entry.profile = frozenProfile({ ...entry.profile, promptSurface });
|
|
238
239
|
await entry.worker.setPromptSurface(promptSurface);
|
|
239
240
|
}
|
|
241
|
+
if (browserEngine !== undefined && browserEngine !== entry.profile.browserEngine) {
|
|
242
|
+
entry.profile = frozenProfile({ ...entry.profile, browserEngine });
|
|
243
|
+
await entry.worker.setBrowserEngine(browserEngine);
|
|
244
|
+
}
|
|
240
245
|
const result = this.openResult(owner, entry);
|
|
241
246
|
entry.attachments++;
|
|
242
247
|
// Retention is a property of the live session: any attach may ask for it, and
|
|
@@ -30,6 +30,7 @@ Commands run by the `bash` and `powershell` tools receive the current Pi session
|
|
|
30
30
|
| `PI_PROVIDER` | Currently selected model provider |
|
|
31
31
|
| `PI_MODEL` | Currently selected model ID |
|
|
32
32
|
| `PI_REASONING_LEVEL` | Current effective reasoning level: `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max` |
|
|
33
|
+
| `OMO_BROWSER_ENGINE` | Browser the opener chose for this session: `connected` (the user's own browser), `builtin` (the app's in-app browser) or `none`. Set only for a session opened with `open_session.browserEngine`; a session that chose none never sees the variable, even if the host process has one |
|
|
33
34
|
|
|
34
35
|
The values are resolved when each command starts. Switching models or changing the reasoning level therefore affects the next shell command without restarting Pi. `PI_PROVIDER` and `PI_MODEL` identify the selected Pi model, not a different upstream model that a router may choose internally.
|
|
35
36
|
|