@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,386 @@
|
|
|
1
|
+
export function createManagedServerLifecycle({
|
|
2
|
+
api,
|
|
3
|
+
dbg,
|
|
4
|
+
fs,
|
|
5
|
+
spawn,
|
|
6
|
+
killProcessTree,
|
|
7
|
+
delay,
|
|
8
|
+
token,
|
|
9
|
+
logDir,
|
|
10
|
+
serverDir,
|
|
11
|
+
serverLogPath,
|
|
12
|
+
serverErrPath,
|
|
13
|
+
relayPort = 3333,
|
|
14
|
+
nodeBin,
|
|
15
|
+
serverStartTimeoutMs,
|
|
16
|
+
}) {
|
|
17
|
+
let managedServerProc = null;
|
|
18
|
+
let managedServerStdoutFd = null;
|
|
19
|
+
let managedServerStderrFd = null;
|
|
20
|
+
let managedServerOwned = false;
|
|
21
|
+
let managedServerStartPromise = null;
|
|
22
|
+
let desiredRunning = false;
|
|
23
|
+
let stoppingManagedServer = false;
|
|
24
|
+
let restartTimer = null;
|
|
25
|
+
let restartAttempts = 0;
|
|
26
|
+
let attemptedSqliteRepair = false;
|
|
27
|
+
// Keep in sync with server/relay-self-restart.mjs. Passing this on argv (not
|
|
28
|
+
// in the env) is deliberate: this extension's own env is inherited from the
|
|
29
|
+
// relay worker that launched the CLI, so an env flag here would be read by
|
|
30
|
+
// unrelated servers further down the tree.
|
|
31
|
+
const RELAY_SUPERVISED_FLAG = "--supervised";
|
|
32
|
+
|
|
33
|
+
const RESTART_BACKOFF_MS = [1000, 2000, 5000, 10000, 20000];
|
|
34
|
+
// Keep in sync with server/relay-exit-codes.mjs
|
|
35
|
+
const RELAY_RESTART_EXIT_CODE = 75;
|
|
36
|
+
const STARTUP_LOG_TAIL_BYTES = 24 * 1024;
|
|
37
|
+
const STATUS_PATH = "/api/status";
|
|
38
|
+
const RELAY_PORT = Number.isInteger(Number(relayPort)) && Number(relayPort) > 0
|
|
39
|
+
? Number(relayPort)
|
|
40
|
+
: 3333;
|
|
41
|
+
const persistentManagedServer = String(process.env.COPILOT_WEB_RELAY_PERSISTENT_SERVER || "true").trim().toLowerCase() !== "false";
|
|
42
|
+
const legacyOwnerPidWatchdog = String(process.env.COPILOT_WEB_RELAY_USE_OWNER_PID || "false").trim().toLowerCase() === "true";
|
|
43
|
+
|
|
44
|
+
function markNonRetryable(error) {
|
|
45
|
+
error.relayRetryable = false;
|
|
46
|
+
return error;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function classifyStatusProbeError(error) {
|
|
50
|
+
const message = String(error?.message || error || "");
|
|
51
|
+
if (message.includes(`HTTP 401 ${STATUS_PATH}`)) {
|
|
52
|
+
return {
|
|
53
|
+
type: "token-mismatch",
|
|
54
|
+
error: markNonRetryable(new Error(
|
|
55
|
+
`Web relay is running on :${RELAY_PORT} but rejected this token (401). `
|
|
56
|
+
+ "Set matching authToken values in extension/server config or restart the relay with --token."
|
|
57
|
+
)),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const httpStatusMatch = message.match(/HTTP\s+(\d+)\s+\/api\/status/i);
|
|
62
|
+
if (httpStatusMatch) {
|
|
63
|
+
const statusCode = Number(httpStatusMatch[1]);
|
|
64
|
+
return {
|
|
65
|
+
type: "port-conflict",
|
|
66
|
+
error: markNonRetryable(new Error(
|
|
67
|
+
`Port ${RELAY_PORT} is already serving HTTP ${statusCode} for ${STATUS_PATH}. `
|
|
68
|
+
+ `Another process is bound to :${RELAY_PORT}; stop it or change the relay port.`
|
|
69
|
+
)),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return { type: "unreachable", error: null };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function classifyStartupFailure(logText = "", fallbackError = null) {
|
|
77
|
+
const text = String(logText || fallbackError?.message || "");
|
|
78
|
+
if (new RegExp(`EADDRINUSE|address already in use|bind .*:${RELAY_PORT}`, "i").test(text)) {
|
|
79
|
+
return markNonRetryable(new Error(
|
|
80
|
+
`Managed web relay failed to start because port :${RELAY_PORT} is already in use. `
|
|
81
|
+
+ "Stop the conflicting process or run the relay on a different port."
|
|
82
|
+
));
|
|
83
|
+
}
|
|
84
|
+
return fallbackError;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function readLogTail(filePath, maxBytes = STARTUP_LOG_TAIL_BYTES) {
|
|
88
|
+
try {
|
|
89
|
+
const stats = fs.statSync(filePath);
|
|
90
|
+
const size = Number(stats?.size || 0);
|
|
91
|
+
if (size <= 0) return "";
|
|
92
|
+
const readFrom = Math.max(0, size - Math.max(1024, Number(maxBytes) || STARTUP_LOG_TAIL_BYTES));
|
|
93
|
+
const readSize = Math.max(0, size - readFrom);
|
|
94
|
+
if (!readSize) return "";
|
|
95
|
+
const fd = fs.openSync(filePath, "r");
|
|
96
|
+
try {
|
|
97
|
+
const chunk = Buffer.alloc(readSize);
|
|
98
|
+
fs.readSync(fd, chunk, 0, readSize, readFrom);
|
|
99
|
+
return chunk.toString("utf8");
|
|
100
|
+
} finally {
|
|
101
|
+
try { fs.closeSync(fd); } catch {}
|
|
102
|
+
}
|
|
103
|
+
} catch {
|
|
104
|
+
return "";
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function hasBetterSqliteAbiMismatch(logText) {
|
|
109
|
+
const text = String(logText || "");
|
|
110
|
+
if (!text) return false;
|
|
111
|
+
if (!/better_sqlite3\.node/i.test(text)) return false;
|
|
112
|
+
return /NODE_MODULE_VERSION|compiled against a different Node\.js version|ERR_DLOPEN_FAILED/i.test(text);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function rebuildBetterSqlite3() {
|
|
116
|
+
const npmBin = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
117
|
+
dbg("attempting native module repair", "command=npm rebuild better-sqlite3");
|
|
118
|
+
|
|
119
|
+
return await new Promise((resolve) => {
|
|
120
|
+
let out = "";
|
|
121
|
+
const rebuildProc = spawn(npmBin, ["rebuild", "better-sqlite3"], {
|
|
122
|
+
cwd: serverDir,
|
|
123
|
+
env: process.env,
|
|
124
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
125
|
+
windowsHide: true,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const append = (prefix, chunk) => {
|
|
129
|
+
const text = String(chunk || "");
|
|
130
|
+
if (!text) return;
|
|
131
|
+
out += `${prefix}${text}`;
|
|
132
|
+
if (out.length > 12_000) {
|
|
133
|
+
out = out.slice(out.length - 12_000);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
rebuildProc.stdout?.on("data", (chunk) => append("", chunk));
|
|
138
|
+
rebuildProc.stderr?.on("data", (chunk) => append("", chunk));
|
|
139
|
+
|
|
140
|
+
rebuildProc.once("error", (error) => {
|
|
141
|
+
resolve({
|
|
142
|
+
ok: false,
|
|
143
|
+
code: null,
|
|
144
|
+
error: error?.message || String(error),
|
|
145
|
+
output: out,
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
rebuildProc.once("exit", (code) => {
|
|
150
|
+
resolve({
|
|
151
|
+
ok: Number(code) === 0,
|
|
152
|
+
code: Number.isFinite(Number(code)) ? Number(code) : null,
|
|
153
|
+
error: null,
|
|
154
|
+
output: out,
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function closeManagedServerStreams() {
|
|
161
|
+
if (managedServerStdoutFd !== null) {
|
|
162
|
+
try { fs.closeSync(managedServerStdoutFd); } catch {}
|
|
163
|
+
managedServerStdoutFd = null;
|
|
164
|
+
}
|
|
165
|
+
if (managedServerStderrFd !== null) {
|
|
166
|
+
try { fs.closeSync(managedServerStderrFd); } catch {}
|
|
167
|
+
managedServerStderrFd = null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function clearRestartTimer() {
|
|
172
|
+
if (!restartTimer) return;
|
|
173
|
+
try { clearTimeout(restartTimer); } catch {}
|
|
174
|
+
restartTimer = null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function scheduleManagedServerRestart(reason = "unknown") {
|
|
178
|
+
if (!desiredRunning) return;
|
|
179
|
+
if (stoppingManagedServer) return;
|
|
180
|
+
if (managedServerStartPromise) return;
|
|
181
|
+
if (managedServerProc) return;
|
|
182
|
+
if (restartTimer) return;
|
|
183
|
+
|
|
184
|
+
const idx = Math.min(restartAttempts, RESTART_BACKOFF_MS.length - 1);
|
|
185
|
+
const delayMs = RESTART_BACKOFF_MS[idx];
|
|
186
|
+
restartAttempts += 1;
|
|
187
|
+
dbg("managed web server restart scheduled", `reason=${reason}`, `in=${delayMs}ms`, `attempt=${restartAttempts}`);
|
|
188
|
+
|
|
189
|
+
restartTimer = setTimeout(() => {
|
|
190
|
+
restartTimer = null;
|
|
191
|
+
if (!desiredRunning || stoppingManagedServer) return;
|
|
192
|
+
void ensureManagedServer().catch((error) => {
|
|
193
|
+
dbg("managed web server restart attempt failed", error?.message || String(error));
|
|
194
|
+
});
|
|
195
|
+
}, delayMs);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function waitForServerReady(timeoutMs = serverStartTimeoutMs) {
|
|
199
|
+
const deadline = Date.now() + timeoutMs;
|
|
200
|
+
while (Date.now() < deadline) {
|
|
201
|
+
try {
|
|
202
|
+
await api("GET", STATUS_PATH);
|
|
203
|
+
return true;
|
|
204
|
+
} catch {
|
|
205
|
+
await delay(300);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function stopManagedServer() {
|
|
212
|
+
desiredRunning = false;
|
|
213
|
+
stoppingManagedServer = true;
|
|
214
|
+
clearRestartTimer();
|
|
215
|
+
|
|
216
|
+
if (persistentManagedServer && !legacyOwnerPidWatchdog) {
|
|
217
|
+
closeManagedServerStreams();
|
|
218
|
+
managedServerProc = null;
|
|
219
|
+
managedServerOwned = false;
|
|
220
|
+
restartAttempts = 0;
|
|
221
|
+
attemptedSqliteRepair = false;
|
|
222
|
+
stoppingManagedServer = false;
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (!managedServerOwned) {
|
|
227
|
+
stoppingManagedServer = false;
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
killProcessTree(managedServerProc);
|
|
232
|
+
|
|
233
|
+
if (managedServerProc) {
|
|
234
|
+
await new Promise((resolve) => {
|
|
235
|
+
if (managedServerProc.exitCode !== null) return resolve();
|
|
236
|
+
managedServerProc.once("exit", resolve);
|
|
237
|
+
setTimeout(resolve, 2000);
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
closeManagedServerStreams();
|
|
242
|
+
managedServerProc = null;
|
|
243
|
+
managedServerOwned = false;
|
|
244
|
+
restartAttempts = 0;
|
|
245
|
+
attemptedSqliteRepair = false;
|
|
246
|
+
stoppingManagedServer = false;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async function ensureManagedServer() {
|
|
250
|
+
desiredRunning = true;
|
|
251
|
+
stoppingManagedServer = false;
|
|
252
|
+
clearRestartTimer();
|
|
253
|
+
if (managedServerStartPromise) return managedServerStartPromise;
|
|
254
|
+
|
|
255
|
+
managedServerStartPromise = (async () => {
|
|
256
|
+
let startedOwnedProcess = false;
|
|
257
|
+
try {
|
|
258
|
+
try {
|
|
259
|
+
await api("GET", STATUS_PATH);
|
|
260
|
+
managedServerOwned = false;
|
|
261
|
+
restartAttempts = 0;
|
|
262
|
+
dbg("web server already running");
|
|
263
|
+
return false;
|
|
264
|
+
} catch (error) {
|
|
265
|
+
const probeError = classifyStatusProbeError(error);
|
|
266
|
+
if (probeError.type !== "unreachable") {
|
|
267
|
+
throw probeError.error;
|
|
268
|
+
}
|
|
269
|
+
// Not running yet.
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (!token) {
|
|
273
|
+
throw new Error("No auth token available to start the web server");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
277
|
+
managedServerStdoutFd = fs.openSync(serverLogPath, "a");
|
|
278
|
+
managedServerStderrFd = fs.openSync(serverErrPath, "a");
|
|
279
|
+
const startupWorkspaceRoot = String(process.env.COPILOT_WORKSPACE_ROOT || "").trim();
|
|
280
|
+
|
|
281
|
+
dbg("starting managed web server", serverDir);
|
|
282
|
+
managedServerOwned = true;
|
|
283
|
+
startedOwnedProcess = true;
|
|
284
|
+
const serverArgs = ["server.js", RELAY_SUPERVISED_FLAG, "--token", token];
|
|
285
|
+
if (legacyOwnerPidWatchdog) {
|
|
286
|
+
serverArgs.push("--owner-pid", String(process.pid));
|
|
287
|
+
}
|
|
288
|
+
const serverEnv = { ...process.env };
|
|
289
|
+
if (startupWorkspaceRoot) {
|
|
290
|
+
serverEnv.COPILOT_WORKSPACE_ROOT = startupWorkspaceRoot;
|
|
291
|
+
}
|
|
292
|
+
managedServerProc = spawn(nodeBin, serverArgs, {
|
|
293
|
+
cwd: serverDir,
|
|
294
|
+
env: serverEnv,
|
|
295
|
+
stdio: ["ignore", managedServerStdoutFd, managedServerStderrFd],
|
|
296
|
+
detached: persistentManagedServer || process.platform !== "win32",
|
|
297
|
+
windowsHide: false,
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
managedServerProc.once("exit", (code, signal) => {
|
|
301
|
+
const normalizedCode = Number.isInteger(Number(code)) ? Number(code) : null;
|
|
302
|
+
const intentionalRestart = normalizedCode === RELAY_RESTART_EXIT_CODE;
|
|
303
|
+
dbg(
|
|
304
|
+
"managed web server exited",
|
|
305
|
+
`code=${code ?? "null"}`,
|
|
306
|
+
`signal=${signal ?? "none"}`,
|
|
307
|
+
intentionalRestart ? "intentionalRestart=true" : "intentionalRestart=false",
|
|
308
|
+
);
|
|
309
|
+
const shouldRestart = desiredRunning && managedServerOwned && !stoppingManagedServer;
|
|
310
|
+
closeManagedServerStreams();
|
|
311
|
+
managedServerProc = null;
|
|
312
|
+
managedServerOwned = false;
|
|
313
|
+
if (shouldRestart) {
|
|
314
|
+
if (intentionalRestart) {
|
|
315
|
+
restartAttempts = 0;
|
|
316
|
+
void ensureManagedServer().catch((error) => {
|
|
317
|
+
dbg("managed web server immediate restart failed", error?.message || String(error));
|
|
318
|
+
});
|
|
319
|
+
} else {
|
|
320
|
+
scheduleManagedServerRestart("process-exit");
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
const ready = await waitForServerReady();
|
|
326
|
+
if (!ready) {
|
|
327
|
+
const startupTail = readLogTail(serverErrPath);
|
|
328
|
+
throw classifyStartupFailure(startupTail, new Error("Managed web server did not become ready"));
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
restartAttempts = 0;
|
|
332
|
+
dbg("managed web server ready");
|
|
333
|
+
return true;
|
|
334
|
+
} catch (error) {
|
|
335
|
+
if (startedOwnedProcess) {
|
|
336
|
+
killProcessTree(managedServerProc);
|
|
337
|
+
}
|
|
338
|
+
const startupErrorTail = readLogTail(serverErrPath);
|
|
339
|
+
const shouldRepairSqlite = startedOwnedProcess
|
|
340
|
+
&& !attemptedSqliteRepair
|
|
341
|
+
&& hasBetterSqliteAbiMismatch(startupErrorTail);
|
|
342
|
+
if (shouldRepairSqlite) {
|
|
343
|
+
attemptedSqliteRepair = true;
|
|
344
|
+
const repairResult = await rebuildBetterSqlite3();
|
|
345
|
+
if (repairResult.ok) {
|
|
346
|
+
restartAttempts = 0;
|
|
347
|
+
dbg("native module repair succeeded", "better-sqlite3 rebuilt for current Node runtime");
|
|
348
|
+
} else {
|
|
349
|
+
dbg(
|
|
350
|
+
"native module repair failed",
|
|
351
|
+
`code=${repairResult.code ?? "null"}`,
|
|
352
|
+
repairResult.error || "unknown error",
|
|
353
|
+
(repairResult.output || "").slice(-400),
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
closeManagedServerStreams();
|
|
358
|
+
managedServerProc = null;
|
|
359
|
+
managedServerOwned = false;
|
|
360
|
+
if (desiredRunning && !stoppingManagedServer && error?.relayRetryable !== false) {
|
|
361
|
+
scheduleManagedServerRestart("start-failed");
|
|
362
|
+
}
|
|
363
|
+
throw error;
|
|
364
|
+
}
|
|
365
|
+
})().finally(() => {
|
|
366
|
+
managedServerStartPromise = null;
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
return managedServerStartPromise;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function killManagedProcessTree() {
|
|
373
|
+
desiredRunning = false;
|
|
374
|
+
stoppingManagedServer = true;
|
|
375
|
+
clearRestartTimer();
|
|
376
|
+
if (!managedServerOwned) return;
|
|
377
|
+
if (persistentManagedServer && !legacyOwnerPidWatchdog) return;
|
|
378
|
+
killProcessTree(managedServerProc);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return {
|
|
382
|
+
ensureManagedServer,
|
|
383
|
+
stopManagedServer,
|
|
384
|
+
killManagedProcessTree,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import {
|
|
2
|
+
applyPreviewInstructions,
|
|
3
|
+
createPreviewInstructionsProvider,
|
|
4
|
+
} from "../../../../shared/preview-instructions.mjs";
|
|
5
|
+
|
|
6
|
+
export { createPreviewInstructionsProvider };
|
|
7
|
+
|
|
8
|
+
export function buildPrompt(message) {
|
|
9
|
+
const text = String(message?.text || "").trim();
|
|
10
|
+
const attachments = Array.isArray(message?.attachments) ? message.attachments : [];
|
|
11
|
+
if (!attachments.length) return text;
|
|
12
|
+
|
|
13
|
+
const attachmentNote = attachments.map((att) => {
|
|
14
|
+
const name = att?.name ? String(att.name) : "attachment";
|
|
15
|
+
const type = att?.type ? String(att.type) : "file";
|
|
16
|
+
if (type.startsWith("image/")) return `${name} (${type}, inline image attachment)`;
|
|
17
|
+
const reference = att?.path ? ` @${att.path}` : "";
|
|
18
|
+
return `${name} (${type})${reference}`;
|
|
19
|
+
}).join(", ");
|
|
20
|
+
const suffix = `\n\n[Attached file${attachments.length === 1 ? "" : "s"}: ${attachmentNote}]`;
|
|
21
|
+
return text ? `${text}${suffix}` : suffix.trimStart();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function normalizeRelayMode(mode) {
|
|
25
|
+
const value = String(mode || "agent").trim().toLowerCase();
|
|
26
|
+
if (value === "plan" || value === "ask" || value === "autopilot" || value === "agent") return value;
|
|
27
|
+
return "agent";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function buildModeInstructionText(mode) {
|
|
31
|
+
if (mode === "plan") {
|
|
32
|
+
return [
|
|
33
|
+
"Draft a concise plan only.",
|
|
34
|
+
"Use read-only inspection tools (glob, rg, view) only when they materially improve plan quality; otherwise draft from provided context.",
|
|
35
|
+
"Do not edit repository files or run mutating commands unless the user explicitly asks for implementation.",
|
|
36
|
+
"If clarification is required, pause and ask through the web relay.",
|
|
37
|
+
"These instructions remain in effect until relay mode changes.",
|
|
38
|
+
].join(" ");
|
|
39
|
+
}
|
|
40
|
+
if (mode === "ask") {
|
|
41
|
+
return [
|
|
42
|
+
"Prioritize clarification questions before doing any implementation work.",
|
|
43
|
+
"If the request is ambiguous or underspecified, pause and ask through the web relay before making assumptions.",
|
|
44
|
+
"Do not make broad assumptions when a question would materially change the result.",
|
|
45
|
+
"These instructions remain in effect until relay mode changes.",
|
|
46
|
+
].join(" ");
|
|
47
|
+
}
|
|
48
|
+
if (mode === "autopilot") {
|
|
49
|
+
return [
|
|
50
|
+
"Act directly on the request and use tools when needed.",
|
|
51
|
+
"Keep moving unless user input is truly blocking.",
|
|
52
|
+
"These instructions remain in effect until relay mode changes.",
|
|
53
|
+
].join(" ");
|
|
54
|
+
}
|
|
55
|
+
return [
|
|
56
|
+
"Proceed as an interactive coding agent and use tools as needed.",
|
|
57
|
+
"If you need clarification, pause and ask through the web relay instead of stalling silently.",
|
|
58
|
+
"These instructions remain in effect until relay mode changes.",
|
|
59
|
+
].join(" ");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function buildModeMarker(mode) {
|
|
63
|
+
return `[Relay mode: ${normalizeRelayMode(mode)}]`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function buildModePrompt(mode, toolInstructions = "", options = {}) {
|
|
67
|
+
const normalizedMode = normalizeRelayMode(mode);
|
|
68
|
+
const includeInstructions = options?.includeInstructions !== false;
|
|
69
|
+
const parts = [buildModeMarker(normalizedMode)];
|
|
70
|
+
if (includeInstructions) {
|
|
71
|
+
parts.push(buildModeInstructionText(normalizedMode));
|
|
72
|
+
if (toolInstructions) {
|
|
73
|
+
parts.push(toolInstructions);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return parts.join(" ");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function buildPromptWithMode(message, toolInstructions = "", options = {}) {
|
|
80
|
+
const modePrompt = buildModePrompt(message?.relayMode, toolInstructions, options);
|
|
81
|
+
const body = buildPrompt(message);
|
|
82
|
+
return [modePrompt, body].filter(Boolean).join(" ");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The relay's prompt prefix builder: tool guidance rides along only when the
|
|
87
|
+
* relay mode changed, so repeat turns in one mode stay cheap.
|
|
88
|
+
*/
|
|
89
|
+
export function createRelayPromptBuilder({
|
|
90
|
+
toolInstructions = "",
|
|
91
|
+
getPreviewInstructions = null,
|
|
92
|
+
} = {}) {
|
|
93
|
+
let lastPromptedRelayMode = null;
|
|
94
|
+
return async function buildPromptWithRelayContext(message) {
|
|
95
|
+
const relayMode = normalizeRelayMode(message?.relayMode);
|
|
96
|
+
const includeInstructions = lastPromptedRelayMode !== relayMode;
|
|
97
|
+
let instructions = "";
|
|
98
|
+
if (includeInstructions) {
|
|
99
|
+
// Guidance is advisory: a preview lookup that fails must cost the block,
|
|
100
|
+
// never the turn.
|
|
101
|
+
const previewBlock = typeof getPreviewInstructions === "function"
|
|
102
|
+
? await Promise.resolve().then(getPreviewInstructions).catch(() => "")
|
|
103
|
+
: "";
|
|
104
|
+
instructions = applyPreviewInstructions(toolInstructions, previewBlock);
|
|
105
|
+
}
|
|
106
|
+
const prompt = buildPromptWithMode({ ...message, relayMode }, instructions, { includeInstructions });
|
|
107
|
+
lastPromptedRelayMode = relayMode;
|
|
108
|
+
return prompt;
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function stripPromptPrefix(text, promptPrefix = "") {
|
|
113
|
+
const value = String(text || "").trim();
|
|
114
|
+
if (!value) return "";
|
|
115
|
+
const prompt = String(promptPrefix || "").trim();
|
|
116
|
+
if (!prompt) return value;
|
|
117
|
+
if (!value.startsWith(prompt)) return value;
|
|
118
|
+
const remainder = value.slice(prompt.length).trim();
|
|
119
|
+
return remainder || "";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function escapeRegExp(value) {
|
|
123
|
+
return String(value || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function whitespaceFlexiblePattern(value) {
|
|
127
|
+
return String(value || "")
|
|
128
|
+
.trim()
|
|
129
|
+
.split(/\s+/)
|
|
130
|
+
.filter(Boolean)
|
|
131
|
+
.map((part) => escapeRegExp(part))
|
|
132
|
+
.join("\\s+");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function stripPromptPrefixFlexible(text, promptPrefix = "") {
|
|
136
|
+
const value = String(text || "").trim();
|
|
137
|
+
const prompt = String(promptPrefix || "").trim();
|
|
138
|
+
if (!value || !prompt) return value;
|
|
139
|
+
const pattern = new RegExp(`^${whitespaceFlexiblePattern(prompt)}\\s*`, "i");
|
|
140
|
+
const stripped = value.replace(pattern, "").trim();
|
|
141
|
+
return stripped || "";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function stripPromptContextPrefix(text, message, toolInstructions = "", promptPrefix = "") {
|
|
145
|
+
const value = String(text || "").trim();
|
|
146
|
+
if (!value) return "";
|
|
147
|
+
const candidates = [
|
|
148
|
+
String(promptPrefix || "").trim(),
|
|
149
|
+
buildPromptWithMode(message, toolInstructions),
|
|
150
|
+
buildPromptWithMode(message, toolInstructions, { includeInstructions: false }),
|
|
151
|
+
buildModeMarker(message?.relayMode),
|
|
152
|
+
].filter(Boolean);
|
|
153
|
+
for (const candidate of candidates) {
|
|
154
|
+
const stripped = stripPromptPrefix(value, candidate);
|
|
155
|
+
if (stripped !== value) return stripped;
|
|
156
|
+
const flexibleStripped = stripPromptPrefixFlexible(value, candidate);
|
|
157
|
+
if (flexibleStripped !== value) return flexibleStripped;
|
|
158
|
+
}
|
|
159
|
+
return value;
|
|
160
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { getActiveSession } from '../runtime/session-registry.mjs';
|
|
2
|
+
import { QUESTION_TIMEOUT_CONTINUATION_TEXT } from "../../../../shared/question-timeout.mjs";
|
|
3
|
+
import { extractRequestedSchema } from "../../../../shared/question-schema.mjs";
|
|
4
|
+
|
|
5
|
+
export function createQuestionBridge({
|
|
6
|
+
api,
|
|
7
|
+
dbg,
|
|
8
|
+
sleep,
|
|
9
|
+
questionWaitTimeoutMs,
|
|
10
|
+
getQuestionWaitTimeoutMs,
|
|
11
|
+
questionPollMs,
|
|
12
|
+
getActiveMessage,
|
|
13
|
+
extractQuestionPrompt,
|
|
14
|
+
extractQuestionChoices,
|
|
15
|
+
serializeRequest,
|
|
16
|
+
}) {
|
|
17
|
+
function firstDefined(...values) {
|
|
18
|
+
for (const value of values) {
|
|
19
|
+
if (value !== undefined && value !== null) return value;
|
|
20
|
+
}
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function extractAllowFreeform(request) {
|
|
25
|
+
const value = firstDefined(
|
|
26
|
+
request?.allow_freeform,
|
|
27
|
+
request?.allowFreeform,
|
|
28
|
+
request?.toolArgs?.allow_freeform,
|
|
29
|
+
request?.toolArgs?.allowFreeform,
|
|
30
|
+
request?.input?.allow_freeform,
|
|
31
|
+
request?.input?.allowFreeform,
|
|
32
|
+
request?.arguments?.allow_freeform,
|
|
33
|
+
request?.arguments?.allowFreeform,
|
|
34
|
+
);
|
|
35
|
+
if (value === undefined) return undefined;
|
|
36
|
+
return !!value;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function resolveQuestionWaitTimeoutMs(timeoutMs = null) {
|
|
40
|
+
if (timeoutMs !== null && timeoutMs !== undefined) {
|
|
41
|
+
const requested = Number(timeoutMs);
|
|
42
|
+
if (Number.isFinite(requested) && requested >= 0) return requested;
|
|
43
|
+
}
|
|
44
|
+
if (typeof getQuestionWaitTimeoutMs === "function") {
|
|
45
|
+
const dynamic = Number(getQuestionWaitTimeoutMs());
|
|
46
|
+
if (Number.isFinite(dynamic) && dynamic >= 0) return dynamic;
|
|
47
|
+
}
|
|
48
|
+
const fallback = Number(questionWaitTimeoutMs);
|
|
49
|
+
return Number.isFinite(fallback) && fallback >= 0 ? fallback : 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function waitForRelayQuestionAnswer(questionId, timeoutMs = null) {
|
|
53
|
+
const started = Date.now();
|
|
54
|
+
const effectiveTimeoutMs = resolveQuestionWaitTimeoutMs(timeoutMs);
|
|
55
|
+
|
|
56
|
+
while (true) {
|
|
57
|
+
const { question } = await api("GET", `/api/relay-question/${questionId}`);
|
|
58
|
+
if (!question) throw new Error("Relay question missing");
|
|
59
|
+
if (question.status === "answered") {
|
|
60
|
+
return {
|
|
61
|
+
answer: String(question.answer || ""),
|
|
62
|
+
structuredAnswer: question.structuredAnswer && typeof question.structuredAnswer === "object"
|
|
63
|
+
? question.structuredAnswer
|
|
64
|
+
: null,
|
|
65
|
+
timedOut: false,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (question.status === "timed_out" || question.status === "cancelled") {
|
|
69
|
+
return {
|
|
70
|
+
answer: QUESTION_TIMEOUT_CONTINUATION_TEXT,
|
|
71
|
+
structuredAnswer: null,
|
|
72
|
+
timedOut: true,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
if (Date.now() - started >= effectiveTimeoutMs) {
|
|
76
|
+
await api("POST", `/api/relay-question/${questionId}/timeout`, {}).catch(() => {});
|
|
77
|
+
return {
|
|
78
|
+
answer: QUESTION_TIMEOUT_CONTINUATION_TEXT,
|
|
79
|
+
structuredAnswer: null,
|
|
80
|
+
timedOut: true,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
await sleep(questionPollMs);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function forwardRelayQuestion(request) {
|
|
88
|
+
const activeMsg = getActiveMessage();
|
|
89
|
+
const choices = extractQuestionChoices(request);
|
|
90
|
+
const allowFreeform = extractAllowFreeform(request);
|
|
91
|
+
const activeSession = getActiveSession();
|
|
92
|
+
const questionTimeoutMs = resolveQuestionWaitTimeoutMs();
|
|
93
|
+
const requestedSchema = extractRequestedSchema(request);
|
|
94
|
+
const questionPayload = {
|
|
95
|
+
queueId: activeMsg?.id,
|
|
96
|
+
messageId: activeMsg?.id,
|
|
97
|
+
conversationId: activeMsg?.conversationId,
|
|
98
|
+
mode: activeMsg?.relayMode || "agent",
|
|
99
|
+
prompt: extractQuestionPrompt(request),
|
|
100
|
+
choices,
|
|
101
|
+
allowFreeform: allowFreeform ?? !choices.length,
|
|
102
|
+
requestedSchema: requestedSchema || undefined,
|
|
103
|
+
sdk_session_id: activeSession?.sdkSessionId || undefined,
|
|
104
|
+
timeout_ms: questionTimeoutMs,
|
|
105
|
+
context: {
|
|
106
|
+
source: "onUserInputRequest",
|
|
107
|
+
rationale: "Agent requested clarification to continue this turn.",
|
|
108
|
+
queueMessageId: activeMsg?.id || null,
|
|
109
|
+
conversationId: activeMsg?.conversationId || null,
|
|
110
|
+
relayMode: activeMsg?.relayMode || "agent",
|
|
111
|
+
},
|
|
112
|
+
request: serializeRequest(request),
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const created = await api("POST", "/api/relay-question", questionPayload);
|
|
116
|
+
const questionId = created?.question?.id;
|
|
117
|
+
if (!questionId) {
|
|
118
|
+
throw new Error("Relay question could not be created");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
dbg(
|
|
122
|
+
"relay question created",
|
|
123
|
+
questionId,
|
|
124
|
+
"for msgId",
|
|
125
|
+
activeMsg.id,
|
|
126
|
+
"prompt=",
|
|
127
|
+
questionPayload.prompt,
|
|
128
|
+
"choices=",
|
|
129
|
+
String(questionPayload.choices?.length || 0),
|
|
130
|
+
"fields=",
|
|
131
|
+
String(requestedSchema?.properties ? Object.keys(requestedSchema.properties).length : 0),
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
return waitForRelayQuestionAnswer(questionId, questionTimeoutMs);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
forwardRelayQuestion,
|
|
139
|
+
waitForRelayQuestionAnswer,
|
|
140
|
+
};
|
|
141
|
+
}
|