@oar-sh/oar 0.9.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/.github/extensions/web-relay/README.md +190 -0
- package/.github/extensions/web-relay/extension.mjs +972 -0
- package/.github/extensions/web-relay/model-api/model-switching.mjs +338 -0
- package/.github/extensions/web-relay/polling/heartbeat.mjs +55 -0
- package/.github/extensions/web-relay/polling/polling-loop.mjs +1105 -0
- package/.github/extensions/web-relay/runtime/api-client.mjs +34 -0
- package/.github/extensions/web-relay/runtime/banner-state.mjs +126 -0
- package/.github/extensions/web-relay/runtime/config-loader.mjs +104 -0
- package/.github/extensions/web-relay/runtime/debug-log.mjs +27 -0
- package/.github/extensions/web-relay/runtime/send-and-wait-errors.mjs +170 -0
- package/.github/extensions/web-relay/runtime/session-binding.mjs +78 -0
- package/.github/extensions/web-relay/runtime/session-io.mjs +349 -0
- package/.github/extensions/web-relay/runtime/session-join-retry.mjs +44 -0
- package/.github/extensions/web-relay/runtime/session-registry.mjs +29 -0
- package/.github/extensions/web-relay/runtime/session-runtime-manager.mjs +156 -0
- package/.github/extensions/web-relay/runtime/session-sync-bridge.mjs +74 -0
- package/.github/extensions/web-relay/runtime/worker-websocket-link.mjs +325 -0
- package/.github/extensions/web-relay/runtime/workspace-root.mjs +55 -0
- package/.github/extensions/web-relay/server-lifecycle/managed-server.mjs +386 -0
- package/.github/extensions/web-relay/skills/prompt-context.mjs +160 -0
- package/.github/extensions/web-relay/skills/question-bridge.mjs +141 -0
- package/.github/extensions/web-relay/skills/question-routing-hooks.mjs +463 -0
- package/.github/extensions/web-relay/skills/reasoning-stream.mjs +285 -0
- package/.github/extensions/web-relay/skills/subagent-lifecycle.mjs +345 -0
- package/.github/extensions/web-relay/skills/tool-activity.mjs +243 -0
- package/.github/extensions/web-relay/utils/process-utils.mjs +25 -0
- package/.github/extensions/web-relay/utils/token-generation.mjs +58 -0
- package/CHANGELOG.md +25 -0
- package/LICENSE +21 -0
- package/README.md +717 -0
- package/bin/bat/oar.bat +46 -0
- package/bin/oar.js +665 -0
- package/package.json +68 -0
- package/server/README.md +1379 -0
- package/server/claude-worker/claude-attachments.mjs +134 -0
- package/server/claude-worker/claude-preview-tool.mjs +57 -0
- package/server/claude-worker/claude-sdk-adapter.mjs +420 -0
- package/server/claude-worker/claude-session-process.mjs +2391 -0
- package/server/claude-worker/claude-session-test-harness.mjs +236 -0
- package/server/claude-worker/claude-session-worker.mjs +186 -0
- package/server/claude-worker/claude-transcript-relocator.mjs +189 -0
- package/server/claude-worker/claude-turn-publisher.mjs +291 -0
- package/server/claude-worker/fixtures/workflow-run-record.json +1 -0
- package/server/claude-worker/sdk-message-normalizer.mjs +629 -0
- package/server/claude-worker/workflow-progress-digest.mjs +269 -0
- package/server/copilot-sdk-runtime.mjs +126 -0
- package/server/copilot-worker/copilot-attachments.mjs +97 -0
- package/server/copilot-worker/copilot-byok-provider.mjs +121 -0
- package/server/copilot-worker/copilot-continuation-signals.mjs +349 -0
- package/server/copilot-worker/copilot-plan-board.mjs +110 -0
- package/server/copilot-worker/copilot-prompt-context.mjs +86 -0
- package/server/copilot-worker/copilot-question-bridge.mjs +216 -0
- package/server/copilot-worker/copilot-sdk-adapter.mjs +494 -0
- package/server/copilot-worker/copilot-sdk-event-normalizer.mjs +800 -0
- package/server/copilot-worker/copilot-sdk-session-process.mjs +1729 -0
- package/server/copilot-worker/copilot-sdk-session-worker.mjs +167 -0
- package/server/copilot-worker/copilot-sdk-test-harness.mjs +329 -0
- package/server/copilot-worker/fixtures/abort-turn.json +588 -0
- package/server/copilot-worker/fixtures/ask-user-turn.json +1154 -0
- package/server/copilot-worker/fixtures/background-timer-continuation.json +445 -0
- package/server/copilot-worker/fixtures/background-timer-turn.json +497 -0
- package/server/copilot-worker/fixtures/happy-turn.json +566 -0
- package/server/copilot-worker/fixtures/quota-turn.json +647 -0
- package/server/copilot-worker/fixtures/reasoning-turn.json +625 -0
- package/server/copilot-worker/fixtures/resume-turn.json +700 -0
- package/server/copilot-worker/fixtures/subagent-turn.json +1378 -0
- package/server/copilot-worker/fixtures/tool-permission-turn.json +1293 -0
- package/server/cursor-worker/cursor-ask-user-tool.mjs +83 -0
- package/server/cursor-worker/cursor-attachments.mjs +88 -0
- package/server/cursor-worker/cursor-context-usage.mjs +85 -0
- package/server/cursor-worker/cursor-preview-tool.mjs +44 -0
- package/server/cursor-worker/cursor-sdk-adapter.mjs +554 -0
- package/server/cursor-worker/cursor-session-worker.mjs +145 -0
- package/server/cursor-worker/cursor-subagent-roster.mjs +81 -0
- package/server/cursor-worker/cursor-turn-runner.mjs +717 -0
- package/server/cursor-worker/sdk-message-normalizer.mjs +485 -0
- package/server/db-schema.mjs +932 -0
- package/server/features.mjs +106 -0
- package/server/grok-worker/acp-client.mjs +434 -0
- package/server/grok-worker/acp-host-services.mjs +277 -0
- package/server/grok-worker/grok-context-usage.mjs +77 -0
- package/server/grok-worker/grok-sdk-adapter.mjs +334 -0
- package/server/grok-worker/grok-session-worker.mjs +145 -0
- package/server/grok-worker/grok-turn-runner.mjs +523 -0
- package/server/grok-worker/sdk-message-normalizer.mjs +311 -0
- package/server/migrations/0001-add-structured-answer.mjs +80 -0
- package/server/migrations/0002-recent-workspace-roots-path-key.mjs +121 -0
- package/server/migrations/0003-push-subscriptions.mjs +87 -0
- package/server/public/app/action-confirmations.js +267 -0
- package/server/public/app/activity-replay-state.mjs +149 -0
- package/server/public/app/api-client.js +1082 -0
- package/server/public/app/ask-user-view.js +545 -0
- package/server/public/app/attachments-view.js +2112 -0
- package/server/public/app/auto-compact-window-options.mjs +59 -0
- package/server/public/app/background-tasks-view.mjs +494 -0
- package/server/public/app/bootstrap.js +4347 -0
- package/server/public/app/claude-auth-ui.js +394 -0
- package/server/public/app/claude-thinking-options.mjs +38 -0
- package/server/public/app/cli-install-ui.js +455 -0
- package/server/public/app/code-copy.mjs +85 -0
- package/server/public/app/composer-attachment-cache.mjs +151 -0
- package/server/public/app/composer-control-state.mjs +107 -0
- package/server/public/app/composer-paste.mjs +132 -0
- package/server/public/app/context-tier-options.mjs +68 -0
- package/server/public/app/context-usage-view.mjs +338 -0
- package/server/public/app/conversation-draft-timestamp-utils.mjs +11 -0
- package/server/public/app/conversation-preferences.mjs +91 -0
- package/server/public/app/conversation-provider-indicator.mjs +88 -0
- package/server/public/app/conversation-view.js +2898 -0
- package/server/public/app/cwd-menu-placement.mjs +85 -0
- package/server/public/app/cwd-picker.js +605 -0
- package/server/public/app/emoji-view.js +101 -0
- package/server/public/app/external-link-policy.mjs +80 -0
- package/server/public/app/file-preview-navigation.mjs +51 -0
- package/server/public/app/font-scaling.js +262 -0
- package/server/public/app/git-changes-view.js +339 -0
- package/server/public/app/git-diff-model.mjs +109 -0
- package/server/public/app/grok-auth-ui.js +387 -0
- package/server/public/app/image-downscale.mjs +174 -0
- package/server/public/app/in-flight-snapshot.mjs +40 -0
- package/server/public/app/infinite-loader.js +164 -0
- package/server/public/app/journal-view.js +1141 -0
- package/server/public/app/known-cwd-options.mjs +41 -0
- package/server/public/app/launch-cwd-path.mjs +23 -0
- package/server/public/app/live-message-dedupe.mjs +57 -0
- package/server/public/app/message-search-view.js +242 -0
- package/server/public/app/model-selector-options.mjs +31 -0
- package/server/public/app/model-vision-support.mjs +56 -0
- package/server/public/app/new-conversation-model-choice.mjs +67 -0
- package/server/public/app/plan-usage-view.mjs +307 -0
- package/server/public/app/preview-cards.mjs +273 -0
- package/server/public/app/preview-command.mjs +103 -0
- package/server/public/app/push-settings.js +370 -0
- package/server/public/app/pwa-install.js +456 -0
- package/server/public/app/question-schema-view.mjs +71 -0
- package/server/public/app/reasoning-effort-labels.mjs +42 -0
- package/server/public/app/relay-board-view.js +117 -0
- package/server/public/app/relay-dot-state.mjs +69 -0
- package/server/public/app/relay-error-ctas.mjs +52 -0
- package/server/public/app/relay-prompt-sanitizer.mjs +114 -0
- package/server/public/app/relay-thoughts.mjs +98 -0
- package/server/public/app/relay-watchdog-policy.mjs +29 -0
- package/server/public/app/repo-browser-preferences.mjs +79 -0
- package/server/public/app/repo-browser-tree-state.mjs +97 -0
- package/server/public/app/router.js +657 -0
- package/server/public/app/selection-guard.mjs +93 -0
- package/server/public/app/settings-modal.js +1484 -0
- package/server/public/app/settings-tabs.js +129 -0
- package/server/public/app/slash-autocomplete.mjs +188 -0
- package/server/public/app/slash-commands.mjs +183 -0
- package/server/public/app/socket-handlers.js +703 -0
- package/server/public/app/status-store.mjs +242 -0
- package/server/public/app/status-view.mjs +315 -0
- package/server/public/app/store.js +1454 -0
- package/server/public/app/stream-state.mjs +73 -0
- package/server/public/app/streaming-dom-patch.mjs +23 -0
- package/server/public/app/sync-outbox.mjs +208 -0
- package/server/public/app/tap-actions.js +62 -0
- package/server/public/app/thread-order.mjs +54 -0
- package/server/public/app/tmux-inspector-view.js +422 -0
- package/server/public/app/transcript-separators.mjs +282 -0
- package/server/public/app-icon-192.png +0 -0
- package/server/public/app-icon-512.png +0 -0
- package/server/public/app-icon.svg +4 -0
- package/server/public/favicon.ico +0 -0
- package/server/public/index.html +5743 -0
- package/server/public/manifest.webmanifest +32 -0
- package/server/public/sw.js +307 -0
- package/server/relay-exit-codes.mjs +4 -0
- package/server/relay-self-restart.mjs +170 -0
- package/server/relay-tools.md +27 -0
- package/server/relay.mjs +1026 -0
- package/server/repositories/image-conversation-repository.mjs +317 -0
- package/server/repositories/message-repository.mjs +434 -0
- package/server/repositories/question-repository.mjs +161 -0
- package/server/repositories/session-repository.mjs +320 -0
- package/server/repositories/share-visibility-statements.mjs +24 -0
- package/server/routes/ask-user-routes.mjs +292 -0
- package/server/routes/cache-routes.mjs +44 -0
- package/server/routes/git-routes.mjs +92 -0
- package/server/routes/messages-routes-test-harness.mjs +111 -0
- package/server/routes/messages-routes.mjs +7107 -0
- package/server/routes/preview-routes.mjs +108 -0
- package/server/routes/push-routes.mjs +59 -0
- package/server/routes/relay-board-routes.mjs +268 -0
- package/server/routes/sessions-routes.mjs +6150 -0
- package/server/server-runtime.mjs +7463 -0
- package/server/server.js +24 -0
- package/server/services/ask-user-routing-service.mjs +117 -0
- package/server/services/cache-rebuild-service.mjs +236 -0
- package/server/services/claude-auth-service.mjs +578 -0
- package/server/services/claude-context-usage.mjs +240 -0
- package/server/services/claude-session-root-service.mjs +228 -0
- package/server/services/cli-install-service.mjs +1152 -0
- package/server/services/cli-process-runner.mjs +181 -0
- package/server/services/cloudflared-tunnel-service.mjs +358 -0
- package/server/services/context-snapshot-service.mjs +499 -0
- package/server/services/context-usage-view.mjs +160 -0
- package/server/services/conversation-preferences-service.mjs +208 -0
- package/server/services/cursor-dashboard-usage.mjs +217 -0
- package/server/services/cursor-session-root-service.mjs +62 -0
- package/server/services/delete-archive-service.mjs +154 -0
- package/server/services/drives-path-helpers.mjs +66 -0
- package/server/services/fixtures/claude-auth-stub.sh +62 -0
- package/server/services/fixtures/cli-install-stub.sh +75 -0
- package/server/services/fixtures/grok-stub.sh +86 -0
- package/server/services/generated-image-cleanup-service.mjs +114 -0
- package/server/services/git-changes-service.mjs +195 -0
- package/server/services/github-billing-usage.mjs +133 -0
- package/server/services/grok-auth-service.mjs +593 -0
- package/server/services/grok-billing-usage.mjs +120 -0
- package/server/services/image-context-service.mjs +106 -0
- package/server/services/image-operation-service.mjs +135 -0
- package/server/services/keyed-mutex-service.mjs +82 -0
- package/server/services/mime-sniffer.mjs +118 -0
- package/server/services/oar-cli-helpers.mjs +109 -0
- package/server/services/oar-state-migration-service.mjs +243 -0
- package/server/services/plan-usage-claude.mjs +387 -0
- package/server/services/plan-usage-contract.mjs +271 -0
- package/server/services/plan-usage-copilot.mjs +421 -0
- package/server/services/plan-usage-cursor.mjs +566 -0
- package/server/services/plan-usage-grok.mjs +436 -0
- package/server/services/plan-usage-service.mjs +487 -0
- package/server/services/preview-health-probe.mjs +82 -0
- package/server/services/preview-proxy-server.mjs +498 -0
- package/server/services/preview-registry-service.mjs +457 -0
- package/server/services/preview-static-handler.mjs +237 -0
- package/server/services/provider-model-selection.mjs +91 -0
- package/server/services/provider-reasoning-effort.mjs +94 -0
- package/server/services/push-active-device-service.mjs +57 -0
- package/server/services/push-dispatch-service.mjs +459 -0
- package/server/services/rate-limit-service.mjs +63 -0
- package/server/services/relay-bridge-owner-service.mjs +84 -0
- package/server/services/relay-cli-launcher-service.mjs +209 -0
- package/server/services/relay-prompt-sanitizer.mjs +137 -0
- package/server/services/relay-queue-gate-service.mjs +38 -0
- package/server/services/relay-restart-orchestrator-service.mjs +935 -0
- package/server/services/relay-singleton-guard.mjs +124 -0
- package/server/services/safe-served-content.mjs +57 -0
- package/server/services/sdk-session-import-service.mjs +271 -0
- package/server/services/sdk-session-sync-service.mjs +207 -0
- package/server/services/session-history-refresh-service.mjs +497 -0
- package/server/services/session-transcript-service.mjs +419 -0
- package/server/services/session-worker-launch-service.mjs +786 -0
- package/server/services/session-worker-process-service.mjs +319 -0
- package/server/services/session-worker-registry-service.mjs +194 -0
- package/server/services/session-worker-stop-service.mjs +141 -0
- package/server/services/session-worker-supervisor-service.mjs +898 -0
- package/server/services/session-worker-websocket-service.mjs +483 -0
- package/server/services/ssh-tunnel-manager-service.mjs +423 -0
- package/server/services/status-event-service.mjs +158 -0
- package/server/services/tmux-inspector-access-policy.mjs +77 -0
- package/server/services/tmux-inspector-socket-service.mjs +191 -0
- package/server/services/tmux-inspector-stream-service.mjs +552 -0
- package/server/services/tunnel-worker-path-guard.mjs +107 -0
- package/server/services/upload-sweep.mjs +60 -0
- package/server/services/usage-snapshot-helpers.mjs +209 -0
- package/server/services/windows-autostart-service.mjs +115 -0
- package/server/services/workspace-root-defaults-service.mjs +34 -0
- package/server/services/workspace-root-path-policy.mjs +196 -0
- package/server/services/workspace-root-relaunch-service.mjs +132 -0
- package/server/services/workspace-symlink-guard.mjs +54 -0
- package/server/tools/capture-proxy.mjs +173 -0
- package/server/tty-console-bootstrap.mjs +75 -0
- package/server/uploads/README.md +36 -0
- package/server/uploads/image_input.md +540 -0
- package/server/windows-terminal-launcher.mjs +29 -0
- package/server/workspace-root.mjs +87 -0
- package/shared/ask-user-bridge.mjs +131 -0
- package/shared/auto-compact-window.mjs +116 -0
- package/shared/background-task-timeout.mjs +52 -0
- package/shared/claude-thinking.mjs +101 -0
- package/shared/context-window-fallbacks.mjs +148 -0
- package/shared/control-poller.mjs +115 -0
- package/shared/empty-turn-completion.mjs +20 -0
- package/shared/model-catalog-freshness.mjs +11 -0
- package/shared/model-descriptors.mjs +118 -0
- package/shared/model-id.mjs +147 -0
- package/shared/model-refresh.mjs +24 -0
- package/shared/openai-reasoning.mjs +13 -0
- package/shared/plan-lines.mjs +13 -0
- package/shared/preview-instructions.mjs +77 -0
- package/shared/preview-tool-core.mjs +191 -0
- package/shared/provider-routing.mjs +29 -0
- package/shared/question-schema.mjs +315 -0
- package/shared/question-timeout.mjs +18 -0
- package/shared/stream-emit-gating.mjs +28 -0
- package/shared/subagent-run-id.mjs +17 -0
- package/shared/thought-cap.mjs +13 -0
- package/shared/tool-activity.mjs +98 -0
- package/shared/turn-ceiling.mjs +53 -0
- package/shared/worker-bootstrap.mjs +44 -0
- package/shared/worker-crash-guard.mjs +60 -0
|
@@ -0,0 +1,972 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* web-relay — Copilot CLI Extension
|
|
3
|
+
*
|
|
4
|
+
* Bridges the web server (localhost:3333) to this CLI session.
|
|
5
|
+
* Polls for queued messages from the browser and submits them to
|
|
6
|
+
* the Copilot agent. Captures responses, forwards clarification
|
|
7
|
+
* questions back to the browser, and posts replies back.
|
|
8
|
+
*/
|
|
9
|
+
import { joinSession } from "@github/copilot-sdk/extension";
|
|
10
|
+
import { spawn } from "child_process";
|
|
11
|
+
import fs from "fs";
|
|
12
|
+
import path from "path";
|
|
13
|
+
import { generateHighEntropyToken } from "./utils/token-generation.mjs";
|
|
14
|
+
import { delay, killProcessTree, sleep } from "./utils/process-utils.mjs";
|
|
15
|
+
import {
|
|
16
|
+
resolveRelayPaths,
|
|
17
|
+
loadTokenFromConfig,
|
|
18
|
+
loadRelayInstructionsFromFile,
|
|
19
|
+
resolveRelayServerUrl,
|
|
20
|
+
} from "./runtime/config-loader.mjs";
|
|
21
|
+
import { createDebugLogger } from "./runtime/debug-log.mjs";
|
|
22
|
+
import { createApiClient } from "./runtime/api-client.mjs";
|
|
23
|
+
import {
|
|
24
|
+
formatToolActivity,
|
|
25
|
+
formatToolResultActivity,
|
|
26
|
+
isAskUserTool,
|
|
27
|
+
normalizeActivityText,
|
|
28
|
+
extractQuestionChoices,
|
|
29
|
+
extractQuestionPrompt,
|
|
30
|
+
serializeRequest,
|
|
31
|
+
} from "./skills/tool-activity.mjs";
|
|
32
|
+
import { createPreviewInstructionsProvider, createRelayPromptBuilder } from "./skills/prompt-context.mjs";
|
|
33
|
+
import { createQuestionBridge } from "./skills/question-bridge.mjs";
|
|
34
|
+
import { createQuestionRoutingHooks } from "./skills/question-routing-hooks.mjs";
|
|
35
|
+
import { createReasoningStreamHandlers } from "./skills/reasoning-stream.mjs";
|
|
36
|
+
import { createSubagentLifecycleHandlers } from "./skills/subagent-lifecycle.mjs";
|
|
37
|
+
import { createHeartbeatController } from "./polling/heartbeat.mjs";
|
|
38
|
+
import { createPollingLoop } from "./polling/polling-loop.mjs";
|
|
39
|
+
import { createManagedServerLifecycle } from "./server-lifecycle/managed-server.mjs";
|
|
40
|
+
import { createModelSwitchingService } from "./model-api/model-switching.mjs";
|
|
41
|
+
import { createSessionRuntimeManager } from "./runtime/session-runtime-manager.mjs";
|
|
42
|
+
import { createSessionIoHelpers } from "./runtime/session-io.mjs";
|
|
43
|
+
import { createWorkerWebSocketLink } from "./runtime/worker-websocket-link.mjs";
|
|
44
|
+
import { resolveSessionBinding } from "./runtime/session-binding.mjs";
|
|
45
|
+
import { createBannerStateStore } from "./runtime/banner-state.mjs";
|
|
46
|
+
import { clearSession, getActiveSession, registerSession } from "./runtime/session-registry.mjs";
|
|
47
|
+
import { syncSessionToServer, syncWorkspaceRootToServer } from "./runtime/session-sync-bridge.mjs";
|
|
48
|
+
import { joinSessionWithRetry } from "./runtime/session-join-retry.mjs";
|
|
49
|
+
import { resolveWorkspaceRootPath } from "./runtime/workspace-root.mjs";
|
|
50
|
+
|
|
51
|
+
const POLL_MS = 10_000;
|
|
52
|
+
const HEARTBEAT_MS = 10_000;
|
|
53
|
+
|
|
54
|
+
const TOKEN_FALLBACK = generateHighEntropyToken();
|
|
55
|
+
|
|
56
|
+
const {
|
|
57
|
+
CONFIG_PATH,
|
|
58
|
+
RELAY_TOOLS_PATH,
|
|
59
|
+
SERVER_DIR,
|
|
60
|
+
LOG_DIR,
|
|
61
|
+
SERVER_LOG_PATH,
|
|
62
|
+
SERVER_ERR_PATH,
|
|
63
|
+
} = resolveRelayPaths(import.meta.url);
|
|
64
|
+
const SERVER_URL = resolveRelayServerUrl({ configPath: CONFIG_PATH });
|
|
65
|
+
const SERVER_START_TIMEOUT_MS = 20_000;
|
|
66
|
+
const NODE_BIN = process.env.COPILOT_WEB_RELAY_NODE || "node";
|
|
67
|
+
|
|
68
|
+
const CONFIG_TOKEN = loadTokenFromConfig(CONFIG_PATH);
|
|
69
|
+
const TOKEN = CONFIG_TOKEN || TOKEN_FALLBACK;
|
|
70
|
+
const TOKEN_WAS_GENERATED = !CONFIG_TOKEN;
|
|
71
|
+
const RELAY_TOOL_INSTRUCTIONS = loadRelayInstructionsFromFile(RELAY_TOOLS_PATH);
|
|
72
|
+
const QUESTION_POLL_MS = 1500;
|
|
73
|
+
const MAX_TOOL_DETAIL_LENGTH = 140;
|
|
74
|
+
const RESTART_CONTROL_STARTUP_GRACE_MS = 20_000;
|
|
75
|
+
const STARTUP_VERIFICATION_DELAY_MS = 1500;
|
|
76
|
+
|
|
77
|
+
const MODEL_SNAPSHOT_MIN_INTERVAL_MS = 30_000;
|
|
78
|
+
const BANNER_DEDUPE_TTL_MS = 6 * 60 * 60 * 1000;
|
|
79
|
+
const BANNER_DEDUPE_COOLDOWN_MS = 15 * 1000;
|
|
80
|
+
const { dbg } = createDebugLogger({ logDir: LOG_DIR });
|
|
81
|
+
const api = createApiClient({
|
|
82
|
+
serverUrl: SERVER_URL,
|
|
83
|
+
token: TOKEN,
|
|
84
|
+
getHeaders: () => ({
|
|
85
|
+
"X-Relay-Process-Pid": String(process.pid),
|
|
86
|
+
"X-Relay-Parent-Pid": String(process.ppid),
|
|
87
|
+
"X-Relay-Session-Id": String(session?.sessionId || ""),
|
|
88
|
+
"X-Relay-Conversation-Id": String(getCurrentConversationId() || ""),
|
|
89
|
+
}),
|
|
90
|
+
});
|
|
91
|
+
const bannerStateStore = createBannerStateStore({
|
|
92
|
+
stateFilePath: path.resolve(LOG_DIR, "relay-banner-state.json"),
|
|
93
|
+
dbg,
|
|
94
|
+
ttlMs: BANNER_DEDUPE_TTL_MS,
|
|
95
|
+
cooldownMs: BANNER_DEDUPE_COOLDOWN_MS,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const buildPromptWithRelayContext = createRelayPromptBuilder({
|
|
99
|
+
toolInstructions: RELAY_TOOL_INSTRUCTIONS,
|
|
100
|
+
getPreviewInstructions: createPreviewInstructionsProvider({ api }),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const managedServerLifecycle = createManagedServerLifecycle({
|
|
104
|
+
api,
|
|
105
|
+
dbg,
|
|
106
|
+
fs,
|
|
107
|
+
spawn,
|
|
108
|
+
killProcessTree,
|
|
109
|
+
delay,
|
|
110
|
+
token: TOKEN,
|
|
111
|
+
logDir: LOG_DIR,
|
|
112
|
+
serverDir: SERVER_DIR,
|
|
113
|
+
serverLogPath: SERVER_LOG_PATH,
|
|
114
|
+
serverErrPath: SERVER_ERR_PATH,
|
|
115
|
+
relayPort: Number(new URL(SERVER_URL).port) || 3333,
|
|
116
|
+
nodeBin: NODE_BIN,
|
|
117
|
+
serverStartTimeoutMs: SERVER_START_TIMEOUT_MS,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const ensureManagedServer = managedServerLifecycle.ensureManagedServer;
|
|
121
|
+
const stopManagedServer = managedServerLifecycle.stopManagedServer;
|
|
122
|
+
|
|
123
|
+
process.on("exit", () => {
|
|
124
|
+
shutdownStarted = true;
|
|
125
|
+
sessionReady = false;
|
|
126
|
+
clearSession();
|
|
127
|
+
pollingLoopController?.stopPolling?.();
|
|
128
|
+
stopHeartbeat();
|
|
129
|
+
workerWebSocketLink?.stop?.();
|
|
130
|
+
managedServerLifecycle.killManagedProcessTree();
|
|
131
|
+
});
|
|
132
|
+
process.on("SIGINT", () => {
|
|
133
|
+
void gracefulShutdown("SIGINT");
|
|
134
|
+
});
|
|
135
|
+
process.on("SIGTERM", () => {
|
|
136
|
+
void gracefulShutdown("SIGTERM");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
if (!TOKEN) {
|
|
140
|
+
console.warn("[web-relay] No authToken found in server/config.json; API calls will fail until configured.");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
dbg("extension.mjs loaded, TOKEN:", TOKEN ? TOKEN.slice(0,3)+"***" : "(none)");
|
|
144
|
+
|
|
145
|
+
// ─── State ────────────────────────────────────────────────────────────────────
|
|
146
|
+
let activeMsg = null; // the web message currently being processed
|
|
147
|
+
let waitingForAI = false; // true while the agent is generating a response
|
|
148
|
+
let relayTurnActive = false; // true while processing a relay-originated turn
|
|
149
|
+
let sessionReady = false;
|
|
150
|
+
const SEND_TIMEOUT = 2 * 60 * 60_000; // keep long ask_user pauses viable without letting broken turns linger all day
|
|
151
|
+
let heartbeatTimer = null;
|
|
152
|
+
let pollingLoopStarted = false;
|
|
153
|
+
let activatingRelay = null;
|
|
154
|
+
let shutdownStarted = false;
|
|
155
|
+
let lastRenderedRelayBannerKey = "";
|
|
156
|
+
let preferredConversationSessionMode = "isolated";
|
|
157
|
+
let warnedConversationModeFallback = false;
|
|
158
|
+
|
|
159
|
+
const relayScopeState = new Map();
|
|
160
|
+
let activeRelayScopeKey = null;
|
|
161
|
+
|
|
162
|
+
let session = null;
|
|
163
|
+
let heartbeatController = null;
|
|
164
|
+
let pollingLoopController = null;
|
|
165
|
+
let restartControlPromise = null;
|
|
166
|
+
let startupVerificationTimer = null;
|
|
167
|
+
let restartControlGraceUntilMs = 0;
|
|
168
|
+
let deferredSessionEnd = false;
|
|
169
|
+
let workerWebSocketLink = null;
|
|
170
|
+
let reasoningStreamUnsubscribe = null;
|
|
171
|
+
let subagentLifecycleUnsubscribe = null;
|
|
172
|
+
|
|
173
|
+
function getCurrentConversationId() {
|
|
174
|
+
const conversationId = String(activeMsg?.conversationId || "").trim();
|
|
175
|
+
if (conversationId) return conversationId;
|
|
176
|
+
const persistedConversationId = String(getActiveSession()?.conversationId || "").trim();
|
|
177
|
+
return persistedConversationId || null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function getCurrentWorkspaceRootPath() {
|
|
181
|
+
return resolveWorkspaceRootPath({
|
|
182
|
+
session,
|
|
183
|
+
env: process.env,
|
|
184
|
+
cwd: process.cwd(),
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function normalizeRelayScopeValue(value) {
|
|
189
|
+
const text = String(value || "").trim();
|
|
190
|
+
return text || null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function buildRelayScopeKey({ conversationId, sdkSessionId } = {}) {
|
|
194
|
+
const convId = normalizeRelayScopeValue(conversationId);
|
|
195
|
+
const sessionId = normalizeRelayScopeValue(sdkSessionId);
|
|
196
|
+
if (sessionId && convId) return `${sessionId}::${convId}`;
|
|
197
|
+
if (convId) return `conv::${convId}`;
|
|
198
|
+
if (sessionId) return `sdk::${sessionId}`;
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function getRelayScopeKey() {
|
|
203
|
+
if (activeRelayScopeKey) return activeRelayScopeKey;
|
|
204
|
+
return buildRelayScopeKey({
|
|
205
|
+
conversationId: activeMsg?.conversationId,
|
|
206
|
+
sdkSessionId: session?.sessionId,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function ensureRelayScopeState(scopeKey) {
|
|
211
|
+
if (!scopeKey) return null;
|
|
212
|
+
let state = relayScopeState.get(scopeKey);
|
|
213
|
+
if (!state) {
|
|
214
|
+
state = {
|
|
215
|
+
lastActivityText: "",
|
|
216
|
+
lastAskUserBridge: null,
|
|
217
|
+
pendingAskUserRequest: null,
|
|
218
|
+
};
|
|
219
|
+
relayScopeState.set(scopeKey, state);
|
|
220
|
+
}
|
|
221
|
+
return state;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function getScopedRelayState() {
|
|
225
|
+
const scopeKey = getRelayScopeKey();
|
|
226
|
+
const state = ensureRelayScopeState(scopeKey);
|
|
227
|
+
return { scopeKey, state };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function getScopedLastActivityText() {
|
|
231
|
+
const { state } = getScopedRelayState();
|
|
232
|
+
return state?.lastActivityText || "";
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function setScopedLastActivityText(value) {
|
|
236
|
+
const { state } = getScopedRelayState();
|
|
237
|
+
if (!state) return;
|
|
238
|
+
state.lastActivityText = String(value || "");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function getScopedLastAskUserBridge() {
|
|
242
|
+
const { state } = getScopedRelayState();
|
|
243
|
+
return state?.lastAskUserBridge || null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function setScopedLastAskUserBridge(value) {
|
|
247
|
+
const { state } = getScopedRelayState();
|
|
248
|
+
if (!state) return;
|
|
249
|
+
state.lastAskUserBridge = value || null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function getScopedPendingAskUserRequest() {
|
|
253
|
+
const { state } = getScopedRelayState();
|
|
254
|
+
return state?.pendingAskUserRequest || null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function setScopedPendingAskUserRequest(value) {
|
|
258
|
+
const { state } = getScopedRelayState();
|
|
259
|
+
if (!state) return;
|
|
260
|
+
state.pendingAskUserRequest = value || null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function clearScopedRelayState() {
|
|
264
|
+
const { scopeKey } = getScopedRelayState();
|
|
265
|
+
if (!scopeKey) return;
|
|
266
|
+
relayScopeState.delete(scopeKey);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function refreshSessionRegistry() {
|
|
270
|
+
const sdkSessionId = String(session?.sessionId || "").trim();
|
|
271
|
+
if (!sdkSessionId) {
|
|
272
|
+
clearSession();
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const currentConversationId = getCurrentConversationId();
|
|
277
|
+
const existingConversationId = String(getActiveSession()?.conversationId || "").trim() || null;
|
|
278
|
+
return registerSession(sdkSessionId, currentConversationId || existingConversationId);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function syncActiveSession(reason, forceSync = false) {
|
|
282
|
+
const activeSession = refreshSessionRegistry();
|
|
283
|
+
if (!activeSession?.sdkSessionId || !activeSession?.conversationId) return false;
|
|
284
|
+
|
|
285
|
+
try {
|
|
286
|
+
return await syncSessionToServer(activeSession.sdkSessionId, activeSession.conversationId, api, forceSync, {
|
|
287
|
+
workspaceRootPath: getCurrentWorkspaceRootPath(),
|
|
288
|
+
});
|
|
289
|
+
} catch (e) {
|
|
290
|
+
dbg("session sync failed:", reason, e?.message || String(e));
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async function syncStartupWorkspaceRoot(reason = "startup") {
|
|
296
|
+
const currentWorkspaceRootPath = resolveWorkspaceRootPath({
|
|
297
|
+
session,
|
|
298
|
+
env: process.env,
|
|
299
|
+
cwd: process.cwd(),
|
|
300
|
+
includeProcessCwd: true,
|
|
301
|
+
});
|
|
302
|
+
if (!currentWorkspaceRootPath) return false;
|
|
303
|
+
try {
|
|
304
|
+
return await syncWorkspaceRootToServer(currentWorkspaceRootPath, api, {
|
|
305
|
+
sdkSessionId: String(session?.sessionId || "").trim() || null,
|
|
306
|
+
});
|
|
307
|
+
} catch (e) {
|
|
308
|
+
dbg("workspace root sync failed:", reason, e?.message || String(e));
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async function ensureSessionForConversation(conversationId, reason = "dequeue") {
|
|
314
|
+
const convId = String(conversationId || "").trim();
|
|
315
|
+
if (!convId) {
|
|
316
|
+
return {
|
|
317
|
+
ok: false,
|
|
318
|
+
reason: "conversation-id-missing",
|
|
319
|
+
retryable: false,
|
|
320
|
+
message: "Missing conversation id for session binding check",
|
|
321
|
+
activeSessionId: String(session?.sessionId || "").trim() || null,
|
|
322
|
+
targetSessionId: null,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
let details = null;
|
|
327
|
+
try {
|
|
328
|
+
details = await api("GET", `/api/conversation/${encodeURIComponent(convId)}`);
|
|
329
|
+
} catch (error) {
|
|
330
|
+
return {
|
|
331
|
+
ok: false,
|
|
332
|
+
reason: "conversation-load-failed",
|
|
333
|
+
retryable: true,
|
|
334
|
+
message: `Failed to load conversation binding: ${error?.message || String(error)}`,
|
|
335
|
+
activeSessionId: String(session?.sessionId || "").trim() || null,
|
|
336
|
+
targetSessionId: null,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const binding = resolveSessionBinding({
|
|
341
|
+
conversationId: convId,
|
|
342
|
+
details,
|
|
343
|
+
activeSessionId: String(session?.sessionId || "").trim() || null,
|
|
344
|
+
});
|
|
345
|
+
if (binding?.ok) {
|
|
346
|
+
registerSession(binding.activeSessionId, convId);
|
|
347
|
+
return {
|
|
348
|
+
ok: true,
|
|
349
|
+
switched: false,
|
|
350
|
+
via: binding?.via || "session-liveness",
|
|
351
|
+
activeSessionId: binding.activeSessionId,
|
|
352
|
+
targetSessionId: binding.targetSessionId,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (binding?.canClaim === true && binding?.activeSessionId) {
|
|
357
|
+
try {
|
|
358
|
+
await syncSessionToServer(binding.activeSessionId, convId, api, true, {
|
|
359
|
+
workspaceRootPath: getCurrentWorkspaceRootPath(),
|
|
360
|
+
});
|
|
361
|
+
registerSession(binding.activeSessionId, convId);
|
|
362
|
+
return {
|
|
363
|
+
ok: true,
|
|
364
|
+
switched: false,
|
|
365
|
+
via: "session-claim",
|
|
366
|
+
activeSessionId: binding.activeSessionId,
|
|
367
|
+
targetSessionId: binding.activeSessionId,
|
|
368
|
+
};
|
|
369
|
+
} catch (error) {
|
|
370
|
+
return {
|
|
371
|
+
ok: false,
|
|
372
|
+
reason: "session-claim-failed",
|
|
373
|
+
retryable: true,
|
|
374
|
+
message: `Failed to claim conversation binding: ${error?.message || String(error)}`,
|
|
375
|
+
activeSessionId: binding.activeSessionId || String(session?.sessionId || "").trim() || null,
|
|
376
|
+
targetSessionId: null,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const targetSessionId = String(binding?.targetSessionId || "").trim();
|
|
382
|
+
if (targetSessionId) {
|
|
383
|
+
const switched = await sessionRuntimeManager.activateSession(targetSessionId, reason);
|
|
384
|
+
if (!switched?.ok) {
|
|
385
|
+
return {
|
|
386
|
+
ok: false,
|
|
387
|
+
reason: switched?.reason || binding?.reason || "session-switch-failed",
|
|
388
|
+
retryable: switched?.retryable === true || binding?.retryable === true,
|
|
389
|
+
message: switched?.error
|
|
390
|
+
? `Failed to activate bound SDK session: ${switched.error}`
|
|
391
|
+
: (binding?.message || "Failed to activate bound SDK session"),
|
|
392
|
+
activeSessionId: String(session?.sessionId || "").trim() || null,
|
|
393
|
+
targetSessionId,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
const switchedSessionId = String(switched?.activeSessionId || targetSessionId).trim();
|
|
397
|
+
try {
|
|
398
|
+
await syncSessionToServer(switchedSessionId, convId, api, true, {
|
|
399
|
+
workspaceRootPath: getCurrentWorkspaceRootPath(),
|
|
400
|
+
});
|
|
401
|
+
registerSession(switchedSessionId, convId);
|
|
402
|
+
return {
|
|
403
|
+
ok: true,
|
|
404
|
+
switched: true,
|
|
405
|
+
via: switched?.source || "runtime-switch",
|
|
406
|
+
activeSessionId: switchedSessionId,
|
|
407
|
+
targetSessionId,
|
|
408
|
+
};
|
|
409
|
+
} catch (error) {
|
|
410
|
+
return {
|
|
411
|
+
ok: false,
|
|
412
|
+
reason: "session-sync-after-switch-failed",
|
|
413
|
+
retryable: true,
|
|
414
|
+
message: `Switched to SDK session ${targetSessionId} but failed to sync binding: ${error?.message || String(error)}`,
|
|
415
|
+
activeSessionId: switchedSessionId || null,
|
|
416
|
+
targetSessionId,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return binding;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const sessionIo = createSessionIoHelpers({
|
|
425
|
+
getSession: () => session,
|
|
426
|
+
sleep,
|
|
427
|
+
dbg,
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
const modelSwitchingService = createModelSwitchingService({
|
|
431
|
+
api,
|
|
432
|
+
dbg,
|
|
433
|
+
getSession: () => session,
|
|
434
|
+
modelSnapshotMinIntervalMs: MODEL_SNAPSHOT_MIN_INTERVAL_MS,
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
const sessionRuntimeManager = createSessionRuntimeManager({
|
|
438
|
+
dbg,
|
|
439
|
+
getSession: () => session,
|
|
440
|
+
setSession: (nextSession) => {
|
|
441
|
+
if (nextSession && typeof nextSession === "object") {
|
|
442
|
+
session = nextSession;
|
|
443
|
+
}
|
|
444
|
+
},
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
const getCurrentModelId = modelSwitchingService.getCurrentModelId;
|
|
448
|
+
const setModelForMessage = modelSwitchingService.setModelForMessage;
|
|
449
|
+
const publishModelSnapshot = modelSwitchingService.publishModelSnapshot;
|
|
450
|
+
|
|
451
|
+
const questionBridge = createQuestionBridge({
|
|
452
|
+
api,
|
|
453
|
+
dbg,
|
|
454
|
+
sleep,
|
|
455
|
+
getQuestionWaitTimeoutMs: () => SEND_TIMEOUT,
|
|
456
|
+
questionPollMs: QUESTION_POLL_MS,
|
|
457
|
+
getActiveMessage: () => activeMsg,
|
|
458
|
+
extractQuestionPrompt,
|
|
459
|
+
extractQuestionChoices,
|
|
460
|
+
serializeRequest,
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
function startHeartbeat() {
|
|
464
|
+
if (!heartbeatController) {
|
|
465
|
+
heartbeatController = createHeartbeatController({
|
|
466
|
+
api,
|
|
467
|
+
pollMs: HEARTBEAT_MS,
|
|
468
|
+
getSessionReady: () => sessionReady,
|
|
469
|
+
getHeartbeatTimer: () => heartbeatTimer,
|
|
470
|
+
setHeartbeatTimer: (timer) => { heartbeatTimer = timer; },
|
|
471
|
+
getActiveQueueMessageId: () => ((waitingForAI || relayTurnActive) ? activeMsg?.id || null : null),
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
heartbeatController.startHeartbeat();
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
async function pulseHeartbeat(reason = "unknown") {
|
|
478
|
+
if (!heartbeatController) {
|
|
479
|
+
startHeartbeat();
|
|
480
|
+
}
|
|
481
|
+
const ok = await heartbeatController?.pulseHeartbeat?.();
|
|
482
|
+
if (!ok) {
|
|
483
|
+
dbg("heartbeat pulse failed", reason);
|
|
484
|
+
}
|
|
485
|
+
return !!ok;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function stopHeartbeat() {
|
|
489
|
+
heartbeatController?.stopHeartbeat();
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function flushDeferredSessionEnd(reason = "deferred-onSessionEnd") {
|
|
493
|
+
if (!deferredSessionEnd || shutdownStarted || waitingForAI || relayTurnActive) return;
|
|
494
|
+
deferredSessionEnd = false;
|
|
495
|
+
queueMicrotask(() => {
|
|
496
|
+
void gracefulShutdown(reason);
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
async function startPolling() {
|
|
501
|
+
if (!pollingLoopController) {
|
|
502
|
+
pollingLoopController = createPollingLoop({
|
|
503
|
+
sleep,
|
|
504
|
+
pollMs: POLL_MS,
|
|
505
|
+
api,
|
|
506
|
+
dbg,
|
|
507
|
+
session,
|
|
508
|
+
sendTimeout: SEND_TIMEOUT,
|
|
509
|
+
publishModelSnapshot,
|
|
510
|
+
setModelForMessage,
|
|
511
|
+
buildPromptWithRelayContext,
|
|
512
|
+
sendAndWaitWithHardTimeout,
|
|
513
|
+
sendWithBestEffortStreaming,
|
|
514
|
+
extractFinalText,
|
|
515
|
+
extractGeneratedImages,
|
|
516
|
+
getLastActivityText: () => getScopedLastActivityText(),
|
|
517
|
+
getCurrentModelId,
|
|
518
|
+
getPreferredConversationSessionMode: () => preferredConversationSessionMode,
|
|
519
|
+
getSupportsIsolatedSessions: () => false,
|
|
520
|
+
getWarnedConversationModeFallback: () => warnedConversationModeFallback,
|
|
521
|
+
setWarnedConversationModeFallback: (value) => { warnedConversationModeFallback = value; },
|
|
522
|
+
getPollingLoopStarted: () => pollingLoopStarted,
|
|
523
|
+
setPollingLoopStarted: (value) => { pollingLoopStarted = value; },
|
|
524
|
+
getSessionReady: () => sessionReady,
|
|
525
|
+
getWaitingForAI: () => waitingForAI,
|
|
526
|
+
setActiveMsg: (value) => { activeMsg = value; },
|
|
527
|
+
setWaitingForAI: (value) => {
|
|
528
|
+
waitingForAI = value;
|
|
529
|
+
if (!waitingForAI) {
|
|
530
|
+
flushDeferredSessionEnd();
|
|
531
|
+
}
|
|
532
|
+
},
|
|
533
|
+
setRelayTurnActive: (value, message = null) => {
|
|
534
|
+
relayTurnActive = !!value;
|
|
535
|
+
if (relayTurnActive) {
|
|
536
|
+
activeRelayScopeKey = buildRelayScopeKey({
|
|
537
|
+
conversationId: message?.conversationId || activeMsg?.conversationId,
|
|
538
|
+
sdkSessionId: session?.sessionId,
|
|
539
|
+
});
|
|
540
|
+
ensureRelayScopeState(activeRelayScopeKey);
|
|
541
|
+
} else {
|
|
542
|
+
activeRelayScopeKey = null;
|
|
543
|
+
void subagentLifecycleHandlers.finalizeTurn(message || activeMsg).finally(() => {
|
|
544
|
+
subagentLifecycleHandlers.reset();
|
|
545
|
+
});
|
|
546
|
+
reasoningStreamHandlers.reset();
|
|
547
|
+
flushDeferredSessionEnd();
|
|
548
|
+
}
|
|
549
|
+
},
|
|
550
|
+
setLastActivityText: (value) => { setScopedLastActivityText(value); },
|
|
551
|
+
setLastAskUserBridge: (value) => { setScopedLastAskUserBridge(value); },
|
|
552
|
+
setPendingAskUserRequest: (value) => { setScopedPendingAskUserRequest(value); },
|
|
553
|
+
clearRelayScopeState: () => { clearScopedRelayState(); },
|
|
554
|
+
// Worker-routed turns are delivered over the session-worker WebSocket.
|
|
555
|
+
// Keep this loop for heartbeat/maintenance only; do not dequeue via HTTP
|
|
556
|
+
// when the worker socket is disconnected or stale.
|
|
557
|
+
shouldFetchPending: () => false,
|
|
558
|
+
syncActiveSession,
|
|
559
|
+
ensureSessionForConversation,
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
await pollingLoopController.startPolling();
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function startWorkerWebSocketLink() {
|
|
567
|
+
if (!pollingLoopController || workerWebSocketLink) return;
|
|
568
|
+
workerWebSocketLink = createWorkerWebSocketLink({
|
|
569
|
+
serverUrl: SERVER_URL,
|
|
570
|
+
token: TOKEN,
|
|
571
|
+
dbg,
|
|
572
|
+
getPid: () => process.pid,
|
|
573
|
+
getSessionReady: () => sessionReady,
|
|
574
|
+
getSessionId: () => session?.sessionId || null,
|
|
575
|
+
onDeliver: async (pending, reason = "ws-deliver") => {
|
|
576
|
+
await pollingLoopController?.handlePendingPayload?.(pending, reason);
|
|
577
|
+
},
|
|
578
|
+
});
|
|
579
|
+
workerWebSocketLink.start();
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
async function forwardRelayQuestion(request) {
|
|
583
|
+
return questionBridge.forwardRelayQuestion(request);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const subagentLifecycleHandlers = createSubagentLifecycleHandlers({
|
|
587
|
+
api,
|
|
588
|
+
dbg,
|
|
589
|
+
getRelayTurnActive: () => relayTurnActive,
|
|
590
|
+
getActiveMessage: () => activeMsg,
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
const questionRoutingHooks = createQuestionRoutingHooks({
|
|
594
|
+
api,
|
|
595
|
+
dbg,
|
|
596
|
+
forwardRelayQuestion,
|
|
597
|
+
isAskUserTool,
|
|
598
|
+
normalizeActivityText,
|
|
599
|
+
formatToolActivity,
|
|
600
|
+
formatToolResultActivity,
|
|
601
|
+
extractQuestionChoices,
|
|
602
|
+
maxToolDetailLength: MAX_TOOL_DETAIL_LENGTH,
|
|
603
|
+
getRelayTurnActive: () => relayTurnActive,
|
|
604
|
+
getActiveMessage: () => activeMsg,
|
|
605
|
+
setLastAskUserBridge: (value) => {
|
|
606
|
+
setScopedLastAskUserBridge(value);
|
|
607
|
+
},
|
|
608
|
+
getLastActivityText: () => getScopedLastActivityText(),
|
|
609
|
+
setLastActivityText: (value) => {
|
|
610
|
+
setScopedLastActivityText(value);
|
|
611
|
+
},
|
|
612
|
+
setPendingAskUserRequest: (value) => {
|
|
613
|
+
setScopedPendingAskUserRequest(value);
|
|
614
|
+
},
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
const reasoningStreamHandlers = createReasoningStreamHandlers({
|
|
618
|
+
api,
|
|
619
|
+
dbg,
|
|
620
|
+
getRelayTurnActive: () => relayTurnActive,
|
|
621
|
+
getActiveMessage: () => activeMsg,
|
|
622
|
+
notifySubagentAgentId: (agentId) => subagentLifecycleHandlers.ensureFromAgentId(agentId),
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
async function requeueActiveMessage(reason) {
|
|
626
|
+
if (!waitingForAI || !activeMsg?.id) return false;
|
|
627
|
+
const messageId = activeMsg.id;
|
|
628
|
+
dbg("re-queue active relay message", `msgId=${messageId}`, `reason=${reason}`);
|
|
629
|
+
try {
|
|
630
|
+
await api("POST", "/api/requeue", { messageId });
|
|
631
|
+
return true;
|
|
632
|
+
} catch (e) {
|
|
633
|
+
dbg("re-queue active relay message failed", `msgId=${messageId}`, e?.message || String(e));
|
|
634
|
+
return false;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
async function gracefulShutdown(reason) {
|
|
639
|
+
if (shutdownStarted) return;
|
|
640
|
+
if (reason === "onSessionEnd" && (waitingForAI || relayTurnActive)) {
|
|
641
|
+
deferredSessionEnd = true;
|
|
642
|
+
dbg("graceful shutdown deferred until active relay turn settles", reason);
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
shutdownStarted = true;
|
|
646
|
+
dbg("graceful shutdown", reason);
|
|
647
|
+
if (startupVerificationTimer) {
|
|
648
|
+
clearTimeout(startupVerificationTimer);
|
|
649
|
+
startupVerificationTimer = null;
|
|
650
|
+
}
|
|
651
|
+
pollingLoopController?.stopPolling?.();
|
|
652
|
+
stopHeartbeat();
|
|
653
|
+
try {
|
|
654
|
+
reasoningStreamUnsubscribe?.();
|
|
655
|
+
} catch (e) {
|
|
656
|
+
dbg("reasoning-stream unsubscribe failed:", e?.message || String(e));
|
|
657
|
+
}
|
|
658
|
+
reasoningStreamUnsubscribe = null;
|
|
659
|
+
try {
|
|
660
|
+
subagentLifecycleUnsubscribe?.();
|
|
661
|
+
} catch (e) {
|
|
662
|
+
dbg("subagent-lifecycle unsubscribe failed:", e?.message || String(e));
|
|
663
|
+
}
|
|
664
|
+
subagentLifecycleUnsubscribe = null;
|
|
665
|
+
workerWebSocketLink?.stop?.();
|
|
666
|
+
workerWebSocketLink = null;
|
|
667
|
+
sessionReady = false;
|
|
668
|
+
clearSession();
|
|
669
|
+
await Promise.race([
|
|
670
|
+
requeueActiveMessage(reason),
|
|
671
|
+
sleep(500).then(() => false),
|
|
672
|
+
]).catch(() => false);
|
|
673
|
+
await stopManagedServer();
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function scheduleStartupVerification(reason, attemptsRemaining = 4) {
|
|
677
|
+
if (shutdownStarted) return;
|
|
678
|
+
if (startupVerificationTimer) {
|
|
679
|
+
clearTimeout(startupVerificationTimer);
|
|
680
|
+
startupVerificationTimer = null;
|
|
681
|
+
}
|
|
682
|
+
const remaining = Math.max(0, Number(attemptsRemaining || 0));
|
|
683
|
+
if (remaining <= 0) return;
|
|
684
|
+
startupVerificationTimer = setTimeout(() => {
|
|
685
|
+
startupVerificationTimer = null;
|
|
686
|
+
void verifyRelayStartup(reason, remaining);
|
|
687
|
+
}, STARTUP_VERIFICATION_DELAY_MS);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
async function verifyRelayStartup(reason, attemptsRemaining) {
|
|
691
|
+
if (shutdownStarted || !sessionReady) return;
|
|
692
|
+
const remaining = Math.max(0, Number(attemptsRemaining || 0));
|
|
693
|
+
const status = await api("GET", "/api/status").catch(() => null);
|
|
694
|
+
if (status?.cliOnline === true) {
|
|
695
|
+
dbg("startup verification confirmed cliOnline", reason);
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
dbg(
|
|
699
|
+
"startup verification retry",
|
|
700
|
+
reason,
|
|
701
|
+
`remaining=${remaining}`,
|
|
702
|
+
`polling=${pollingLoopStarted ? "started" : "stopped"}`,
|
|
703
|
+
);
|
|
704
|
+
await syncActiveSession(`${reason}-startup-verify`, true).catch(() => false);
|
|
705
|
+
await pulseHeartbeat(`${reason}-startup-verify`);
|
|
706
|
+
startHeartbeat();
|
|
707
|
+
startPolling().catch((error) => {
|
|
708
|
+
dbg("startup verification polling restart failed", error?.message || String(error));
|
|
709
|
+
});
|
|
710
|
+
if (remaining > 1) {
|
|
711
|
+
scheduleStartupVerification(reason, remaining - 1);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function relayBannerCacheKey(readyBanner = null) {
|
|
716
|
+
if (!readyBanner || typeof readyBanner !== "object") return "";
|
|
717
|
+
const parts = [
|
|
718
|
+
String(readyBanner.localUrl || ""),
|
|
719
|
+
String(readyBanner.networkText || ""),
|
|
720
|
+
String(readyBanner.remoteUrl || ""),
|
|
721
|
+
String(readyBanner.pollingUrl || ""),
|
|
722
|
+
String(readyBanner.authText || ""),
|
|
723
|
+
String(readyBanner.listenHost || ""),
|
|
724
|
+
];
|
|
725
|
+
return parts.join("|");
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function summarizeRelayConnection(status, readyBanner) {
|
|
729
|
+
const localUrl = String(readyBanner?.localUrl || `${SERVER_URL}/`).trim();
|
|
730
|
+
const networkText = String(
|
|
731
|
+
readyBanner?.networkText
|
|
732
|
+
|| (status?.localhostOnly ? "disabled (localhost only)" : `enabled (${String(status?.listenHost || "0.0.0.0")})`)
|
|
733
|
+
|| "unavailable"
|
|
734
|
+
).trim();
|
|
735
|
+
const remoteUrl = String(readyBanner?.remoteUrl || "").trim();
|
|
736
|
+
const authText = `token=${TOKEN}`;
|
|
737
|
+
const pollingUrl = String(readyBanner?.pollingUrl || `${SERVER_URL}/api/pending`).trim();
|
|
738
|
+
return {
|
|
739
|
+
localUrl,
|
|
740
|
+
networkText,
|
|
741
|
+
remoteUrl,
|
|
742
|
+
authText,
|
|
743
|
+
pollingUrl,
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function buildFallbackReadyBannerFromStatus(status = null) {
|
|
748
|
+
const localUrl = `${SERVER_URL}/`;
|
|
749
|
+
const pollingUrl = `${SERVER_URL}/api/pending`;
|
|
750
|
+
const localhostOnly = status?.localhostOnly === true || String(status?.localhostOnly || "").toLowerCase() === "true";
|
|
751
|
+
const listenHost = String(status?.listenHost || "").trim();
|
|
752
|
+
const networkText = localhostOnly
|
|
753
|
+
? "disabled (localhost only)"
|
|
754
|
+
: `enabled (${listenHost || "0.0.0.0"})`;
|
|
755
|
+
|
|
756
|
+
const tunnelEnabled = status?.sshTunnel?.enabled === true;
|
|
757
|
+
const tunnelHost = String(status?.sshTunnel?.host || "").trim();
|
|
758
|
+
const remoteUrl = tunnelEnabled && tunnelHost ? `https://${tunnelHost.slice(0, 30)}/` : null;
|
|
759
|
+
const authText = `token=${TOKEN}`;
|
|
760
|
+
|
|
761
|
+
const fmt = (label, value) => `║ ${String(label || "").padEnd(11)}${String(value || "").padEnd(46)}║`;
|
|
762
|
+
return {
|
|
763
|
+
title: "Copilot Web Proxy - Ready",
|
|
764
|
+
localUrl,
|
|
765
|
+
networkUrl: localhostOnly ? null : "(computed from status)",
|
|
766
|
+
networkText,
|
|
767
|
+
remoteUrl,
|
|
768
|
+
authText,
|
|
769
|
+
pollingUrl,
|
|
770
|
+
localhostOnly,
|
|
771
|
+
listenHost: listenHost || (localhostOnly ? "127.0.0.1" : "0.0.0.0"),
|
|
772
|
+
lines: [
|
|
773
|
+
"╔══════════════════════════════════════════════════════════════╗",
|
|
774
|
+
"║ Copilot Web Proxy - Ready ║",
|
|
775
|
+
"╠══════════════════════════════════════════════════════════════╣",
|
|
776
|
+
fmt("Local:", localUrl),
|
|
777
|
+
fmt("Network:", networkText),
|
|
778
|
+
...(remoteUrl ? [fmt("Remote:", remoteUrl)] : []),
|
|
779
|
+
fmt("Auth:", authText),
|
|
780
|
+
"╠══════════════════════════════════════════════════════════════╣",
|
|
781
|
+
"║ CLI polling URL (for monitoring mode): ║",
|
|
782
|
+
fmt("GET:", pollingUrl),
|
|
783
|
+
"╚══════════════════════════════════════════════════════════════╝",
|
|
784
|
+
],
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
async function renderRelayReadyBannerFromStatus(status, { force = false } = {}) {
|
|
789
|
+
if (!session) return;
|
|
790
|
+
const readyBanner = status?.readyBanner && typeof status.readyBanner === "object"
|
|
791
|
+
? status.readyBanner
|
|
792
|
+
: (status && typeof status === "object" ? buildFallbackReadyBannerFromStatus(status) : null);
|
|
793
|
+
if (!readyBanner) {
|
|
794
|
+
await session.log("🌐 Web relay connected (status banner unavailable)", { ephemeral: true });
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
const nextKey = relayBannerCacheKey(readyBanner);
|
|
799
|
+
if (!force && nextKey && nextKey === lastRenderedRelayBannerKey) {
|
|
800
|
+
dbg("relay banner suppressed: in-memory cache key match");
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
const suppress = bannerStateStore.shouldSuppress({
|
|
805
|
+
sessionId: String(session?.sessionId || "").trim() || "unknown",
|
|
806
|
+
bannerKey: nextKey || "fallback-banner",
|
|
807
|
+
token: TOKEN,
|
|
808
|
+
force,
|
|
809
|
+
});
|
|
810
|
+
if (suppress.suppress) {
|
|
811
|
+
dbg("relay banner suppressed:", suppress.reason);
|
|
812
|
+
if (nextKey) lastRenderedRelayBannerKey = nextKey;
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
const info = summarizeRelayConnection(status, readyBanner);
|
|
817
|
+
await session.log("🌐 Web relay connected", { ephemeral: true });
|
|
818
|
+
await session.log(`🏠 Local: ${info.localUrl}`, { ephemeral: true });
|
|
819
|
+
await session.log(`🖧 Network: ${info.networkText}`, { ephemeral: true });
|
|
820
|
+
if (info.remoteUrl) {
|
|
821
|
+
await session.log(`🌍 Remote: ${info.remoteUrl}`, { ephemeral: true });
|
|
822
|
+
}
|
|
823
|
+
await session.log(`🔐 Auth: ${info.authText}`, { ephemeral: true });
|
|
824
|
+
await session.log(`📡 Pending: ${info.pollingUrl}`, { ephemeral: true });
|
|
825
|
+
|
|
826
|
+
if (nextKey) {
|
|
827
|
+
lastRenderedRelayBannerKey = nextKey;
|
|
828
|
+
}
|
|
829
|
+
bannerStateStore.markShown({
|
|
830
|
+
sessionId: String(session?.sessionId || "").trim() || "unknown",
|
|
831
|
+
bannerKey: nextKey || "fallback-banner",
|
|
832
|
+
token: TOKEN,
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// Start the web server as soon as the extension loads so users get relay availability
|
|
837
|
+
// even before the SDK session emits onSessionStart.
|
|
838
|
+
try {
|
|
839
|
+
await ensureManagedServer();
|
|
840
|
+
} catch (e) {
|
|
841
|
+
dbg("managed web server bootstrap failed:", e?.message || String(e));
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// ─── Join the session ─────────────────────────────────────────────────────────
|
|
845
|
+
session = await joinSessionWithRetry({
|
|
846
|
+
joinSessionImpl: joinSession,
|
|
847
|
+
dbg,
|
|
848
|
+
delay: sleep,
|
|
849
|
+
retries: 5,
|
|
850
|
+
retryDelayMs: 1500,
|
|
851
|
+
joinOptions: {
|
|
852
|
+
// Enable streaming so assistant.reasoning_delta and assistant.message_delta are emitted.
|
|
853
|
+
// (The complete assistant.reasoning event fires regardless; this flag only adds deltas.)
|
|
854
|
+
streaming: true,
|
|
855
|
+
// onUserInputRequest must be a TOP-LEVEL property (not inside hooks) so the SDK calls
|
|
856
|
+
// session.registerUserInputHandler() and sends requestUserInput: true to the CLI runtime.
|
|
857
|
+
// When inside hooks it is silently ignored, causing the CLI to show its own terminal prompt.
|
|
858
|
+
onUserInputRequest: async (request) => {
|
|
859
|
+
dbg("onUserInputRequest CALLED!", "keys=", Object.keys(request || {}).join(","), "platform=", process.platform);
|
|
860
|
+
return questionRoutingHooks.onUserInputRequest(request);
|
|
861
|
+
},
|
|
862
|
+
// onElicitationRequest handles multi-field structured `ask_user` forms (requestedSchema).
|
|
863
|
+
// Must be a TOP-LEVEL property so the SDK registers the elicitation capability and routes
|
|
864
|
+
// structured form requests here instead of flattening them through onUserInputRequest.
|
|
865
|
+
onElicitationRequest: async (request) => {
|
|
866
|
+
dbg("onElicitationRequest CALLED!", "keys=", Object.keys(request || {}).join(","), "mode=", request?.mode || "form", "platform=", process.platform);
|
|
867
|
+
return questionRoutingHooks.onElicitationRequest(request);
|
|
868
|
+
},
|
|
869
|
+
hooks: {
|
|
870
|
+
onSessionStart: async () => {
|
|
871
|
+
dbg("onSessionStart fired");
|
|
872
|
+
await syncStartupWorkspaceRoot("onSessionStart");
|
|
873
|
+
try {
|
|
874
|
+
await ensureRelayActive("onSessionStart");
|
|
875
|
+
} catch (e) {
|
|
876
|
+
await session.log(`⚠️ Web relay activation failed: ${e?.message || String(e)}`, { ephemeral: true });
|
|
877
|
+
dbg("onSessionStart activation failed:", e?.message || String(e));
|
|
878
|
+
}
|
|
879
|
+
try {
|
|
880
|
+
await session.log(`🔐 Web relay token: ${TOKEN}`, { ephemeral: true });
|
|
881
|
+
await session.log(
|
|
882
|
+
TOKEN_WAS_GENERATED
|
|
883
|
+
? "🔐 Token source: generated for this CLI session"
|
|
884
|
+
: "🔐 Token source: server/config.json",
|
|
885
|
+
{ ephemeral: true }
|
|
886
|
+
);
|
|
887
|
+
await session.log("🌐 Web relay loaded — worker websocket delivery enabled with heartbeat maintenance", { ephemeral: true });
|
|
888
|
+
} catch (e) { dbg("session.log error:", e.message); }
|
|
889
|
+
},
|
|
890
|
+
onPreToolUse: async (request) => {
|
|
891
|
+
await subagentLifecycleHandlers.onPreToolUse(request);
|
|
892
|
+
return questionRoutingHooks.onPreToolUse(request);
|
|
893
|
+
},
|
|
894
|
+
onPostToolUse: async (request, result) => {
|
|
895
|
+
await subagentLifecycleHandlers.onPostToolUse(request, result);
|
|
896
|
+
await questionRoutingHooks.onPostToolUse(request, result);
|
|
897
|
+
},
|
|
898
|
+
onSessionEnd: async () => {
|
|
899
|
+
dbg("onSessionEnd fired");
|
|
900
|
+
await gracefulShutdown("onSessionEnd");
|
|
901
|
+
},
|
|
902
|
+
},
|
|
903
|
+
},
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
refreshSessionRegistry();
|
|
907
|
+
dbg("copilot session id:", session?.sessionId || "(none)");
|
|
908
|
+
dbg("joinSession resolved");
|
|
909
|
+
dbg("platform:", process.platform, "onUserInputRequest handler registered at top level");
|
|
910
|
+
try {
|
|
911
|
+
reasoningStreamUnsubscribe = reasoningStreamHandlers.attach(session);
|
|
912
|
+
subagentLifecycleUnsubscribe = subagentLifecycleHandlers.attach(session);
|
|
913
|
+
} catch (e) {
|
|
914
|
+
dbg("reasoning-stream attach failed:", e?.message || String(e));
|
|
915
|
+
}
|
|
916
|
+
setTimeout(() => {
|
|
917
|
+
ensureRelayActive("post-join-fallback").catch((e) => {
|
|
918
|
+
dbg("post-join fallback activation failed:", e?.message || String(e));
|
|
919
|
+
});
|
|
920
|
+
}, 1000);
|
|
921
|
+
|
|
922
|
+
// ─── Polling loop ─────────────────────────────────────────────────────────────
|
|
923
|
+
async function ensureRelayActive(reason) {
|
|
924
|
+
if (shutdownStarted) {
|
|
925
|
+
dbg("ensureRelayActive skipped: shutdown in progress", reason);
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
if (activatingRelay) return activatingRelay;
|
|
929
|
+
activatingRelay = (async () => {
|
|
930
|
+
dbg("ensureRelayActive", reason);
|
|
931
|
+
await ensureManagedServer();
|
|
932
|
+
sessionReady = true;
|
|
933
|
+
await syncActiveSession(reason);
|
|
934
|
+
const status = await api("GET", "/api/status").catch(() => null);
|
|
935
|
+
const restartState = String(status?.restartOrchestrator?.state || "").trim().toLowerCase();
|
|
936
|
+
const restartTx = String(status?.restartOrchestrator?.transactionId || "").trim();
|
|
937
|
+
const restartTarget = String(status?.restartOrchestrator?.targetSessionId || "").trim();
|
|
938
|
+
dbg(
|
|
939
|
+
"relay status snapshot",
|
|
940
|
+
`reason=${reason}`,
|
|
941
|
+
`cliOnline=${status?.cliOnline === true ? "yes" : "no"}`,
|
|
942
|
+
`orchestrator=${restartState || "none"}`,
|
|
943
|
+
`transaction=${restartTx || "none"}`,
|
|
944
|
+
`target=${restartTarget || "none"}`,
|
|
945
|
+
);
|
|
946
|
+
await renderRelayReadyBannerFromStatus(status, { force: false }).catch((e) => {
|
|
947
|
+
dbg("render relay banner failed:", e?.message || String(e));
|
|
948
|
+
});
|
|
949
|
+
const requestedMode = String(status?.conversationSessionMode || "").trim().toLowerCase();
|
|
950
|
+
preferredConversationSessionMode = requestedMode || "isolated";
|
|
951
|
+
warnedConversationModeFallback = false;
|
|
952
|
+
await api("POST", "/api/relay/recover-processing", { maxAgeMs: status?.processingTimeoutMs || (10 * 60 * 1000) }).catch(() => {});
|
|
953
|
+
await publishModelSnapshot("relay-active", true);
|
|
954
|
+
startHeartbeat();
|
|
955
|
+
await pulseHeartbeat(`${reason}-initial`);
|
|
956
|
+
restartControlGraceUntilMs = Date.now() + RESTART_CONTROL_STARTUP_GRACE_MS;
|
|
957
|
+
startPolling().catch((error) => {
|
|
958
|
+
dbg("startPolling failed", reason, error?.message || String(error));
|
|
959
|
+
});
|
|
960
|
+
startWorkerWebSocketLink();
|
|
961
|
+
scheduleStartupVerification(reason);
|
|
962
|
+
dbg("relay active", reason);
|
|
963
|
+
})().finally(() => {
|
|
964
|
+
activatingRelay = null;
|
|
965
|
+
});
|
|
966
|
+
return activatingRelay;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
const extractFinalText = sessionIo.extractFinalText;
|
|
970
|
+
const extractGeneratedImages = sessionIo.extractGeneratedImages;
|
|
971
|
+
const sendAndWaitWithHardTimeout = sessionIo.sendAndWaitWithHardTimeout;
|
|
972
|
+
const sendWithBestEffortStreaming = sessionIo.sendWithBestEffortStreaming;
|