@oar-sh/oar 0.9.1 → 0.9.3
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/extension.mjs +0 -3
- package/.github/extensions/web-relay/polling/heartbeat.mjs +2 -55
- package/.github/extensions/web-relay/polling/polling-loop.mjs +0 -1
- package/.github/extensions/web-relay/runtime/api-client.mjs +2 -34
- package/.github/extensions/web-relay/runtime/config-loader.mjs +2 -104
- package/.github/extensions/web-relay/runtime/send-and-wait-errors.mjs +2 -170
- package/.github/extensions/web-relay/runtime/session-io.mjs +1 -71
- package/.github/extensions/web-relay/runtime/worker-websocket-link.mjs +2 -325
- package/.github/extensions/web-relay/skills/prompt-context.mjs +2 -160
- package/CHANGELOG.md +269 -0
- package/README.md +407 -424
- package/bin/oar.js +142 -11
- package/package.json +8 -4
- package/server/README.md +59 -22
- package/server/claude-worker/claude-sdk-adapter.mjs +7 -2
- package/server/claude-worker/claude-session-process.mjs +1013 -45
- package/server/claude-worker/claude-session-worker.mjs +54 -9
- package/server/claude-worker/claude-turn-publisher.mjs +110 -19
- package/server/claude-worker/claude-worker-link-wiring.mjs +9 -0
- package/server/copilot-sdk-runtime.mjs +12 -3
- package/server/copilot-worker/copilot-ask-user-tool.mjs +89 -0
- package/server/copilot-worker/copilot-attachments.mjs +35 -2
- package/server/copilot-worker/copilot-continuation-signals.mjs +55 -0
- package/server/copilot-worker/copilot-model-switch.mjs +425 -0
- package/server/copilot-worker/copilot-prompt-context.mjs +20 -8
- package/server/copilot-worker/copilot-question-bridge.mjs +129 -16
- package/server/copilot-worker/copilot-sdk-adapter.mjs +59 -11
- package/server/copilot-worker/copilot-sdk-event-normalizer.mjs +136 -12
- package/server/copilot-worker/copilot-sdk-session-process.mjs +2753 -527
- package/server/copilot-worker/copilot-sdk-session-worker.mjs +94 -25
- package/server/cursor-worker/cursor-session-worker.mjs +5 -5
- package/server/cursor-worker/cursor-turn-runner.mjs +50 -4
- package/server/db-schema.mjs +61 -1
- package/server/features.mjs +155 -41
- package/server/grok-worker/grok-session-worker.mjs +5 -5
- package/server/grok-worker/grok-turn-runner.mjs +45 -4
- package/server/migrations/0004-steer-settle-markers.mjs +119 -0
- package/server/public/app/annotate-core.mjs +181 -0
- package/server/public/app/annotate-editor.js +617 -0
- package/server/public/app/api-client.js +86 -5
- package/server/public/app/ask-user-view.js +102 -13
- package/server/public/app/attachments-view.js +172 -3
- package/server/public/app/background-task-timeout-options.mjs +9 -0
- package/server/public/app/background-tasks-view.mjs +34 -19
- package/server/public/app/bootstrap.js +153 -69
- package/server/public/app/composer-control-state.mjs +52 -34
- package/server/public/app/context-tier-options.mjs +24 -4
- package/server/public/app/conversation-draft-sync.mjs +187 -0
- package/server/public/app/conversation-list-filter.mjs +86 -0
- package/server/public/app/conversation-preferences.mjs +47 -10
- package/server/public/app/conversation-provider-indicator.mjs +8 -0
- package/server/public/app/conversation-view.js +1032 -218
- package/server/public/app/emoji-view.js +2 -0
- package/server/public/app/font-scaling.js +4 -37
- package/server/public/app/journal-view.js +190 -24
- package/server/public/app/model-selector-options.mjs +185 -1
- package/server/public/app/new-conversation-model-choice.mjs +29 -9
- package/server/public/app/pwa-install.js +64 -136
- package/server/public/app/question-multi-select.mjs +52 -0
- package/server/public/app/reasoning-effort-labels.mjs +13 -3
- package/server/public/app/relay-prompt-sanitizer.mjs +26 -3
- package/server/public/app/router.js +120 -11
- package/server/public/app/settings-modal.js +225 -20
- package/server/public/app/settings-tabs.js +1 -1
- package/server/public/app/socket-handlers.js +47 -4
- package/server/public/app/steered-turn-merge.mjs +100 -0
- package/server/public/app/store.js +159 -16
- package/server/public/app/sync-outbox.mjs +16 -8
- package/server/public/app/tool-activity-emoji.mjs +36 -0
- package/server/public/app/transcript-viewport-anchor.mjs +210 -0
- package/server/public/app/update-ui.js +194 -0
- package/server/public/index.html +483 -24
- package/server/public/sw.js +7 -0
- package/server/relay-tools.md +2 -0
- package/server/relay.mjs +23 -21
- package/server/repositories/message-repository.mjs +78 -21
- package/server/repositories/question-repository.mjs +19 -1
- package/server/repositories/session-repository.mjs +7 -31
- package/server/routes/ask-user-routes.mjs +10 -0
- package/server/routes/messages-routes.mjs +689 -159
- package/server/routes/sessions-routes.mjs +574 -80
- package/server/server-runtime.mjs +393 -925
- package/server/services/claude-auth-service.mjs +7 -1
- package/server/services/cli-install-service.mjs +8 -27
- package/server/services/cli-process-runner.mjs +30 -0
- package/server/services/copilot-model-discovery-service.mjs +202 -0
- package/server/services/data-dir-resolution.mjs +47 -0
- package/server/services/grok-auth-service.mjs +9 -1
- package/server/services/live-turn-picker.mjs +47 -0
- package/server/services/model-variant-catalog-service.mjs +1183 -0
- package/server/services/oar-cli-helpers.mjs +6 -2
- package/server/services/relay-prompt-sanitizer.mjs +30 -3
- package/server/services/relay-question-context.mjs +46 -0
- package/server/services/sdk-session-import-service.mjs +144 -16
- package/server/services/sdk-session-sync-service.mjs +84 -27
- package/server/services/session-worker-registry-service.mjs +116 -3
- package/server/services/session-worker-supervisor-service.mjs +7 -1
- package/server/services/session-worker-websocket-service.mjs +119 -24
- package/server/services/status-event-service.mjs +8 -2
- package/server/services/steer-resend.mjs +45 -0
- package/server/services/update-check-service.mjs +220 -0
- package/server/services/update-install-service.mjs +279 -0
- package/server/services/windows-autostart-service.mjs +42 -6
- package/server/services/windows-boot-autostart-service.mjs +487 -0
- package/shared/ask-user-bridge.mjs +37 -5
- package/shared/background-task-timeout.mjs +12 -4
- package/shared/claude-model-catalog.mjs +49 -0
- package/shared/context-window-fallbacks.mjs +1 -0
- package/shared/control-poller.mjs +4 -1
- package/shared/copilot-model-order.mjs +182 -0
- package/shared/media-embed-instructions.mjs +35 -0
- package/shared/model-descriptors.mjs +181 -30
- package/shared/model-id.mjs +6 -0
- package/shared/pwa-app-name.mjs +44 -0
- package/shared/question-multi-select.mjs +16 -0
- package/shared/relay-stop-failure.mjs +15 -0
- package/shared/slash-command-guard.mjs +76 -0
- package/shared/steer-note.mjs +66 -0
- package/shared/steer-settle-failure.mjs +89 -0
- package/shared/steer-settle-markers.mjs +18 -0
- package/shared/update-semver.mjs +50 -0
- package/shared/worker-crash-guard.mjs +40 -14
- package/shared/worker-runtime/api-client.mjs +34 -0
- package/shared/worker-runtime/config-loader.mjs +104 -0
- package/shared/worker-runtime/heartbeat.mjs +109 -0
- package/shared/worker-runtime/prompt-context.mjs +160 -0
- package/shared/worker-runtime/runner-link-wiring.mjs +44 -0
- package/shared/worker-runtime/send-and-wait-errors.mjs +170 -0
- package/shared/worker-runtime/worker-websocket-link.mjs +436 -0
- package/server/claude-worker/claude-session-test-harness.mjs +0 -236
- package/server/claude-worker/fixtures/workflow-run-record.json +0 -1
- package/server/copilot-worker/copilot-sdk-test-harness.mjs +0 -329
- package/server/copilot-worker/fixtures/abort-turn.json +0 -588
- package/server/copilot-worker/fixtures/ask-user-turn.json +0 -1154
- package/server/copilot-worker/fixtures/background-timer-continuation.json +0 -445
- package/server/copilot-worker/fixtures/background-timer-turn.json +0 -497
- package/server/copilot-worker/fixtures/happy-turn.json +0 -566
- package/server/copilot-worker/fixtures/quota-turn.json +0 -647
- package/server/copilot-worker/fixtures/reasoning-turn.json +0 -625
- package/server/copilot-worker/fixtures/resume-turn.json +0 -700
- package/server/copilot-worker/fixtures/subagent-turn.json +0 -1378
- package/server/copilot-worker/fixtures/tool-permission-turn.json +0 -1293
- package/server/routes/messages-routes-test-harness.mjs +0 -111
- package/server/services/fixtures/claude-auth-stub.sh +0 -62
- package/server/services/fixtures/cli-install-stub.sh +0 -75
- package/server/services/fixtures/grok-stub.sh +0 -86
|
@@ -24,6 +24,9 @@ import {
|
|
|
24
24
|
normalizeWorkspaceRootKey,
|
|
25
25
|
} from './services/workspace-root-path-policy.mjs';
|
|
26
26
|
import { stopSessionWorkerProcesses } from './services/session-worker-stop-service.mjs';
|
|
27
|
+
import { pickLiveTurnRowId } from './services/live-turn-picker.mjs';
|
|
28
|
+
import { buildSteerSettleFailure, steerAgentLabelForProvider } from '../shared/steer-settle-failure.mjs';
|
|
29
|
+
import { sanitizeRelayQuestionContext as sanitizeRelayQuestionContextWith } from './services/relay-question-context.mjs';
|
|
27
30
|
import { isRealPathWithinRoot } from './services/workspace-symlink-guard.mjs';
|
|
28
31
|
import { applySchema } from './db-schema.mjs';
|
|
29
32
|
import { createSessionRepository } from './repositories/session-repository.mjs';
|
|
@@ -64,20 +67,21 @@ import { createCursorDashboardUsageFetcher, readCursorIdeSessionToken } from './
|
|
|
64
67
|
import { fetchPersonalBillingUsage } from './services/github-billing-usage.mjs';
|
|
65
68
|
import { createSessionTranscriptService } from './services/session-transcript-service.mjs';
|
|
66
69
|
import { createSdkSessionImportService } from './services/sdk-session-import-service.mjs';
|
|
67
|
-
import {
|
|
70
|
+
import { createCopilotModelDiscoveryService } from './services/copilot-model-discovery-service.mjs';
|
|
71
|
+
import { createInstalledCopilotClient, getCopilotBaseDirs, resolveInstalledCopilotPaths } from './copilot-sdk-runtime.mjs';
|
|
68
72
|
import { createSessionHistoryRefreshService } from './services/session-history-refresh-service.mjs';
|
|
69
73
|
import { createContextSnapshotService } from './services/context-snapshot-service.mjs';
|
|
70
74
|
import { createClaudeSessionRootResolver } from './services/claude-session-root-service.mjs';
|
|
71
75
|
import { createCursorSessionRootResolver } from './services/cursor-session-root-service.mjs';
|
|
72
76
|
import { createGitChangesService } from './services/git-changes-service.mjs';
|
|
73
77
|
import { createRelaySingletonGuard } from './services/relay-singleton-guard.mjs';
|
|
78
|
+
import { resolveDataDir } from './services/data-dir-resolution.mjs';
|
|
74
79
|
import { createRelayRestartOrchestrator } from './services/relay-restart-orchestrator-service.mjs';
|
|
75
80
|
import { createRelayBridgeOwnerService } from './services/relay-bridge-owner-service.mjs';
|
|
76
81
|
import { createRelayCliLauncherService } from './services/relay-cli-launcher-service.mjs';
|
|
77
82
|
import { createSessionWorkerRegistry } from './services/session-worker-registry-service.mjs';
|
|
78
83
|
import { createSessionWorkerSupervisor } from './services/session-worker-supervisor-service.mjs';
|
|
79
84
|
import { createSessionWorkerProcessInspector } from './services/session-worker-process-service.mjs';
|
|
80
|
-
import { latestModelCatalogRefresh } from '../shared/model-catalog-freshness.mjs';
|
|
81
85
|
import { applyClaudeProviderEnvironment, applyCopilotSdkProviderEnvironment, applyCursorProviderEnvironment, applyGrokProviderEnvironment, applyOpenAIProviderEnvironment, copilotSdkEngineUnavailableReason, killTmuxSession, launchSessionCli } from './services/session-worker-launch-service.mjs';
|
|
82
86
|
import { createSshTunnelManager } from './services/ssh-tunnel-manager-service.mjs';
|
|
83
87
|
import { createCloudflaredTunnelManager } from './services/cloudflared-tunnel-service.mjs';
|
|
@@ -89,6 +93,7 @@ import { createPreviewProxyServer } from './services/preview-proxy-server.mjs';
|
|
|
89
93
|
import { createPreviewHealthProbe } from './services/preview-health-probe.mjs';
|
|
90
94
|
import { createTunnelWorkerPathGuard } from './services/tunnel-worker-path-guard.mjs';
|
|
91
95
|
import { createWindowsAutostartService } from './services/windows-autostart-service.mjs';
|
|
96
|
+
import { createWindowsBootAutostartService } from './services/windows-boot-autostart-service.mjs';
|
|
92
97
|
import { createSessionWorkerWebSocketService } from './services/session-worker-websocket-service.mjs';
|
|
93
98
|
import { createTmuxInspectorAccessPolicy } from './services/tmux-inspector-access-policy.mjs';
|
|
94
99
|
import { createTmuxInspectorStreamService } from './services/tmux-inspector-stream-service.mjs';
|
|
@@ -98,7 +103,13 @@ import {
|
|
|
98
103
|
resolveLaunchWorkspaceRootPath,
|
|
99
104
|
} from './services/workspace-root-defaults-service.mjs';
|
|
100
105
|
import { maybeStartTtyConsole } from './tty-console-bootstrap.mjs';
|
|
101
|
-
import {
|
|
106
|
+
import {
|
|
107
|
+
computeFeatureFlagState,
|
|
108
|
+
FEATURE_REGISTRY,
|
|
109
|
+
featureSettingKey,
|
|
110
|
+
migrateLegacyConfigFeatures,
|
|
111
|
+
resolveBootFeatureFlags,
|
|
112
|
+
} from './features.mjs';
|
|
102
113
|
import { RELAY_RESTART_EXIT_CODE } from './relay-exit-codes.mjs';
|
|
103
114
|
import { DEFAULT_QUESTION_TIMEOUT_MS, questionExpiresAt } from '../shared/question-timeout.mjs';
|
|
104
115
|
import {
|
|
@@ -106,6 +117,24 @@ import {
|
|
|
106
117
|
readTurnCeilingSetting,
|
|
107
118
|
turnCeilingMinutesToMs,
|
|
108
119
|
} from '../shared/turn-ceiling.mjs';
|
|
120
|
+
import {
|
|
121
|
+
normalizePwaAppName,
|
|
122
|
+
readPwaAppNameSetting,
|
|
123
|
+
resolvePwaManifestNames,
|
|
124
|
+
} from '../shared/pwa-app-name.mjs';
|
|
125
|
+
import {
|
|
126
|
+
createUpdateCheckService,
|
|
127
|
+
isUpdateCheckKilled,
|
|
128
|
+
resolveUpdateManifestUrl,
|
|
129
|
+
} from './services/update-check-service.mjs';
|
|
130
|
+
import {
|
|
131
|
+
createUpdateInstallService,
|
|
132
|
+
reconcileUpdateAttempt,
|
|
133
|
+
} from './services/update-install-service.mjs';
|
|
134
|
+
import {
|
|
135
|
+
mergeClaudeModelEfforts,
|
|
136
|
+
mergeDiscoveredClaudeModels,
|
|
137
|
+
} from '../shared/claude-model-catalog.mjs';
|
|
109
138
|
import { parseAutoCompactWindow } from '../shared/auto-compact-window.mjs';
|
|
110
139
|
import { parseThinkingDisplay, parseThinkingEnabled } from '../shared/claude-thinking.mjs';
|
|
111
140
|
import {
|
|
@@ -116,19 +145,30 @@ import {
|
|
|
116
145
|
import { normalizeRelayThoughtList } from './public/app/relay-thoughts.mjs';
|
|
117
146
|
import { capRelayActivityEntries } from './public/app/activity-replay-state.mjs';
|
|
118
147
|
import {
|
|
119
|
-
canonicalizeModelId,
|
|
120
|
-
filterValidModelIds,
|
|
121
148
|
isOpenAIModelId,
|
|
122
149
|
isSafeClaudeModelId,
|
|
123
150
|
isSafeCursorModelId,
|
|
124
151
|
isSafeGrokModelId,
|
|
125
152
|
isSafeProviderModelId,
|
|
126
|
-
isValidModelId,
|
|
127
153
|
} from '../shared/model-id.mjs';
|
|
128
|
-
import {
|
|
154
|
+
import { createModelVariantCatalogService } from './services/model-variant-catalog-service.mjs';
|
|
129
155
|
|
|
130
156
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
131
157
|
const REPO_ROOT = path.resolve(__dirname, '..');
|
|
158
|
+
// The installed package root: set by bin/oar.js for launched relays, falling
|
|
159
|
+
// back to the checkout for direct `node server/server.js` runs. Version and
|
|
160
|
+
// install method feed the update mechanism and /api/status.
|
|
161
|
+
const PACKAGE_ROOT = String(process.env.COPILOT_WEB_RELAY_ROOT || '').trim()
|
|
162
|
+
? path.resolve(String(process.env.COPILOT_WEB_RELAY_ROOT).trim())
|
|
163
|
+
: REPO_ROOT;
|
|
164
|
+
const RUNNING_VERSION = (() => {
|
|
165
|
+
try {
|
|
166
|
+
return String(JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, 'package.json'), 'utf8')).version || '0.0.0');
|
|
167
|
+
} catch {
|
|
168
|
+
return '0.0.0';
|
|
169
|
+
}
|
|
170
|
+
})();
|
|
171
|
+
const INSTALL_METHOD = fs.existsSync(path.join(PACKAGE_ROOT, '.git')) ? 'git-checkout' : 'npm-global';
|
|
132
172
|
const PUBLIC_DIR = path.join(__dirname, 'public');
|
|
133
173
|
const INDEX_HTML_PATH = path.join(PUBLIC_DIR, 'index.html');
|
|
134
174
|
const PWA_MANIFEST_PATH = path.join(PUBLIC_DIR, 'manifest.webmanifest');
|
|
@@ -146,9 +186,10 @@ const ttyConsoleRuntime = await maybeStartTtyConsole({
|
|
|
146
186
|
const CONFIG_PATH = process.env.COPILOT_WEB_RELAY_CONFIG
|
|
147
187
|
? path.resolve(String(process.env.COPILOT_WEB_RELAY_CONFIG))
|
|
148
188
|
: path.join(__dirname, 'config.json');
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
189
|
+
// env var > config.json `dataDir` > <server>/data — see data-dir-resolution.mjs
|
|
190
|
+
// for why the config can carry it (a dev checkout pointing at a release
|
|
191
|
+
// install's data, and autostart launchers that only pin the config path).
|
|
192
|
+
const DATA_DIR = resolveDataDir({ env: process.env, configPath: CONFIG_PATH, serverDir: __dirname });
|
|
152
193
|
const DB_PATH = path.join(DATA_DIR, 'copilot.db');
|
|
153
194
|
const RELAY_LOCK_PATH = path.join(DATA_DIR, 'relay-server.lock');
|
|
154
195
|
const UPLOAD_DIR = process.env.COPILOT_WEB_RELAY_DATA_DIR
|
|
@@ -492,9 +533,17 @@ async function refreshClaudeProviderModels() {
|
|
|
492
533
|
return { ok: false, models: settings.models, error: 'Claude model discovery returned no models' };
|
|
493
534
|
}
|
|
494
535
|
const nowIso = new Date().toISOString();
|
|
495
|
-
|
|
536
|
+
// Merge rather than replace: supportedModels() reports only the current
|
|
537
|
+
// lineup, so replacing would drop a model mid-use the moment a successor
|
|
538
|
+
// ships (Fable 5 -> Fable 5.1). Retained ids still run via `--model`.
|
|
539
|
+
const models = mergeDiscoveredClaudeModels({
|
|
540
|
+
defaultModel: settings.model,
|
|
541
|
+
discovered,
|
|
542
|
+
previouslyKnown: readClaudeModelListSetting(CLAUDE_MODELS_SETTING_KEY),
|
|
543
|
+
});
|
|
544
|
+
const mergedEfforts = mergeClaudeModelEfforts(readClaudeModelEffortsSetting(), effortsByModel);
|
|
496
545
|
stmts.upsertAppSetting.run(CLAUDE_MODELS_SETTING_KEY, JSON.stringify(models), nowIso);
|
|
497
|
-
stmts.upsertAppSetting.run(CLAUDE_MODEL_EFFORTS_SETTING_KEY, JSON.stringify(
|
|
546
|
+
stmts.upsertAppSetting.run(CLAUDE_MODEL_EFFORTS_SETTING_KEY, JSON.stringify(mergedEfforts), nowIso);
|
|
498
547
|
return { ok: true, models, error: null };
|
|
499
548
|
} catch (error) {
|
|
500
549
|
return {
|
|
@@ -1222,7 +1271,12 @@ const DEFAULT_OPENAI_BASE_URL = 'https://api.openai.com/v1';
|
|
|
1222
1271
|
// spawn, so model catalogs, usage cards and routing all stay put.
|
|
1223
1272
|
const COPILOT_ENGINE_SETTING_KEY = 'copilot_engine';
|
|
1224
1273
|
const COPILOT_ENGINES = ['extension', 'sdk'];
|
|
1225
|
-
|
|
1274
|
+
// Phase 6 of the extension retirement (2026-09-07): the headless SDK worker is
|
|
1275
|
+
// the default engine. The extension remains reachable two ways until deletion:
|
|
1276
|
+
// an explicit stored setting, or automatically when this relay cannot run the
|
|
1277
|
+
// SDK engine at all (no resolvable Copilot CLI, or session-worker routing off).
|
|
1278
|
+
const DEFAULT_COPILOT_ENGINE = 'sdk';
|
|
1279
|
+
const FALLBACK_COPILOT_ENGINE = 'extension';
|
|
1226
1280
|
const CLAUDE_ENABLED_SETTING_KEY = 'claude_enabled';
|
|
1227
1281
|
const CLAUDE_MODEL_SETTING_KEY = 'claude_model';
|
|
1228
1282
|
const CLAUDE_MODELS_SETTING_KEY = 'claude_models';
|
|
@@ -1533,8 +1587,6 @@ const CURATED_MODEL_IDS = [
|
|
|
1533
1587
|
'claude-sonnet-4.6',
|
|
1534
1588
|
'claude-haiku-4.5',
|
|
1535
1589
|
];
|
|
1536
|
-
const SUPPORTED_REASONING_EFFORTS = ['none', 'low', 'medium', 'high', 'xhigh', 'max'];
|
|
1537
|
-
const REASONING_VARIANT_SUPPORTED_PREFIXES = ['gpt-', 'claude-'];
|
|
1538
1590
|
|
|
1539
1591
|
let config = { ...DEFAULT_CONFIG };
|
|
1540
1592
|
if (fs.existsSync(CONFIG_PATH)) {
|
|
@@ -1704,15 +1756,6 @@ function socketIoPath() {
|
|
|
1704
1756
|
const remotePath = normalizeRemotePath(config.remotePath);
|
|
1705
1757
|
const COOKIE_PATH = remotePath || '/';
|
|
1706
1758
|
|
|
1707
|
-
let modelCatalog = {
|
|
1708
|
-
models: [DEFAULT_MODEL],
|
|
1709
|
-
currentModel: DEFAULT_MODEL,
|
|
1710
|
-
defaultModel: DEFAULT_MODEL,
|
|
1711
|
-
source: 'bootstrap',
|
|
1712
|
-
refreshedAt: null,
|
|
1713
|
-
error: null,
|
|
1714
|
-
};
|
|
1715
|
-
let modelSelectorSql = null;
|
|
1716
1759
|
|
|
1717
1760
|
const workspaceFileMetaCache = new Map();
|
|
1718
1761
|
let workspaceFileWatcher = null;
|
|
@@ -1810,14 +1853,22 @@ function readAppSettingValue(key) {
|
|
|
1810
1853
|
}
|
|
1811
1854
|
|
|
1812
1855
|
/**
|
|
1813
|
-
* The Copilot engine setting, normalised. An
|
|
1814
|
-
* reads as the default
|
|
1815
|
-
*
|
|
1816
|
-
*
|
|
1856
|
+
* The Copilot engine setting, normalised. An explicit stored value always
|
|
1857
|
+
* wins. Unset (or unrecognised) reads as the SDK default — but only when this
|
|
1858
|
+
* relay can actually run the SDK engine: the save path refuses to persist
|
|
1859
|
+
* 'sdk' on a relay that cannot honour it, and the default must not sneak past
|
|
1860
|
+
* that same guard. A relay with no resolvable Copilot CLI or with
|
|
1861
|
+
* session-worker routing disabled falls back to the extension engine so a
|
|
1862
|
+
* Copilot conversation can still start at all.
|
|
1817
1863
|
*/
|
|
1818
1864
|
function getCopilotEngine() {
|
|
1819
1865
|
const stored = readAppSettingValue(COPILOT_ENGINE_SETTING_KEY).toLowerCase();
|
|
1820
|
-
|
|
1866
|
+
if (COPILOT_ENGINES.includes(stored)) return stored;
|
|
1867
|
+
const unavailable = copilotSdkEngineUnavailableReason({
|
|
1868
|
+
env: sessionWorkerLaunchEnv,
|
|
1869
|
+
routingEnabled: featureFlags?.SESSION_WORKER_ROUTING_ENABLED === true,
|
|
1870
|
+
});
|
|
1871
|
+
return unavailable ? FALLBACK_COPILOT_ENGINE : DEFAULT_COPILOT_ENGINE;
|
|
1821
1872
|
}
|
|
1822
1873
|
|
|
1823
1874
|
function getCopilotProviderSettings() {
|
|
@@ -1861,6 +1912,54 @@ function setTurnCeilingMinutes(value) {
|
|
|
1861
1912
|
return { ok: true, ceilingMinutes: parsed.minutes };
|
|
1862
1913
|
}
|
|
1863
1914
|
|
|
1915
|
+
const PWA_APP_NAME_SETTING_KEY = 'pwa_app_name';
|
|
1916
|
+
|
|
1917
|
+
/** '' means "no custom name": the manifest falls back to the template default. */
|
|
1918
|
+
function getPwaAppName() {
|
|
1919
|
+
return readPwaAppNameSetting(readAppSettingValue(PWA_APP_NAME_SETTING_KEY));
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
function setPwaAppName(rawValue) {
|
|
1923
|
+
if (typeof stmts?.upsertAppSetting?.run !== 'function' || typeof stmts?.deleteAppSetting?.run !== 'function') {
|
|
1924
|
+
// status carries the intended HTTP mapping so the route never has to key
|
|
1925
|
+
// off the error text (a reworded message must not turn 500s into 400s).
|
|
1926
|
+
return { ok: false, status: 500, error: 'App name settings are unavailable' };
|
|
1927
|
+
}
|
|
1928
|
+
const normalized = normalizePwaAppName(rawValue);
|
|
1929
|
+
if (!normalized.ok) return normalized;
|
|
1930
|
+
if (normalized.value) {
|
|
1931
|
+
stmts.upsertAppSetting.run(PWA_APP_NAME_SETTING_KEY, normalized.value, new Date().toISOString());
|
|
1932
|
+
} else {
|
|
1933
|
+
stmts.deleteAppSetting.run(PWA_APP_NAME_SETTING_KEY);
|
|
1934
|
+
}
|
|
1935
|
+
return { ok: true, appName: normalized.value };
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
function getFeatureFlagsSettingsState() {
|
|
1939
|
+
// `featureFlags` is const-declared later in boot; this only runs from request
|
|
1940
|
+
// handlers, well after the snapshot exists (same shape as setCopilotProviderSettings).
|
|
1941
|
+
return computeFeatureFlagState({
|
|
1942
|
+
readSetting: readAppSettingValue,
|
|
1943
|
+
env: process.env,
|
|
1944
|
+
activeFlags: featureFlags,
|
|
1945
|
+
});
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
function setFeatureFlagSetting(name, enabled) {
|
|
1949
|
+
if (typeof stmts?.upsertAppSetting?.run !== 'function') {
|
|
1950
|
+
return { ok: false, error: 'Feature settings are unavailable' };
|
|
1951
|
+
}
|
|
1952
|
+
const normalized = String(name || '').trim();
|
|
1953
|
+
if (!FEATURE_REGISTRY.some((entry) => entry.name === normalized)) {
|
|
1954
|
+
return { ok: false, error: 'Unknown feature flag' };
|
|
1955
|
+
}
|
|
1956
|
+
if (typeof enabled !== 'boolean') {
|
|
1957
|
+
return { ok: false, error: 'enabled must be a boolean' };
|
|
1958
|
+
}
|
|
1959
|
+
stmts.upsertAppSetting.run(featureSettingKey(normalized), enabled ? '1' : '0', new Date().toISOString());
|
|
1960
|
+
return { ok: true };
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1864
1963
|
const BACKGROUND_TASK_TIMEOUT_SETTING_KEY = 'background_task_timeout_minutes';
|
|
1865
1964
|
|
|
1866
1965
|
function getBackgroundTaskTimeoutMinutes() {
|
|
@@ -2164,447 +2263,30 @@ function uniqueStringList(values) {
|
|
|
2164
2263
|
return out;
|
|
2165
2264
|
}
|
|
2166
2265
|
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
if (
|
|
2191
|
-
text.startsWith('gpt-')
|
|
2192
|
-
|| text.startsWith('o1-')
|
|
2193
|
-
|| text.startsWith('o3-')
|
|
2194
|
-
|| text.startsWith('codex-')
|
|
2195
|
-
|| text.startsWith('openai/')
|
|
2196
|
-
) return 'openai';
|
|
2197
|
-
if (text.startsWith('claude-')) return 'anthropic';
|
|
2198
|
-
if (text.startsWith('gemini-')) return 'google';
|
|
2199
|
-
if (text.startsWith('mai-')) return 'microsoft';
|
|
2200
|
-
return 'other';
|
|
2201
|
-
}
|
|
2202
|
-
|
|
2203
|
-
function titleCaseWord(word) {
|
|
2204
|
-
const text = String(word || '').trim();
|
|
2205
|
-
if (!text) return '';
|
|
2206
|
-
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
2207
|
-
}
|
|
2208
|
-
|
|
2209
|
-
function modelDisplayLabel(modelId) {
|
|
2210
|
-
const text = String(modelId || '').trim();
|
|
2211
|
-
if (!text) return '';
|
|
2212
|
-
if (/^gpt-/i.test(text)) {
|
|
2213
|
-
return text
|
|
2214
|
-
.replace(/^gpt-/i, 'GPT-')
|
|
2215
|
-
.replace(/-codex$/i, ' Codex')
|
|
2216
|
-
.replace(/-mini$/i, ' Mini')
|
|
2217
|
-
.replace(/\bmini\b/gi, 'Mini')
|
|
2218
|
-
.replace(/\s+/g, ' ')
|
|
2219
|
-
.trim();
|
|
2220
|
-
}
|
|
2221
|
-
if (/^claude-/i.test(text)) {
|
|
2222
|
-
return text
|
|
2223
|
-
.replace(/^claude-/i, 'Claude ')
|
|
2224
|
-
.split('-')
|
|
2225
|
-
.map((part) => (/^\d+(\.\d+)?$/.test(part) ? part : titleCaseWord(part)))
|
|
2226
|
-
.join(' ')
|
|
2227
|
-
.replace(/\s+/g, ' ')
|
|
2228
|
-
.trim();
|
|
2229
|
-
}
|
|
2230
|
-
if (/^gemini-/i.test(text)) {
|
|
2231
|
-
return text
|
|
2232
|
-
.replace(/^gemini-/i, 'Gemini ')
|
|
2233
|
-
.split('-')
|
|
2234
|
-
.map((part) => (/^\d+(\.\d+)?$/.test(part) ? part : titleCaseWord(part)))
|
|
2235
|
-
.join(' ')
|
|
2236
|
-
.replace(/\s+/g, ' ')
|
|
2237
|
-
.trim();
|
|
2238
|
-
}
|
|
2239
|
-
return text;
|
|
2240
|
-
}
|
|
2241
|
-
|
|
2242
|
-
function buildModelVariantId(baseModelId, reasoningEffort = null) {
|
|
2243
|
-
const base = String(baseModelId || '').trim();
|
|
2244
|
-
if (!base) return '';
|
|
2245
|
-
const effort = normalizeReasoningEffort(reasoningEffort);
|
|
2246
|
-
if (!effort) return base;
|
|
2247
|
-
return `${base}-${effort}`;
|
|
2248
|
-
}
|
|
2249
|
-
|
|
2250
|
-
function parseModelVariantId(variantId = '', {
|
|
2251
|
-
knownBaseModels = [],
|
|
2252
|
-
} = {}) {
|
|
2253
|
-
const value = String(variantId || '').trim();
|
|
2254
|
-
if (!value) return null;
|
|
2255
|
-
const known = Array.isArray(knownBaseModels)
|
|
2256
|
-
? knownBaseModels.map((entry) => String(entry || '').trim()).filter(Boolean)
|
|
2257
|
-
: [];
|
|
2258
|
-
const orderedKnown = known.sort((a, b) => b.length - a.length);
|
|
2259
|
-
for (const candidate of orderedKnown) {
|
|
2260
|
-
if (!value.toLowerCase().startsWith(`${candidate.toLowerCase()}-`)) continue;
|
|
2261
|
-
const suffix = value.slice(candidate.length + 1);
|
|
2262
|
-
const effort = normalizeReasoningEffort(suffix);
|
|
2263
|
-
if (effort) {
|
|
2264
|
-
return {
|
|
2265
|
-
variantId: buildModelVariantId(candidate, effort),
|
|
2266
|
-
baseModelId: candidate,
|
|
2267
|
-
reasoningEffort: effort,
|
|
2268
|
-
};
|
|
2269
|
-
}
|
|
2270
|
-
}
|
|
2271
|
-
const trailingEffortMatch = value.match(/^(.*?)-([a-z]+)$/i);
|
|
2272
|
-
if (trailingEffortMatch) {
|
|
2273
|
-
const effort = normalizeReasoningEffort(trailingEffortMatch[2]);
|
|
2274
|
-
const maybeBase = String(trailingEffortMatch[1] || '').trim();
|
|
2275
|
-
if (effort && maybeBase) {
|
|
2276
|
-
return {
|
|
2277
|
-
variantId: buildModelVariantId(maybeBase, effort),
|
|
2278
|
-
baseModelId: maybeBase,
|
|
2279
|
-
reasoningEffort: effort,
|
|
2280
|
-
};
|
|
2281
|
-
}
|
|
2282
|
-
}
|
|
2283
|
-
return {
|
|
2284
|
-
variantId: value,
|
|
2285
|
-
baseModelId: value,
|
|
2286
|
-
reasoningEffort: null,
|
|
2287
|
-
};
|
|
2288
|
-
}
|
|
2289
|
-
|
|
2290
|
-
function normalizeModelVariantRow(row = {}) {
|
|
2291
|
-
const rawBaseModelId = String(row?.base_model_id || row?.baseModelId || '').trim();
|
|
2292
|
-
const baseModelId = canonicalizeModelId(rawBaseModelId) || rawBaseModelId;
|
|
2293
|
-
const reasoningEffort = normalizeReasoningEffort(row?.reasoning_effort ?? row?.reasoningEffort);
|
|
2294
|
-
const rawVariantId = String(row?.variant_id || row?.variantId || buildModelVariantId(baseModelId, reasoningEffort)).trim();
|
|
2295
|
-
const parsed = parseModelVariantId(rawVariantId, { knownBaseModels: baseModelId ? [baseModelId] : [] });
|
|
2296
|
-
const normalizedBaseModelId = canonicalizeModelId(parsed?.baseModelId || baseModelId) || baseModelId;
|
|
2297
|
-
const normalizedReasoningEffort = normalizeReasoningEffort(parsed?.reasoningEffort || reasoningEffort);
|
|
2298
|
-
const variantId = buildModelVariantId(normalizedBaseModelId, normalizedReasoningEffort);
|
|
2299
|
-
const provider = String(row?.provider || row?.providerId || modelProviderForId(baseModelId)).trim() || 'other';
|
|
2300
|
-
const label = String(row?.label || row?.displayName || modelDisplayLabel(normalizedBaseModelId)).trim() || normalizedBaseModelId;
|
|
2301
|
-
const enabledValue = Number(row?.enabled);
|
|
2302
|
-
const enabled = Number.isFinite(enabledValue) ? enabledValue === 1 : !!row?.enabled;
|
|
2303
|
-
return {
|
|
2304
|
-
variantId,
|
|
2305
|
-
baseModelId: normalizedBaseModelId,
|
|
2306
|
-
provider,
|
|
2307
|
-
reasoningEffort: normalizedReasoningEffort,
|
|
2308
|
-
label,
|
|
2309
|
-
releaseStatus: String(row?.release_status || row?.releaseStatus || '').trim() || null,
|
|
2310
|
-
contextLimitTokens: Number.isFinite(Number(row?.context_limit_tokens ?? row?.contextLimitTokens))
|
|
2311
|
-
&& Number(row?.context_limit_tokens ?? row?.contextLimitTokens) > 0
|
|
2312
|
-
? Math.round(Number(row?.context_limit_tokens ?? row?.contextLimitTokens))
|
|
2313
|
-
: null,
|
|
2314
|
-
longContextLimitTokens: Number.isFinite(Number(row?.long_context_limit_tokens ?? row?.longContextLimitTokens))
|
|
2315
|
-
&& Number(row?.long_context_limit_tokens ?? row?.longContextLimitTokens) > 0
|
|
2316
|
-
? Math.round(Number(row?.long_context_limit_tokens ?? row?.longContextLimitTokens))
|
|
2317
|
-
: null,
|
|
2318
|
-
pricing: (() => {
|
|
2319
|
-
const value = row?.pricing_json ?? row?.pricing;
|
|
2320
|
-
if (!value || typeof value === 'object') return value || null;
|
|
2321
|
-
try { return JSON.parse(value); } catch { return null; }
|
|
2322
|
-
})(),
|
|
2323
|
-
enabled,
|
|
2324
|
-
sortOrder: Number.isFinite(Number(row?.sort_order ?? row?.sortOrder))
|
|
2325
|
-
? Math.max(0, Math.trunc(Number(row?.sort_order ?? row?.sortOrder)))
|
|
2326
|
-
: 0,
|
|
2327
|
-
updatedAt: row?.updated_at || row?.updatedAt || null,
|
|
2328
|
-
};
|
|
2329
|
-
}
|
|
2330
|
-
|
|
2331
|
-
function buildModelVariantEntries(baseModels = [], {
|
|
2332
|
-
defaultEnabled = true,
|
|
2333
|
-
contextLimitsByModel = {},
|
|
2334
|
-
modelMetadataByModel = {},
|
|
2335
|
-
} = {}) {
|
|
2336
|
-
const models = uniqueStringList(baseModels);
|
|
2337
|
-
const entries = [];
|
|
2338
|
-
let sortOrder = 0;
|
|
2339
|
-
for (const baseModelId of models) {
|
|
2340
|
-
const provider = modelProviderForId(baseModelId);
|
|
2341
|
-
const label = modelDisplayLabel(baseModelId);
|
|
2342
|
-
const contextLimitTokens = Number(contextLimitsByModel?.[baseModelId]);
|
|
2343
|
-
const normalizedContextLimitTokens = Number.isFinite(contextLimitTokens) && contextLimitTokens > 0
|
|
2344
|
-
? Math.round(contextLimitTokens)
|
|
2345
|
-
: null;
|
|
2346
|
-
const metadata = modelMetadataByModel?.[baseModelId] || {};
|
|
2347
|
-
if (isReasoningVariantEligibleModel(baseModelId)) {
|
|
2348
|
-
for (const effort of SUPPORTED_REASONING_EFFORTS) {
|
|
2349
|
-
entries.push({
|
|
2350
|
-
variantId: buildModelVariantId(baseModelId, effort),
|
|
2351
|
-
baseModelId,
|
|
2352
|
-
provider,
|
|
2353
|
-
label,
|
|
2354
|
-
reasoningEffort: effort,
|
|
2355
|
-
releaseStatus: null,
|
|
2356
|
-
contextLimitTokens: normalizedContextLimitTokens,
|
|
2357
|
-
longContextLimitTokens: toNullableInt(metadata.longContextLimitTokens),
|
|
2358
|
-
pricing: metadata.pricing || null,
|
|
2359
|
-
enabled: defaultEnabled ? 1 : 0,
|
|
2360
|
-
sortOrder: sortOrder++,
|
|
2361
|
-
});
|
|
2362
|
-
}
|
|
2363
|
-
continue;
|
|
2364
|
-
}
|
|
2365
|
-
entries.push({
|
|
2366
|
-
variantId: buildModelVariantId(baseModelId),
|
|
2367
|
-
baseModelId,
|
|
2368
|
-
provider,
|
|
2369
|
-
label,
|
|
2370
|
-
reasoningEffort: null,
|
|
2371
|
-
releaseStatus: null,
|
|
2372
|
-
contextLimitTokens: normalizedContextLimitTokens,
|
|
2373
|
-
longContextLimitTokens: toNullableInt(metadata.longContextLimitTokens),
|
|
2374
|
-
pricing: metadata.pricing || null,
|
|
2375
|
-
enabled: defaultEnabled ? 1 : 0,
|
|
2376
|
-
sortOrder: sortOrder++,
|
|
2377
|
-
});
|
|
2378
|
-
}
|
|
2379
|
-
|
|
2380
|
-
return entries;
|
|
2381
|
-
}
|
|
2382
|
-
|
|
2383
|
-
function normalizeModelMetadataByModel(value = {}) {
|
|
2384
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
|
|
2385
|
-
const normalized = {};
|
|
2386
|
-
for (const [rawModelId, rawMetadata] of Object.entries(value)) {
|
|
2387
|
-
const modelId = canonicalizeModelId(rawModelId);
|
|
2388
|
-
if (!isValidModelId(modelId) || !rawMetadata || typeof rawMetadata !== 'object') continue;
|
|
2389
|
-
const defaultContextLimitTokens = toNullableInt(rawMetadata.defaultContextLimitTokens);
|
|
2390
|
-
const longContextLimitTokens = toNullableInt(rawMetadata.longContextLimitTokens);
|
|
2391
|
-
const pricing = rawMetadata.pricing && typeof rawMetadata.pricing === 'object' ? rawMetadata.pricing : null;
|
|
2392
|
-
if (defaultContextLimitTokens === null && longContextLimitTokens === null && pricing === null) continue;
|
|
2393
|
-
normalized[modelId] = { defaultContextLimitTokens, longContextLimitTokens, pricing };
|
|
2394
|
-
}
|
|
2395
|
-
return normalized;
|
|
2396
|
-
}
|
|
2397
|
-
|
|
2398
|
-
function normalizeContextLimitsByModel(value = {}) {
|
|
2399
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
|
|
2400
|
-
const normalized = {};
|
|
2401
|
-
for (const [rawModelId, rawLimit] of Object.entries(value)) {
|
|
2402
|
-
const modelId = canonicalizeModelId(rawModelId);
|
|
2403
|
-
const limit = Number(rawLimit);
|
|
2404
|
-
if (!isValidModelId(modelId) || !Number.isFinite(limit) || limit <= 0) continue;
|
|
2405
|
-
normalized[modelId] = Math.round(limit);
|
|
2406
|
-
}
|
|
2407
|
-
return normalized;
|
|
2408
|
-
}
|
|
2409
|
-
|
|
2410
|
-
function hasValidReasoningByModel(reasoningByModel = {}) {
|
|
2411
|
-
if (!reasoningByModel || typeof reasoningByModel !== 'object') return false;
|
|
2412
|
-
const modelIds = Object.keys(reasoningByModel).filter((modelId) => modelId !== AUTO_MODEL_SENTINEL);
|
|
2413
|
-
if (!modelIds.length) return false;
|
|
2414
|
-
return modelIds.every((modelId) => {
|
|
2415
|
-
const efforts = reasoningByModel[modelId];
|
|
2416
|
-
return Array.isArray(efforts) && efforts.length > 0;
|
|
2417
|
-
});
|
|
2418
|
-
}
|
|
2419
|
-
|
|
2420
|
-
function getModelCatalogState() {
|
|
2421
|
-
const selectorState = getModelVariantSelectorState();
|
|
2422
|
-
const enabledRows = listEnabledModelVariantRows();
|
|
2423
|
-
const modelRows = enabledRows.length ? enabledRows : listModelVariantRows().filter((row) => row.enabled);
|
|
2424
|
-
const allRows = listModelVariantRows();
|
|
2425
|
-
const reasoningByModel = {};
|
|
2426
|
-
const contextLimitsByModel = {};
|
|
2427
|
-
const modelMetadataByModel = {};
|
|
2428
|
-
const models = [];
|
|
2429
|
-
const seenModels = new Set();
|
|
2430
|
-
for (const row of modelRows) {
|
|
2431
|
-
const baseModelId = String(row?.baseModelId || '').trim();
|
|
2432
|
-
if (!baseModelId) continue;
|
|
2433
|
-
if (!seenModels.has(baseModelId)) {
|
|
2434
|
-
seenModels.add(baseModelId);
|
|
2435
|
-
models.push(baseModelId);
|
|
2436
|
-
}
|
|
2437
|
-
const effort = normalizeReasoningEffort(row?.reasoningEffort || 'none') || 'none';
|
|
2438
|
-
const current = reasoningByModel[baseModelId] || [];
|
|
2439
|
-
if (!current.includes(effort)) current.push(effort);
|
|
2440
|
-
reasoningByModel[baseModelId] = current;
|
|
2441
|
-
if (row.contextLimitTokens !== null && row.contextLimitTokens > 0) {
|
|
2442
|
-
contextLimitsByModel[baseModelId] = row.contextLimitTokens;
|
|
2443
|
-
}
|
|
2444
|
-
if (!modelMetadataByModel[baseModelId]) {
|
|
2445
|
-
modelMetadataByModel[baseModelId] = {
|
|
2446
|
-
defaultContextLimitTokens: row.contextLimitTokens,
|
|
2447
|
-
longContextLimitTokens: row.longContextLimitTokens,
|
|
2448
|
-
pricing: row.pricing,
|
|
2449
|
-
};
|
|
2450
|
-
}
|
|
2451
|
-
}
|
|
2452
|
-
const knownModelIds = new Set(Object.keys(reasoningByModel));
|
|
2453
|
-
for (const row of allRows) {
|
|
2454
|
-
const baseModelId = String(row?.baseModelId || '').trim();
|
|
2455
|
-
if (!baseModelId) continue;
|
|
2456
|
-
knownModelIds.add(baseModelId);
|
|
2457
|
-
const effort = normalizeReasoningEffort(row?.reasoningEffort || 'none') || 'none';
|
|
2458
|
-
const current = reasoningByModel[baseModelId] || [];
|
|
2459
|
-
if (!current.includes(effort)) current.push(effort);
|
|
2460
|
-
reasoningByModel[baseModelId] = current;
|
|
2461
|
-
if (row.contextLimitTokens !== null && row.contextLimitTokens > 0) {
|
|
2462
|
-
contextLimitsByModel[baseModelId] = row.contextLimitTokens;
|
|
2463
|
-
}
|
|
2464
|
-
if (!modelMetadataByModel[baseModelId]) {
|
|
2465
|
-
modelMetadataByModel[baseModelId] = {
|
|
2466
|
-
defaultContextLimitTokens: row.contextLimitTokens,
|
|
2467
|
-
longContextLimitTokens: row.longContextLimitTokens,
|
|
2468
|
-
pricing: row.pricing,
|
|
2469
|
-
};
|
|
2470
|
-
}
|
|
2471
|
-
}
|
|
2472
|
-
for (const modelId of knownModelIds) {
|
|
2473
|
-
const efforts = uniqueStringList(
|
|
2474
|
-
(reasoningByModel[modelId] || [])
|
|
2475
|
-
.map((value) => normalizeReasoningEffort(value))
|
|
2476
|
-
.filter(Boolean),
|
|
2477
|
-
);
|
|
2478
|
-
reasoningByModel[modelId] = efforts;
|
|
2479
|
-
}
|
|
2480
|
-
const autoEfforts = uniqueStringList(
|
|
2481
|
-
Object.entries(reasoningByModel)
|
|
2482
|
-
.filter(([modelId]) => modelId !== AUTO_MODEL_SENTINEL)
|
|
2483
|
-
.flatMap(([, list]) => Array.isArray(list) ? list : [])
|
|
2484
|
-
.map((value) => normalizeReasoningEffort(value))
|
|
2485
|
-
.filter(Boolean),
|
|
2486
|
-
);
|
|
2487
|
-
if (autoEfforts.length) {
|
|
2488
|
-
reasoningByModel[AUTO_MODEL_SENTINEL] = autoEfforts;
|
|
2489
|
-
} else {
|
|
2490
|
-
delete reasoningByModel[AUTO_MODEL_SENTINEL];
|
|
2491
|
-
}
|
|
2492
|
-
const catalogModels = [AUTO_MODEL_SENTINEL, ...models.filter((value) => value.toLowerCase() !== AUTO_MODEL_SENTINEL)];
|
|
2493
|
-
const currentResolved = parseModelVariantSelection(selectorState.currentModel);
|
|
2494
|
-
const defaultResolved = parseModelVariantSelection(selectorState.defaultModel);
|
|
2495
|
-
const currentModel = String(currentResolved?.baseModelId || selectorState.currentModel || '').trim() || catalogModels[0] || DEFAULT_MODEL;
|
|
2496
|
-
const defaultModel = String(defaultResolved?.baseModelId || selectorState.defaultModel || '').trim() || currentModel || catalogModels[0] || DEFAULT_MODEL;
|
|
2497
|
-
const reasoningMetadataValid = hasValidReasoningByModel(reasoningByModel);
|
|
2498
|
-
const inMemoryRefresh = modelCatalog.refreshedAt || null;
|
|
2499
|
-
const refreshedAt = latestModelCatalogRefresh(selectorState.refreshedAt, inMemoryRefresh);
|
|
2500
|
-
const metadataError = !reasoningMetadataValid || !!selectorState.error || modelRows.length === 0;
|
|
2501
|
-
const stale = metadataError;
|
|
2502
|
-
const metadataValid = !metadataError;
|
|
2503
|
-
const warning = selectorState.warning || null;
|
|
2504
|
-
const reasoningEfforts = uniqueStringList(
|
|
2505
|
-
Object.values(reasoningByModel)
|
|
2506
|
-
.flatMap((list) => Array.isArray(list) ? list : [])
|
|
2507
|
-
.map((value) => normalizeReasoningEffort(value))
|
|
2508
|
-
.filter(Boolean),
|
|
2509
|
-
);
|
|
2510
|
-
return {
|
|
2511
|
-
models: catalogModels,
|
|
2512
|
-
currentModel,
|
|
2513
|
-
defaultModel,
|
|
2514
|
-
source: selectorState.source,
|
|
2515
|
-
refreshedAt,
|
|
2516
|
-
stale,
|
|
2517
|
-
metadataValid,
|
|
2518
|
-
reasoningMetadataValid,
|
|
2519
|
-
warning,
|
|
2520
|
-
error: selectorState.error,
|
|
2521
|
-
reasoningByModel,
|
|
2522
|
-
reasoningEfforts,
|
|
2523
|
-
contextLimitsByModel,
|
|
2524
|
-
modelMetadataByModel,
|
|
2525
|
-
};
|
|
2526
|
-
}
|
|
2527
|
-
|
|
2528
|
-
function touchModelSelectorState({
|
|
2529
|
-
source = 'snapshot',
|
|
2530
|
-
error = null,
|
|
2531
|
-
refreshedAt = new Date().toISOString(),
|
|
2532
|
-
} = {}) {
|
|
2533
|
-
if (!modelSelectorSql?.upsertSelectorState?.run) return;
|
|
2534
|
-
const timestamp = latestModelCatalogRefresh(refreshedAt) || new Date().toISOString();
|
|
2535
|
-
modelSelectorSql.upsertSelectorState.run(
|
|
2536
|
-
String(source || 'snapshot').trim() || 'snapshot',
|
|
2537
|
-
timestamp,
|
|
2538
|
-
error ? String(error).trim().slice(0, 300) : null,
|
|
2539
|
-
timestamp,
|
|
2540
|
-
);
|
|
2541
|
-
}
|
|
2266
|
+
// The Copilot model catalog (snapshot state + model_variants rows) lives in
|
|
2267
|
+
// its own service so it can be tested against an in-memory database; the
|
|
2268
|
+
// runtime only wires it to its constants and the copilot CLI runner. It is
|
|
2269
|
+
// created BEFORE the database so the bootstrap catalog publish below keeps
|
|
2270
|
+
// its historical ordering; bindDatabase() attaches the statements later.
|
|
2271
|
+
const modelVariantCatalog = createModelVariantCatalogService({
|
|
2272
|
+
defaultModel: DEFAULT_MODEL,
|
|
2273
|
+
curatedModelIds: CURATED_MODEL_IDS,
|
|
2274
|
+
autoModelSentinel: AUTO_MODEL_SENTINEL,
|
|
2275
|
+
runCopilotCliCommand,
|
|
2276
|
+
});
|
|
2277
|
+
const {
|
|
2278
|
+
SUPPORTED_REASONING_EFFORTS,
|
|
2279
|
+
normalizeReasoningEffort,
|
|
2280
|
+
buildModelVariantId,
|
|
2281
|
+
getModelCatalogState,
|
|
2282
|
+
updateModelCatalog,
|
|
2283
|
+
listModelVariantRows,
|
|
2284
|
+
getModelContextLimitTokens,
|
|
2285
|
+
parseModelVariantSelection,
|
|
2286
|
+
setEnabledModelVariants,
|
|
2287
|
+
refreshModelVariantCatalogFromCli,
|
|
2288
|
+
} = modelVariantCatalog;
|
|
2542
2289
|
|
|
2543
|
-
function updateModelCatalog(snapshot = {}) {
|
|
2544
|
-
const incomingModels = validatedModelIdList(Array.isArray(snapshot.models) ? snapshot.models : []);
|
|
2545
|
-
const incomingCurrentRaw = String(snapshot.currentModel || '').trim();
|
|
2546
|
-
const incomingDefaultRaw = String(snapshot.defaultModel || '').trim();
|
|
2547
|
-
const incomingCurrent = isValidModelId(incomingCurrentRaw) ? incomingCurrentRaw : '';
|
|
2548
|
-
const incomingDefault = isValidModelId(incomingDefaultRaw) ? incomingDefaultRaw : '';
|
|
2549
|
-
const contextLimitsByModel = normalizeContextLimitsByModel(snapshot.contextLimitsByModel);
|
|
2550
|
-
const modelMetadataByModel = normalizeModelMetadataByModel(snapshot.modelMetadataByModel);
|
|
2551
|
-
const receivedMetadata = incomingModels.length > 0 || Object.keys(contextLimitsByModel).length > 0 || Object.keys(modelMetadataByModel).length > 0;
|
|
2552
|
-
const merged = validatedModelIdList([
|
|
2553
|
-
...curatedModelList(),
|
|
2554
|
-
...incomingModels,
|
|
2555
|
-
incomingCurrent,
|
|
2556
|
-
incomingDefault,
|
|
2557
|
-
...(Array.isArray(modelCatalog.models) ? modelCatalog.models : []),
|
|
2558
|
-
modelCatalog.currentModel,
|
|
2559
|
-
modelCatalog.defaultModel,
|
|
2560
|
-
]);
|
|
2561
|
-
if (!merged.length) merged.push(DEFAULT_MODEL);
|
|
2562
|
-
const currentModel = incomingCurrent || modelCatalog.currentModel || incomingDefault || merged[0] || DEFAULT_MODEL;
|
|
2563
|
-
const defaultModel = incomingDefault || modelCatalog.defaultModel || currentModel || DEFAULT_MODEL;
|
|
2564
|
-
const models = uniqueStringList([currentModel, defaultModel, ...merged]);
|
|
2565
|
-
|
|
2566
|
-
modelCatalog = {
|
|
2567
|
-
models,
|
|
2568
|
-
currentModel,
|
|
2569
|
-
defaultModel,
|
|
2570
|
-
source: String(snapshot.source || modelCatalog.source || 'unknown').trim() || 'unknown',
|
|
2571
|
-
refreshedAt: receivedMetadata ? new Date().toISOString() : modelCatalog.refreshedAt,
|
|
2572
|
-
error: snapshot.error ? String(snapshot.error).trim().slice(0, 300) : null,
|
|
2573
|
-
};
|
|
2574
|
-
if (modelSelectorSql?.upsertVariant?.run) {
|
|
2575
|
-
const existingBaseIds = new Set(listModelVariantRows().map((r) => r.baseModelId));
|
|
2576
|
-
const newBaseIds = models.filter((id) => !existingBaseIds.has(id));
|
|
2577
|
-
if (newBaseIds.length) {
|
|
2578
|
-
const newEntries = buildModelVariantEntries(newBaseIds, { defaultEnabled: false, contextLimitsByModel, modelMetadataByModel });
|
|
2579
|
-
upsertModelVariantCatalogEntries(newEntries, {
|
|
2580
|
-
source: String(modelCatalog.source || 'snapshot').trim() || 'snapshot',
|
|
2581
|
-
error: modelCatalog.error || null,
|
|
2582
|
-
preserveEnabled: true,
|
|
2583
|
-
});
|
|
2584
|
-
}
|
|
2585
|
-
const nowIso = new Date().toISOString();
|
|
2586
|
-
for (const [modelId, contextLimitTokens] of Object.entries(contextLimitsByModel)) {
|
|
2587
|
-
modelSelectorSql.updateContextLimitForBase.run(contextLimitTokens, nowIso, modelId);
|
|
2588
|
-
}
|
|
2589
|
-
for (const [modelId, metadata] of Object.entries(modelMetadataByModel)) {
|
|
2590
|
-
modelSelectorSql.updateModelMetadataForBase.run(
|
|
2591
|
-
metadata.defaultContextLimitTokens,
|
|
2592
|
-
metadata.longContextLimitTokens,
|
|
2593
|
-
metadata.pricing ? JSON.stringify(metadata.pricing) : null,
|
|
2594
|
-
nowIso,
|
|
2595
|
-
modelId,
|
|
2596
|
-
);
|
|
2597
|
-
}
|
|
2598
|
-
}
|
|
2599
|
-
if (receivedMetadata && modelSelectorSql?.upsertSelectorState?.run) {
|
|
2600
|
-
touchModelSelectorState({
|
|
2601
|
-
source: modelCatalog.source,
|
|
2602
|
-
error: modelCatalog.error,
|
|
2603
|
-
refreshedAt: modelCatalog.refreshedAt,
|
|
2604
|
-
});
|
|
2605
|
-
}
|
|
2606
|
-
return getModelCatalogState();
|
|
2607
|
-
}
|
|
2608
2290
|
|
|
2609
2291
|
function getSupportedReasoningEffortsForModel(model = '') {
|
|
2610
2292
|
const modelId = String(model || '').trim().toLowerCase();
|
|
@@ -2693,200 +2375,6 @@ function resolveRequestedModel(model) {
|
|
|
2693
2375
|
};
|
|
2694
2376
|
}
|
|
2695
2377
|
|
|
2696
|
-
function listModelVariantRows() {
|
|
2697
|
-
if (!modelSelectorSql?.listVariants?.all) return [];
|
|
2698
|
-
return modelSelectorSql.listVariants.all().map((row) => normalizeModelVariantRow(row));
|
|
2699
|
-
}
|
|
2700
|
-
|
|
2701
|
-
function listEnabledModelVariantRows() {
|
|
2702
|
-
if (!modelSelectorSql?.listEnabledVariants?.all) return [];
|
|
2703
|
-
return modelSelectorSql.listEnabledVariants.all().map((row) => normalizeModelVariantRow(row));
|
|
2704
|
-
}
|
|
2705
|
-
|
|
2706
|
-
function getModelContextLimitTokens(modelId = '') {
|
|
2707
|
-
const normalizedModelId = canonicalizeModelId(modelId);
|
|
2708
|
-
if (!normalizedModelId) return null;
|
|
2709
|
-
const row = listModelVariantRows().find((entry) => entry.baseModelId === normalizedModelId
|
|
2710
|
-
&& entry.contextLimitTokens !== null
|
|
2711
|
-
&& entry.contextLimitTokens > 0);
|
|
2712
|
-
return row?.contextLimitTokens || null;
|
|
2713
|
-
}
|
|
2714
|
-
|
|
2715
|
-
function parseModelVariantSelection(value) {
|
|
2716
|
-
const variantId = String(value || '').trim();
|
|
2717
|
-
if (!variantId) return null;
|
|
2718
|
-
const knownBaseModels = listModelVariantRows().map((row) => row.baseModelId);
|
|
2719
|
-
const parsed = parseModelVariantId(variantId, { knownBaseModels });
|
|
2720
|
-
if (!parsed) return null;
|
|
2721
|
-
const match = listModelVariantRows().find((row) => row.variantId === parsed.variantId);
|
|
2722
|
-
if (match) {
|
|
2723
|
-
return {
|
|
2724
|
-
variantId: match.variantId,
|
|
2725
|
-
baseModelId: match.baseModelId,
|
|
2726
|
-
reasoningEffort: match.reasoningEffort,
|
|
2727
|
-
provider: match.provider,
|
|
2728
|
-
label: match.label,
|
|
2729
|
-
};
|
|
2730
|
-
}
|
|
2731
|
-
return parsed;
|
|
2732
|
-
}
|
|
2733
|
-
|
|
2734
|
-
function getModelVariantSelectorState() {
|
|
2735
|
-
const fallbackVariant = buildModelVariantId(DEFAULT_MODEL, isReasoningVariantEligibleModel(DEFAULT_MODEL) ? 'none' : null);
|
|
2736
|
-
if (!modelSelectorSql?.listEnabledVariants?.all) {
|
|
2737
|
-
const fallbackModels = buildModelVariantEntries(curatedModelList(), { defaultEnabled: true }).map((entry) => entry.variantId);
|
|
2738
|
-
const models = fallbackModels.length ? fallbackModels : [fallbackVariant];
|
|
2739
|
-
return {
|
|
2740
|
-
models,
|
|
2741
|
-
currentModel: models[0] || fallbackVariant,
|
|
2742
|
-
defaultModel: models[0] || fallbackVariant,
|
|
2743
|
-
source: 'bootstrap',
|
|
2744
|
-
refreshedAt: null,
|
|
2745
|
-
warning: null,
|
|
2746
|
-
error: null,
|
|
2747
|
-
};
|
|
2748
|
-
}
|
|
2749
|
-
const enabledRows = listEnabledModelVariantRows();
|
|
2750
|
-
const selectorState = modelSelectorSql.getSelectorState.get() || null;
|
|
2751
|
-
const enabledVariants = enabledRows.map((row) => row.variantId);
|
|
2752
|
-
const models = enabledVariants.length ? enabledVariants : [fallbackVariant];
|
|
2753
|
-
const warning = enabledVariants.length
|
|
2754
|
-
? null
|
|
2755
|
-
: 'No model variants are enabled. Using fallback.';
|
|
2756
|
-
return {
|
|
2757
|
-
models,
|
|
2758
|
-
currentModel: models[0] || fallbackVariant,
|
|
2759
|
-
defaultModel: models[0] || fallbackVariant,
|
|
2760
|
-
source: String(selectorState?.source || 'db').trim() || 'db',
|
|
2761
|
-
refreshedAt: selectorState?.refreshed_at || null,
|
|
2762
|
-
warning,
|
|
2763
|
-
error: selectorState?.error ? String(selectorState.error) : null,
|
|
2764
|
-
};
|
|
2765
|
-
}
|
|
2766
|
-
|
|
2767
|
-
function upsertModelVariantCatalogEntries(entries = [], {
|
|
2768
|
-
source = 'manual-refresh',
|
|
2769
|
-
error = null,
|
|
2770
|
-
preserveEnabled = true,
|
|
2771
|
-
} = {}) {
|
|
2772
|
-
if (!modelSelectorSql?.upsertVariant?.run || !modelSelectorSql?.upsertSelectorState?.run) {
|
|
2773
|
-
return getModelVariantSelectorState();
|
|
2774
|
-
}
|
|
2775
|
-
const normalizedEntries = Array.isArray(entries)
|
|
2776
|
-
? entries.map((entry) => normalizeModelVariantRow(entry)).filter((entry) => entry.variantId && entry.baseModelId)
|
|
2777
|
-
: [];
|
|
2778
|
-
const existingEnabled = new Map(
|
|
2779
|
-
listModelVariantRows().map((row) => [row.variantId, row.enabled ? 1 : 0]),
|
|
2780
|
-
);
|
|
2781
|
-
const incomingIds = new Set(normalizedEntries.map((entry) => entry.variantId));
|
|
2782
|
-
const incomingBaseIds = new Set(normalizedEntries.map((entry) => entry.baseModelId));
|
|
2783
|
-
const nowIso = new Date().toISOString();
|
|
2784
|
-
const tx = db.transaction(() => {
|
|
2785
|
-
for (const entry of normalizedEntries) {
|
|
2786
|
-
const enabled = preserveEnabled && existingEnabled.has(entry.variantId)
|
|
2787
|
-
? existingEnabled.get(entry.variantId)
|
|
2788
|
-
: (entry.enabled ? 1 : 0);
|
|
2789
|
-
modelSelectorSql.upsertVariant.run(
|
|
2790
|
-
entry.variantId,
|
|
2791
|
-
entry.baseModelId,
|
|
2792
|
-
entry.provider || modelProviderForId(entry.baseModelId),
|
|
2793
|
-
entry.label || modelDisplayLabel(entry.baseModelId),
|
|
2794
|
-
entry.releaseStatus || null,
|
|
2795
|
-
entry.reasoningEffort || null,
|
|
2796
|
-
entry.contextLimitTokens || null,
|
|
2797
|
-
entry.longContextLimitTokens || null,
|
|
2798
|
-
entry.pricing ? JSON.stringify(entry.pricing) : null,
|
|
2799
|
-
enabled,
|
|
2800
|
-
entry.sortOrder,
|
|
2801
|
-
nowIso,
|
|
2802
|
-
);
|
|
2803
|
-
}
|
|
2804
|
-
for (const row of listModelVariantRows()) {
|
|
2805
|
-
if (incomingIds.has(row.variantId)) continue;
|
|
2806
|
-
const shouldMarkUnavailable = row.enabled || incomingBaseIds.has(row.baseModelId);
|
|
2807
|
-
if (shouldMarkUnavailable) {
|
|
2808
|
-
modelSelectorSql.upsertVariant.run(
|
|
2809
|
-
row.variantId,
|
|
2810
|
-
row.baseModelId,
|
|
2811
|
-
row.provider || modelProviderForId(row.baseModelId),
|
|
2812
|
-
row.label || modelDisplayLabel(row.baseModelId),
|
|
2813
|
-
'unavailable',
|
|
2814
|
-
row.reasoningEffort || null,
|
|
2815
|
-
row.contextLimitTokens || null,
|
|
2816
|
-
row.longContextLimitTokens || null,
|
|
2817
|
-
row.pricing ? JSON.stringify(row.pricing) : null,
|
|
2818
|
-
row.enabled ? 1 : 0,
|
|
2819
|
-
row.sortOrder,
|
|
2820
|
-
nowIso,
|
|
2821
|
-
);
|
|
2822
|
-
continue;
|
|
2823
|
-
}
|
|
2824
|
-
// Variants that are both disabled and fully absent from the incoming base set
|
|
2825
|
-
// are no longer relevant; prune them so they do not linger as selectable.
|
|
2826
|
-
modelSelectorSql.deleteVariant.run(row.variantId);
|
|
2827
|
-
}
|
|
2828
|
-
modelSelectorSql.upsertSelectorState.run(
|
|
2829
|
-
String(source || 'manual-refresh').trim() || 'manual-refresh',
|
|
2830
|
-
nowIso,
|
|
2831
|
-
error ? String(error).trim().slice(0, 300) : null,
|
|
2832
|
-
nowIso,
|
|
2833
|
-
);
|
|
2834
|
-
});
|
|
2835
|
-
tx();
|
|
2836
|
-
return getModelVariantSelectorState();
|
|
2837
|
-
}
|
|
2838
|
-
|
|
2839
|
-
function setEnabledModelVariants(variantIds = []) {
|
|
2840
|
-
if (!modelSelectorSql?.disableAllVariants?.run || !modelSelectorSql?.enableVariant?.run) {
|
|
2841
|
-
return getModelVariantSelectorState();
|
|
2842
|
-
}
|
|
2843
|
-
const nextEnabled = new Set(
|
|
2844
|
-
Array.isArray(variantIds) ? variantIds.map((value) => String(value || '').trim()).filter(Boolean) : [],
|
|
2845
|
-
);
|
|
2846
|
-
const existingRows = listModelVariantRows();
|
|
2847
|
-
const nowIso = new Date().toISOString();
|
|
2848
|
-
const tx = db.transaction(() => {
|
|
2849
|
-
modelSelectorSql.disableAllVariants.run(nowIso);
|
|
2850
|
-
for (const row of existingRows) {
|
|
2851
|
-
if (!nextEnabled.has(row.variantId)) continue;
|
|
2852
|
-
modelSelectorSql.enableVariant.run(nowIso, row.variantId);
|
|
2853
|
-
}
|
|
2854
|
-
});
|
|
2855
|
-
tx();
|
|
2856
|
-
return getModelVariantSelectorState();
|
|
2857
|
-
}
|
|
2858
|
-
|
|
2859
|
-
function parseModelsFromHelpConfigOutput(text) {
|
|
2860
|
-
const content = String(text || '');
|
|
2861
|
-
if (!content) return [];
|
|
2862
|
-
const sectionMatch = content.match(/`model`:[\s\S]*?(?=\n\s*`[a-zA-Z][^`]*`:\s|$)/);
|
|
2863
|
-
const section = sectionMatch ? sectionMatch[0] : content;
|
|
2864
|
-
const models = [];
|
|
2865
|
-
const regex = /"([^"]+)"/g;
|
|
2866
|
-
let match;
|
|
2867
|
-
while ((match = regex.exec(section))) {
|
|
2868
|
-
const candidate = String(match[1] || '').trim();
|
|
2869
|
-
if (!candidate || candidate === 'auto') continue;
|
|
2870
|
-
if (!isValidModelId(candidate)) continue;
|
|
2871
|
-
models.push(candidate);
|
|
2872
|
-
}
|
|
2873
|
-
return validatedModelIdList(models);
|
|
2874
|
-
}
|
|
2875
|
-
|
|
2876
|
-
function parseReasoningEffortsFromHelpOutput(text) {
|
|
2877
|
-
const content = String(text || '');
|
|
2878
|
-
if (!content) return SUPPORTED_REASONING_EFFORTS.slice();
|
|
2879
|
-
const values = [];
|
|
2880
|
-
const regex = /"([a-z]+)"/g;
|
|
2881
|
-
let match;
|
|
2882
|
-
while ((match = regex.exec(content))) {
|
|
2883
|
-
const effort = normalizeReasoningEffort(match[1]);
|
|
2884
|
-
if (effort) values.push(effort);
|
|
2885
|
-
}
|
|
2886
|
-
const unique = uniqueStringList(values);
|
|
2887
|
-
return unique.length ? unique : SUPPORTED_REASONING_EFFORTS.slice();
|
|
2888
|
-
}
|
|
2889
|
-
|
|
2890
2378
|
function runCopilotCliCommand(args = [], timeoutMs = 30_000) {
|
|
2891
2379
|
const commandArgs = Array.isArray(args) ? args.map((item) => String(item || '').trim()).filter(Boolean) : [];
|
|
2892
2380
|
return new Promise((resolve, reject) => {
|
|
@@ -2915,71 +2403,6 @@ function runCopilotCliCommand(args = [], timeoutMs = 30_000) {
|
|
|
2915
2403
|
});
|
|
2916
2404
|
}
|
|
2917
2405
|
|
|
2918
|
-
async function refreshModelVariantCatalogFromCli() {
|
|
2919
|
-
let source = 'rpc-snapshot';
|
|
2920
|
-
let modelIds = [];
|
|
2921
|
-
let reasoningEfforts = SUPPORTED_REASONING_EFFORTS.slice();
|
|
2922
|
-
const hasAuthoritativeSnapshot = /^(web-relay-extension|standalone-relay):/.test(String(modelCatalog.source || ''));
|
|
2923
|
-
const refreshSelectionFromSnapshot = selectModelIdsForVariantRefresh({
|
|
2924
|
-
snapshotModels: hasAuthoritativeSnapshot && Array.isArray(modelCatalog.models) ? modelCatalog.models : [],
|
|
2925
|
-
currentModel: modelCatalog.currentModel,
|
|
2926
|
-
defaultModel: modelCatalog.defaultModel,
|
|
2927
|
-
helpModelIds: [],
|
|
2928
|
-
});
|
|
2929
|
-
source = refreshSelectionFromSnapshot.source;
|
|
2930
|
-
modelIds = refreshSelectionFromSnapshot.modelIds;
|
|
2931
|
-
const genericHelpText = await runCopilotCliCommand(['help']).catch(() => '');
|
|
2932
|
-
reasoningEfforts = parseReasoningEffortsFromHelpOutput(genericHelpText);
|
|
2933
|
-
if (!modelIds.length) {
|
|
2934
|
-
const configHelpText = await runCopilotCliCommand(['help', 'config']);
|
|
2935
|
-
const helpModelIds = parseModelsFromHelpConfigOutput(configHelpText);
|
|
2936
|
-
const refreshSelectionFromHelp = selectModelIdsForVariantRefresh({
|
|
2937
|
-
snapshotModels: [],
|
|
2938
|
-
currentModel: '',
|
|
2939
|
-
defaultModel: '',
|
|
2940
|
-
helpModelIds,
|
|
2941
|
-
});
|
|
2942
|
-
source = refreshSelectionFromHelp.source;
|
|
2943
|
-
modelIds = refreshSelectionFromHelp.modelIds;
|
|
2944
|
-
}
|
|
2945
|
-
const entries = [];
|
|
2946
|
-
let sortOrder = 0;
|
|
2947
|
-
for (const modelId of modelIds) {
|
|
2948
|
-
const provider = modelProviderForId(modelId);
|
|
2949
|
-
const label = modelDisplayLabel(modelId);
|
|
2950
|
-
if (isReasoningVariantEligibleModel(modelId)) {
|
|
2951
|
-
for (const effort of reasoningEfforts) {
|
|
2952
|
-
entries.push({
|
|
2953
|
-
variantId: buildModelVariantId(modelId, effort),
|
|
2954
|
-
baseModelId: modelId,
|
|
2955
|
-
provider,
|
|
2956
|
-
label,
|
|
2957
|
-
reasoningEffort: effort,
|
|
2958
|
-
enabled: 0,
|
|
2959
|
-
sortOrder: sortOrder++,
|
|
2960
|
-
});
|
|
2961
|
-
}
|
|
2962
|
-
continue;
|
|
2963
|
-
}
|
|
2964
|
-
entries.push({
|
|
2965
|
-
variantId: buildModelVariantId(modelId),
|
|
2966
|
-
baseModelId: modelId,
|
|
2967
|
-
provider,
|
|
2968
|
-
label,
|
|
2969
|
-
reasoningEffort: null,
|
|
2970
|
-
enabled: 0,
|
|
2971
|
-
sortOrder: sortOrder++,
|
|
2972
|
-
});
|
|
2973
|
-
}
|
|
2974
|
-
if (!entries.length) {
|
|
2975
|
-
throw new Error('No models found in snapshot/help model output');
|
|
2976
|
-
}
|
|
2977
|
-
return upsertModelVariantCatalogEntries(entries, {
|
|
2978
|
-
source,
|
|
2979
|
-
error: null,
|
|
2980
|
-
preserveEnabled: true,
|
|
2981
|
-
});
|
|
2982
|
-
}
|
|
2983
2406
|
|
|
2984
2407
|
function normalizeRelayMode(mode) {
|
|
2985
2408
|
const value = String(mode || '').trim().toLowerCase();
|
|
@@ -3115,154 +2538,7 @@ const imageOperationService = createImageOperationService({
|
|
|
3115
2538
|
uuidv4,
|
|
3116
2539
|
});
|
|
3117
2540
|
|
|
3118
|
-
|
|
3119
|
-
listVariants: db.prepare(`
|
|
3120
|
-
SELECT variant_id, base_model_id, provider, label, release_status, reasoning_effort, context_limit_tokens, long_context_limit_tokens, pricing_json, enabled, sort_order, updated_at
|
|
3121
|
-
FROM model_variants
|
|
3122
|
-
ORDER BY provider ASC, sort_order ASC, variant_id ASC
|
|
3123
|
-
`),
|
|
3124
|
-
listEnabledVariants: db.prepare(`
|
|
3125
|
-
SELECT variant_id, base_model_id, provider, label, release_status, reasoning_effort, context_limit_tokens, long_context_limit_tokens, pricing_json, enabled, sort_order, updated_at
|
|
3126
|
-
FROM model_variants
|
|
3127
|
-
WHERE enabled = 1
|
|
3128
|
-
ORDER BY provider ASC, sort_order ASC, variant_id ASC
|
|
3129
|
-
`),
|
|
3130
|
-
upsertVariant: db.prepare(`
|
|
3131
|
-
INSERT INTO model_variants (
|
|
3132
|
-
variant_id, base_model_id, provider, label, release_status, reasoning_effort, context_limit_tokens, long_context_limit_tokens, pricing_json, enabled, sort_order, updated_at
|
|
3133
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
3134
|
-
ON CONFLICT(variant_id) DO UPDATE SET
|
|
3135
|
-
base_model_id = excluded.base_model_id,
|
|
3136
|
-
provider = excluded.provider,
|
|
3137
|
-
label = excluded.label,
|
|
3138
|
-
release_status = excluded.release_status,
|
|
3139
|
-
reasoning_effort = excluded.reasoning_effort,
|
|
3140
|
-
context_limit_tokens = COALESCE(excluded.context_limit_tokens, model_variants.context_limit_tokens),
|
|
3141
|
-
long_context_limit_tokens = COALESCE(excluded.long_context_limit_tokens, model_variants.long_context_limit_tokens),
|
|
3142
|
-
pricing_json = COALESCE(excluded.pricing_json, model_variants.pricing_json),
|
|
3143
|
-
enabled = excluded.enabled,
|
|
3144
|
-
sort_order = excluded.sort_order,
|
|
3145
|
-
updated_at = excluded.updated_at
|
|
3146
|
-
`),
|
|
3147
|
-
updateContextLimitForBase: db.prepare(`
|
|
3148
|
-
UPDATE model_variants
|
|
3149
|
-
SET context_limit_tokens = ?, updated_at = ?
|
|
3150
|
-
WHERE base_model_id = ?
|
|
3151
|
-
`),
|
|
3152
|
-
updateModelMetadataForBase: db.prepare(`
|
|
3153
|
-
UPDATE model_variants
|
|
3154
|
-
SET context_limit_tokens = COALESCE(?, context_limit_tokens),
|
|
3155
|
-
long_context_limit_tokens = COALESCE(?, long_context_limit_tokens),
|
|
3156
|
-
pricing_json = COALESCE(?, pricing_json),
|
|
3157
|
-
updated_at = ?
|
|
3158
|
-
WHERE base_model_id = ?
|
|
3159
|
-
`),
|
|
3160
|
-
disableAllVariants: db.prepare(`UPDATE model_variants SET enabled = 0, updated_at = ?`),
|
|
3161
|
-
enableVariant: db.prepare(`UPDATE model_variants SET enabled = 1, updated_at = ? WHERE variant_id = ?`),
|
|
3162
|
-
deleteVariant: db.prepare(`DELETE FROM model_variants WHERE variant_id = ?`),
|
|
3163
|
-
getSelectorState: db.prepare(`SELECT source, refreshed_at, error, updated_at FROM model_selector_state WHERE id = 1 LIMIT 1`),
|
|
3164
|
-
upsertSelectorState: db.prepare(`
|
|
3165
|
-
INSERT INTO model_selector_state (id, source, refreshed_at, error, updated_at)
|
|
3166
|
-
VALUES (1, ?, ?, ?, ?)
|
|
3167
|
-
ON CONFLICT(id) DO UPDATE SET
|
|
3168
|
-
source = excluded.source,
|
|
3169
|
-
refreshed_at = excluded.refreshed_at,
|
|
3170
|
-
error = excluded.error,
|
|
3171
|
-
updated_at = excluded.updated_at
|
|
3172
|
-
`),
|
|
3173
|
-
};
|
|
3174
|
-
|
|
3175
|
-
{
|
|
3176
|
-
const existingCount = Number(db.prepare(`SELECT COUNT(*) AS cnt FROM model_variants`).get()?.cnt || 0);
|
|
3177
|
-
const normalizeLegacyVariantIdsTx = db.transaction(() => {
|
|
3178
|
-
const rows = modelSelectorSql.listVariants.all();
|
|
3179
|
-
const canonicalRows = new Map();
|
|
3180
|
-
for (const rawRow of rows) {
|
|
3181
|
-
const row = normalizeModelVariantRow(rawRow);
|
|
3182
|
-
const canonicalBaseModelId = canonicalizeModelId(row.baseModelId);
|
|
3183
|
-
if (!canonicalBaseModelId) continue;
|
|
3184
|
-
const canonicalVariantId = buildModelVariantId(canonicalBaseModelId, row.reasoningEffort);
|
|
3185
|
-
const existing = canonicalRows.get(canonicalVariantId);
|
|
3186
|
-
if (!existing) {
|
|
3187
|
-
canonicalRows.set(canonicalVariantId, {
|
|
3188
|
-
variantId: canonicalVariantId,
|
|
3189
|
-
baseModelId: canonicalBaseModelId,
|
|
3190
|
-
provider: row.provider || modelProviderForId(canonicalBaseModelId),
|
|
3191
|
-
label: row.label || modelDisplayLabel(canonicalBaseModelId),
|
|
3192
|
-
releaseStatus: row.releaseStatus || null,
|
|
3193
|
-
reasoningEffort: row.reasoningEffort || null,
|
|
3194
|
-
enabled: row.enabled ? 1 : 0,
|
|
3195
|
-
sortOrder: row.sortOrder,
|
|
3196
|
-
updatedAt: row.updatedAt,
|
|
3197
|
-
});
|
|
3198
|
-
continue;
|
|
3199
|
-
}
|
|
3200
|
-
existing.enabled = existing.enabled || (row.enabled ? 1 : 0) ? 1 : 0;
|
|
3201
|
-
if (existing.releaseStatus !== null && row.releaseStatus === null) {
|
|
3202
|
-
existing.releaseStatus = null;
|
|
3203
|
-
}
|
|
3204
|
-
existing.sortOrder = Math.min(existing.sortOrder, row.sortOrder);
|
|
3205
|
-
if (!existing.label && row.label) existing.label = row.label;
|
|
3206
|
-
}
|
|
3207
|
-
|
|
3208
|
-
const nowIso = new Date().toISOString();
|
|
3209
|
-
for (const entry of canonicalRows.values()) {
|
|
3210
|
-
modelSelectorSql.upsertVariant.run(
|
|
3211
|
-
entry.variantId,
|
|
3212
|
-
entry.baseModelId,
|
|
3213
|
-
entry.provider || modelProviderForId(entry.baseModelId),
|
|
3214
|
-
entry.label || modelDisplayLabel(entry.baseModelId),
|
|
3215
|
-
entry.releaseStatus || null,
|
|
3216
|
-
entry.reasoningEffort || null,
|
|
3217
|
-
entry.contextLimitTokens || null,
|
|
3218
|
-
entry.longContextLimitTokens || null,
|
|
3219
|
-
entry.pricing ? JSON.stringify(entry.pricing) : null,
|
|
3220
|
-
entry.enabled ? 1 : 0,
|
|
3221
|
-
entry.sortOrder,
|
|
3222
|
-
nowIso,
|
|
3223
|
-
);
|
|
3224
|
-
}
|
|
3225
|
-
for (const rawRow of rows) {
|
|
3226
|
-
const rawVariantId = String(rawRow?.variant_id || '').trim();
|
|
3227
|
-
const rawBaseModelId = String(rawRow?.base_model_id || '').trim();
|
|
3228
|
-
const canonicalBaseModelId = canonicalizeModelId(rawBaseModelId);
|
|
3229
|
-
if (!canonicalBaseModelId) continue;
|
|
3230
|
-
const canonicalReasoningEffort = normalizeReasoningEffort(rawRow?.reasoning_effort);
|
|
3231
|
-
const canonicalVariantId = buildModelVariantId(canonicalBaseModelId, canonicalReasoningEffort);
|
|
3232
|
-
if (rawVariantId !== canonicalVariantId) {
|
|
3233
|
-
modelSelectorSql.deleteVariant.run(rawVariantId);
|
|
3234
|
-
}
|
|
3235
|
-
}
|
|
3236
|
-
});
|
|
3237
|
-
if (existingCount === 0) {
|
|
3238
|
-
const nowIso = new Date().toISOString();
|
|
3239
|
-
const seedEntries = buildModelVariantEntries(curatedModelList(), { defaultEnabled: true });
|
|
3240
|
-
const tx = db.transaction(() => {
|
|
3241
|
-
for (const entry of seedEntries) {
|
|
3242
|
-
modelSelectorSql.upsertVariant.run(
|
|
3243
|
-
entry.variantId,
|
|
3244
|
-
entry.baseModelId,
|
|
3245
|
-
entry.provider,
|
|
3246
|
-
entry.label,
|
|
3247
|
-
entry.releaseStatus || null,
|
|
3248
|
-
entry.reasoningEffort || null,
|
|
3249
|
-
entry.contextLimitTokens || null,
|
|
3250
|
-
entry.longContextLimitTokens || null,
|
|
3251
|
-
entry.pricing ? JSON.stringify(entry.pricing) : null,
|
|
3252
|
-
entry.enabled ? 1 : 0,
|
|
3253
|
-
entry.sortOrder,
|
|
3254
|
-
nowIso,
|
|
3255
|
-
);
|
|
3256
|
-
}
|
|
3257
|
-
modelSelectorSql.upsertSelectorState.run('bootstrap-seed', nowIso, null, nowIso);
|
|
3258
|
-
});
|
|
3259
|
-
tx();
|
|
3260
|
-
} else if (!modelSelectorSql.getSelectorState.get()) {
|
|
3261
|
-
const nowIso = new Date().toISOString();
|
|
3262
|
-
modelSelectorSql.upsertSelectorState.run('legacy', nowIso, null, nowIso);
|
|
3263
|
-
}
|
|
3264
|
-
normalizeLegacyVariantIdsTx();
|
|
3265
|
-
}
|
|
2541
|
+
modelVariantCatalog.bindDatabase(db);
|
|
3266
2542
|
|
|
3267
2543
|
const deleteArchiveService = createDeleteArchiveService(db, null, { resolveSessionStateRoot });
|
|
3268
2544
|
void deleteArchiveService.retryPendingDeletesOnStartup()
|
|
@@ -3311,21 +2587,32 @@ function buildSessionWorkerLaunchEnv() {
|
|
|
3311
2587
|
const candidate = path.join(versionDir, 'preloads', 'extension_bootstrap.mjs');
|
|
3312
2588
|
if (fs.existsSync(candidate)) return candidate;
|
|
3313
2589
|
}
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
2590
|
+
// The CLI cache lives in a per-platform location (LOCALAPPDATA on Windows,
|
|
2591
|
+
// Application Support on macOS, XDG dirs on Linux) — scan the same base
|
|
2592
|
+
// dirs as the SDK client instead of assuming the Linux layout. Selection
|
|
2593
|
+
// deliberately keys on the bootstrap file itself rather than the bundled
|
|
2594
|
+
// SDK: CLI versions without a copilot-sdk directory must still resolve
|
|
2595
|
+
// here for the extension engine.
|
|
2596
|
+
const subdirs = [`${process.platform}-${process.arch}`, 'universal'];
|
|
2597
|
+
const candidates = [];
|
|
2598
|
+
for (const [baseDirIdx, baseDir] of getCopilotBaseDirs().entries()) {
|
|
2599
|
+
for (const [subdirIdx, subdir] of subdirs.entries()) {
|
|
2600
|
+
let entries = [];
|
|
2601
|
+
try { entries = fs.readdirSync(path.join(baseDir, subdir)); } catch { continue; }
|
|
2602
|
+
for (const version of entries) {
|
|
2603
|
+
if (!/^\d+\.\d+\.\d+$/.test(version)) continue;
|
|
2604
|
+
const candidate = path.join(baseDir, subdir, version, 'preloads', 'extension_bootstrap.mjs');
|
|
2605
|
+
if (fs.existsSync(candidate)) candidates.push({ version, baseDirIdx, subdirIdx, candidate });
|
|
2606
|
+
}
|
|
3325
2607
|
}
|
|
3326
|
-
} catch {
|
|
3327
|
-
// The CLI cache is optional; retain the normal fallback when unavailable.
|
|
3328
2608
|
}
|
|
2609
|
+
candidates.sort((a, b) => {
|
|
2610
|
+
const aVersion = a.version.split('.').map(Number);
|
|
2611
|
+
const bVersion = b.version.split('.').map(Number);
|
|
2612
|
+
return bVersion[0] - aVersion[0] || bVersion[1] - aVersion[1] || bVersion[2] - aVersion[2]
|
|
2613
|
+
|| a.baseDirIdx - b.baseDirIdx || a.subdirIdx - b.subdirIdx;
|
|
2614
|
+
});
|
|
2615
|
+
if (candidates.length) return candidates[0].candidate;
|
|
3329
2616
|
return null;
|
|
3330
2617
|
};
|
|
3331
2618
|
|
|
@@ -3503,7 +2790,65 @@ const sessionWorkerSupervisor = createSessionWorkerSupervisor({
|
|
|
3503
2790
|
diagnosticPlanReference: () => path.join(currentWorkspaceRootPath(), '.cursor', 'plans', 'worker-startup-monitoring-plan.md'),
|
|
3504
2791
|
log: (message) => console.warn(`${runtimeLogPrefix()}${message}`),
|
|
3505
2792
|
});
|
|
3506
|
-
|
|
2793
|
+
// Feature flags moved from config.json into app_settings; adopt any legacy
|
|
2794
|
+
// `features` key once, then resolve the boot snapshot (defaults < stored rows
|
|
2795
|
+
// < COPILOT_REMOTE_* env). The snapshot is frozen for the process lifetime —
|
|
2796
|
+
// the Features settings tab persists changes and asks for a relay restart.
|
|
2797
|
+
migrateLegacyConfigFeatures({
|
|
2798
|
+
configPath: CONFIG_PATH,
|
|
2799
|
+
readSetting: readAppSettingValue,
|
|
2800
|
+
writeSetting: (key, value) => stmts.upsertAppSetting.run(key, value, new Date().toISOString()),
|
|
2801
|
+
});
|
|
2802
|
+
const featureFlags = resolveBootFeatureFlags({ readSetting: readAppSettingValue });
|
|
2803
|
+
|
|
2804
|
+
// Settle any persisted self-update attempt from the previous run: an unchanged
|
|
2805
|
+
// version after the restart becomes a visible failure carrying the npm log.
|
|
2806
|
+
reconcileUpdateAttempt({
|
|
2807
|
+
readSetting: readAppSettingValue,
|
|
2808
|
+
writeSetting: (key, value) => stmts.upsertAppSetting.run(key, value, new Date().toISOString()),
|
|
2809
|
+
deleteSetting: (key) => stmts.deleteAppSetting.run(key),
|
|
2810
|
+
runningVersion: RUNNING_VERSION,
|
|
2811
|
+
});
|
|
2812
|
+
|
|
2813
|
+
// Update checking is opt-in (zero telemetry by default): the service exists so
|
|
2814
|
+
// manual "check now" and the settings toggle work, but never fetches unless
|
|
2815
|
+
// the stored setting says so. OAR_NO_UPDATE_CHECK=1 kills even that.
|
|
2816
|
+
const updateCheckKilled = isUpdateCheckKilled(process.env);
|
|
2817
|
+
const updateCheckService = updateCheckKilled ? null : createUpdateCheckService({
|
|
2818
|
+
runningVersion: RUNNING_VERSION,
|
|
2819
|
+
installMethod: INSTALL_METHOD,
|
|
2820
|
+
manifestUrl: resolveUpdateManifestUrl(process.env),
|
|
2821
|
+
readSetting: readAppSettingValue,
|
|
2822
|
+
writeSetting: (key, value) => stmts.upsertAppSetting.run(key, value, new Date().toISOString()),
|
|
2823
|
+
logger: console,
|
|
2824
|
+
onStateChange: () => emitUpdateState(),
|
|
2825
|
+
});
|
|
2826
|
+
const updateInstallService = createUpdateInstallService({
|
|
2827
|
+
runningVersion: RUNNING_VERSION,
|
|
2828
|
+
installMethod: INSTALL_METHOD,
|
|
2829
|
+
readSetting: readAppSettingValue,
|
|
2830
|
+
writeSetting: (key, value) => stmts.upsertAppSetting.run(key, value, new Date().toISOString()),
|
|
2831
|
+
deleteSetting: (key) => stmts.deleteAppSetting.run(key),
|
|
2832
|
+
requestRelayShutdown: (options) => requestRelayShutdown(options),
|
|
2833
|
+
logger: console,
|
|
2834
|
+
});
|
|
2835
|
+
updateInstallService.subscribe(() => emitUpdateState());
|
|
2836
|
+
|
|
2837
|
+
function buildUpdateStatePayload() {
|
|
2838
|
+
return {
|
|
2839
|
+
runningVersion: RUNNING_VERSION,
|
|
2840
|
+
installMethod: INSTALL_METHOD,
|
|
2841
|
+
checkKilled: updateCheckKilled,
|
|
2842
|
+
check: updateCheckService?.getSnapshot() || null,
|
|
2843
|
+
install: updateInstallService.getSnapshot(),
|
|
2844
|
+
};
|
|
2845
|
+
}
|
|
2846
|
+
|
|
2847
|
+
function emitUpdateState() {
|
|
2848
|
+
// `io` is created later in boot; emits only fire from user actions and
|
|
2849
|
+
// timers, both post-boot, so the reference is safe by then.
|
|
2850
|
+
try { io.emit('update_state', buildUpdateStatePayload()); } catch {}
|
|
2851
|
+
}
|
|
3507
2852
|
|
|
3508
2853
|
async function stopSessionWorkerForProviderRebind(sdkSessionId) {
|
|
3509
2854
|
const sessionId = String(sdkSessionId || '').trim();
|
|
@@ -5538,8 +4883,35 @@ const backgroundTaskStore = {
|
|
|
5538
4883
|
},
|
|
5539
4884
|
};
|
|
5540
4885
|
|
|
4886
|
+
// The live turn among a conversation's processing rows. Mid-turn steering can
|
|
4887
|
+
// leave several processing at once — the running turn, steered messages the
|
|
4888
|
+
// CLI folded into it (no output of their own), a handed-off row still settling
|
|
4889
|
+
// — so the live turn is the row that produced output most recently; with
|
|
4890
|
+
// nothing to separate them yet, the oldest processing row (the turn a steered
|
|
4891
|
+
// message joins) wins. See live-turn-picker.mjs for why recency, not per-row
|
|
4892
|
+
// stream seq. Returns the whole row so callers get relay_mode/timestamps too.
|
|
4893
|
+
function resolveLiveTurnQueueRow(conversationId) {
|
|
4894
|
+
const rows = stmts.listProcessingQueueByConversation?.all(conversationId)
|
|
4895
|
+
|| (stmts.getLatestProcessingQueueByConversation.get(conversationId)
|
|
4896
|
+
? [stmts.getLatestProcessingQueueByConversation.get(conversationId)]
|
|
4897
|
+
: []);
|
|
4898
|
+
if (!rows.length) return null;
|
|
4899
|
+
if (rows.length === 1) return rows[0];
|
|
4900
|
+
const byId = new Map(rows.map((row) => [String(row.id), row]));
|
|
4901
|
+
const liveId = pickLiveTurnRowId(rows.map((row) => {
|
|
4902
|
+
const output = stmts.getLastOutputAtByQueueMessage?.get({ id: row.id }) || null;
|
|
4903
|
+
return {
|
|
4904
|
+
id: String(row.id),
|
|
4905
|
+
lastOutputAtMs: Date.parse(String(output?.last_output_at || '')) || 0,
|
|
4906
|
+
streamDone: Number(output?.stream_done || 0) === 1,
|
|
4907
|
+
processingAtMs: Date.parse(String(row.processing_at || row.timestamp || '')) || 0,
|
|
4908
|
+
};
|
|
4909
|
+
}));
|
|
4910
|
+
return byId.get(liveId) || rows[0];
|
|
4911
|
+
}
|
|
4912
|
+
|
|
5541
4913
|
function inFlightStateForConversation(conversationId) {
|
|
5542
|
-
const row =
|
|
4914
|
+
const row = resolveLiveTurnQueueRow(conversationId);
|
|
5543
4915
|
if (!row) return null;
|
|
5544
4916
|
const streamEvents = relayStreamEventsForQueueMessage(row.id);
|
|
5545
4917
|
const lastStreamEvent = streamEvents.length ? streamEvents[streamEvents.length - 1] : null;
|
|
@@ -5564,14 +4936,17 @@ function inFlightStateForConversation(conversationId) {
|
|
|
5564
4936
|
// retry_count like /api/requeue does, but bypasses that route's budget check,
|
|
5565
4937
|
// so the sweep enforces it here). Minimal mirror of the route-side
|
|
5566
4938
|
// failQueueMessage: mark failed, insert the assistant failure message, emit.
|
|
5567
|
-
function failRecoveredRowTerminally(row, reason) {
|
|
4939
|
+
function failRecoveredRowTerminally(row, reason, { failureRecord = null, failureText: presetText = null } = {}) {
|
|
5568
4940
|
const now = new Date().toISOString();
|
|
5569
4941
|
const responseId = uuidv4();
|
|
5570
4942
|
const retryCount = Number(row?.retry_count || 0) + 1;
|
|
5571
|
-
const failureText = `Relay recovery limit reached after ${retryCount} attempts (${reason}). The worker kept dying before completing this turn, so the message was failed to keep the queue moving. Send it again to retry.`;
|
|
4943
|
+
const failureText = presetText || `Relay recovery limit reached after ${retryCount} attempts (${reason}). The worker kept dying before completing this turn, so the message was failed to keep the queue moving. Send it again to retry.`;
|
|
5572
4944
|
const requestedModel = String(row?.model || '').trim() || null;
|
|
5573
4945
|
const tx = db.transaction(() => {
|
|
5574
|
-
|
|
4946
|
+
// setFailed is compare-and-set on status (processing/pending/parked): a
|
|
4947
|
+
// row that settled between the sweep's listing and this write must not
|
|
4948
|
+
// grow an orphan failure bubble on top of its real outcome.
|
|
4949
|
+
const result = stmts.setFailed.run(JSON.stringify(failureRecord || {
|
|
5575
4950
|
kind: 'recovery-limit',
|
|
5576
4951
|
error: 'recovery-limit',
|
|
5577
4952
|
code: 'recovery-limit',
|
|
@@ -5580,6 +4955,7 @@ function failRecoveredRowTerminally(row, reason) {
|
|
|
5580
4955
|
retryCount,
|
|
5581
4956
|
failedAt: now,
|
|
5582
4957
|
}), row.id);
|
|
4958
|
+
if (result.changes === 0) return false;
|
|
5583
4959
|
stmts.setQueueResponseMessageId?.run(responseId, row.id);
|
|
5584
4960
|
stmts.insertMsg.run(
|
|
5585
4961
|
responseId,
|
|
@@ -5594,12 +4970,14 @@ function failRecoveredRowTerminally(row, reason) {
|
|
|
5594
4970
|
null,
|
|
5595
4971
|
requestedModel ? 'user' : 'auto',
|
|
5596
4972
|
);
|
|
4973
|
+
stmts.setMessageSourceId?.run(row.id, responseId);
|
|
5597
4974
|
stmts.linkActivityToResponse?.run(responseId, row.id);
|
|
5598
4975
|
stmts.linkStreamEventsToResponse?.run(responseId, row.id);
|
|
5599
4976
|
stmts.linkThoughtsToResponse?.run(responseId, row.id);
|
|
5600
4977
|
stmts.updateConvTime.run(now, row.conversation_id);
|
|
4978
|
+
return true;
|
|
5601
4979
|
});
|
|
5602
|
-
tx();
|
|
4980
|
+
if (!tx()) return;
|
|
5603
4981
|
io.emit('assistant_message', {
|
|
5604
4982
|
conversationId: row.conversation_id,
|
|
5605
4983
|
sourceMessageId: row.id,
|
|
@@ -5607,7 +4985,20 @@ function failRecoveredRowTerminally(row, reason) {
|
|
|
5607
4985
|
message: { role: 'assistant', text: failureText, model: requestedModel || 'unknown', mode: String(row?.relay_mode || '').trim() || null, timestamp: now },
|
|
5608
4986
|
});
|
|
5609
4987
|
io.emit('message_status', { messageId: row.id, conversationId: row.conversation_id, status: 'failed' });
|
|
5610
|
-
console.warn(`${runtimeLogPrefix()}RECOVERY LIMIT ${String(row.id).slice(0, 8)} conv=${String(row.conversation_id).slice(0, 8)} retries=${retryCount} reason=${reason}`);
|
|
4988
|
+
console.warn(`${runtimeLogPrefix()}${failureRecord ? 'CONSUMED FAILED' : 'RECOVERY LIMIT'} ${String(row.id).slice(0, 8)} conv=${String(row.conversation_id).slice(0, 8)} retries=${retryCount} reason=${reason}`);
|
|
4989
|
+
}
|
|
4990
|
+
|
|
4991
|
+
// A row whose prompt the worker's runtime already consumed (queue.consumed_at)
|
|
4992
|
+
// is at-most-once: recovery must never requeue it — a re-delivery would run the
|
|
4993
|
+
// prompt twice — so it fails terminally with the steer-settle wording, naming
|
|
4994
|
+
// the provider's runtime the way the workers' own settle wording does.
|
|
4995
|
+
function failConsumedRowTerminally(row, reason) {
|
|
4996
|
+
const providerType = stmts.getRuntimeSessionByConversation?.get?.(row?.conversation_id)?.provider_type;
|
|
4997
|
+
const failureRecord = buildSteerSettleFailure(row, { agentLabel: steerAgentLabelForProvider(providerType) });
|
|
4998
|
+
failRecoveredRowTerminally(row, reason, {
|
|
4999
|
+
failureRecord: { ...failureRecord, error: 'terminal-error', reason: String(reason || 'recovery') },
|
|
5000
|
+
failureText: `${failureRecord.message} Error code: ${failureRecord.stableCode}. ${failureRecord.guidance}`,
|
|
5001
|
+
});
|
|
5611
5002
|
}
|
|
5612
5003
|
|
|
5613
5004
|
function recoverProcessingOlderThan(cutoffIso, requeueAtIso, { ceilingBeforeIso = null } = {}) {
|
|
@@ -5619,7 +5010,9 @@ function recoverProcessingOlderThan(cutoffIso, requeueAtIso, { ceilingBeforeIso
|
|
|
5619
5010
|
stmts.dropStaleContinuation?.run?.(row.id);
|
|
5620
5011
|
io.emit('message_status', { messageId: row.id, conversationId: row.conversation_id, status: 'failed' });
|
|
5621
5012
|
}
|
|
5622
|
-
const
|
|
5013
|
+
const listedRows = stmts.listRecoverableProcessing.all(params);
|
|
5014
|
+
for (const row of listedRows.filter((entry) => entry?.consumed_at)) failConsumedRowTerminally(row, 'stale-recovery');
|
|
5015
|
+
const allRows = listedRows.filter((entry) => !entry?.consumed_at);
|
|
5623
5016
|
if (!allRows.length) return [];
|
|
5624
5017
|
// Enforce the retry budget before requeueing: failing these rows first
|
|
5625
5018
|
// flips them out of 'processing' so the recovery UPDATE below skips them.
|
|
@@ -5873,9 +5266,9 @@ function renderIndexHtmlWithPwaVersion(appConfigOverrides = {}) {
|
|
|
5873
5266
|
|
|
5874
5267
|
function loadPwaManifestTemplate() {
|
|
5875
5268
|
const fallback = {
|
|
5876
|
-
name: '
|
|
5877
|
-
short_name: '
|
|
5878
|
-
description: '
|
|
5269
|
+
name: 'OAR',
|
|
5270
|
+
short_name: 'OAR',
|
|
5271
|
+
description: 'OAR — Open Agent Relay: drive your local coding agents from any browser.',
|
|
5879
5272
|
display_override: ['standalone'],
|
|
5880
5273
|
display: 'standalone',
|
|
5881
5274
|
background_color: '#161b22',
|
|
@@ -5896,9 +5289,15 @@ function loadPwaManifestTemplate() {
|
|
|
5896
5289
|
|
|
5897
5290
|
function buildScopedPwaManifest({ shared = false } = {}) {
|
|
5898
5291
|
const manifest = loadPwaManifestTemplate();
|
|
5292
|
+
// The custom name must be baked into every served manifest: Android's WebAPK
|
|
5293
|
+
// update check re-fetches this route outside any page session, so a name the
|
|
5294
|
+
// route doesn't know reverts on the phone with an accept-or-uninstall prompt.
|
|
5295
|
+
const names = resolvePwaManifestNames(getPwaAppName(), manifest.name);
|
|
5899
5296
|
if (shared) {
|
|
5900
5297
|
return {
|
|
5901
5298
|
...manifest,
|
|
5299
|
+
name: names.name,
|
|
5300
|
+
short_name: names.short_name,
|
|
5902
5301
|
id: './__copilot_remote_shared__',
|
|
5903
5302
|
start_url: './',
|
|
5904
5303
|
scope: './',
|
|
@@ -5909,6 +5308,8 @@ function buildScopedPwaManifest({ shared = false } = {}) {
|
|
|
5909
5308
|
}
|
|
5910
5309
|
return {
|
|
5911
5310
|
...manifest,
|
|
5311
|
+
name: names.name,
|
|
5312
|
+
short_name: names.short_name,
|
|
5912
5313
|
id: './__copilot_remote_pwa__',
|
|
5913
5314
|
start_url: './',
|
|
5914
5315
|
scope: './',
|
|
@@ -5942,6 +5343,30 @@ const sdkSessionImportService = createSdkSessionImportService({
|
|
|
5942
5343
|
parseSessionEventsToMessages,
|
|
5943
5344
|
replaceRetrievableHistory: sessionHistoryRefreshService.replaceRetrievableHistory,
|
|
5944
5345
|
ensureRuntimeSessionBinding,
|
|
5346
|
+
// A registered/supervised worker means the relay executes this session even
|
|
5347
|
+
// when no queue row is visible (e.g. between turns) — the importer must not
|
|
5348
|
+
// overwrite its history.
|
|
5349
|
+
hasRelayExecutionSignal: (sdkSessionId) => !!(
|
|
5350
|
+
sessionWorkerRegistry?.getWorker?.(sdkSessionId)
|
|
5351
|
+
|| sessionWorkerSupervisor?.getWorkerState?.(sdkSessionId)
|
|
5352
|
+
),
|
|
5353
|
+
logger: console,
|
|
5354
|
+
});
|
|
5355
|
+
// Phase 5A: server-side Copilot model discovery. Once the extension retires,
|
|
5356
|
+
// nothing populates the model catalog on a fresh boot until a worker spawns;
|
|
5357
|
+
// this asks the installed CLI runtime for `listModels()` directly (deferred —
|
|
5358
|
+
// see the listen callback) and feeds updateModelCatalog in-process. It
|
|
5359
|
+
// feature-detects the CLI before spawning anything, so a relay without Copilot
|
|
5360
|
+
// stays silent.
|
|
5361
|
+
const copilotModelDiscoveryService = createCopilotModelDiscoveryService({
|
|
5362
|
+
createClient: () => createInstalledCopilotClient({
|
|
5363
|
+
config,
|
|
5364
|
+
cwd: currentWorkspaceRootPath(),
|
|
5365
|
+
baseDirectory: path.dirname(resolveSessionStateRoot()),
|
|
5366
|
+
logLevel: 'error',
|
|
5367
|
+
}),
|
|
5368
|
+
resolveInstalledPaths: () => resolveInstalledCopilotPaths({ config }),
|
|
5369
|
+
updateModelCatalog,
|
|
5945
5370
|
logger: console,
|
|
5946
5371
|
});
|
|
5947
5372
|
const contextSnapshotService = createContextSnapshotService({
|
|
@@ -6325,16 +5750,24 @@ async function recoverUndeliveredSessionWorkerMessage({ pending = null, sessionI
|
|
|
6325
5750
|
|
|
6326
5751
|
const retryCount = Number(row.retry_count || 0) + 1;
|
|
6327
5752
|
const nextAttemptAt = addMsIso(Math.min(5_000, computeRetryDelayMs(retryCount)));
|
|
5753
|
+
// The undelivered payload names the exact attempt that was minted for it, so
|
|
5754
|
+
// this requeue can be fenced precisely: if the row has moved on to another
|
|
5755
|
+
// attempt since the failed send, leave it alone.
|
|
5756
|
+
const deliveredAttemptId = String(pending?.message?.attemptId || '').trim() || null;
|
|
6328
5757
|
const result = db.prepare(`
|
|
6329
5758
|
UPDATE queue
|
|
6330
5759
|
SET status = 'pending',
|
|
6331
5760
|
processing_at = NULL,
|
|
5761
|
+
attempt_id = NULL,
|
|
6332
5762
|
retry_count = ?,
|
|
6333
5763
|
next_attempt_at = ?,
|
|
6334
5764
|
owner_lease_expires_at = NULL
|
|
6335
5765
|
WHERE id = ?
|
|
6336
|
-
AND status = 'processing'
|
|
6337
|
-
|
|
5766
|
+
AND status = 'processing'${deliveredAttemptId ? `
|
|
5767
|
+
AND attempt_id = ?` : ''}
|
|
5768
|
+
`).run(...(deliveredAttemptId
|
|
5769
|
+
? [retryCount, nextAttemptAt, messageId, deliveredAttemptId]
|
|
5770
|
+
: [retryCount, nextAttemptAt, messageId]));
|
|
6338
5771
|
if (result.changes <= 0) return false;
|
|
6339
5772
|
|
|
6340
5773
|
if (requesterSessionId) {
|
|
@@ -6356,7 +5789,11 @@ async function recoverUndeliveredSessionWorkerMessage({ pending = null, sessionI
|
|
|
6356
5789
|
const WORKER_DEATH_GRACE_MS = 15_000;
|
|
6357
5790
|
|
|
6358
5791
|
function isWorkerSessionProcessAlive(sessionId, hintPid = null) {
|
|
6359
|
-
|
|
5792
|
+
// A merely-retained socket is not proof of life: a dead worker's socket
|
|
5793
|
+
// lingers until its close event lands, and counting it stalled recovery.
|
|
5794
|
+
// The socket only counts with a recent heartbeat or a PID that probes
|
|
5795
|
+
// alive; otherwise fall through to the process probes below.
|
|
5796
|
+
if (sessionWorkerWebSocketService?.hasLiveWorkerSocket?.(sessionId)) return true;
|
|
6360
5797
|
try {
|
|
6361
5798
|
if (sessionWorkerProcessInspector?.findProcessForSession?.(sessionId)) return true;
|
|
6362
5799
|
} catch {}
|
|
@@ -6381,6 +5818,10 @@ function recoverDeadWorkerProcessingRows(sessionId, reason) {
|
|
|
6381
5818
|
io.emit('message_status', { messageId: row.id, conversationId: row.conversation_id, status: 'failed' });
|
|
6382
5819
|
continue;
|
|
6383
5820
|
}
|
|
5821
|
+
if (row.consumed_at) {
|
|
5822
|
+
failConsumedRowTerminally(row, `worker-died-${reason}`);
|
|
5823
|
+
continue;
|
|
5824
|
+
}
|
|
6384
5825
|
if (Number(row.retry_count || 0) + 1 >= MAX_REQUEUE_RETRIES) {
|
|
6385
5826
|
failRecoveredRowTerminally(row, `worker-died-${reason}`);
|
|
6386
5827
|
continue;
|
|
@@ -6423,11 +5864,15 @@ function handleWorkerSocketClosed({ sessionId, pid = null } = {}) {
|
|
|
6423
5864
|
const sid = String(sessionId || '').trim();
|
|
6424
5865
|
if (!sid) return;
|
|
6425
5866
|
const owned = stmts.listProcessingRowsForOwner?.all?.(sid) || [];
|
|
6426
|
-
|
|
5867
|
+
// A dead worker's steering snapshot must not keep holding the composer even
|
|
5868
|
+
// when it owned no rows (the hold can outlive them, e.g. a continuation's).
|
|
5869
|
+
const heldSteering = Boolean(sessionWorkerRegistry?.getWorker?.(sid)?.steering);
|
|
5870
|
+
if (!owned.length && !heldSteering) return;
|
|
6427
5871
|
const timer = setTimeout(() => {
|
|
6428
5872
|
try {
|
|
6429
5873
|
if (runtimeShutdownStarted) return;
|
|
6430
5874
|
if (isWorkerSessionProcessAlive(sid, pid)) return; // reconnect or socket flap
|
|
5875
|
+
sessionWorkerRegistry?.clearSteering?.(sid);
|
|
6431
5876
|
const stillOwned = stmts.listProcessingRowsForOwner?.all?.(sid) || [];
|
|
6432
5877
|
if (!stillOwned.length) return;
|
|
6433
5878
|
console.warn(`${runtimeLogPrefix()}WORKER DIED session=${sid.slice(0, 8)} rows=${stillOwned.length} trigger=socket-close — recovering owned turns`);
|
|
@@ -6605,6 +6050,10 @@ const windowsAutostartService = createWindowsAutostartService({
|
|
|
6605
6050
|
packageRoot: REPO_ROOT,
|
|
6606
6051
|
configPath: CONFIG_PATH,
|
|
6607
6052
|
});
|
|
6053
|
+
const windowsBootAutostartService = createWindowsBootAutostartService({
|
|
6054
|
+
packageRoot: REPO_ROOT,
|
|
6055
|
+
configPath: CONFIG_PATH,
|
|
6056
|
+
});
|
|
6608
6057
|
|
|
6609
6058
|
const gitChangesService = createGitChangesService();
|
|
6610
6059
|
const sharedRouteDeps = {
|
|
@@ -6718,6 +6167,7 @@ const sharedRouteDeps = {
|
|
|
6718
6167
|
previewCardsForResponse,
|
|
6719
6168
|
sanitizeActivityText,
|
|
6720
6169
|
inFlightStateForConversation,
|
|
6170
|
+
resolveLiveTurnQueueRow,
|
|
6721
6171
|
backgroundTaskStore,
|
|
6722
6172
|
// A closure, not the registry itself: this deps object is built before the
|
|
6723
6173
|
// preview lane is constructed further down the file.
|
|
@@ -6741,6 +6191,7 @@ const sharedRouteDeps = {
|
|
|
6741
6191
|
readContextFromSessionEvents,
|
|
6742
6192
|
sessionHistoryRefreshService,
|
|
6743
6193
|
sdkSessionImportService,
|
|
6194
|
+
copilotModelDiscoveryService,
|
|
6744
6195
|
readSessionTranscriptMessages,
|
|
6745
6196
|
readSessionUsageSummary,
|
|
6746
6197
|
collectOrphanedUploadsFromConversation,
|
|
@@ -6783,15 +6234,24 @@ const sharedRouteDeps = {
|
|
|
6783
6234
|
setTurnCeilingMinutes,
|
|
6784
6235
|
getBackgroundTaskTimeoutMinutes,
|
|
6785
6236
|
setBackgroundTaskTimeoutMinutes,
|
|
6237
|
+
getFeatureFlagsSettingsState,
|
|
6238
|
+
setFeatureFlagSetting,
|
|
6239
|
+
getPwaAppName,
|
|
6240
|
+
setPwaAppName,
|
|
6786
6241
|
requestRelayShutdown,
|
|
6787
6242
|
markSharedViewerPresence,
|
|
6788
6243
|
getSharedWatcherCount,
|
|
6789
6244
|
statusEventService,
|
|
6790
6245
|
claudeAuthService,
|
|
6791
6246
|
cliInstallService,
|
|
6247
|
+
updateCheckService,
|
|
6248
|
+
updateInstallService,
|
|
6249
|
+
buildUpdateStatePayload,
|
|
6250
|
+
runningVersion: RUNNING_VERSION,
|
|
6792
6251
|
grokAuthService,
|
|
6793
6252
|
imageOperationService,
|
|
6794
6253
|
windowsAutostartService,
|
|
6254
|
+
windowsBootAutostartService,
|
|
6795
6255
|
pushDispatchService,
|
|
6796
6256
|
};
|
|
6797
6257
|
registerMessagesRoutes(app, sharedRouteDeps);
|
|
@@ -6895,6 +6355,8 @@ if (sweptUploads > 0) {
|
|
|
6895
6355
|
}
|
|
6896
6356
|
runtimeTimers.uploadSweep = setInterval(sweepUnreferencedUploadBlobs, 6 * 60 * 60 * 1000);
|
|
6897
6357
|
if (typeof runtimeTimers.uploadSweep.unref === 'function') runtimeTimers.uploadSweep.unref();
|
|
6358
|
+
// Arms the 12h update poller only when the user opted in; otherwise silent.
|
|
6359
|
+
updateCheckService?.startIfEnabled();
|
|
6898
6360
|
const runtimeBindingsBootstrapped = bootstrapRuntimeSessionBindings();
|
|
6899
6361
|
if (runtimeBindingsBootstrapped > 0) {
|
|
6900
6362
|
console.log(`${runtimeLogPrefix()}Runtime sessions bootstrapped: ${runtimeBindingsBootstrapped}`);
|
|
@@ -6959,7 +6421,10 @@ function ensureRuntimeSessionBinding(
|
|
|
6959
6421
|
const normalizedModel = String(model || '').trim() || null;
|
|
6960
6422
|
const normalizedSdkSessionId = String(sdkSessionId || '').trim() || null;
|
|
6961
6423
|
if (normalizedSdkSessionId) {
|
|
6962
|
-
|
|
6424
|
+
// Statement shape is (sdk_session_id, updated_at, WHERE id): these args
|
|
6425
|
+
// were swapped for as long as sdk session ids equalled conversation ids,
|
|
6426
|
+
// which made the bug invisible — distinct ids made it a silent no-op.
|
|
6427
|
+
stmts.setConvSdkSessionIdIfMissing.run(normalizedSdkSessionId, nowIso, normalizedConversationId);
|
|
6963
6428
|
}
|
|
6964
6429
|
const existing = stmts.getRuntimeSessionByConversation.get(normalizedConversationId);
|
|
6965
6430
|
if (existing?.id) {
|
|
@@ -7113,25 +6578,10 @@ function sanitizeRelayQuestionRequest(requestBody) {
|
|
|
7113
6578
|
}
|
|
7114
6579
|
}
|
|
7115
6580
|
|
|
6581
|
+
// The allow-list lives in services/relay-question-context.mjs (unit-tested);
|
|
6582
|
+
// this binds it to the relay's own mode normalizer.
|
|
7116
6583
|
function sanitizeRelayQuestionContext(rawContext) {
|
|
7117
|
-
|
|
7118
|
-
const context = {};
|
|
7119
|
-
if (typeof rawContext.source === 'string' && rawContext.source.trim()) {
|
|
7120
|
-
context.source = rawContext.source.trim().slice(0, 64);
|
|
7121
|
-
}
|
|
7122
|
-
if (typeof rawContext.rationale === 'string' && rawContext.rationale.trim()) {
|
|
7123
|
-
context.rationale = rawContext.rationale.trim().slice(0, 240);
|
|
7124
|
-
}
|
|
7125
|
-
if (typeof rawContext.queueMessageId === 'string' && rawContext.queueMessageId.trim()) {
|
|
7126
|
-
context.queueMessageId = rawContext.queueMessageId.trim();
|
|
7127
|
-
}
|
|
7128
|
-
if (typeof rawContext.conversationId === 'string' && rawContext.conversationId.trim()) {
|
|
7129
|
-
context.conversationId = rawContext.conversationId.trim();
|
|
7130
|
-
}
|
|
7131
|
-
if (typeof rawContext.relayMode === 'string' && rawContext.relayMode.trim()) {
|
|
7132
|
-
context.relayMode = normalizeRelayMode(rawContext.relayMode) || DEFAULT_RELAY_MODE;
|
|
7133
|
-
}
|
|
7134
|
-
return Object.keys(context).length ? context : null;
|
|
6584
|
+
return sanitizeRelayQuestionContextWith(rawContext, { normalizeRelayMode, defaultRelayMode: DEFAULT_RELAY_MODE });
|
|
7135
6585
|
}
|
|
7136
6586
|
|
|
7137
6587
|
|
|
@@ -7303,9 +6753,19 @@ function shutdownRuntime(reason = 'unknown', { exitCode = 0 } = {}) {
|
|
|
7303
6753
|
runtimeShutdownStarted = true;
|
|
7304
6754
|
console.log(`${runtimeLogPrefix()}Runtime shutdown started (${reason}, exitCode=${runtimeShutdownExitCode})`);
|
|
7305
6755
|
clearRuntimeTimers();
|
|
7306
|
-
|
|
6756
|
+
try { updateCheckService?.stop(); } catch {}
|
|
6757
|
+
// Started here (flips the importer's closing flag synchronously) but awaited
|
|
6758
|
+
// below before the transports close: fire-and-forget disposal let a
|
|
6759
|
+
// mid-import sweep keep running while the process exited under it.
|
|
6760
|
+
const importerShutdown = sdkSessionImportService.dispose().catch((error) => {
|
|
7307
6761
|
console.warn(`${runtimeLogPrefix()}SDK session importer shutdown failed: ${error?.message || error}`);
|
|
7308
6762
|
});
|
|
6763
|
+
// Same treatment for a model discovery in flight: its dispose() tears the
|
|
6764
|
+
// live client down first, so this settles promptly rather than waiting out
|
|
6765
|
+
// the discovery timeout (the force-exit timer below still caps everything).
|
|
6766
|
+
const discoveryShutdown = copilotModelDiscoveryService.dispose().catch((error) => {
|
|
6767
|
+
console.warn(`${runtimeLogPrefix()}Copilot model discovery shutdown failed: ${error?.message || error}`);
|
|
6768
|
+
});
|
|
7309
6769
|
try { claudeAuthService.dispose(); } catch (error) {
|
|
7310
6770
|
console.warn(`${runtimeLogPrefix()}Claude auth service shutdown failed: ${error?.message || error}`);
|
|
7311
6771
|
}
|
|
@@ -7364,7 +6824,12 @@ function shutdownRuntime(reason = 'unknown', { exitCode = 0 } = {}) {
|
|
|
7364
6824
|
// inside the 2s force-exit budget above.
|
|
7365
6825
|
// Deliberately not unref'd: this timer must fire for the process to exit
|
|
7366
6826
|
// cleanly. The unref'd force-exit timer above is the backstop if it does not.
|
|
7367
|
-
|
|
6827
|
+
// Importer and discovery disposal are awaited first so an in-flight
|
|
6828
|
+
// session import or model discovery settles before the process exits; the
|
|
6829
|
+
// force-exit timer caps how long that can take.
|
|
6830
|
+
setTimeout(() => {
|
|
6831
|
+
void Promise.allSettled([importerShutdown, discoveryShutdown]).then(closeTransports);
|
|
6832
|
+
}, SHUTDOWN_SOCKET_FLUSH_MS);
|
|
7368
6833
|
});
|
|
7369
6834
|
|
|
7370
6835
|
return runtimeShutdownPromise;
|
|
@@ -7460,4 +6925,7 @@ httpServer.listen(config.port, listenHost, () => {
|
|
|
7460
6925
|
void sdkSessionImportService.runStartupImport().catch((error) => {
|
|
7461
6926
|
console.warn(`${runtimeLogPrefix()}SDK session import startup failed: ${error?.message || error}`);
|
|
7462
6927
|
});
|
|
6928
|
+
// Deferred and unref'd (a few seconds behind the startup import's own CLI
|
|
6929
|
+
// spawn); a relay without an installed Copilot CLI skips it entirely.
|
|
6930
|
+
copilotModelDiscoveryService.scheduleStartupRefresh();
|
|
7463
6931
|
});
|