@rynx-ai/server 0.1.11-beta.45 → 0.1.11-beta.48
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/dist/control-api.d.ts +2 -2
- package/dist/control-api.js +101 -6
- package/dist/desktop-browser-host.d.ts +9 -0
- package/dist/desktop-browser-host.js +55 -6
- package/dist/remote-runtime-dispatcher.js +1 -0
- package/dist/server.d.ts +3 -1
- package/dist/session-browser-service.d.ts +28 -2
- package/dist/session-browser-service.js +105 -10
- package/dist/session-browser-surface-coordinator.d.ts +2 -0
- package/dist/session-browser-surface-coordinator.js +15 -0
- package/package.json +7 -7
package/dist/control-api.d.ts
CHANGED
|
@@ -226,6 +226,6 @@ export declare function createControlRouter(opts: {
|
|
|
226
226
|
providerClis?: ProviderCliManagementHost;
|
|
227
227
|
}): Router;
|
|
228
228
|
/** Derive a one-line session title from the first user message: collapse
|
|
229
|
-
* whitespace, trim, and
|
|
230
|
-
*
|
|
229
|
+
* whitespace, trim, and fit within `limit` characters, including an ellipsis
|
|
230
|
+
* when truncated. No LLM call is made. */
|
|
231
231
|
export declare function synthesizeSessionTitle(message: string, limit?: number): string;
|
package/dist/control-api.js
CHANGED
|
@@ -15,7 +15,7 @@ import { DIRECT_RUNTIME_CONTROL_PATH, encodePairingCode, } from "@rynx-ai/protoc
|
|
|
15
15
|
import { PLUGIN_CONSOLE_INSTALL_PREPARATIONS_PATH, PLUGIN_CONSOLE_MARKETPLACES_PATH, PLUGIN_INSTALL_PREPARATIONS_PATH, PLUGIN_MARKETPLACES_PATH, parsePluginInstallCommitInput, parsePluginInstallPrepareInput, parsePluginMarketplaceAddInput, } from "@rynx-ai/protocol/plugin-management";
|
|
16
16
|
import { parseDaemonStatus } from "@rynx-ai/protocol/remote-runtime";
|
|
17
17
|
import { materializeRemoteRuntimeSessionEvent, materializeRemoteRuntimeSessionRuntimeSnapshot, parseRemoteRuntimeRpcRequest, REMOTE_RUNTIME_RPC_MAX_FRAME_BYTES, SESSION_IMAGE_MEDIA_TYPES, SESSION_RESOURCE_MAX_IMAGE_BYTES, SESSION_RESOURCE_MAX_FILENAME_CHARS, SESSION_RESOURCE_TRANSFER_CHUNK_BYTES, REMOTE_RUNTIME_SESSION_MAX_PAGE_SIZE, } from "@rynx-ai/protocol/remote-runtime-rpc";
|
|
18
|
-
import { RUNTIME_BROWSER_BOOTSTRAP_PATH, RUNTIME_BROWSER_CAPABILITY_HEADER, RUNTIME_BROWSER_MANAGEMENT_PATH_PREFIX, RUNTIME_BROWSER_RPC_PATH, RUNTIME_BROWSER_SESSION_ID_HEADER, parseRuntimeBrowserBootstrapCredential, } from "@rynx-ai/protocol/runtime-browser-bootstrap";
|
|
18
|
+
import { RUNTIME_BROWSER_BOOTSTRAP_PATH, RUNTIME_BROWSER_CAPABILITY_HEADER, RUNTIME_BROWSER_MANAGEMENT_PATH_PREFIX, RUNTIME_BROWSER_RPC_PATH, RUNTIME_BROWSER_EXECUTE_PATH, RUNTIME_BROWSER_SESSION_ID_HEADER, parseRuntimeBrowserBootstrapCredential, parseRuntimeBrowserAutomationCommand, } from "@rynx-ai/protocol/runtime-browser-bootstrap";
|
|
19
19
|
import { parseRuntimeBrowserStateGetParams } from "@rynx-ai/protocol/runtime-browser";
|
|
20
20
|
import { encodeDaemonStatus } from "./remote-runtime.js";
|
|
21
21
|
import { SessionRuntimeIndex } from "./session-runtime-index.js";
|
|
@@ -2124,6 +2124,71 @@ export function createControlRouter(opts) {
|
|
|
2124
2124
|
router.post("/api/internal/runtimes/:selector/emulator/gesture", internalRuntimeCall(runtimeGestureEmulator));
|
|
2125
2125
|
router.post("/api/internal/runtimes/:selector/emulator/kill", internalRuntimeCall((ctx) => runtimeDeviceCommand(ctx, "emulator.kill")));
|
|
2126
2126
|
router.post("/api/internal/runtimes/:selector/emulator/shutdown", internalRuntimeCall((ctx) => runtimeDeviceCommand(ctx, "emulator.shutdown")));
|
|
2127
|
+
const executeLocalBrowser = async (ctx, management) => {
|
|
2128
|
+
if (!runtimeLocalBrowser?.executeForSession) {
|
|
2129
|
+
notFound(ctx, "Page automation requires a Runtime upgrade");
|
|
2130
|
+
return;
|
|
2131
|
+
}
|
|
2132
|
+
if (!isLoopbackRequest(ctx)) {
|
|
2133
|
+
ctx.status = 403;
|
|
2134
|
+
ctx.body = { error: "loopback_only" };
|
|
2135
|
+
return;
|
|
2136
|
+
}
|
|
2137
|
+
let sessionId;
|
|
2138
|
+
if (management) {
|
|
2139
|
+
if (!isConfiguredDaemonManagement(daemonManagement)) {
|
|
2140
|
+
notFound(ctx, "Daemon management is unavailable");
|
|
2141
|
+
return;
|
|
2142
|
+
}
|
|
2143
|
+
if (!authorizeLoopbackManagementRequest(ctx, daemonManagement.managementToken))
|
|
2144
|
+
return;
|
|
2145
|
+
try {
|
|
2146
|
+
sessionId = parseRuntimeBrowserStateGetParams({ sessionId: ctx.params.sessionId }).sessionId;
|
|
2147
|
+
}
|
|
2148
|
+
catch {
|
|
2149
|
+
bad(ctx, "invalid Session id");
|
|
2150
|
+
return;
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
else {
|
|
2154
|
+
try {
|
|
2155
|
+
const credential = parseRuntimeBrowserBootstrapCredential({
|
|
2156
|
+
sessionId: ctx.get(RUNTIME_BROWSER_SESSION_ID_HEADER), capability: ctx.get(RUNTIME_BROWSER_CAPABILITY_HEADER),
|
|
2157
|
+
});
|
|
2158
|
+
if (!await runtimeLocalBrowser.authorizeCredential(credential))
|
|
2159
|
+
throw new Error("unauthorized");
|
|
2160
|
+
sessionId = credential.sessionId;
|
|
2161
|
+
}
|
|
2162
|
+
catch {
|
|
2163
|
+
ctx.status = 401;
|
|
2164
|
+
ctx.body = { error: "invalid Runtime Browser Session capability" };
|
|
2165
|
+
return;
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
if (!ctx.is("application/json")) {
|
|
2169
|
+
ctx.status = 415;
|
|
2170
|
+
ctx.body = { error: "unsupported_media_type" };
|
|
2171
|
+
return;
|
|
2172
|
+
}
|
|
2173
|
+
let command;
|
|
2174
|
+
try {
|
|
2175
|
+
command = parseRuntimeBrowserAutomationCommand(await readBoundedJson(ctx, 128 * 1024));
|
|
2176
|
+
}
|
|
2177
|
+
catch (error) {
|
|
2178
|
+
ctx.status = error instanceof RequestBodyTooLargeError ? 413 : 400;
|
|
2179
|
+
ctx.body = { error: "invalid_request" };
|
|
2180
|
+
return;
|
|
2181
|
+
}
|
|
2182
|
+
try {
|
|
2183
|
+
ctx.set("Cache-Control", "no-store");
|
|
2184
|
+
ctx.body = await runtimeLocalBrowser.executeForSession(sessionId, command);
|
|
2185
|
+
}
|
|
2186
|
+
catch (error) {
|
|
2187
|
+
runtimeLocalBrowserError(ctx, error);
|
|
2188
|
+
}
|
|
2189
|
+
};
|
|
2190
|
+
router.post(RUNTIME_BROWSER_EXECUTE_PATH, (ctx) => executeLocalBrowser(ctx, false));
|
|
2191
|
+
router.post(`${RUNTIME_BROWSER_MANAGEMENT_PATH_PREFIX}/:sessionId/execute`, (ctx) => executeLocalBrowser(ctx, true));
|
|
2127
2192
|
router.get(RUNTIME_BROWSER_BOOTSTRAP_PATH, async (ctx) => {
|
|
2128
2193
|
if (!runtimeLocalBrowser) {
|
|
2129
2194
|
return notFound(ctx, "Runtime-local Browser automation is not configured");
|
|
@@ -3539,7 +3604,7 @@ export function createControlRouter(opts) {
|
|
|
3539
3604
|
// ── sessions (channel-agnostic agent runs) ───────────────────────────────
|
|
3540
3605
|
// Create a session bound to a preset agent (`agent`) or an inline spec
|
|
3541
3606
|
// (`config`, the config-driven / remote-ready path), then drive it with
|
|
3542
|
-
// messages over SSE.
|
|
3607
|
+
// messages over SSE. Creating the session does not submit a message.
|
|
3543
3608
|
// One unified list: every session is a machine-session record. The registry
|
|
3544
3609
|
// (console + every channel) supplies identity — `source` is a plain field, not
|
|
3545
3610
|
// parsed from the id — and the canonical log supplies recency. A legacy session
|
|
@@ -3891,8 +3956,8 @@ function abortAsNull(signal) {
|
|
|
3891
3956
|
});
|
|
3892
3957
|
}
|
|
3893
3958
|
/** Derive a one-line session title from the first user message: collapse
|
|
3894
|
-
* whitespace, trim, and
|
|
3895
|
-
*
|
|
3959
|
+
* whitespace, trim, and fit within `limit` characters, including an ellipsis
|
|
3960
|
+
* when truncated. No LLM call is made. */
|
|
3896
3961
|
export function synthesizeSessionTitle(message, limit = 60) {
|
|
3897
3962
|
const collapsed = message.replace(/\s+/g, " ").trim();
|
|
3898
3963
|
if (collapsed.length <= limit)
|
|
@@ -4341,9 +4406,19 @@ function runtimeLocalBrowserError(ctx, error) {
|
|
|
4341
4406
|
? error.code
|
|
4342
4407
|
: undefined;
|
|
4343
4408
|
switch (code) {
|
|
4409
|
+
case "human_control_active":
|
|
4410
|
+
ctx.status = 409;
|
|
4411
|
+
ctx.body = { error: code, hint: "Release Browser Control before Agent actions." };
|
|
4412
|
+
return;
|
|
4413
|
+
case "command_timeout":
|
|
4414
|
+
ctx.status = 504;
|
|
4415
|
+
ctx.body = { error: "command_timeout", outcome: "unknown" };
|
|
4416
|
+
return;
|
|
4344
4417
|
case "invalid_request":
|
|
4345
4418
|
ctx.status = 400;
|
|
4346
|
-
ctx.body = { error: "invalid_request"
|
|
4419
|
+
ctx.body = { error: "invalid_request", ...(error instanceof Error && error.name === "PageAutomationError"
|
|
4420
|
+
? { message: error.message.slice(0, 2048), hint: "Run rynx skills get browser for Browser resources; after opening a Browser, use rynx browser exec -- --help for page commands." }
|
|
4421
|
+
: {}) };
|
|
4347
4422
|
return;
|
|
4348
4423
|
case "not_found":
|
|
4349
4424
|
ctx.status = 404;
|
|
@@ -4355,10 +4430,30 @@ function runtimeLocalBrowserError(ctx, error) {
|
|
|
4355
4430
|
ctx.body = { error: "unavailable" };
|
|
4356
4431
|
return;
|
|
4357
4432
|
case "host_failure":
|
|
4358
|
-
case "outcome_unknown":
|
|
4359
4433
|
ctx.status = 502;
|
|
4360
4434
|
ctx.body = { error: "operation_failed" };
|
|
4361
4435
|
return;
|
|
4436
|
+
case "outcome_unknown":
|
|
4437
|
+
ctx.status = 502;
|
|
4438
|
+
ctx.body = { error: "outcome_unknown", outcome: "unknown", hint: "Do not replay this action. Run rynx browser exec -- snapshot -i to inspect its outcome." };
|
|
4439
|
+
return;
|
|
4440
|
+
case "driver_restarted":
|
|
4441
|
+
case "generation_mismatch":
|
|
4442
|
+
ctx.status = 409;
|
|
4443
|
+
ctx.body = { error: code, hint: "Run rynx browser exec -- snapshot -i for fresh Page references." };
|
|
4444
|
+
return;
|
|
4445
|
+
case "capacity":
|
|
4446
|
+
ctx.status = 429;
|
|
4447
|
+
ctx.body = { error: "capacity" };
|
|
4448
|
+
return;
|
|
4449
|
+
case "unsupported":
|
|
4450
|
+
ctx.status = 501;
|
|
4451
|
+
ctx.body = { error: "unsupported", hint: "Upgrade the Runtime and Desktop Host together." };
|
|
4452
|
+
return;
|
|
4453
|
+
case "command_failed":
|
|
4454
|
+
ctx.status = 422;
|
|
4455
|
+
ctx.body = { error: "command_failed", message: error instanceof Error ? error.message : "agent-browser command failed", hint: "Run rynx browser exec -- --help, or inspect a fresh snapshot before another action." };
|
|
4456
|
+
return;
|
|
4362
4457
|
default:
|
|
4363
4458
|
ctx.status = 500;
|
|
4364
4459
|
ctx.body = { error: "internal_error" };
|
|
@@ -32,6 +32,7 @@ interface DesktopBrowserLease {
|
|
|
32
32
|
surface: DesktopBrowserHostSurfaceCapability;
|
|
33
33
|
socket: WebSocket;
|
|
34
34
|
pending: Map<string, PendingCommand>;
|
|
35
|
+
expiredCommands: Set<string>;
|
|
35
36
|
nextEventSequence: number;
|
|
36
37
|
detached: boolean;
|
|
37
38
|
}
|
|
@@ -120,6 +121,14 @@ declare class DesktopBrowserHandle implements SessionBrowserHostHandle {
|
|
|
120
121
|
endpoint: string;
|
|
121
122
|
engineVersion: string;
|
|
122
123
|
}>;
|
|
124
|
+
getPageAutomationEndpoint(hostPageId: string): Promise<{
|
|
125
|
+
pageTargetId: string;
|
|
126
|
+
endpoint: string;
|
|
127
|
+
engineVersion: string;
|
|
128
|
+
}>;
|
|
129
|
+
acquireAutomationVisibility(hostPageId: string): Promise<{
|
|
130
|
+
release: () => Promise<void>;
|
|
131
|
+
}>;
|
|
123
132
|
openSurface(input: SessionBrowserHostSurfaceOpenInput): Promise<SessionBrowserHostSurfaceSource>;
|
|
124
133
|
snapshot(): Promise<SessionBrowserHostSnapshot>;
|
|
125
134
|
createPage(url?: string): Promise<void>;
|
|
@@ -133,6 +133,7 @@ export class DesktopBrowserHostRegistry {
|
|
|
133
133
|
surface: hello.capabilities.surface,
|
|
134
134
|
socket,
|
|
135
135
|
pending: new Map(),
|
|
136
|
+
expiredCommands: new Set(),
|
|
136
137
|
nextEventSequence: 1,
|
|
137
138
|
detached: false,
|
|
138
139
|
};
|
|
@@ -253,8 +254,13 @@ export class DesktopBrowserHostRegistry {
|
|
|
253
254
|
if (!pending)
|
|
254
255
|
return;
|
|
255
256
|
lease.pending.delete(commandId);
|
|
256
|
-
reject(
|
|
257
|
-
if (
|
|
257
|
+
reject(commandDeadlineError(command, `Desktop Browser Host ${command.method} timed out`));
|
|
258
|
+
if (isRecoverableDeadline(command)) {
|
|
259
|
+
lease.expiredCommands.add(commandId);
|
|
260
|
+
if (lease.expiredCommands.size > 256)
|
|
261
|
+
lease.expiredCommands.delete(lease.expiredCommands.values().next().value);
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
258
264
|
this.failLease(lease, CLOSE_REPLACED_OR_STALE, `Desktop Browser Host ${command.method} outcome is unknown`);
|
|
259
265
|
}
|
|
260
266
|
}, this.commandTimeoutMilliseconds);
|
|
@@ -290,13 +296,15 @@ export class DesktopBrowserHostRegistry {
|
|
|
290
296
|
receiveReply(lease, frame) {
|
|
291
297
|
const pending = lease.pending.get(frame.commandId);
|
|
292
298
|
if (!pending) {
|
|
299
|
+
if (lease.expiredCommands.delete(frame.commandId))
|
|
300
|
+
return;
|
|
293
301
|
throw new DesktopBrowserHostProtocolError("reply commandId is unknown or expired", CLOSE_REPLACED_OR_STALE);
|
|
294
302
|
}
|
|
295
303
|
lease.pending.delete(frame.commandId);
|
|
296
304
|
clearTimeout(pending.timer);
|
|
297
305
|
if (this.now() >= pending.deadlineUnixMilliseconds) {
|
|
298
|
-
pending.reject(
|
|
299
|
-
if (!
|
|
306
|
+
pending.reject(commandDeadlineError(pending.command, `Desktop Browser Host ${pending.command.method} replied after its deadline`));
|
|
307
|
+
if (!isRecoverableDeadline(pending.command)) {
|
|
300
308
|
this.failLease(lease, CLOSE_REPLACED_OR_STALE, `Desktop Browser Host ${pending.command.method} outcome is unknown`);
|
|
301
309
|
}
|
|
302
310
|
return;
|
|
@@ -536,6 +544,36 @@ class DesktopBrowserHandle {
|
|
|
536
544
|
getPageCdpEndpoint(hostPageId) {
|
|
537
545
|
return this.command({ method: "page.cdp-endpoint", ...this.pageScope(hostPageId) }, parseDesktopBrowserHostPageCdpEndpointResult);
|
|
538
546
|
}
|
|
547
|
+
async getPageAutomationEndpoint(hostPageId) {
|
|
548
|
+
if (!this.capabilities.includes("page-automation-v1"))
|
|
549
|
+
throw new SessionBrowserHostError("unsupported", "Upgrade the Desktop Host for page automation");
|
|
550
|
+
const result = await this.command({ method: "page.automation-endpoint", ...this.pageScope(hostPageId) }, parseDesktopBrowserHostCdpEndpointResult);
|
|
551
|
+
if (!result.pageTargetId)
|
|
552
|
+
throw new SessionBrowserHostError("unsupported", "Desktop Host does not support page automation");
|
|
553
|
+
return { ...result, pageTargetId: result.pageTargetId };
|
|
554
|
+
}
|
|
555
|
+
async acquireAutomationVisibility(hostPageId) {
|
|
556
|
+
if (!this.capabilities.includes("page-automation-visibility-v1")) {
|
|
557
|
+
throw new SessionBrowserHostError("unsupported", "Upgrade the Desktop Host for automation rendering");
|
|
558
|
+
}
|
|
559
|
+
const scope = { ...this.pageScope(hostPageId), visibilityId: this.registry.createId("visibility") };
|
|
560
|
+
const release = async () => {
|
|
561
|
+
if (this.available && !this.closed)
|
|
562
|
+
await this.completed({ method: "page.automation-visibility.release", ...scope });
|
|
563
|
+
};
|
|
564
|
+
try {
|
|
565
|
+
await this.completed({ method: "page.automation-visibility.acquire", ...scope });
|
|
566
|
+
}
|
|
567
|
+
catch (error) {
|
|
568
|
+
await release().catch(() => undefined);
|
|
569
|
+
throw error;
|
|
570
|
+
}
|
|
571
|
+
let released = false;
|
|
572
|
+
return { release: async () => { if (!released) {
|
|
573
|
+
released = true;
|
|
574
|
+
await release();
|
|
575
|
+
} } };
|
|
576
|
+
}
|
|
539
577
|
async openSurface(input) {
|
|
540
578
|
if (this.lease.surface !== "binary-v1") {
|
|
541
579
|
throw new SessionBrowserSurfaceUnsupportedError();
|
|
@@ -930,10 +968,14 @@ function mapReplyError(frame, command) {
|
|
|
930
968
|
return new SessionBrowserHostError("capacity", "Desktop Browser Host capacity was reached");
|
|
931
969
|
}
|
|
932
970
|
if (frame.error.code === "unavailable" ||
|
|
933
|
-
frame.error.code === "stale_generation"
|
|
934
|
-
frame.error.code === "deadline_exceeded")
|
|
971
|
+
frame.error.code === "stale_generation")
|
|
935
972
|
return hostUnavailable("Desktop Browser Host is unavailable");
|
|
973
|
+
if (frame.error.code === "deadline_exceeded") {
|
|
974
|
+
return new SessionBrowserHostError("command_timeout", "Desktop Browser command deadline exceeded");
|
|
975
|
+
}
|
|
936
976
|
if (frame.error.code === "internal") {
|
|
977
|
+
if (isReadOnlyCommand(command))
|
|
978
|
+
return new SessionBrowserHostError("operation_failed", "Desktop Browser observation failed temporarily");
|
|
937
979
|
return commandOutcomeError(command, "Desktop Browser Host command outcome is unknown");
|
|
938
980
|
}
|
|
939
981
|
return new Error("Desktop Browser Host command failed");
|
|
@@ -942,9 +984,16 @@ function commandOutcomeError(command, message, cause) {
|
|
|
942
984
|
const readOnly = isReadOnlyCommand(command);
|
|
943
985
|
return new SessionBrowserHostError(readOnly ? "unavailable" : "outcome_unknown", message, cause === undefined ? undefined : { cause });
|
|
944
986
|
}
|
|
987
|
+
function commandDeadlineError(command, message) {
|
|
988
|
+
return new SessionBrowserHostError(isReadOnlyCommand(command) ? "command_timeout" : "outcome_unknown", message);
|
|
989
|
+
}
|
|
990
|
+
function isRecoverableDeadline(command) {
|
|
991
|
+
return isReadOnlyCommand(command) || ["page.navigate", "page.reload", "page.back", "page.forward", "page.activate"].includes(command.method);
|
|
992
|
+
}
|
|
945
993
|
function isReadOnlyCommand(command) {
|
|
946
994
|
return command.method === "browser.snapshot" ||
|
|
947
995
|
command.method === "browser.cdp-endpoint" ||
|
|
996
|
+
command.method === "page.automation-endpoint" ||
|
|
948
997
|
command.method === "page.cdp-endpoint";
|
|
949
998
|
}
|
|
950
999
|
class BoundedAsyncQueue {
|
|
@@ -352,6 +352,7 @@ function mapHostError(request, error) {
|
|
|
352
352
|
case "shutting_down":
|
|
353
353
|
return { code: "failed_precondition", message: "Browser operation is unavailable" };
|
|
354
354
|
case "outcome_unknown":
|
|
355
|
+
case "command_timeout":
|
|
355
356
|
return { code: "outcome_unknown", message: "Browser operation may have been applied" };
|
|
356
357
|
case "host_failure":
|
|
357
358
|
return { code: "internal", message: "Browser Host request failed" };
|
package/dist/server.d.ts
CHANGED
|
@@ -27,13 +27,14 @@ import { type SessionTerminalHost } from "./session-terminal-host.js";
|
|
|
27
27
|
import type { ProviderCliManagementHost } from "./provider-cli-host.js";
|
|
28
28
|
import { type RuntimeWebAccessPolicy } from "./runtime-web-auth.js";
|
|
29
29
|
import type { SessionBrowserRequestHeadersPort } from "./session-browser-service.js";
|
|
30
|
+
import type { RuntimeBrowserAutomationCommand, RuntimeBrowserAutomationResult } from "@rynx-ai/protocol/runtime-browser-bootstrap";
|
|
30
31
|
export type { ProviderCliManagementHost, ProviderCliStatusHost, } from "./provider-cli-host.js";
|
|
31
32
|
export { MachineSessionService, MachineSessionServiceFailure, MachineSessionServiceInputError, synthesizeSessionTitle, } from "./machine-session-service.js";
|
|
32
33
|
export { SessionPendingInputIndex } from "./session-pending-inputs.js";
|
|
33
34
|
export type { MachineSessionAgentCatalogPort, MachineSessionCreateInput, MachineSessionCreateResult, MachineSessionDeleteResult, MachineSessionForkInput, MachineSessionForkOperationRecord, MachineSessionForkOperationState, MachineSessionForkResult, MachineSessionForkStorePort, MachineSessionNativeRotationInput, MachineSessionInterruptPort, MachineSessionInterruptResult, MachineSessionInteractionResult, MachineSessionListInput, MachineSessionListPage, MachineSessionMessageResult, MachineSessionMessageEnqueueResult, MachineSessionMessageInput, MachineSessionMessageOperationState, MachineSessionPendingMessage, MachineSessionPendingMessagePort, MachineSessionPreparedInput, MachineSessionResourcePort, MachineSessionRunExecutionSelection, MachineSessionRunExecutionSnapshot, MachineSessionRunnerPort, MachineSessionRuntimeStatePort, MachineSessionServiceFailureCode, MachineSessionServiceOptions, MachineSessionServicePorts, MachineSessionSnapshotInput, MachineSessionSnapshotPage, MachineSessionWatch, StartedMachineSessionRun, } from "./machine-session-service.js";
|
|
34
35
|
export type { MachineSessionPendingInput, MachineSessionPendingInputPort, } from "./session-pending-inputs.js";
|
|
35
36
|
export { SessionBrowserHostError, SessionBrowserService, SessionBrowserServiceError, SessionBrowserSurfaceUnsupportedError, } from "./session-browser-service.js";
|
|
36
|
-
export type { SessionBrowserHost, SessionBrowserHostCreateInput, SessionBrowserHostErrorCode, SessionBrowserHostEvent, SessionBrowserHostHandle, SessionBrowserHostPage, SessionBrowserHostSurfaceFrame, SessionBrowserHostSurfaceOpenInput, SessionBrowserHostSurfaceSource, SessionBrowserHostSurfaceTrustedInput, SessionBrowserHostSurfaceViewport, SessionBrowserHostSnapshot, SessionBrowserInspectionEndpoint, SessionBrowserInspectionTarget, SessionBrowserRegistryPort, SessionBrowserRequestHeadersPort, SessionBrowserServiceErrorCode, SessionBrowserServiceOptions, SessionBrowserServicePorts, SessionBrowserSurfaceOpenInput, SessionBrowserSurfaceSource, } from "./session-browser-service.js";
|
|
37
|
+
export type { SessionBrowserHost, SessionBrowserAutomationBinding, SessionBrowserAutomationVisibility, SessionBrowserHostCreateInput, SessionBrowserHostErrorCode, SessionBrowserHostEvent, SessionBrowserHostHandle, SessionBrowserHostPage, SessionBrowserHostSurfaceFrame, SessionBrowserHostSurfaceOpenInput, SessionBrowserHostSurfaceSource, SessionBrowserHostSurfaceTrustedInput, SessionBrowserHostSurfaceViewport, SessionBrowserHostSnapshot, SessionBrowserInspectionEndpoint, SessionBrowserInspectionTarget, SessionBrowserRegistryPort, SessionBrowserRequestHeadersPort, SessionBrowserServiceErrorCode, SessionBrowserServiceOptions, SessionBrowserServicePorts, SessionBrowserSurfaceOpenInput, SessionBrowserSurfaceSource, } from "./session-browser-service.js";
|
|
37
38
|
export { SessionEmulatorBindingConflictError, SessionEmulatorService, SessionEmulatorServiceError, } from "./session-emulator-service.js";
|
|
38
39
|
export type { SessionEmulatorBindingRecord, SessionEmulatorBindingStore, SessionEmulatorServiceErrorCode, SessionEmulatorServiceOptions, SessionEmulatorSurface, } from "./session-emulator-service.js";
|
|
39
40
|
export { SessionBrowserSurfaceCoordinator } from "./session-browser-surface-coordinator.js";
|
|
@@ -286,6 +287,7 @@ export interface DaemonManagementHost {
|
|
|
286
287
|
* half of the credential was wrong.
|
|
287
288
|
*/
|
|
288
289
|
export interface RuntimeLocalBrowserAutomationHost {
|
|
290
|
+
executeForSession?(sessionId: string, command: RuntimeBrowserAutomationCommand): Promise<RuntimeBrowserAutomationResult>;
|
|
289
291
|
authorizeCredential(credential: RuntimeBrowserBootstrapCredential): boolean | Promise<boolean>;
|
|
290
292
|
endpointForCredential(credential: RuntimeBrowserBootstrapCredential): Promise<RuntimeBrowserEndpointDescriptor | undefined>;
|
|
291
293
|
endpointForSession(sessionId: string): Promise<RuntimeBrowserEndpointDescriptor>;
|
|
@@ -19,6 +19,7 @@ export interface SessionBrowserHostPage {
|
|
|
19
19
|
loading: boolean;
|
|
20
20
|
canGoBack: boolean;
|
|
21
21
|
canGoForward: boolean;
|
|
22
|
+
observationStale?: boolean;
|
|
22
23
|
}
|
|
23
24
|
export interface SessionBrowserHostSnapshot {
|
|
24
25
|
pages: SessionBrowserHostPage[];
|
|
@@ -128,6 +129,12 @@ export interface SessionBrowserHostHandle {
|
|
|
128
129
|
}>;
|
|
129
130
|
/** Optional exact physical Page handoff for the daemon-local inspection gateway. */
|
|
130
131
|
getPageCdpEndpoint?(hostPageId: string): Promise<SessionBrowserInspectionEndpoint>;
|
|
132
|
+
/** Automation facade, distinct from raw DevTools inspection; binds exactly one existing Page. */
|
|
133
|
+
getPageAutomationEndpoint?(hostPageId: string): Promise<SessionBrowserInspectionEndpoint & {
|
|
134
|
+
pageTargetId: string;
|
|
135
|
+
}>;
|
|
136
|
+
/** Temporary renderability, never semantic Page selection or human window focus. */
|
|
137
|
+
acquireAutomationVisibility?(hostPageId: string, canActivate?: () => boolean): Promise<SessionBrowserAutomationVisibility>;
|
|
131
138
|
/** Optional physical presentation primitive; it never projects native CDP. */
|
|
132
139
|
openSurface?(input: SessionBrowserHostSurfaceOpenInput): Promise<SessionBrowserHostSurfaceSource>;
|
|
133
140
|
snapshot(): Promise<SessionBrowserHostSnapshot>;
|
|
@@ -158,6 +165,10 @@ export interface SessionBrowserServicePorts {
|
|
|
158
165
|
sessions?: SessionBrowserRegistryPort;
|
|
159
166
|
}
|
|
160
167
|
export interface SessionBrowserServiceOptions {
|
|
168
|
+
onObservationError?: (error: unknown, target: {
|
|
169
|
+
sessionId: string;
|
|
170
|
+
browserGeneration: number;
|
|
171
|
+
}) => void;
|
|
161
172
|
/** Injectable only so opaque identity behavior is deterministic in tests. */
|
|
162
173
|
pageIdFactory?: () => string;
|
|
163
174
|
/** Injectable boot nonce; production randomization fences stale pre-restart mutations. */
|
|
@@ -170,7 +181,18 @@ export interface SessionBrowserServiceOptions {
|
|
|
170
181
|
*/
|
|
171
182
|
admissionReserve?: () => AdmissionReservation | undefined;
|
|
172
183
|
}
|
|
173
|
-
export
|
|
184
|
+
export interface SessionBrowserAutomationVisibility {
|
|
185
|
+
/** Always releases resources; false suppresses physical focus restoration after human takeover. */
|
|
186
|
+
release(restoreFocus?: boolean): Promise<void>;
|
|
187
|
+
}
|
|
188
|
+
export interface SessionBrowserAutomationBinding extends RuntimeBrowserEndpointDescriptor {
|
|
189
|
+
pageId: string;
|
|
190
|
+
pageTargetId: string;
|
|
191
|
+
/** Synchronous physical identity fence; long driver commands never hold the Session lock. */
|
|
192
|
+
isCurrent(): boolean;
|
|
193
|
+
acquireAutomationVisibility(canActivate?: () => boolean): Promise<SessionBrowserAutomationVisibility>;
|
|
194
|
+
}
|
|
195
|
+
export type SessionBrowserHostErrorCode = "unavailable" | "command_timeout" | "operation_failed" | "unsupported" | "page_not_found" | "capacity" | "outcome_unknown";
|
|
174
196
|
/** Expected failure reported by a Browser Host implementation. */
|
|
175
197
|
export declare class SessionBrowserHostError extends Error {
|
|
176
198
|
readonly code: SessionBrowserHostErrorCode;
|
|
@@ -181,7 +203,7 @@ export interface SessionBrowserRequestHeadersPort {
|
|
|
181
203
|
getRequestHeaders(input: RuntimeBrowserRequestHeadersGetParams): Promise<RuntimeBrowserRequestHeadersPolicy>;
|
|
182
204
|
setRequestHeaders(input: RuntimeBrowserRequestHeadersSetParams): Promise<RuntimeBrowserRequestHeadersPolicy>;
|
|
183
205
|
}
|
|
184
|
-
export type SessionBrowserServiceErrorCode = "invalid_request" | "not_found" | "generation_mismatch" | "revision_mismatch" | "unsupported" | "unavailable" | "capacity" | "host_failure" | "outcome_unknown" | "shutting_down";
|
|
206
|
+
export type SessionBrowserServiceErrorCode = "invalid_request" | "command_timeout" | "not_found" | "generation_mismatch" | "revision_mismatch" | "unsupported" | "unavailable" | "capacity" | "host_failure" | "outcome_unknown" | "shutting_down";
|
|
185
207
|
/** Stable application error; transports map the code without exposing Host diagnostics. */
|
|
186
208
|
export declare class SessionBrowserServiceError extends Error {
|
|
187
209
|
readonly code: SessionBrowserServiceErrorCode;
|
|
@@ -193,11 +215,13 @@ export declare class SessionBrowserServiceError extends Error {
|
|
|
193
215
|
*/
|
|
194
216
|
export declare class SessionBrowserService implements SessionBrowserRequestHeadersPort {
|
|
195
217
|
private readonly ports;
|
|
218
|
+
private readonly options;
|
|
196
219
|
private readonly records;
|
|
197
220
|
private readonly generations;
|
|
198
221
|
private readonly requestHeaderPolicies;
|
|
199
222
|
private readonly queues;
|
|
200
223
|
private readonly deletingSessions;
|
|
224
|
+
private readonly closingAutomationPages;
|
|
201
225
|
private readonly observers;
|
|
202
226
|
private readonly pageIdFactory;
|
|
203
227
|
private readonly bootGenerationSeed;
|
|
@@ -229,6 +253,7 @@ export declare class SessionBrowserService implements SessionBrowserRequestHeade
|
|
|
229
253
|
* Runtime boundary.
|
|
230
254
|
*/
|
|
231
255
|
getRuntimeLocalEndpoint(sessionId: string): Promise<RuntimeBrowserEndpointDescriptor>;
|
|
256
|
+
resolveAutomationPage(sessionId: string, pageId?: string): Promise<SessionBrowserAutomationBinding>;
|
|
232
257
|
/**
|
|
233
258
|
* Resolve one opaque Page to its generation-local Host source. This is the
|
|
234
259
|
* only presentation path allowed to cross the semantic/physical Page seam.
|
|
@@ -260,6 +285,7 @@ export declare class SessionBrowserService implements SessionBrowserRequestHeade
|
|
|
260
285
|
private reconcileForRead;
|
|
261
286
|
private reconcileForMutation;
|
|
262
287
|
private tryReconcileAfterRejectedMutation;
|
|
288
|
+
private retainRecoverableObservation;
|
|
263
289
|
private reconcile;
|
|
264
290
|
private createPageId;
|
|
265
291
|
private project;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { diagnosticEvents, sessionDiagnosticDir } from "@rynx-ai/core";
|
|
2
4
|
import { RUNTIME_BROWSER_MAX_PAGES, parseRuntimeBrowserRequestHeadersGetParams, parseRuntimeBrowserRequestHeadersSetParams, parseRuntimeBrowserCloseParams, parseRuntimeBrowserOpenParams, parseRuntimeBrowserPageCreateParams, parseRuntimeBrowserPageMutationParams, parseRuntimeBrowserPageNavigateParams, parseRuntimeBrowserState, parseRuntimeBrowserStateGetParams, } from "@rynx-ai/protocol/runtime-browser";
|
|
3
5
|
import { parseRuntimeBrowserEndpointDescriptor, } from "@rynx-ai/protocol/runtime-browser-bootstrap";
|
|
4
6
|
const MAX_HOST_PAGE_ID_CHARS = 2_048;
|
|
@@ -35,11 +37,13 @@ export class SessionBrowserServiceError extends Error {
|
|
|
35
37
|
*/
|
|
36
38
|
export class SessionBrowserService {
|
|
37
39
|
ports;
|
|
40
|
+
options;
|
|
38
41
|
records = new Map();
|
|
39
42
|
generations = new Map();
|
|
40
43
|
requestHeaderPolicies = new Map();
|
|
41
44
|
queues = new Map();
|
|
42
45
|
deletingSessions = new Set();
|
|
46
|
+
closingAutomationPages = new Map();
|
|
43
47
|
observers = new Set();
|
|
44
48
|
pageIdFactory;
|
|
45
49
|
bootGenerationSeed;
|
|
@@ -50,6 +54,7 @@ export class SessionBrowserService {
|
|
|
50
54
|
closeAllPromise;
|
|
51
55
|
constructor(ports, options = {}) {
|
|
52
56
|
this.ports = ports;
|
|
57
|
+
this.options = options;
|
|
53
58
|
this.pageIdFactory = options.pageIdFactory ?? (() => `page_${randomUUID()}`);
|
|
54
59
|
this.bootGenerationSeed = options.bootGenerationSeed ?? randomBootGenerationSeed();
|
|
55
60
|
this.requestHeaderRevisionSeed = options.requestHeaderRevisionSeed ??
|
|
@@ -132,7 +137,7 @@ export class SessionBrowserService {
|
|
|
132
137
|
}
|
|
133
138
|
catch (error) {
|
|
134
139
|
if (error instanceof SessionBrowserHostError &&
|
|
135
|
-
|
|
140
|
+
error.code === "unavailable") {
|
|
136
141
|
record.status = "unavailable";
|
|
137
142
|
}
|
|
138
143
|
throw mapHostError(error, "Browser inspection endpoint is unavailable");
|
|
@@ -242,11 +247,53 @@ export class SessionBrowserService {
|
|
|
242
247
|
});
|
|
243
248
|
}
|
|
244
249
|
catch (error) {
|
|
245
|
-
record
|
|
250
|
+
if (!this.retainRecoverableObservation(record, error))
|
|
251
|
+
record.status = "unavailable";
|
|
246
252
|
throw mapHostError(error, "Browser automation endpoint is unavailable");
|
|
247
253
|
}
|
|
248
254
|
});
|
|
249
255
|
}
|
|
256
|
+
async resolveAutomationPage(sessionId, pageId) {
|
|
257
|
+
parseInput(() => parseRuntimeBrowserStateGetParams({ sessionId }), "Browser Session is invalid");
|
|
258
|
+
return this.exclusive(sessionId, async () => {
|
|
259
|
+
this.ensureRunning();
|
|
260
|
+
await this.ensureSessionExists(sessionId);
|
|
261
|
+
const record = this.requireRecord(sessionId);
|
|
262
|
+
if (record.status !== "ready")
|
|
263
|
+
throw new SessionBrowserServiceError("unavailable", "Browser is not available");
|
|
264
|
+
const selected = pageId ?? record.activePageId;
|
|
265
|
+
const page = selected ? record.pagesById.get(selected) : undefined;
|
|
266
|
+
if (!page)
|
|
267
|
+
throw new SessionBrowserServiceError("not_found", "Browser Page was not found; run browser pages");
|
|
268
|
+
if (!record.handle.getPageAutomationEndpoint) {
|
|
269
|
+
throw new SessionBrowserServiceError("unsupported", "Browser Host needs an upgrade for page automation");
|
|
270
|
+
}
|
|
271
|
+
const endpoint = await record.handle.getPageAutomationEndpoint(page.hostPageId);
|
|
272
|
+
const descriptor = parseRuntimeBrowserEndpointDescriptor({
|
|
273
|
+
sessionId, browserGeneration: record.browserGeneration,
|
|
274
|
+
executionBackend: record.executionBackend, ...endpoint,
|
|
275
|
+
});
|
|
276
|
+
const isCurrent = () => !this.shuttingDown && !this.deletingSessions.has(sessionId) && this.records.get(sessionId) === record &&
|
|
277
|
+
record.status === "ready" && !this.closingAutomationPages.has(JSON.stringify([sessionId, record.browserGeneration, page.pageId])) &&
|
|
278
|
+
record.pagesById.get(page.pageId)?.hostPageId === page.hostPageId;
|
|
279
|
+
return {
|
|
280
|
+
...descriptor, pageId: page.pageId, pageTargetId: endpoint.pageTargetId, isCurrent,
|
|
281
|
+
acquireAutomationVisibility: async (canActivate) => {
|
|
282
|
+
if (!isCurrent())
|
|
283
|
+
throw new SessionBrowserServiceError("not_found", "Browser Page closed before render admission");
|
|
284
|
+
if (!record.handle.acquireAutomationVisibility) {
|
|
285
|
+
throw new SessionBrowserServiceError("unsupported", "Upgrade the Browser Host for automation rendering");
|
|
286
|
+
}
|
|
287
|
+
const visibility = await record.handle.acquireAutomationVisibility(page.hostPageId, canActivate);
|
|
288
|
+
if (!isCurrent()) {
|
|
289
|
+
await visibility.release(false).catch(() => undefined);
|
|
290
|
+
throw new SessionBrowserServiceError("not_found", "Browser Page closed during render admission");
|
|
291
|
+
}
|
|
292
|
+
return visibility;
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
});
|
|
296
|
+
}
|
|
250
297
|
/**
|
|
251
298
|
* Resolve one opaque Page to its generation-local Host source. This is the
|
|
252
299
|
* only presentation path allowed to cross the semantic/physical Page seam.
|
|
@@ -361,8 +408,20 @@ export class SessionBrowserService {
|
|
|
361
408
|
return this.runMutation(record, () => record.handle.createPage(params.url));
|
|
362
409
|
});
|
|
363
410
|
}
|
|
364
|
-
closePage(input) {
|
|
365
|
-
|
|
411
|
+
async closePage(input) {
|
|
412
|
+
const params = parseInput(() => parseRuntimeBrowserPageMutationParams(input), "Browser page close request is invalid");
|
|
413
|
+
const key = JSON.stringify([params.sessionId, params.browserGeneration, params.pageId]);
|
|
414
|
+
this.closingAutomationPages.set(key, (this.closingAutomationPages.get(key) ?? 0) + 1);
|
|
415
|
+
try {
|
|
416
|
+
return await this.pageMutation(params, "Browser page close request is invalid", (record, hostPageId) => record.handle.closePage(hostPageId));
|
|
417
|
+
}
|
|
418
|
+
finally {
|
|
419
|
+
const remaining = this.closingAutomationPages.get(key) - 1;
|
|
420
|
+
if (remaining)
|
|
421
|
+
this.closingAutomationPages.set(key, remaining);
|
|
422
|
+
else
|
|
423
|
+
this.closingAutomationPages.delete(key);
|
|
424
|
+
}
|
|
366
425
|
}
|
|
367
426
|
activatePage(input) {
|
|
368
427
|
return this.pageMutation(input, "Browser page activation request is invalid", (record, hostPageId) => record.handle.activatePage(hostPageId));
|
|
@@ -583,7 +642,7 @@ export class SessionBrowserService {
|
|
|
583
642
|
}
|
|
584
643
|
catch (error) {
|
|
585
644
|
if (error instanceof SessionBrowserHostError &&
|
|
586
|
-
|
|
645
|
+
error.code === "unavailable") {
|
|
587
646
|
record.status = "unavailable";
|
|
588
647
|
}
|
|
589
648
|
if (error instanceof SessionBrowserHostError && error.code === "page_not_found") {
|
|
@@ -596,7 +655,8 @@ export class SessionBrowserService {
|
|
|
596
655
|
return this.project(record);
|
|
597
656
|
}
|
|
598
657
|
catch (error) {
|
|
599
|
-
record
|
|
658
|
+
if (!this.retainRecoverableObservation(record, error))
|
|
659
|
+
record.status = "unavailable";
|
|
600
660
|
throw new SessionBrowserServiceError("outcome_unknown", "Browser operation completed but its outcome could not be reconciled", { cause: error });
|
|
601
661
|
}
|
|
602
662
|
}
|
|
@@ -605,6 +665,8 @@ export class SessionBrowserService {
|
|
|
605
665
|
await this.reconcile(record);
|
|
606
666
|
}
|
|
607
667
|
catch (error) {
|
|
668
|
+
if (this.retainRecoverableObservation(record, error))
|
|
669
|
+
return;
|
|
608
670
|
record.status = "unavailable";
|
|
609
671
|
throw new SessionBrowserServiceError("host_failure", "Browser Host state could not be reconciled", { cause: error });
|
|
610
672
|
}
|
|
@@ -614,6 +676,9 @@ export class SessionBrowserService {
|
|
|
614
676
|
await this.reconcile(record);
|
|
615
677
|
}
|
|
616
678
|
catch (error) {
|
|
679
|
+
if (this.retainRecoverableObservation(record, error)) {
|
|
680
|
+
throw mapHostError(error, "Browser state observation is temporarily unavailable");
|
|
681
|
+
}
|
|
617
682
|
record.status = "unavailable";
|
|
618
683
|
throw new SessionBrowserServiceError("unavailable", "Browser became unavailable while reconciling Host state", { cause: error });
|
|
619
684
|
}
|
|
@@ -622,9 +687,24 @@ export class SessionBrowserService {
|
|
|
622
687
|
try {
|
|
623
688
|
await this.reconcile(record);
|
|
624
689
|
}
|
|
625
|
-
catch {
|
|
626
|
-
record
|
|
690
|
+
catch (error) {
|
|
691
|
+
if (!this.retainRecoverableObservation(record, error))
|
|
692
|
+
record.status = "unavailable";
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
retainRecoverableObservation(record, error) {
|
|
696
|
+
if (!(error instanceof SessionBrowserHostError) ||
|
|
697
|
+
!["command_timeout", "operation_failed", "outcome_unknown"].includes(error.code))
|
|
698
|
+
return false;
|
|
699
|
+
record.observationStale = true;
|
|
700
|
+
try {
|
|
701
|
+
this.options.onObservationError?.(error, {
|
|
702
|
+
sessionId: record.sessionId,
|
|
703
|
+
browserGeneration: record.browserGeneration,
|
|
704
|
+
});
|
|
627
705
|
}
|
|
706
|
+
catch { /* A diagnostic sink cannot change Browser readiness. */ }
|
|
707
|
+
return true;
|
|
628
708
|
}
|
|
629
709
|
async reconcile(record) {
|
|
630
710
|
const snapshot = await record.handle.snapshot();
|
|
@@ -652,6 +732,7 @@ export class SessionBrowserService {
|
|
|
652
732
|
loading: hostPage.loading,
|
|
653
733
|
canGoBack: hostPage.canGoBack,
|
|
654
734
|
canGoForward: hostPage.canGoForward,
|
|
735
|
+
...(hostPage.observationStale === undefined ? {} : { observationStale: hostPage.observationStale }),
|
|
655
736
|
});
|
|
656
737
|
}
|
|
657
738
|
if (snapshot.activeHostPageId !== null &&
|
|
@@ -704,6 +785,7 @@ export class SessionBrowserService {
|
|
|
704
785
|
record.pageOrder = parsed.pages.map((page) => page.pageId);
|
|
705
786
|
record.activePageId = parsed.activePageId;
|
|
706
787
|
record.capabilities = parsed.capabilities;
|
|
788
|
+
record.observationStale = parsed.pages.some((page) => page.observationStale);
|
|
707
789
|
}
|
|
708
790
|
createPageId(record, pending) {
|
|
709
791
|
for (let attempt = 0; attempt < MAX_PAGE_ID_FACTORY_ATTEMPTS; attempt += 1) {
|
|
@@ -724,6 +806,7 @@ export class SessionBrowserService {
|
|
|
724
806
|
activePageId: record.activePageId,
|
|
725
807
|
pages: record.pageOrder.map((pageId) => record.pagesById.get(pageId)),
|
|
726
808
|
capabilities: record.capabilities,
|
|
809
|
+
...(record.observationStale ? { observationStale: true } : {}),
|
|
727
810
|
});
|
|
728
811
|
if (!parsed)
|
|
729
812
|
throw new Error("Browser record unexpectedly projected to absence");
|
|
@@ -734,9 +817,13 @@ export class SessionBrowserService {
|
|
|
734
817
|
this.observers.add(observer);
|
|
735
818
|
}
|
|
736
819
|
async consumeEvents(record) {
|
|
820
|
+
const log = diagnosticEvents(join(sessionDiagnosticDir("browser", record.sessionId), "page-events.jsonl"), { sessionId: record.sessionId });
|
|
821
|
+
const lifecycle = diagnosticEvents(join(sessionDiagnosticDir("browser", record.sessionId), "browser-host.log"), { sessionId: record.sessionId, browserGeneration: record.browserGeneration });
|
|
822
|
+
lifecycle("browser.observer.started");
|
|
737
823
|
let failed;
|
|
738
824
|
try {
|
|
739
825
|
for await (const event of record.handle.events) {
|
|
826
|
+
log("browser.observation", { ...event });
|
|
740
827
|
await this.exclusive(record.sessionId, async () => {
|
|
741
828
|
if (!this.isCurrent(record, event.browserGeneration))
|
|
742
829
|
return;
|
|
@@ -751,8 +838,9 @@ export class SessionBrowserService {
|
|
|
751
838
|
try {
|
|
752
839
|
await this.reconcile(record);
|
|
753
840
|
}
|
|
754
|
-
catch {
|
|
755
|
-
record
|
|
841
|
+
catch (error) {
|
|
842
|
+
if (!this.retainRecoverableObservation(record, error))
|
|
843
|
+
record.status = "unavailable";
|
|
756
844
|
}
|
|
757
845
|
}, true);
|
|
758
846
|
}
|
|
@@ -760,6 +848,7 @@ export class SessionBrowserService {
|
|
|
760
848
|
catch (error) {
|
|
761
849
|
failed = error;
|
|
762
850
|
}
|
|
851
|
+
lifecycle("browser.observer.ended", { ...(failed ? { error: String(failed) } : {}) });
|
|
763
852
|
await this.exclusive(record.sessionId, async () => {
|
|
764
853
|
if (!this.isCurrent(record, record.browserGeneration))
|
|
765
854
|
return;
|
|
@@ -848,6 +937,12 @@ function mapHostError(error, fallbackMessage) {
|
|
|
848
937
|
if (error instanceof SessionBrowserServiceError)
|
|
849
938
|
return error;
|
|
850
939
|
if (error instanceof SessionBrowserHostError) {
|
|
940
|
+
if (error.code === "command_timeout") {
|
|
941
|
+
return new SessionBrowserServiceError("command_timeout", "Browser command timed out; do not automatically repeat an accepted action", { cause: error });
|
|
942
|
+
}
|
|
943
|
+
if (error.code === "operation_failed") {
|
|
944
|
+
return new SessionBrowserServiceError("host_failure", fallbackMessage, { cause: error });
|
|
945
|
+
}
|
|
851
946
|
if (error.code === "page_not_found") {
|
|
852
947
|
return new SessionBrowserServiceError("not_found", "Browser Page was not found", {
|
|
853
948
|
cause: error,
|
|
@@ -20,5 +20,7 @@ export declare class SessionBrowserSurfaceCoordinator implements DirectRuntimeBr
|
|
|
20
20
|
private readonly maxPendingEventBytes;
|
|
21
21
|
constructor(browsers: SessionBrowserService, options?: SessionBrowserSurfaceCoordinatorOptions);
|
|
22
22
|
open(frame: RuntimeBrowserSurfaceOpenFrame, context: Parameters<DirectRuntimeBrowserSurfaceHost["open"]>[1]): Promise<DirectRuntimeBrowserSurfaceAttachment>;
|
|
23
|
+
isHumanControlled(sessionId: string, browserGeneration: number, pageId: string): boolean;
|
|
24
|
+
isBrowserHumanControlled(sessionId: string, browserGeneration: number): boolean;
|
|
23
25
|
private exclusivePage;
|
|
24
26
|
}
|
|
@@ -95,6 +95,17 @@ export class SessionBrowserSurfaceCoordinator {
|
|
|
95
95
|
return attachment;
|
|
96
96
|
});
|
|
97
97
|
}
|
|
98
|
+
isHumanControlled(sessionId, browserGeneration, pageId) {
|
|
99
|
+
const page = this.pages.get(`${sessionId}\u0000${pageId}`);
|
|
100
|
+
return page?.browserGeneration === browserGeneration && page.hasHumanControl();
|
|
101
|
+
}
|
|
102
|
+
isBrowserHumanControlled(sessionId, browserGeneration) {
|
|
103
|
+
for (const [key, page] of this.pages) {
|
|
104
|
+
if (key.startsWith(`${sessionId}\u0000`) && page.browserGeneration === browserGeneration && page.hasHumanControl())
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
98
109
|
exclusivePage(pageKey, operation) {
|
|
99
110
|
const previous = this.pageQueues.get(pageKey) ?? Promise.resolve();
|
|
100
111
|
const result = previous.then(operation, operation);
|
|
@@ -173,6 +184,9 @@ class SharedPageSurface {
|
|
|
173
184
|
getViewport() {
|
|
174
185
|
return this.page.source.getViewport();
|
|
175
186
|
}
|
|
187
|
+
hasHumanControl() {
|
|
188
|
+
return Boolean(this.owner || this.desiredOwner);
|
|
189
|
+
}
|
|
176
190
|
driverState() {
|
|
177
191
|
return this.owner
|
|
178
192
|
? { driver: "stream", driverSubscriptionId: this.owner.subscriptionId }
|
|
@@ -647,6 +661,7 @@ function mapSurfaceOpenError(error) {
|
|
|
647
661
|
case "shutting_down":
|
|
648
662
|
return new DirectRuntimeBrowserSurfaceHostError("browser_closed", error.message, { cause: error });
|
|
649
663
|
case "host_failure":
|
|
664
|
+
case "command_timeout":
|
|
650
665
|
case "outcome_unknown":
|
|
651
666
|
return new DirectRuntimeBrowserSurfaceHostError("internal_error", error.message, { cause: error });
|
|
652
667
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rynx-ai/server",
|
|
3
|
-
"version": "0.1.11-beta.
|
|
3
|
+
"version": "0.1.11-beta.48",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/rynx-ai/rynx.git",
|
|
@@ -27,15 +27,15 @@
|
|
|
27
27
|
"@koa/router": "^15.4.0",
|
|
28
28
|
"koa": "^3.2.0",
|
|
29
29
|
"ws": "^8.21.0",
|
|
30
|
-
"@rynx-ai/core": "0.1.11-beta.
|
|
31
|
-
"@rynx-ai/plugin-sdk": "0.1.11-beta.
|
|
32
|
-
"@rynx-ai/protocol": "0.1.11-beta.
|
|
33
|
-
"@rynx-ai/remote-runtime-e2ee": "0.1.11-beta.
|
|
34
|
-
"@rynx-ai/runtime": "0.1.11-beta.
|
|
30
|
+
"@rynx-ai/core": "0.1.11-beta.48",
|
|
31
|
+
"@rynx-ai/plugin-sdk": "0.1.11-beta.48",
|
|
32
|
+
"@rynx-ai/protocol": "0.1.11-beta.48",
|
|
33
|
+
"@rynx-ai/remote-runtime-e2ee": "0.1.11-beta.48",
|
|
34
|
+
"@rynx-ai/runtime": "0.1.11-beta.48"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/ws": "^8.18.1",
|
|
38
|
-
"@rynx-ai/control-web": "0.1.11-beta.
|
|
38
|
+
"@rynx-ai/control-web": "0.1.11-beta.48"
|
|
39
39
|
},
|
|
40
40
|
"scripts": {
|
|
41
41
|
"build": "rm -rf dist && tsc -p tsconfig.json && mkdir -p dist/control-web && cp -R ../control-web/dist/. dist/control-web/",
|