oysterun 1.5.7 → 1.6.0-beta.0
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/dev/client/connectivity/completed-message-visibility.d.mts +30 -0
- package/dev/client/connectivity/completed-message-visibility.mjs +67 -0
- package/dev/client/connectivity/internal-connectivity-status.d.mts +11 -0
- package/dev/client/connectivity/internal-connectivity-status.mjs +45 -2
- package/dev/client/connectivity/managed-host-browser.d.mts +1 -0
- package/dev/client/connectivity/managed-host-browser.mjs +130 -25
- package/dev/client/connectivity/session-switcher-state.d.mts +16 -0
- package/dev/client/connectivity/session-switcher-state.mjs +72 -0
- package/dev/client/connectivity/session-transition-state.mjs +116 -0
- package/dev/client/web/index.html +1715 -792
- package/dev/client/web-chat/dist/assets/{ReactPrism-C_qvQuWU.js → ReactPrism-jJu8nX0A.js} +1 -1
- package/dev/client/web-chat/dist/assets/ReactPrism-jJu8nX0A.js.map +1 -0
- package/dev/client/web-chat/dist/assets/index-B9fWrEk-.js.map +1 -1
- package/dev/client/web-chat/dist/assets/{index-BgxQAT-U.js → index-BFCvSvbT.js} +93 -93
- package/dev/client/web-chat/dist/assets/index-BFCvSvbT.js.map +1 -0
- package/dev/client/web-chat/dist/assets/{index-B44MyL7f.js → index-CIqQrfEb.js} +2 -2
- package/dev/client/web-chat/dist/assets/index-CIqQrfEb.js.map +1 -0
- package/dev/client/web-chat/dist/assets/{index-DdoJ_noi.js → index-CmRIDO-s.js} +1 -1
- package/dev/client/web-chat/dist/assets/index-CmRIDO-s.js.map +1 -0
- package/dev/client/web-chat/dist/assets/{index-1o8_vyRd.css → index-NYnaSGBb.css} +1 -1
- package/dev/client/web-chat/dist/assets/{pdf-DgSk769O.js → pdf-DKH_xqif.js} +3 -3
- package/dev/client/web-chat/dist/assets/pdf-DKH_xqif.js.map +1 -0
- package/dev/client/web-chat/dist/assets/{web-CIst0rFB.js → web-D7o80CWH.js} +1 -1
- package/dev/client/web-chat/dist/assets/web-D7o80CWH.js.map +1 -0
- package/dev/client/web-chat/dist/assets/{web-D3DR1arO.js → web-DoWTwn9c.js} +1 -1
- package/dev/client/web-chat/dist/assets/web-DoWTwn9c.js.map +1 -0
- package/dev/client/web-chat/dist/index.html +10 -4
- package/docs/routec/dev/npmjs_publish.md +7 -1
- package/docs/routec/form_UI_fields.md +17 -1
- package/docs/routec/oysterun_operation_interface_spec.md +8 -1
- package/docs/routec/provider_contract.md +25 -2
- package/host-service/README.md +9 -1
- package/host-service/adapters/debug-fixture-provider-adapter.mjs +98 -0
- package/host-service/adapters/grok-acp-adapter.mjs +1097 -0
- package/host-service/agent-config.mjs +147 -35
- package/host-service/assets/product-skills/Oysterun/modules/oysterun-sessions/SKILL.md +7 -0
- package/host-service/assets/product-skills/oysterun-sessions/SKILL.md +7 -0
- package/host-service/callback-durable-store.mjs +668 -0
- package/host-service/callback-foundation.mjs +459 -0
- package/host-service/callback-host-integration.mjs +364 -0
- package/host-service/cli/product-cli.mjs +98 -3
- package/host-service/completed-session-unread.mjs +286 -0
- package/host-service/completed-turn-navigation.mjs +479 -0
- package/host-service/config.mjs +77 -2
- package/host-service/connectivity/managed-host-origin-authority.mjs +70 -0
- package/host-service/connectivity/webrtc-host-dispatcher.mjs +69 -2
- package/host-service/connectivity/webrtc-signaling-service.mjs +28 -1
- package/host-service/fixtures/p204-grok-acp-1.0.25-fixture.mjs +262 -0
- package/host-service/grok-acp-model-discovery.mjs +301 -0
- package/host-service/grok-version.mjs +102 -0
- package/host-service/mail-store.mjs +54 -0
- package/host-service/matrix-event-writer.mjs +151 -0
- package/host-service/matrix-facade.mjs +53 -13
- package/host-service/p203-callback-restart-fixture.mjs +229 -0
- package/host-service/package.json +4 -2
- package/host-service/provider-auth.mjs +20 -1
- package/host-service/provider-command-detection.mjs +17 -4
- package/host-service/provider-model-params.mjs +5 -0
- package/host-service/provider-model-refresh-runner.mjs +64 -1
- package/host-service/provider-registry.mjs +124 -5
- package/host-service/provider-startup-preflight.mjs +6 -1
- package/host-service/routec-matrix-sqlite-store.mjs +336 -1
- package/host-service/routec-matrix-storage-adapter.mjs +73 -0
- package/host-service/runtime-capability-catalog.mjs +63 -0
- package/host-service/runtime-capability-lifecycle.mjs +47 -0
- package/host-service/runtime-capability-retirement.mjs +42 -0
- package/host-service/scheduler-mutation-audit.mjs +87 -0
- package/host-service/scheduler-run-product-contract.mjs +203 -0
- package/host-service/scheduler-service.mjs +92 -43
- package/host-service/scheduler-setup-snapshot-contract.mjs +5 -2
- package/host-service/server.mjs +813 -240
- package/host-service/session-history.mjs +1 -1
- package/host-service/session-manager.mjs +131 -12
- package/host-service/session-switcher-projection.mjs +36 -0
- package/host-service/setup.mjs +8 -1
- package/host-service/telegram-bridge.mjs +10 -1
- package/package.json +2 -1
- package/static/site/docs/generated_manifest.json +5 -5
- package/static/site/docs/understand-agent-activity/en/index.html +5 -0
- package/static/site/docs/understand-agent-activity/en/index.md +12 -0
- package/static/site/docs/understand-agent-activity/zh/index.html +5 -0
- package/static/site/docs/understand-agent-activity/zh/index.md +12 -0
- package/static/site_source/content/tutorials/published/understand-agent-activity/en/article.html +5 -0
- package/static/site_source/content/tutorials/published/understand-agent-activity/zh/article.html +5 -0
- package/dev/client/web-chat/dist/assets/ReactPrism-C_qvQuWU.js.map +0 -1
- package/dev/client/web-chat/dist/assets/index-B44MyL7f.js.map +0 -1
- package/dev/client/web-chat/dist/assets/index-BgxQAT-U.js.map +0 -1
- package/dev/client/web-chat/dist/assets/index-DdoJ_noi.js.map +0 -1
- package/dev/client/web-chat/dist/assets/pdf-DgSk769O.js.map +0 -1
- package/dev/client/web-chat/dist/assets/web-CIst0rFB.js.map +0 -1
- package/dev/client/web-chat/dist/assets/web-D3DR1arO.js.map +0 -1
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export type VisibilityRect = {
|
|
2
|
+
top: number;
|
|
3
|
+
right: number;
|
|
4
|
+
bottom: number;
|
|
5
|
+
left: number;
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function isCompletedMessageViewportVisible(input: {
|
|
11
|
+
elementRect: VisibilityRect | DOMRect;
|
|
12
|
+
viewportRect: VisibilityRect | DOMRect;
|
|
13
|
+
}): boolean;
|
|
14
|
+
|
|
15
|
+
export function evaluateCompletedMessageVisibilityAcknowledgement(input: {
|
|
16
|
+
routeCChatShell: boolean;
|
|
17
|
+
currentHostSessionId?: string;
|
|
18
|
+
targetHostSessionId?: string;
|
|
19
|
+
currentMatrixRoomId?: string;
|
|
20
|
+
targetMatrixRoomId?: string;
|
|
21
|
+
latestCompletedEventId?: string | null;
|
|
22
|
+
renderedEventId?: string | null;
|
|
23
|
+
hasUnreadCompletedMessage?: boolean;
|
|
24
|
+
documentVisibilityState?: string;
|
|
25
|
+
documentHasFocus?: boolean;
|
|
26
|
+
elementConnected?: boolean;
|
|
27
|
+
elementHidden?: boolean;
|
|
28
|
+
elementRect?: VisibilityRect | DOMRect;
|
|
29
|
+
viewportRect?: VisibilityRect | DOMRect;
|
|
30
|
+
}): { qualified: boolean; reason: string };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
function finite(value) {
|
|
2
|
+
return Number.isFinite(value) ? Number(value) : 0;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function isCompletedMessageViewportVisible({
|
|
6
|
+
elementRect,
|
|
7
|
+
viewportRect,
|
|
8
|
+
}) {
|
|
9
|
+
if (!elementRect || !viewportRect) return false;
|
|
10
|
+
const width = Math.max(
|
|
11
|
+
0,
|
|
12
|
+
Math.min(finite(elementRect.right), finite(viewportRect.right)) -
|
|
13
|
+
Math.max(finite(elementRect.left), finite(viewportRect.left))
|
|
14
|
+
);
|
|
15
|
+
const height = Math.max(
|
|
16
|
+
0,
|
|
17
|
+
Math.min(finite(elementRect.bottom), finite(viewportRect.bottom)) -
|
|
18
|
+
Math.max(finite(elementRect.top), finite(viewportRect.top))
|
|
19
|
+
);
|
|
20
|
+
return (
|
|
21
|
+
width >= Math.min(16, Math.max(1, finite(elementRect.width))) &&
|
|
22
|
+
height >= Math.min(16, Math.max(1, finite(elementRect.height)))
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function evaluateCompletedMessageVisibilityAcknowledgement({
|
|
27
|
+
routeCChatShell,
|
|
28
|
+
currentHostSessionId,
|
|
29
|
+
targetHostSessionId,
|
|
30
|
+
currentMatrixRoomId,
|
|
31
|
+
targetMatrixRoomId,
|
|
32
|
+
latestCompletedEventId,
|
|
33
|
+
renderedEventId,
|
|
34
|
+
hasUnreadCompletedMessage,
|
|
35
|
+
documentVisibilityState,
|
|
36
|
+
documentHasFocus,
|
|
37
|
+
elementConnected,
|
|
38
|
+
elementHidden,
|
|
39
|
+
elementRect,
|
|
40
|
+
viewportRect,
|
|
41
|
+
}) {
|
|
42
|
+
if (routeCChatShell !== true)
|
|
43
|
+
return { qualified: false, reason: "not_current_chat" };
|
|
44
|
+
if (!currentHostSessionId || currentHostSessionId !== targetHostSessionId) {
|
|
45
|
+
return { qualified: false, reason: "other_session" };
|
|
46
|
+
}
|
|
47
|
+
if (!currentMatrixRoomId || currentMatrixRoomId !== targetMatrixRoomId) {
|
|
48
|
+
return { qualified: false, reason: "other_room" };
|
|
49
|
+
}
|
|
50
|
+
if (hasUnreadCompletedMessage !== true)
|
|
51
|
+
return { qualified: false, reason: "not_unread" };
|
|
52
|
+
if (!latestCompletedEventId || latestCompletedEventId !== renderedEventId) {
|
|
53
|
+
return { qualified: false, reason: "stale_or_unrendered_event" };
|
|
54
|
+
}
|
|
55
|
+
if (documentVisibilityState !== "visible") {
|
|
56
|
+
return { qualified: false, reason: "hidden_document" };
|
|
57
|
+
}
|
|
58
|
+
if (documentHasFocus !== true)
|
|
59
|
+
return { qualified: false, reason: "unfocused_document" };
|
|
60
|
+
if (elementConnected !== true || elementHidden === true) {
|
|
61
|
+
return { qualified: false, reason: "event_not_materially_rendered" };
|
|
62
|
+
}
|
|
63
|
+
if (!isCompletedMessageViewportVisible({ elementRect, viewportRect })) {
|
|
64
|
+
return { qualified: false, reason: "event_outside_viewport" };
|
|
65
|
+
}
|
|
66
|
+
return { qualified: true, reason: "visible_latest_successful_final" };
|
|
67
|
+
}
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
export type ManagedWebRtcStageStatus =
|
|
2
|
+
| "Configuration"
|
|
3
|
+
| "Browser offer"
|
|
4
|
+
| "ICE gathering"
|
|
5
|
+
| "Browser ticket"
|
|
6
|
+
| "Signaling"
|
|
7
|
+
| "Answer application"
|
|
8
|
+
| "Data channel";
|
|
9
|
+
|
|
1
10
|
export type InternalConnectivityStatus = {
|
|
2
11
|
matrix: "Connected" | "Reconnecting" | "Error" | "Offline";
|
|
3
12
|
activePath:
|
|
@@ -10,6 +19,8 @@ export type InternalConnectivityStatus = {
|
|
|
10
19
|
| "Direct"
|
|
11
20
|
| "None";
|
|
12
21
|
promotion: "Idle" | `Attempt ${number}/4` | "Quiesced" | "Not applicable";
|
|
22
|
+
stage?: ManagedWebRtcStageStatus;
|
|
23
|
+
lastIssue?: `${ManagedWebRtcStageStatus} timeout`;
|
|
13
24
|
};
|
|
14
25
|
|
|
15
26
|
export const INTERNAL_CONNECTIVITY_PROMOTION_ATTEMPT_LIMIT: 4;
|
|
@@ -21,6 +21,26 @@ const WEBRTC_SELECTED_PATH_STATUS = Object.freeze({
|
|
|
21
21
|
ipv6: "WebRTC (IPv6 Direct)",
|
|
22
22
|
});
|
|
23
23
|
|
|
24
|
+
const MANAGED_WEBRTC_STAGE_STATUS = Object.freeze({
|
|
25
|
+
configuration_fetch: "Configuration",
|
|
26
|
+
browser_offer: "Browser offer",
|
|
27
|
+
browser_ice_gathering: "ICE gathering",
|
|
28
|
+
browser_ticket: "Browser ticket",
|
|
29
|
+
signaling_exchange: "Signaling",
|
|
30
|
+
answer_application: "Answer application",
|
|
31
|
+
datachannel_open: "Data channel",
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const MANAGED_WEBRTC_STAGE_TIMEOUT_CODE = Object.freeze({
|
|
35
|
+
configuration_fetch: "managed_webrtc_configuration_fetch_timeout",
|
|
36
|
+
browser_offer: "managed_webrtc_browser_offer_timeout",
|
|
37
|
+
browser_ice_gathering: "managed_webrtc_browser_ice_gather_timeout",
|
|
38
|
+
browser_ticket: "managed_webrtc_browser_ticket_timeout",
|
|
39
|
+
signaling_exchange: "managed_webrtc_signaling_exchange_timeout",
|
|
40
|
+
answer_application: "managed_webrtc_answer_application_timeout",
|
|
41
|
+
datachannel_open: "managed_webrtc_datachannel_open_timeout",
|
|
42
|
+
});
|
|
43
|
+
|
|
24
44
|
export const INTERNAL_CONNECTIVITY_PROMOTION_ATTEMPT_LIMIT = 4;
|
|
25
45
|
|
|
26
46
|
function finiteNonNegativeInteger(value) {
|
|
@@ -62,9 +82,32 @@ export function deriveInternalConnectivityStatus({
|
|
|
62
82
|
: "Idle";
|
|
63
83
|
}
|
|
64
84
|
|
|
65
|
-
|
|
85
|
+
const activeStageKey = transportStatus.managed_webrtc_active_stage;
|
|
86
|
+
const activeStage = MANAGED_WEBRTC_STAGE_STATUS[activeStageKey];
|
|
87
|
+
const lastIssueStageKey = transportStatus.managed_webrtc_last_issue_stage;
|
|
88
|
+
const lastIssueStage = MANAGED_WEBRTC_STAGE_STATUS[lastIssueStageKey];
|
|
89
|
+
const lastIssue =
|
|
90
|
+
!activeStage &&
|
|
91
|
+
lastIssueStage &&
|
|
92
|
+
transportStatus.managed_webrtc_last_issue_code ===
|
|
93
|
+
MANAGED_WEBRTC_STAGE_TIMEOUT_CODE[lastIssueStageKey]
|
|
94
|
+
? `${lastIssueStage} timeout`
|
|
95
|
+
: null;
|
|
96
|
+
|
|
97
|
+
return Object.freeze({
|
|
98
|
+
matrix,
|
|
99
|
+
activePath,
|
|
100
|
+
promotion,
|
|
101
|
+
...(activeStage ? { stage: activeStage } : {}),
|
|
102
|
+
...(lastIssue ? { lastIssue } : {}),
|
|
103
|
+
});
|
|
66
104
|
}
|
|
67
105
|
|
|
68
106
|
export function internalConnectivityStatusText(status) {
|
|
69
|
-
|
|
107
|
+
const detail = status.stage
|
|
108
|
+
? ` · Stage: ${status.stage}`
|
|
109
|
+
: status.lastIssue
|
|
110
|
+
? ` · Last issue: ${status.lastIssue}`
|
|
111
|
+
: "";
|
|
112
|
+
return `Debug Network Connection Status · Matrix: ${status.matrix} · Active path: ${status.activePath} · WebRTC promotion: ${status.promotion}${detail}`;
|
|
70
113
|
}
|
|
@@ -25,6 +25,7 @@ export type ManagedHostTicketProvider = (input: {
|
|
|
25
25
|
configuration: Record<string, unknown>;
|
|
26
26
|
connectionAttemptId: string;
|
|
27
27
|
dtlsFingerprintSha256: string;
|
|
28
|
+
signal?: AbortSignal | null;
|
|
28
29
|
}) => Promise<{
|
|
29
30
|
credential: string;
|
|
30
31
|
client_authority_kind: "app_installation" | "host_owner_browser";
|
|
@@ -11,6 +11,7 @@ const REQUIRED_CHANNELS = Object.freeze(["control", "events", "bulk"]);
|
|
|
11
11
|
const SUPPORTED_TRANSPORT_PROTOCOL_VERSIONS = Object.freeze([1]);
|
|
12
12
|
const MANAGED_TRANSPORT_CONTRACT = "oysterun.managed_webrtc.v1";
|
|
13
13
|
const SIGNALING_TIMEOUT_MS = 20_000;
|
|
14
|
+
const MANAGED_WEBRTC_STAGE_TIMEOUT_MS = 30_000;
|
|
14
15
|
const MANAGED_ICE_CANDIDATE_SETTLE_MS = 100;
|
|
15
16
|
const BOOTSTRAP_SAFE_REQUEST_TIMEOUT_MS = 12_000;
|
|
16
17
|
const BOOTSTRAP_MATRIX_SYNC_TIMEOUT_MS = 45_000;
|
|
@@ -80,6 +81,15 @@ const MANAGED_WEBRTC_STAGE_FAILURE_CODES = Object.freeze({
|
|
|
80
81
|
datachannel_open: "managed_webrtc_datachannel_open_timeout",
|
|
81
82
|
logical_path_promotion: "managed_webrtc_logical_path_promotion_failed",
|
|
82
83
|
});
|
|
84
|
+
const MANAGED_WEBRTC_STAGE_TIMEOUT_CODES = Object.freeze({
|
|
85
|
+
configuration_fetch: "managed_webrtc_configuration_fetch_timeout",
|
|
86
|
+
browser_offer: "managed_webrtc_browser_offer_timeout",
|
|
87
|
+
browser_ice_gathering: "managed_webrtc_browser_ice_gather_timeout",
|
|
88
|
+
browser_ticket: "managed_webrtc_browser_ticket_timeout",
|
|
89
|
+
signaling_exchange: "managed_webrtc_signaling_exchange_timeout",
|
|
90
|
+
answer_application: "managed_webrtc_answer_application_timeout",
|
|
91
|
+
datachannel_open: "managed_webrtc_datachannel_open_timeout",
|
|
92
|
+
});
|
|
83
93
|
const PRESERVED_MANAGED_WEBRTC_ERROR_CODES = new Set([
|
|
84
94
|
"managed_webrtc_not_enabled",
|
|
85
95
|
"managed_webrtc_protocol_incompatible",
|
|
@@ -89,6 +99,7 @@ const PRESERVED_MANAGED_WEBRTC_ERROR_CODES = new Set([
|
|
|
89
99
|
"managed_webrtc_host_answer_timeout",
|
|
90
100
|
"managed_webrtc_host_ice_gather_timeout",
|
|
91
101
|
"managed_webrtc_host_datachannel_open_timeout",
|
|
102
|
+
...Object.values(MANAGED_WEBRTC_STAGE_TIMEOUT_CODES),
|
|
92
103
|
]);
|
|
93
104
|
const MANAGED_MATRIX_DIAGNOSTIC_EVENTS = new Set([
|
|
94
105
|
"matrix_sync_state_changed",
|
|
@@ -371,6 +382,10 @@ function browserConnectionStatusKey(status) {
|
|
|
371
382
|
status.promotion_attempt_count,
|
|
372
383
|
status.retry_quiesced,
|
|
373
384
|
status.reconnect_pending,
|
|
385
|
+
status.managed_webrtc_active_stage,
|
|
386
|
+
status.managed_webrtc_active_stage_started_at,
|
|
387
|
+
status.managed_webrtc_last_issue_stage,
|
|
388
|
+
status.managed_webrtc_last_issue_code,
|
|
374
389
|
status.managed_host_unavailable,
|
|
375
390
|
status.closed,
|
|
376
391
|
status.origin_kind,
|
|
@@ -393,9 +408,7 @@ export function readManagedWebRtcSelectedPath(hostStatus, connectionId) {
|
|
|
393
408
|
}
|
|
394
409
|
const matchingPeers = hostStatus.peers.filter(
|
|
395
410
|
(peer) =>
|
|
396
|
-
peer &&
|
|
397
|
-
typeof peer === "object" &&
|
|
398
|
-
peer.connection_id === connectionId
|
|
411
|
+
peer && typeof peer === "object" && peer.connection_id === connectionId
|
|
399
412
|
);
|
|
400
413
|
if (matchingPeers.length !== 1) return null;
|
|
401
414
|
return normalizeManagedWebRtcSelectedPath(matchingPeers[0].selected_path);
|
|
@@ -683,9 +696,7 @@ function createManagedIceCandidateReadiness(
|
|
|
683
696
|
} else if (!settled) {
|
|
684
697
|
timeoutTimer = setTimeout(
|
|
685
698
|
() =>
|
|
686
|
-
reject(
|
|
687
|
-
new Error("Browser ICE candidate gathering timed out")
|
|
688
|
-
),
|
|
699
|
+
reject(new Error("Browser ICE candidate gathering timed out")),
|
|
689
700
|
timeoutMs
|
|
690
701
|
);
|
|
691
702
|
}
|
|
@@ -1063,6 +1074,7 @@ export class ManagedHostBrowserConnection {
|
|
|
1063
1074
|
bootstrapSafeRequestTimeoutMs = BOOTSTRAP_SAFE_REQUEST_TIMEOUT_MS,
|
|
1064
1075
|
bootstrapMatrixSyncTimeoutMs = BOOTSTRAP_MATRIX_SYNC_TIMEOUT_MS,
|
|
1065
1076
|
presenceHealthRecoveryDelaysMs = PRESENCE_HEALTH_RECOVERY_DELAYS_MS,
|
|
1077
|
+
managedWebRtcStageTimeoutMs = MANAGED_WEBRTC_STAGE_TIMEOUT_MS,
|
|
1066
1078
|
} = {}) {
|
|
1067
1079
|
this.origin = origin.replace(/\/+$/, "");
|
|
1068
1080
|
this.fetchImpl = fetchImpl;
|
|
@@ -1076,6 +1088,14 @@ export class ManagedHostBrowserConnection {
|
|
|
1076
1088
|
) {
|
|
1077
1089
|
throw new Error("Managed Bootstrap request timeouts must be positive");
|
|
1078
1090
|
}
|
|
1091
|
+
if (
|
|
1092
|
+
!Number.isInteger(managedWebRtcStageTimeoutMs) ||
|
|
1093
|
+
managedWebRtcStageTimeoutMs <= 0
|
|
1094
|
+
) {
|
|
1095
|
+
throw new Error(
|
|
1096
|
+
"Managed WebRTC stage timeout must be a positive integer"
|
|
1097
|
+
);
|
|
1098
|
+
}
|
|
1079
1099
|
if (
|
|
1080
1100
|
!Array.isArray(presenceHealthRecoveryDelaysMs) ||
|
|
1081
1101
|
presenceHealthRecoveryDelaysMs.length === 0 ||
|
|
@@ -1096,6 +1116,7 @@ export class ManagedHostBrowserConnection {
|
|
|
1096
1116
|
this.presenceHealthRecoveryDelaysMs = Object.freeze([
|
|
1097
1117
|
...presenceHealthRecoveryDelaysMs,
|
|
1098
1118
|
]);
|
|
1119
|
+
this.managedWebRtcStageTimeoutMs = managedWebRtcStageTimeoutMs;
|
|
1099
1120
|
this.clientInstanceId = getClientInstanceId();
|
|
1100
1121
|
this.bootstrapTransport = new BootstrapFetchTransport({
|
|
1101
1122
|
origin: this.origin,
|
|
@@ -1144,6 +1165,8 @@ export class ManagedHostBrowserConnection {
|
|
|
1144
1165
|
this.selectedPathStatusReadConnectionId = null;
|
|
1145
1166
|
this.selectedPathStatusReadPromise = null;
|
|
1146
1167
|
this.lastRequestFailed = false;
|
|
1168
|
+
this.activeWebRtcStage = null;
|
|
1169
|
+
this.lastWebRtcStageIssue = null;
|
|
1147
1170
|
this.accounting = createTransportAccounting();
|
|
1148
1171
|
this.reconciliationOwner = new EpochBoundTransportReconciliationOwner({
|
|
1149
1172
|
reconcile: (request) => this.runRestartWebRtc(request),
|
|
@@ -1190,9 +1213,7 @@ export class ManagedHostBrowserConnection {
|
|
|
1190
1213
|
status.active_path_kind === "webrtc" &&
|
|
1191
1214
|
this.activeConnection?.connectionId
|
|
1192
1215
|
) {
|
|
1193
|
-
void this.refreshSelectedWebRtcPath(
|
|
1194
|
-
this.activeConnection.connectionId
|
|
1195
|
-
);
|
|
1216
|
+
void this.refreshSelectedWebRtcPath(this.activeConnection.connectionId);
|
|
1196
1217
|
}
|
|
1197
1218
|
return () => {
|
|
1198
1219
|
this.statusListeners.delete(listener);
|
|
@@ -1243,6 +1264,17 @@ export class ManagedHostBrowserConnection {
|
|
|
1243
1264
|
|
|
1244
1265
|
async runConnectionStage(stage, attempt, operation) {
|
|
1245
1266
|
const startedAt = Date.now();
|
|
1267
|
+
const timeoutCode = MANAGED_WEBRTC_STAGE_TIMEOUT_CODES[stage] || null;
|
|
1268
|
+
const abortController = timeoutCode ? new AbortController() : null;
|
|
1269
|
+
const activeStage = timeoutCode
|
|
1270
|
+
? {
|
|
1271
|
+
stage,
|
|
1272
|
+
startedAt,
|
|
1273
|
+
startedAtIso: new Date(startedAt).toISOString(),
|
|
1274
|
+
attempt,
|
|
1275
|
+
abortController,
|
|
1276
|
+
}
|
|
1277
|
+
: null;
|
|
1246
1278
|
const fields = {
|
|
1247
1279
|
connection_attempt_id: attempt.connectionAttemptId,
|
|
1248
1280
|
connection_generation: attempt.connectionGeneration,
|
|
@@ -1255,8 +1287,31 @@ export class ManagedHostBrowserConnection {
|
|
|
1255
1287
|
event_type: "managed_webrtc_stage_started",
|
|
1256
1288
|
...fields,
|
|
1257
1289
|
});
|
|
1290
|
+
if (activeStage) {
|
|
1291
|
+
this.activeWebRtcStage = activeStage;
|
|
1292
|
+
this.notifyStatusListeners();
|
|
1293
|
+
}
|
|
1294
|
+
let timeoutTimer = null;
|
|
1258
1295
|
try {
|
|
1259
|
-
const
|
|
1296
|
+
const operationPromise = Promise.resolve().then(() =>
|
|
1297
|
+
operation({ signal: abortController?.signal || null })
|
|
1298
|
+
);
|
|
1299
|
+
const result = timeoutCode
|
|
1300
|
+
? await Promise.race([
|
|
1301
|
+
operationPromise,
|
|
1302
|
+
new Promise((_, reject) => {
|
|
1303
|
+
timeoutTimer = setTimeout(() => {
|
|
1304
|
+
const timeoutError = managedWebRtcAttemptError(
|
|
1305
|
+
timeoutCode,
|
|
1306
|
+
`Managed WebRTC ${stage} stage timed out`
|
|
1307
|
+
);
|
|
1308
|
+
reject(timeoutError);
|
|
1309
|
+
abortController.abort(timeoutError);
|
|
1310
|
+
this.cancelTimedOutWebRtcAttempt(attempt, timeoutError);
|
|
1311
|
+
}, this.managedWebRtcStageTimeoutMs);
|
|
1312
|
+
}),
|
|
1313
|
+
])
|
|
1314
|
+
: await operationPromise;
|
|
1260
1315
|
this.emitDiagnostic({
|
|
1261
1316
|
event_type: "managed_webrtc_stage_completed",
|
|
1262
1317
|
...fields,
|
|
@@ -1265,6 +1320,13 @@ export class ManagedHostBrowserConnection {
|
|
|
1265
1320
|
return result;
|
|
1266
1321
|
} catch (error) {
|
|
1267
1322
|
const normalized = stageSpecificManagedWebRtcError(error, stage);
|
|
1323
|
+
if (normalized.code === timeoutCode) {
|
|
1324
|
+
this.lastWebRtcStageIssue = {
|
|
1325
|
+
stage,
|
|
1326
|
+
errorCode: normalized.code,
|
|
1327
|
+
at: new Date().toISOString(),
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1268
1330
|
this.emitDiagnostic({
|
|
1269
1331
|
event_type: "managed_webrtc_stage_failed",
|
|
1270
1332
|
...fields,
|
|
@@ -1272,10 +1334,31 @@ export class ManagedHostBrowserConnection {
|
|
|
1272
1334
|
stage_duration_ms: Math.max(0, Date.now() - startedAt),
|
|
1273
1335
|
});
|
|
1274
1336
|
throw normalized;
|
|
1337
|
+
} finally {
|
|
1338
|
+
if (timeoutTimer !== null) clearTimeout(timeoutTimer);
|
|
1339
|
+
if (this.activeWebRtcStage === activeStage) {
|
|
1340
|
+
this.activeWebRtcStage = null;
|
|
1341
|
+
this.notifyStatusListeners();
|
|
1342
|
+
}
|
|
1275
1343
|
}
|
|
1276
1344
|
}
|
|
1277
1345
|
|
|
1278
|
-
|
|
1346
|
+
cancelTimedOutWebRtcAttempt(attempt, timeoutError) {
|
|
1347
|
+
if (this.connectionGeneration === attempt.connectionGeneration) {
|
|
1348
|
+
this.connectionGeneration += 1;
|
|
1349
|
+
}
|
|
1350
|
+
const peer = attempt.peer || null;
|
|
1351
|
+
if (peer && this.pendingIceCandidateReadiness?.peer === peer) {
|
|
1352
|
+
this.pendingIceCandidateReadiness.cancel(timeoutError);
|
|
1353
|
+
this.pendingIceCandidateReadiness = null;
|
|
1354
|
+
}
|
|
1355
|
+
if (peer && this.connectingPeer === peer) {
|
|
1356
|
+
this.connectingPeer = null;
|
|
1357
|
+
peer.close();
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
async nativeJson(path, body, { signal = null } = {}) {
|
|
1279
1362
|
const token = getStoredDashboardToken();
|
|
1280
1363
|
if (!token) throw new Error("Dashboard authentication is unavailable");
|
|
1281
1364
|
const response = await this.fetchImpl(`${this.origin}${path}`, {
|
|
@@ -1289,6 +1372,7 @@ export class ManagedHostBrowserConnection {
|
|
|
1289
1372
|
...body,
|
|
1290
1373
|
}),
|
|
1291
1374
|
credentials: "same-origin",
|
|
1375
|
+
...(signal ? { signal } : {}),
|
|
1292
1376
|
});
|
|
1293
1377
|
const data = await response.json().catch(() => ({}));
|
|
1294
1378
|
if (!response.ok) {
|
|
@@ -1492,7 +1576,14 @@ export class ManagedHostBrowserConnection {
|
|
|
1492
1576
|
const configurationPayload = await this.runConnectionStage(
|
|
1493
1577
|
"configuration_fetch",
|
|
1494
1578
|
attempt,
|
|
1495
|
-
(
|
|
1579
|
+
({ signal }) =>
|
|
1580
|
+
this.nativeJson(
|
|
1581
|
+
SIGNALING_PATH_PREFIX + "configuration",
|
|
1582
|
+
{},
|
|
1583
|
+
{
|
|
1584
|
+
signal,
|
|
1585
|
+
}
|
|
1586
|
+
)
|
|
1496
1587
|
);
|
|
1497
1588
|
const configuration = await this.runConnectionStage(
|
|
1498
1589
|
"configuration_validation",
|
|
@@ -1515,6 +1606,7 @@ export class ManagedHostBrowserConnection {
|
|
|
1515
1606
|
iceServers: configuration.ice_servers,
|
|
1516
1607
|
iceTransportPolicy,
|
|
1517
1608
|
});
|
|
1609
|
+
attempt.peer = peer;
|
|
1518
1610
|
this.connectingPeer = peer;
|
|
1519
1611
|
let iceCandidateReadiness = null;
|
|
1520
1612
|
try {
|
|
@@ -1551,12 +1643,13 @@ export class ManagedHostBrowserConnection {
|
|
|
1551
1643
|
const ticket = await this.runConnectionStage(
|
|
1552
1644
|
"browser_ticket",
|
|
1553
1645
|
attempt,
|
|
1554
|
-
async () =>
|
|
1646
|
+
async ({ signal }) =>
|
|
1555
1647
|
this.ticketProvider
|
|
1556
1648
|
? await this.ticketProvider({
|
|
1557
1649
|
configuration,
|
|
1558
1650
|
connectionAttemptId,
|
|
1559
1651
|
dtlsFingerprintSha256,
|
|
1652
|
+
signal,
|
|
1560
1653
|
})
|
|
1561
1654
|
: {
|
|
1562
1655
|
...(await this.nativeJson(
|
|
@@ -1564,7 +1657,8 @@ export class ManagedHostBrowserConnection {
|
|
|
1564
1657
|
{
|
|
1565
1658
|
connection_attempt_id: connectionAttemptId,
|
|
1566
1659
|
dtls_fingerprint_sha256: dtlsFingerprintSha256,
|
|
1567
|
-
}
|
|
1660
|
+
},
|
|
1661
|
+
{ signal }
|
|
1568
1662
|
)),
|
|
1569
1663
|
client_authority_kind: "host_owner_browser",
|
|
1570
1664
|
}
|
|
@@ -1584,18 +1678,22 @@ export class ManagedHostBrowserConnection {
|
|
|
1584
1678
|
const answer = await this.runConnectionStage(
|
|
1585
1679
|
"signaling_exchange",
|
|
1586
1680
|
attempt,
|
|
1587
|
-
() =>
|
|
1588
|
-
this.nativeJson(
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1681
|
+
({ signal }) =>
|
|
1682
|
+
this.nativeJson(
|
|
1683
|
+
SIGNALING_PATH_PREFIX + "connect",
|
|
1684
|
+
{
|
|
1685
|
+
ticket: ticket.credential,
|
|
1686
|
+
client_authority_kind: ticket.client_authority_kind,
|
|
1687
|
+
connection_attempt_id: connectionAttemptId,
|
|
1688
|
+
dtls_fingerprint_sha256: dtlsFingerprintSha256,
|
|
1689
|
+
offer: {
|
|
1690
|
+
type: localDescription.type,
|
|
1691
|
+
sdp: localDescription.sdp,
|
|
1692
|
+
},
|
|
1693
|
+
candidates: iceCandidateReadiness.candidates,
|
|
1596
1694
|
},
|
|
1597
|
-
|
|
1598
|
-
|
|
1695
|
+
{ signal }
|
|
1696
|
+
)
|
|
1599
1697
|
);
|
|
1600
1698
|
assertCurrentAttempt(peer);
|
|
1601
1699
|
await this.runConnectionStage("answer_application", attempt, async () => {
|
|
@@ -1654,6 +1752,7 @@ export class ManagedHostBrowserConnection {
|
|
|
1654
1752
|
);
|
|
1655
1753
|
assertCurrentAttempt(peer);
|
|
1656
1754
|
this.lastRequestFailed = false;
|
|
1755
|
+
this.lastWebRtcStageIssue = null;
|
|
1657
1756
|
this.webrtcSelectedPath = null;
|
|
1658
1757
|
this.webrtcSelectedPathConnectionId = answer.connection_id;
|
|
1659
1758
|
this.notifyStatusListeners();
|
|
@@ -2495,6 +2594,12 @@ export class ManagedHostBrowserConnection {
|
|
|
2495
2594
|
promotion_attempt_count: this.promotionAttemptCount,
|
|
2496
2595
|
retry_quiesced: this.retryQuiesced,
|
|
2497
2596
|
reconnect_pending: Boolean(this.reconnectTimer || this.startPromise),
|
|
2597
|
+
managed_webrtc_active_stage: this.activeWebRtcStage?.stage || null,
|
|
2598
|
+
managed_webrtc_active_stage_started_at:
|
|
2599
|
+
this.activeWebRtcStage?.startedAtIso || null,
|
|
2600
|
+
managed_webrtc_last_issue_stage: this.lastWebRtcStageIssue?.stage || null,
|
|
2601
|
+
managed_webrtc_last_issue_code:
|
|
2602
|
+
this.lastWebRtcStageIssue?.errorCode || null,
|
|
2498
2603
|
reconciliation_epoch: this.reconciliationOwner.status().epoch,
|
|
2499
2604
|
reconciliation_pending:
|
|
2500
2605
|
this.reconciliationOwner.status().in_flight ||
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function deriveSessionSwitcherViewportBounds(input: {
|
|
2
|
+
viewport?: { offsetTop?: number; height?: number } | null;
|
|
3
|
+
innerHeight: number;
|
|
4
|
+
headerHeight?: number;
|
|
5
|
+
bottomObstructionHeight?: number;
|
|
6
|
+
}): { top: number; height: number; bottomInset: number };
|
|
7
|
+
|
|
8
|
+
export function hasOtherUnreadSession(input: {
|
|
9
|
+
previews?: Array<{
|
|
10
|
+
session_id?: string;
|
|
11
|
+
has_unread_completed_message?: boolean;
|
|
12
|
+
}>;
|
|
13
|
+
currentSessionId: string;
|
|
14
|
+
}): boolean;
|
|
15
|
+
|
|
16
|
+
export function assertSessionSwitcherProjection<T>(value: T): T;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export function deriveSessionSwitcherViewportBounds({
|
|
2
|
+
viewport,
|
|
3
|
+
innerHeight,
|
|
4
|
+
headerHeight = 56,
|
|
5
|
+
bottomObstructionHeight = 0,
|
|
6
|
+
}) {
|
|
7
|
+
const visibleTop = Number.isFinite(viewport?.offsetTop)
|
|
8
|
+
? Math.max(0, viewport.offsetTop)
|
|
9
|
+
: 0;
|
|
10
|
+
const visibleHeight = Number.isFinite(viewport?.height)
|
|
11
|
+
? Math.max(0, viewport.height)
|
|
12
|
+
: Math.max(0, Number(innerHeight) || 0);
|
|
13
|
+
const top = Math.max(headerHeight, visibleTop);
|
|
14
|
+
return {
|
|
15
|
+
top,
|
|
16
|
+
height: Math.max(0, visibleTop + visibleHeight - top),
|
|
17
|
+
bottomInset: Math.max(0, Number(bottomObstructionHeight) || 0),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function hasOtherUnreadSession({ previews, currentSessionId }) {
|
|
22
|
+
return (Array.isArray(previews) ? previews : []).some(
|
|
23
|
+
(preview) =>
|
|
24
|
+
preview?.session_id !== currentSessionId &&
|
|
25
|
+
preview?.has_unread_completed_message === true
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function assertSessionSwitcherProjection(value) {
|
|
30
|
+
const rows = Array.isArray(value?.session_rows) ? value.session_rows : [];
|
|
31
|
+
const rowIds = rows.map((row) => row?.session_id);
|
|
32
|
+
const nullableString = (entry) => typeof entry === "string" || entry === null;
|
|
33
|
+
const validRows = rows.every(
|
|
34
|
+
(row) =>
|
|
35
|
+
row &&
|
|
36
|
+
row.kind === "running" &&
|
|
37
|
+
typeof row.session_id === "string" &&
|
|
38
|
+
row.session_id.trim() === row.session_id &&
|
|
39
|
+
row.session_id.length > 0 &&
|
|
40
|
+
typeof row.agent_id === "string" &&
|
|
41
|
+
row.agent_id.trim() === row.agent_id &&
|
|
42
|
+
row.agent_id.length > 0 &&
|
|
43
|
+
typeof row.session_name === "string" &&
|
|
44
|
+
row.session_name.trim().length > 0 &&
|
|
45
|
+
nullableString(row.agent_folder) &&
|
|
46
|
+
nullableString(row.runtime) &&
|
|
47
|
+
nullableString(row.model) &&
|
|
48
|
+
nullableString(row.started_at) &&
|
|
49
|
+
nullableString(row.last_active_at) &&
|
|
50
|
+
nullableString(row.matrix_room_id) &&
|
|
51
|
+
nullableString(row.resume_unavailable_reason) &&
|
|
52
|
+
typeof row.running === "boolean" &&
|
|
53
|
+
row.running === true &&
|
|
54
|
+
typeof row.resume_supported === "boolean" &&
|
|
55
|
+
row.resume_supported === false
|
|
56
|
+
);
|
|
57
|
+
if (
|
|
58
|
+
value?.status !== "session_list_previews" ||
|
|
59
|
+
value.session_switcher_projection_contract !==
|
|
60
|
+
"routec.p217_live_session_switcher_projection.v1" ||
|
|
61
|
+
value.session_switcher_projection_batched !== true ||
|
|
62
|
+
value.session_switcher_per_row_http_requests !== 0 ||
|
|
63
|
+
value.session_switcher_matrix_query_count !== 0 ||
|
|
64
|
+
!Number.isSafeInteger(value.session_count) ||
|
|
65
|
+
value.session_count !== rows.length ||
|
|
66
|
+
new Set(rowIds).size !== rows.length ||
|
|
67
|
+
!validRows
|
|
68
|
+
) {
|
|
69
|
+
throw new Error("Session switcher projection is unavailable.");
|
|
70
|
+
}
|
|
71
|
+
return value;
|
|
72
|
+
}
|