@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,131 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_QUESTION_TIMEOUT_MS,
|
|
3
|
+
QUESTION_TIMEOUT_CONTINUATION_TEXT,
|
|
4
|
+
} from './question-timeout.mjs';
|
|
5
|
+
|
|
6
|
+
function sleepDefault(ms) {
|
|
7
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function normalizeQuestions(input) {
|
|
11
|
+
const questions = Array.isArray(input?.questions) ? input.questions : [];
|
|
12
|
+
return questions
|
|
13
|
+
.map((entry) => ({
|
|
14
|
+
question: String(entry?.question || '').trim(),
|
|
15
|
+
// The SDK joins answers back by EXACT question text; a model question
|
|
16
|
+
// with stray whitespace must still match its answer key.
|
|
17
|
+
rawQuestion: String(entry?.question || ''),
|
|
18
|
+
header: String(entry?.header || '').trim(),
|
|
19
|
+
multiSelect: entry?.multiSelect === true,
|
|
20
|
+
options: (Array.isArray(entry?.options) ? entry.options : [])
|
|
21
|
+
.map((option) => ({
|
|
22
|
+
label: String(option?.label || '').trim(),
|
|
23
|
+
description: String(option?.description || '').trim(),
|
|
24
|
+
}))
|
|
25
|
+
.filter((option) => option.label),
|
|
26
|
+
}))
|
|
27
|
+
.filter((entry) => entry.question);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Bridge a provider worker's ask-user tool onto the relay question cards.
|
|
32
|
+
*
|
|
33
|
+
* `handleAskUserQuestion(input, { signal })` posts one relay question per
|
|
34
|
+
* question entry, waits for the answers, and returns the collected `answers`
|
|
35
|
+
* map (question text -> answer string). Provider workers identify themselves
|
|
36
|
+
* via `questionSource` / `questionRationale` (defaults preserve the Claude
|
|
37
|
+
* worker's original wire payload).
|
|
38
|
+
*/
|
|
39
|
+
export function createAskUserBridge({
|
|
40
|
+
api,
|
|
41
|
+
getActiveMessage,
|
|
42
|
+
sdkSessionId = '',
|
|
43
|
+
sleep = sleepDefault,
|
|
44
|
+
questionPollMs = 1500,
|
|
45
|
+
questionTimeoutMs = DEFAULT_QUESTION_TIMEOUT_MS,
|
|
46
|
+
questionSource = 'AskUserQuestion',
|
|
47
|
+
questionRationale = 'Claude requested clarification to continue this turn.',
|
|
48
|
+
dbg = () => {},
|
|
49
|
+
} = {}) {
|
|
50
|
+
async function waitForRelayQuestionAnswer(questionId, { signal } = {}) {
|
|
51
|
+
const started = Date.now();
|
|
52
|
+
while (true) {
|
|
53
|
+
if (signal?.aborted) {
|
|
54
|
+
await api('POST', `/api/relay-question/${questionId}/timeout`, {}).catch(() => {});
|
|
55
|
+
return { answer: QUESTION_TIMEOUT_CONTINUATION_TEXT, timedOut: true, aborted: true };
|
|
56
|
+
}
|
|
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 { answer: String(question.answer || '').trim(), timedOut: false };
|
|
61
|
+
}
|
|
62
|
+
if (question.status === 'timed_out' || question.status === 'cancelled') {
|
|
63
|
+
return { answer: QUESTION_TIMEOUT_CONTINUATION_TEXT, timedOut: true };
|
|
64
|
+
}
|
|
65
|
+
if (Date.now() - started >= questionTimeoutMs) {
|
|
66
|
+
await api('POST', `/api/relay-question/${questionId}/timeout`, {}).catch(() => {});
|
|
67
|
+
return { answer: QUESTION_TIMEOUT_CONTINUATION_TEXT, timedOut: true };
|
|
68
|
+
}
|
|
69
|
+
await sleep(questionPollMs);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function askSingleQuestion(entry, { signal } = {}) {
|
|
74
|
+
const activeMsg = typeof getActiveMessage === 'function' ? getActiveMessage() : null;
|
|
75
|
+
const choices = entry.options.map((option) => option.label);
|
|
76
|
+
const promptParts = [entry.question];
|
|
77
|
+
const optionDetails = entry.options
|
|
78
|
+
.filter((option) => option.description)
|
|
79
|
+
.map((option) => `- ${option.label}: ${option.description}`);
|
|
80
|
+
if (optionDetails.length) promptParts.push(optionDetails.join('\n'));
|
|
81
|
+
const questionPayload = {
|
|
82
|
+
queueId: activeMsg?.id,
|
|
83
|
+
messageId: activeMsg?.id,
|
|
84
|
+
conversationId: activeMsg?.conversationId,
|
|
85
|
+
mode: activeMsg?.relayMode || 'agent',
|
|
86
|
+
prompt: promptParts.join('\n\n'),
|
|
87
|
+
choices,
|
|
88
|
+
allowFreeform: true,
|
|
89
|
+
sdk_session_id: sdkSessionId || undefined,
|
|
90
|
+
timeout_ms: questionTimeoutMs,
|
|
91
|
+
context: {
|
|
92
|
+
source: questionSource,
|
|
93
|
+
rationale: questionRationale,
|
|
94
|
+
queueMessageId: activeMsg?.id || null,
|
|
95
|
+
conversationId: activeMsg?.conversationId || null,
|
|
96
|
+
relayMode: activeMsg?.relayMode || 'agent',
|
|
97
|
+
header: entry.header || undefined,
|
|
98
|
+
multiSelect: entry.multiSelect || undefined,
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
const created = await api('POST', '/api/relay-question', questionPayload);
|
|
102
|
+
const questionId = created?.question?.id;
|
|
103
|
+
if (!questionId) throw new Error('Relay question could not be created');
|
|
104
|
+
dbg('relay question created', questionId, 'prompt=', entry.question.slice(0, 80));
|
|
105
|
+
return waitForRelayQuestionAnswer(questionId, { signal });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function handleAskUserQuestion(input, { signal } = {}) {
|
|
109
|
+
const questions = normalizeQuestions(input);
|
|
110
|
+
if (!questions.length) {
|
|
111
|
+
return { answers: {}, timedOut: false };
|
|
112
|
+
}
|
|
113
|
+
const answers = {};
|
|
114
|
+
let timedOut = false;
|
|
115
|
+
for (const entry of questions) {
|
|
116
|
+
const result = await askSingleQuestion(entry, { signal });
|
|
117
|
+
answers[entry.question] = result.answer;
|
|
118
|
+
if (entry.rawQuestion && entry.rawQuestion !== entry.question) {
|
|
119
|
+
answers[entry.rawQuestion] = result.answer;
|
|
120
|
+
}
|
|
121
|
+
if (result.timedOut) timedOut = true;
|
|
122
|
+
if (result.aborted) break;
|
|
123
|
+
}
|
|
124
|
+
return { answers, timedOut };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
handleAskUserQuestion,
|
|
129
|
+
waitForRelayQuestionAnswer,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// The per-conversation auto-compact window: how many tokens of context the
|
|
2
|
+
// Claude CLI is allowed to accumulate before it compacts the conversation.
|
|
3
|
+
//
|
|
4
|
+
// This is a TOKEN COUNT, not a percentage — `Settings.autoCompactWindow` in the
|
|
5
|
+
// bundled CLI is compared directly against the model's context window, and the
|
|
6
|
+
// effective threshold is `min(setting, model max)`. Unset (null) means "Auto":
|
|
7
|
+
// the CLI keeps its model-tuned default (e.g. 967k on a 1M-window model), which
|
|
8
|
+
// is what `autoCompactThreshold` in the context-usage payload reports.
|
|
9
|
+
//
|
|
10
|
+
// Shared so the slider, the preferences write path and the worker all agree on
|
|
11
|
+
// the same discrete stops; the browser mirrors these constants in
|
|
12
|
+
// server/public/app/auto-compact-window-options.mjs (only server/public is
|
|
13
|
+
// served) and a unit test asserts the two lists stay identical.
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The CLI silently refuses windows below 100k: it does not merely decline to
|
|
17
|
+
* compact, it drops the value and resolves back to the model default. Probed
|
|
18
|
+
* against the bundled CLI on 2026-08-20 (`claude-opus-5[1m]`):
|
|
19
|
+
*
|
|
20
|
+
* setting 50_000 -> maxTokens 1_000_000, source 'auto' (ignored)
|
|
21
|
+
* setting 60_000 -> maxTokens 1_000_000, source 'auto' (ignored)
|
|
22
|
+
* setting 100_000 -> maxTokens 100_000, source 'settings' (applied)
|
|
23
|
+
*
|
|
24
|
+
* The bundled CLI carries a matching 1e5 constant beside its auto-compact
|
|
25
|
+
* resolver. Offering a smaller stop would put a number on the slider that
|
|
26
|
+
* changes nothing, so the floor is enforced here instead.
|
|
27
|
+
*/
|
|
28
|
+
export const AUTO_COMPACT_WINDOW_MIN_TOKENS = 100_000;
|
|
29
|
+
|
|
30
|
+
/** Index 0 is `null` = Auto (model default). Every other stop is a token count. */
|
|
31
|
+
export const AUTO_COMPACT_WINDOW_STOPS = Object.freeze([
|
|
32
|
+
null,
|
|
33
|
+
100_000,
|
|
34
|
+
150_000,
|
|
35
|
+
200_000,
|
|
36
|
+
300_000,
|
|
37
|
+
500_000,
|
|
38
|
+
1_000_000,
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
const NUMERIC_STOPS = AUTO_COMPACT_WINDOW_STOPS.filter((stop) => stop !== null);
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Normalize any stored/wire value onto a stop.
|
|
45
|
+
*
|
|
46
|
+
* Absent, blank, 'auto', <= 0 and junk all mean Auto (null) — there is no
|
|
47
|
+
* "disabled" state, so anything unusable falls back to the model default rather
|
|
48
|
+
* than pinning an arbitrary window.
|
|
49
|
+
*/
|
|
50
|
+
export function parseAutoCompactWindow(value) {
|
|
51
|
+
if (value === null || value === undefined) return null;
|
|
52
|
+
if (typeof value === 'string') {
|
|
53
|
+
const text = value.trim().toLowerCase();
|
|
54
|
+
if (!text || text === 'auto' || text === 'null') return null;
|
|
55
|
+
}
|
|
56
|
+
const numeric = Number(value);
|
|
57
|
+
if (!Number.isFinite(numeric) || numeric <= 0) return null;
|
|
58
|
+
// Snap to the nearest stop so a hand-edited row or an older client's value
|
|
59
|
+
// still lands on something the slider can represent.
|
|
60
|
+
let best = NUMERIC_STOPS[0];
|
|
61
|
+
let bestDistance = Math.abs(numeric - best);
|
|
62
|
+
for (const stop of NUMERIC_STOPS) {
|
|
63
|
+
const distance = Math.abs(numeric - stop);
|
|
64
|
+
if (distance < bestDistance) {
|
|
65
|
+
best = stop;
|
|
66
|
+
bestDistance = distance;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return best;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** 'Auto' | '100k' | '1M' — the slider's own label, so it must stay short. */
|
|
73
|
+
export function formatAutoCompactWindowLabel(value) {
|
|
74
|
+
const window = parseAutoCompactWindow(value);
|
|
75
|
+
if (window === null) return 'Auto';
|
|
76
|
+
if (window >= 1_000_000) {
|
|
77
|
+
const millions = window / 1_000_000;
|
|
78
|
+
return `${Number.isInteger(millions) ? millions : millions.toFixed(1)}M`;
|
|
79
|
+
}
|
|
80
|
+
return `${Math.round(window / 1000)}k`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function autoCompactWindowToIndex(value) {
|
|
84
|
+
const window = parseAutoCompactWindow(value);
|
|
85
|
+
const index = AUTO_COMPACT_WINDOW_STOPS.indexOf(window);
|
|
86
|
+
return index >= 0 ? index : 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* What a queue delivery's `settings` should do to the worker's current window.
|
|
91
|
+
*
|
|
92
|
+
* Old relays omit `autoCompactWindow` entirely; that must keep whatever the
|
|
93
|
+
* worker already had rather than silently resetting the pin. An explicit
|
|
94
|
+
* `null` (or anything unusable) is the user choosing Auto and must clear it —
|
|
95
|
+
* so the key's *presence*, not its truthiness, decides. A truthiness check
|
|
96
|
+
* here would collapse both cases and break "return to Auto".
|
|
97
|
+
*
|
|
98
|
+
* Not snapped: the worker forwards the value to the CLI as-is, and the write
|
|
99
|
+
* path already snapped it.
|
|
100
|
+
*
|
|
101
|
+
* @param {number|null} current the worker's last known window
|
|
102
|
+
* @param {object|null|undefined} settings the delivery's settings bag
|
|
103
|
+
* @returns {number|null}
|
|
104
|
+
*/
|
|
105
|
+
export function resolveDeliveredAutoCompactWindow(current, settings) {
|
|
106
|
+
if (!settings || typeof settings !== 'object') return current ?? null;
|
|
107
|
+
if (!Object.prototype.hasOwnProperty.call(settings, 'autoCompactWindow')) return current ?? null;
|
|
108
|
+
const delivered = Number(settings.autoCompactWindow);
|
|
109
|
+
return Number.isFinite(delivered) && delivered > 0 ? Math.round(delivered) : null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function autoCompactWindowFromIndex(index) {
|
|
113
|
+
const i = Math.round(Number(index));
|
|
114
|
+
if (!Number.isFinite(i) || i < 0 || i >= AUTO_COMPACT_WINDOW_STOPS.length) return null;
|
|
115
|
+
return AUTO_COMPACT_WINDOW_STOPS[i];
|
|
116
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Ceiling on how long background tasks alone may keep a conversation's
|
|
2
|
+
// persistent Claude CLI process alive, shared so the settings slider and the
|
|
3
|
+
// worker enforcing it agree on bounds and formatting.
|
|
4
|
+
//
|
|
5
|
+
// 0 means no limit — the default: a process holding live tasks stays up until
|
|
6
|
+
// the tasks finish, the user stops them from the composer panel, or the
|
|
7
|
+
// worker/relay goes down. Mirrors shared/turn-ceiling.mjs.
|
|
8
|
+
|
|
9
|
+
export const BACKGROUND_TASK_TIMEOUT_MIN_MINUTES = 0;
|
|
10
|
+
export const BACKGROUND_TASK_TIMEOUT_MAX_MINUTES = 600;
|
|
11
|
+
export const BACKGROUND_TASK_TIMEOUT_STEP_MINUTES = 5;
|
|
12
|
+
export const DEFAULT_BACKGROUND_TASK_TIMEOUT_MINUTES = 0;
|
|
13
|
+
|
|
14
|
+
/** 0 disables the timeout entirely (unlimited). */
|
|
15
|
+
export function normalizeBackgroundTaskTimeoutMinutes(value, fallback = DEFAULT_BACKGROUND_TASK_TIMEOUT_MINUTES) {
|
|
16
|
+
// Number('') and Number(null) are both 0, which would silently read as
|
|
17
|
+
// "no limit". Absent input is absent, not a request to disable the timeout.
|
|
18
|
+
if (value === null || value === undefined) return fallback;
|
|
19
|
+
if (typeof value === 'string' && value.trim() === '') return fallback;
|
|
20
|
+
const numeric = Number(value);
|
|
21
|
+
if (!Number.isFinite(numeric)) return fallback;
|
|
22
|
+
const rounded = Math.round(numeric);
|
|
23
|
+
if (rounded <= BACKGROUND_TASK_TIMEOUT_MIN_MINUTES) return 0;
|
|
24
|
+
return Math.min(BACKGROUND_TASK_TIMEOUT_MAX_MINUTES, rounded);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function backgroundTaskTimeoutMinutesToMs(minutes) {
|
|
28
|
+
return normalizeBackgroundTaskTimeoutMinutes(minutes) * 60_000;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** An unset setting means "never configured", which is the default — not 0. */
|
|
32
|
+
export function readBackgroundTaskTimeoutSetting(storedValue) {
|
|
33
|
+
const text = storedValue === null || storedValue === undefined ? '' : String(storedValue).trim();
|
|
34
|
+
if (!text) return DEFAULT_BACKGROUND_TASK_TIMEOUT_MINUTES;
|
|
35
|
+
return normalizeBackgroundTaskTimeoutMinutes(text);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function parseBackgroundTaskTimeoutUpdate(value) {
|
|
39
|
+
const minutes = normalizeBackgroundTaskTimeoutMinutes(value, null);
|
|
40
|
+
if (minutes === null) return { ok: false, error: 'timeoutMinutes must be a number' };
|
|
41
|
+
return { ok: true, minutes };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function formatBackgroundTaskTimeoutLabel(minutes) {
|
|
45
|
+
const normalized = normalizeBackgroundTaskTimeoutMinutes(minutes);
|
|
46
|
+
if (normalized === 0) return 'No limit';
|
|
47
|
+
if (normalized < 60) return `${normalized} min`;
|
|
48
|
+
const hours = Math.floor(normalized / 60);
|
|
49
|
+
const remainder = normalized % 60;
|
|
50
|
+
const hourLabel = `${hours} h`;
|
|
51
|
+
return remainder ? `${hourLabel} ${remainder} min` : hourLabel;
|
|
52
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Per-conversation thinking control for the Claude worker: whether the CLI's
|
|
2
|
+
// extended thinking runs, and whether its text is visible in the transcript.
|
|
3
|
+
//
|
|
4
|
+
// THE RELAY'S DEFAULTS ARE ON + SUMMARIZED. copilot-remote deliberately takes
|
|
5
|
+
// a position here rather than deferring to the CLI: a relay you read on a
|
|
6
|
+
// phone is a place where seeing the model's reasoning is the point.
|
|
7
|
+
//
|
|
8
|
+
// There is deliberately no "host default" state on either axis:
|
|
9
|
+
// - The worker sets no `settingSources`, so it loads no filesystem settings
|
|
10
|
+
// — "host" only ever meant the CLI's built-in default, which is itself ON
|
|
11
|
+
// for supported models. It was near-synonymous with On even before this.
|
|
12
|
+
// - A host-default state has to be stored as SQL NULL, which is
|
|
13
|
+
// indistinguishable from "never set". Once NULL resolves to the relay
|
|
14
|
+
// default, picking "host default" would silently do nothing.
|
|
15
|
+
// So both axes are two-state, and NULL in the DB means "never set → relay
|
|
16
|
+
// default".
|
|
17
|
+
//
|
|
18
|
+
// Probed against the bundled CLI 2.1.226 / SDK 0.3.226 on 2026-08-26
|
|
19
|
+
// (docs/plans/claude-thinking-control.md → Probe results):
|
|
20
|
+
//
|
|
21
|
+
// - `enabled` — `Settings.alwaysThinkingEnabled`, honored at SPAWN in both
|
|
22
|
+
// directions, but mid-session only ENABLING works: a mid-session `false` is
|
|
23
|
+
// accepted and silently ignored, so turning thinking off takes effect at the
|
|
24
|
+
// next CLI spawn.
|
|
25
|
+
// - `display` — 'summarized' (a readable summary of the reasoning; the raw
|
|
26
|
+
// chain of thought is never exposed by the API on any setting) or 'omitted'
|
|
27
|
+
// (the block still arrives, with empty text). Applies live both ways via
|
|
28
|
+
// `setMaxThinkingTokens(budget, display)`. NOTE: display is visibility only
|
|
29
|
+
// — the model thinks, and bills, identically either way, so 'omitted' is not
|
|
30
|
+
// a cost control. `Thinking: Off` is.
|
|
31
|
+
//
|
|
32
|
+
// Shared so the browser control, the preferences write path and the worker
|
|
33
|
+
// agree; the browser mirror lives in
|
|
34
|
+
// server/public/app/claude-thinking-options.mjs (only server/public is served)
|
|
35
|
+
// and a unit test asserts the two stay identical.
|
|
36
|
+
|
|
37
|
+
/** Thinking runs unless the conversation explicitly turns it off. */
|
|
38
|
+
export const DEFAULT_THINKING_ENABLED = true;
|
|
39
|
+
|
|
40
|
+
/** Thoughts are visible unless the conversation explicitly hides them. */
|
|
41
|
+
export const DEFAULT_THINKING_DISPLAY = 'summarized';
|
|
42
|
+
|
|
43
|
+
export const THINKING_DISPLAY_MODES = Object.freeze(['summarized', 'omitted']);
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Normalize any stored/wire value onto the on/off state.
|
|
47
|
+
*
|
|
48
|
+
* Anything unusable — absent, NULL, junk, an older client's value — resolves
|
|
49
|
+
* to the relay default (on) rather than silently disabling thinking.
|
|
50
|
+
*/
|
|
51
|
+
export function parseThinkingEnabled(value) {
|
|
52
|
+
if (value === false || value === 0) return false;
|
|
53
|
+
if (typeof value === 'string') {
|
|
54
|
+
const text = value.trim().toLowerCase();
|
|
55
|
+
if (text === 'false' || text === '0' || text === 'off') return false;
|
|
56
|
+
if (text === 'true' || text === '1' || text === 'on') return true;
|
|
57
|
+
return DEFAULT_THINKING_ENABLED;
|
|
58
|
+
}
|
|
59
|
+
if (value === true || value === 1) return true;
|
|
60
|
+
return DEFAULT_THINKING_ENABLED;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 'summarized' | 'omitted'; anything unusable is the default, never hidden by accident. */
|
|
64
|
+
export function parseThinkingDisplay(value) {
|
|
65
|
+
const text = String(value ?? '').trim().toLowerCase();
|
|
66
|
+
return text === 'omitted' ? 'omitted' : DEFAULT_THINKING_DISPLAY;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Short label for the control ("On · visible", "Off · hidden"). */
|
|
70
|
+
export function formatThinkingLabel({ enabled = DEFAULT_THINKING_ENABLED, display = DEFAULT_THINKING_DISPLAY } = {}) {
|
|
71
|
+
const enabledLabel = parseThinkingEnabled(enabled) ? 'On' : 'Off';
|
|
72
|
+
const displayLabel = parseThinkingDisplay(display) === 'omitted' ? 'hidden' : 'visible';
|
|
73
|
+
return `${enabledLabel} · ${displayLabel}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* What a queue delivery's `settings` should do to the worker's current
|
|
78
|
+
* thinking state — the twin of `resolveDeliveredAutoCompactWindow`: an absent
|
|
79
|
+
* key (an older relay that does not send these yet) keeps the last known
|
|
80
|
+
* value, while a present key replaces it. Presence, not truthiness, decides —
|
|
81
|
+
* a truthiness check would read an explicit `false` as "not mentioned" and
|
|
82
|
+
* make turning thinking off impossible.
|
|
83
|
+
*
|
|
84
|
+
* @param {{enabled: boolean, display: string}|null} current
|
|
85
|
+
* @param {object|null|undefined} settings the delivery's settings bag
|
|
86
|
+
* @returns {{enabled: boolean, display: string}}
|
|
87
|
+
*/
|
|
88
|
+
export function resolveDeliveredThinking(current, settings) {
|
|
89
|
+
const base = {
|
|
90
|
+
enabled: parseThinkingEnabled(current?.enabled),
|
|
91
|
+
display: parseThinkingDisplay(current?.display),
|
|
92
|
+
};
|
|
93
|
+
if (!settings || typeof settings !== 'object') return base;
|
|
94
|
+
if (Object.prototype.hasOwnProperty.call(settings, 'thinkingEnabled')) {
|
|
95
|
+
base.enabled = parseThinkingEnabled(settings.thinkingEnabled);
|
|
96
|
+
}
|
|
97
|
+
if (Object.prototype.hasOwnProperty.call(settings, 'thinkingDisplay')) {
|
|
98
|
+
base.display = parseThinkingDisplay(settings.thinkingDisplay);
|
|
99
|
+
}
|
|
100
|
+
return base;
|
|
101
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// Static context-window fallbacks shared by the Copilot snapshot service, the
|
|
2
|
+
// Cursor worker and the Copilot SDK worker's BYOK provider config, so the
|
|
3
|
+
// tables cannot drift. Used only when the provider does not advertise a window
|
|
4
|
+
// itself (Copilot events without limits, Cursor models without a `context`
|
|
5
|
+
// parameter, an OpenAI-compatible endpoint the Copilot runtime does not
|
|
6
|
+
// recognise).
|
|
7
|
+
|
|
8
|
+
import { canonicalizeModelId } from './model-id.mjs';
|
|
9
|
+
|
|
10
|
+
// Context window of a model id carrying the `[1m]` capability suffix, whatever
|
|
11
|
+
// the base model's default window is.
|
|
12
|
+
export const LONG_CONTEXT_TOKENS = 1000000;
|
|
13
|
+
|
|
14
|
+
export const MODEL_FALLBACK_LIMITS = Object.freeze({
|
|
15
|
+
// Anthropic Claude Sonnet
|
|
16
|
+
'claude-sonnet-5': 200000,
|
|
17
|
+
'claude-sonnet-4.6': 200000,
|
|
18
|
+
'claude-sonnet-4.5': 200000,
|
|
19
|
+
// Anthropic Claude Haiku
|
|
20
|
+
'claude-haiku-4.5': 200000,
|
|
21
|
+
// Anthropic Claude Fable
|
|
22
|
+
'claude-fable-5': 200000,
|
|
23
|
+
// Anthropic Claude Opus
|
|
24
|
+
'claude-opus-5': 200000,
|
|
25
|
+
'claude-opus-4.8': 200000,
|
|
26
|
+
'claude-opus-4.7': 200000,
|
|
27
|
+
'claude-opus-4.6': 200000,
|
|
28
|
+
'claude-opus-4.6-fast': 200000,
|
|
29
|
+
'claude-opus-4.5': 200000,
|
|
30
|
+
// OpenAI GPT-5 series
|
|
31
|
+
'gpt-5.6-terra': 272000,
|
|
32
|
+
'gpt-5.6-luna': 272000,
|
|
33
|
+
'gpt-5.6-sol': 272000,
|
|
34
|
+
'gpt-5.5': 256000,
|
|
35
|
+
'gpt-5.4': 256000,
|
|
36
|
+
'gpt-5.3-codex': 256000,
|
|
37
|
+
'gpt-5.2-codex': 256000,
|
|
38
|
+
'gpt-5.2': 256000,
|
|
39
|
+
'gpt-5.4-mini': 256000,
|
|
40
|
+
'gpt-5-mini': 256000,
|
|
41
|
+
// Google Gemini
|
|
42
|
+
'gemini-3.1-pro-preview': 1000000,
|
|
43
|
+
'gemini-3.5-flash': 1000000,
|
|
44
|
+
// Cursor-served models without a `context` parameter in `models.list()`.
|
|
45
|
+
// Conservative published defaults; a provider-advertised window always wins.
|
|
46
|
+
'grok-4.5': 256000,
|
|
47
|
+
'composer-2.5': 200000,
|
|
48
|
+
// xAI models the Grok CLI serves when ACP discovery reports no window
|
|
49
|
+
// (public xAI docs values); also reachable through the Cursor catalog.
|
|
50
|
+
'grok-4': 256000,
|
|
51
|
+
'grok-code-fast-1': 256000,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Resolve a fallback context window for a model id.
|
|
56
|
+
*
|
|
57
|
+
* Model ids may carry a bracketed capability suffix (see `isSafeClaudeModelId`),
|
|
58
|
+
* e.g. `claude-opus-5[1m]`. Those are a different context window than the base
|
|
59
|
+
* model, so they are resolved first — falling through to the base entry would
|
|
60
|
+
* report a 1M session as 200k.
|
|
61
|
+
*/
|
|
62
|
+
export function resolveFallbackContextLimitTokens(modelId) {
|
|
63
|
+
const model = String(modelId || '').trim().toLowerCase();
|
|
64
|
+
if (!model) return null;
|
|
65
|
+
if (MODEL_FALLBACK_LIMITS[model]) return MODEL_FALLBACK_LIMITS[model];
|
|
66
|
+
const suffixMatch = model.match(/\[([^\]]+)\]\s*$/);
|
|
67
|
+
if (!suffixMatch) return null;
|
|
68
|
+
if (suffixMatch[1].trim() === '1m') return LONG_CONTEXT_TOKENS;
|
|
69
|
+
return MODEL_FALLBACK_LIMITS[model.slice(0, suffixMatch.index).trim()] || null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Max OUTPUT tokens per model family, matched by longest prefix.
|
|
74
|
+
*
|
|
75
|
+
* Separate from the windows above because the two are not interchangeable: the
|
|
76
|
+
* window is how much a request may total, this is how much of it the completion
|
|
77
|
+
* may claim. Only the OpenAI-compatible families a BYOK endpoint can serve are
|
|
78
|
+
* listed — an unlisted family resolves to no ceilings at all, which is the
|
|
79
|
+
* point (see `resolveModelTokenCeilings`).
|
|
80
|
+
*/
|
|
81
|
+
const MODEL_OUTPUT_TOKEN_LIMITS = Object.freeze([
|
|
82
|
+
Object.freeze({ prefix: 'gpt-4o', maxOutputTokens: 16_384 }),
|
|
83
|
+
Object.freeze({ prefix: 'gpt-4.1', maxOutputTokens: 32_768 }),
|
|
84
|
+
Object.freeze({ prefix: 'gpt-5', maxOutputTokens: 128_000 }),
|
|
85
|
+
Object.freeze({ prefix: 'codex-', maxOutputTokens: 128_000 }),
|
|
86
|
+
Object.freeze({ prefix: 'o3', maxOutputTokens: 100_000 }),
|
|
87
|
+
Object.freeze({ prefix: 'o4-', maxOutputTokens: 100_000 }),
|
|
88
|
+
]);
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Family-level context windows, matched by longest prefix, for ids
|
|
92
|
+
* `MODEL_FALLBACK_LIMITS` does not name individually.
|
|
93
|
+
*
|
|
94
|
+
* Deliberately NOT merged into that table: it is consulted by the Copilot
|
|
95
|
+
* snapshot service and the Cursor worker as an exact-id lookup for models those
|
|
96
|
+
* providers actually serve, and widening it to whole families would start
|
|
97
|
+
* answering questions those callers currently (correctly) answer with "no
|
|
98
|
+
* fallback". This table is only read through `resolveModelTokenCeilings`, whose
|
|
99
|
+
* one consumer is the BYOK provider config, and the exact-id table always wins.
|
|
100
|
+
*/
|
|
101
|
+
const MODEL_FAMILY_CONTEXT_WINDOWS = Object.freeze([
|
|
102
|
+
Object.freeze({ prefix: 'gpt-4o', contextWindow: 128_000 }),
|
|
103
|
+
Object.freeze({ prefix: 'gpt-4.1', contextWindow: 1_047_576 }),
|
|
104
|
+
// The gpt-5 family spans 256k (gpt-5.2/5.4/5.5 and the minis) and 272k (the
|
|
105
|
+
// 5.6 series); the exact-id table separates them. An id this module has never
|
|
106
|
+
// heard of gets the SMALLER of the two, because a window guessed too large is
|
|
107
|
+
// the harmful direction: it converts what would have been early compaction
|
|
108
|
+
// into a hard API rejection with the whole history intact.
|
|
109
|
+
Object.freeze({ prefix: 'gpt-5', contextWindow: 256_000 }),
|
|
110
|
+
Object.freeze({ prefix: 'codex-', contextWindow: 256_000 }),
|
|
111
|
+
Object.freeze({ prefix: 'o3', contextWindow: 200_000 }),
|
|
112
|
+
Object.freeze({ prefix: 'o4-', contextWindow: 200_000 }),
|
|
113
|
+
]);
|
|
114
|
+
|
|
115
|
+
function longestPrefixEntry(table, model) {
|
|
116
|
+
let best = null;
|
|
117
|
+
for (const entry of table) {
|
|
118
|
+
if (!model.startsWith(entry.prefix)) continue;
|
|
119
|
+
if (!best || entry.prefix.length > best.prefix.length) best = entry;
|
|
120
|
+
}
|
|
121
|
+
return best;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Resolve `{ contextWindow, maxPromptTokens, maxOutputTokens }` for a model id,
|
|
126
|
+
* or `{}` when this module cannot answer confidently.
|
|
127
|
+
*
|
|
128
|
+
* The prompt ceiling is DERIVED (`window − output`) rather than tabulated, so
|
|
129
|
+
* the two numbers can never contradict each other: a consumer compares the
|
|
130
|
+
* whole outbound prompt against `maxPromptTokens` before deciding to compact,
|
|
131
|
+
* and the completion still has to fit alongside it inside the same window.
|
|
132
|
+
*
|
|
133
|
+
* Returning `{}` for an unknown model is the deliberate answer, not a gap. A
|
|
134
|
+
* guessed ceiling that is too HIGH turns early compaction into hard API errors,
|
|
135
|
+
* whereas the consumer's own default is merely conservative.
|
|
136
|
+
*/
|
|
137
|
+
export function resolveModelTokenCeilings(modelId) {
|
|
138
|
+
const model = canonicalizeModelId(modelId);
|
|
139
|
+
if (!model) return {};
|
|
140
|
+
const maxOutputTokens = longestPrefixEntry(MODEL_OUTPUT_TOKEN_LIMITS, model)?.maxOutputTokens ?? null;
|
|
141
|
+
const contextWindow = resolveFallbackContextLimitTokens(model)
|
|
142
|
+
?? longestPrefixEntry(MODEL_FAMILY_CONTEXT_WINDOWS, model)?.contextWindow
|
|
143
|
+
?? null;
|
|
144
|
+
if (!contextWindow || !maxOutputTokens) return {};
|
|
145
|
+
const maxPromptTokens = contextWindow - maxOutputTokens;
|
|
146
|
+
if (!(maxPromptTokens > 0)) return {};
|
|
147
|
+
return { contextWindow, maxPromptTokens, maxOutputTokens };
|
|
148
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
function sleepDefault(ms) {
|
|
2
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Poll the relay's control API while a turn is in flight, mirroring the
|
|
7
|
+
* Copilot extension's `checkActiveAbortControl` semantics:
|
|
8
|
+
* - `abort_turn` → invoke `onAbortTurn()` and acknowledge the control.
|
|
9
|
+
* - `abort_subagent` → try `onAbortSubagent(subagentRunId)` when the worker
|
|
10
|
+
* provides one (the Claude worker can stop a BACKGROUNDED subagent via
|
|
11
|
+
* `query.stopTask` using the task↔tool_use_id map); otherwise — or when the
|
|
12
|
+
* handler reports the run unknown — answer "not supported". The full-turn
|
|
13
|
+
* Stop always works.
|
|
14
|
+
*
|
|
15
|
+
* Provider workers customize the acknowledgement note via `abortAckNote`.
|
|
16
|
+
*/
|
|
17
|
+
export function createControlPoller({
|
|
18
|
+
api,
|
|
19
|
+
sdkSessionId,
|
|
20
|
+
pollMs = 1200,
|
|
21
|
+
sleep = sleepDefault,
|
|
22
|
+
abortAckNote = 'query aborted',
|
|
23
|
+
onAbortSubagent = null,
|
|
24
|
+
dbg = () => {},
|
|
25
|
+
} = {}) {
|
|
26
|
+
let active = null;
|
|
27
|
+
|
|
28
|
+
async function checkOnce({ queueMessageId, onAbortTurn }) {
|
|
29
|
+
const ownerSessionId = String(sdkSessionId || '').trim();
|
|
30
|
+
if (!ownerSessionId) return false;
|
|
31
|
+
const pending = await api(
|
|
32
|
+
'GET',
|
|
33
|
+
`/api/control/active?sdkSessionId=${encodeURIComponent(ownerSessionId)}&queueMessageId=${encodeURIComponent(String(queueMessageId || ''))}`,
|
|
34
|
+
).catch(() => null);
|
|
35
|
+
const control = pending?.control || null;
|
|
36
|
+
const controlType = String(control?.type || '').trim();
|
|
37
|
+
if (!control || !controlType) return false;
|
|
38
|
+
|
|
39
|
+
if (controlType === 'abort_subagent') {
|
|
40
|
+
const subagentRunId = String(control.subagentRunId || control.subagent_run_id || '').trim();
|
|
41
|
+
if (typeof onAbortSubagent === 'function' && subagentRunId) {
|
|
42
|
+
try {
|
|
43
|
+
const stopped = await onAbortSubagent(subagentRunId);
|
|
44
|
+
if (stopped) {
|
|
45
|
+
await api('POST', `/api/control/${encodeURIComponent(control.id)}/result`, {
|
|
46
|
+
ok: true,
|
|
47
|
+
note: 'subagent task stopped',
|
|
48
|
+
}).catch(() => {});
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
} catch (error) {
|
|
52
|
+
dbg('abort_subagent handler failed', error?.message || String(error));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
await api('POST', `/api/control/${encodeURIComponent(control.id)}/result`, {
|
|
56
|
+
ok: false,
|
|
57
|
+
error: 'Targeted subagent cancellation is not supported by this runtime.',
|
|
58
|
+
}).catch(() => {});
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (controlType !== 'abort_turn') return false;
|
|
63
|
+
|
|
64
|
+
dbg('abort_turn control received', control.id);
|
|
65
|
+
try {
|
|
66
|
+
await onAbortTurn();
|
|
67
|
+
await api('POST', `/api/control/${encodeURIComponent(control.id)}/result`, {
|
|
68
|
+
ok: true,
|
|
69
|
+
note: abortAckNote,
|
|
70
|
+
}).catch(() => {});
|
|
71
|
+
return true;
|
|
72
|
+
} catch (error) {
|
|
73
|
+
await api('POST', `/api/control/${encodeURIComponent(control.id)}/result`, {
|
|
74
|
+
ok: false,
|
|
75
|
+
error: String(error?.message || error || 'abort failed'),
|
|
76
|
+
}).catch(() => {});
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function start({ queueMessageId, onAbortTurn }) {
|
|
82
|
+
stop();
|
|
83
|
+
const state = { stopped: false };
|
|
84
|
+
active = state;
|
|
85
|
+
(async () => {
|
|
86
|
+
while (!state.stopped) {
|
|
87
|
+
await sleep(pollMs);
|
|
88
|
+
if (state.stopped) return;
|
|
89
|
+
try {
|
|
90
|
+
const aborted = await checkOnce({ queueMessageId, onAbortTurn });
|
|
91
|
+
if (aborted) return;
|
|
92
|
+
} catch (error) {
|
|
93
|
+
dbg('control poll failed', error?.message || String(error));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
})();
|
|
97
|
+
return state;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function stop(handle) {
|
|
101
|
+
// Handle-scoped stop: a caller finishing an old turn must only stop that
|
|
102
|
+
// turn's poller. Without the scoping, a late finalize for a previous
|
|
103
|
+
// context would kill the poller of the turn currently running — leaving
|
|
104
|
+
// its Stop button dead. A bare stop() still stops whatever is active.
|
|
105
|
+
if (handle) {
|
|
106
|
+
handle.stopped = true;
|
|
107
|
+
if (active === handle) active = null;
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (active) active.stopped = true;
|
|
111
|
+
active = null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return { start, stop, checkOnce };
|
|
115
|
+
}
|