@oar-sh/oar 0.9.22 → 0.9.24
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/CHANGELOG.md +258 -0
- package/README.md +9 -9
- package/bin/oar.js +82 -4
- package/package.json +16 -15
- package/server/agent-sdk-loader.mjs +116 -0
- package/server/agent-sdks/claude/entry.mjs +4 -0
- package/server/agent-sdks/claude/package-lock.json +1438 -0
- package/server/agent-sdks/claude/package.json +13 -0
- package/server/agent-sdks/cursor/entry-sqlite.mjs +2 -0
- package/server/agent-sdks/cursor/entry.mjs +3 -0
- package/server/agent-sdks/cursor/package-lock.json +171 -0
- package/server/agent-sdks/cursor/package.json +11 -0
- package/server/agent-sdks/koffi/entry.cjs +2 -0
- package/server/agent-sdks/koffi/package-lock.json +368 -0
- package/server/agent-sdks/koffi/package.json +10 -0
- package/server/claude-worker/claude-cli-pinned-env.mjs +4 -1
- package/server/claude-worker/claude-preview-tool.mjs +5 -2
- package/server/claude-worker/claude-remote-relay-tool.mjs +5 -2
- package/server/claude-worker/claude-sdk-adapter.mjs +4 -1
- package/server/copilot-worker/copilot-sdk-session-process.mjs +7 -4
- package/server/cursor-worker/cursor-sdk-adapter.mjs +5 -3
- package/server/public/app/agent-sdk-ui.js +349 -0
- package/server/public/app/api-client.js +91 -8
- package/server/public/app/bootstrap.js +204 -69
- package/server/public/app/model-refresh-ui.mjs +413 -0
- package/server/public/app/no-conversation-state.mjs +5 -2
- package/server/public/app/provider-readiness.mjs +67 -4
- package/server/public/app/settings-modal.js +167 -0
- package/server/public/app/socket-handlers.js +21 -0
- package/server/public/index.html +95 -2
- package/server/routes/agent-sdk-routes.mjs +73 -0
- package/server/routes/messages-routes.mjs +102 -37
- package/server/routes/sessions-routes.mjs +236 -110
- package/server/server-runtime.mjs +416 -121
- package/server/services/about-service.mjs +26 -6
- package/server/services/agent-sdk-launch-gate.mjs +124 -0
- package/server/services/agent-sdk-paths.mjs +307 -0
- package/server/services/agent-sdk-store.mjs +1211 -0
- package/server/services/claude-api-settings-service.mjs +54 -17
- package/server/services/cli-install-service.mjs +3 -2
- package/server/services/copilot-model-discovery-service.mjs +7 -1
- package/server/services/database-file.mjs +17 -1
- package/server/services/deepseek-settings-service.mjs +58 -20
- package/server/services/earlier-database-service.mjs +5 -3
- package/server/services/file-serving.mjs +13 -0
- package/server/services/model-refresh-scheduler.mjs +153 -0
- package/server/services/model-variant-catalog-service.mjs +141 -9
- package/server/services/native-addon-build-service.mjs +224 -0
- package/server/services/oar-cli-helpers.mjs +76 -10
- package/server/services/oar-state-migration-service.mjs +2 -12
- package/server/services/oar-state-root.mjs +17 -0
- package/server/services/provider-model-refresh.mjs +326 -0
- package/server/services/provider-model-state.mjs +395 -0
- package/server/services/relay-console-log.mjs +2 -3
- package/server/services/relay-singleton-guard.mjs +72 -10
- package/server/services/remote-relay-loopback.mjs +3 -3
- package/server/services/sdk-session-sync-service.mjs +1 -1
- package/server/services/session-worker-launch-service.mjs +13 -0
- package/server/services/session-worker-supervisor-service.mjs +19 -1
- package/server/services/session-worker-websocket-service.mjs +15 -0
- package/server/services/update-install-service.mjs +34 -3
- package/server/services/worker-launch-note-service.mjs +14 -2
- package/server/services/worker-recycle-service.mjs +145 -11
- package/shared/model-refresh-schedule.mjs +120 -0
- package/shared/worker-runtime/direct-openai-image.mjs +8 -4
- package/server/services/package-data-rescue-preinstall.mjs +0 -22
- package/server/services/package-data-rescue.mjs +0 -139
- package/server/tty-console-bootstrap.mjs +0 -75
|
@@ -4,7 +4,7 @@ import express from 'express';
|
|
|
4
4
|
import http from 'http';
|
|
5
5
|
import { Server } from 'socket.io';
|
|
6
6
|
import { WebSocketServer } from 'ws';
|
|
7
|
-
import {
|
|
7
|
+
import { randomUUID as uuidv4 } from 'crypto';
|
|
8
8
|
import fs from 'fs';
|
|
9
9
|
import path from 'path';
|
|
10
10
|
import { execFile, execFileSync, spawn } from 'child_process';
|
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
} from './services/workspace-root-path-policy.mjs';
|
|
26
26
|
import { stopSessionWorkerProcesses } from './services/session-worker-stop-service.mjs';
|
|
27
27
|
import { createDetachedWorkerWatch } from './services/detached-worker-watch-service.mjs';
|
|
28
|
-
import { applyCdnNoStoreHeaders } from './services/file-serving.mjs';
|
|
28
|
+
import { applyCdnNoStoreHeaders, sendOwnFile } from './services/file-serving.mjs';
|
|
29
29
|
import { pickLiveTurnRowId } from './services/live-turn-picker.mjs';
|
|
30
30
|
import { buildSteerSettleFailure, steerAgentLabelForProvider } from '../shared/steer-settle-failure.mjs';
|
|
31
31
|
import { PLAN_BOARD_APPROVE_ONLY, planBoardShownActions } from '../shared/plan-board-actions.mjs';
|
|
@@ -68,12 +68,15 @@ import { createClaudeCloudCredentials } from '../shared/claude-cloud/credentials
|
|
|
68
68
|
import { createClaudeCloudClient } from '../shared/claude-cloud/api-client.mjs';
|
|
69
69
|
import { resolveClaudeCloudBaseUrl } from '../shared/claude-cloud/base-url.mjs';
|
|
70
70
|
import { createClaudeCloudSettingsService } from './services/claude-cloud-settings-service.mjs';
|
|
71
|
-
import { createDeepSeekSettingsService } from './services/deepseek-settings-service.mjs';
|
|
71
|
+
import { DEEPSEEK_API_KEY_SETTING_KEY, createDeepSeekSettingsService } from './services/deepseek-settings-service.mjs';
|
|
72
72
|
import { createWorkerPostRedaction } from './services/worker-secret-redaction.mjs';
|
|
73
73
|
import { createDeepSeekBalanceService } from './services/deepseek-balance-service.mjs';
|
|
74
74
|
import { deepseekCardCurrency } from './services/plan-usage-deepseek.mjs';
|
|
75
75
|
import { createDeepSeekUsageImportService } from './services/deepseek-usage-import.mjs';
|
|
76
|
-
import { createClaudeApiSettingsService } from './services/claude-api-settings-service.mjs';
|
|
76
|
+
import { CLAUDE_API_KEY_SETTING_KEY, createClaudeApiSettingsService } from './services/claude-api-settings-service.mjs';
|
|
77
|
+
import { createProviderModelStateStore } from './services/provider-model-state.mjs';
|
|
78
|
+
import { buildProviderRefreshTable, createModelRefreshLedger, createModelRefreshSettings, refreshProviderModels } from './services/provider-model-refresh.mjs';
|
|
79
|
+
import { createModelRefreshScheduler } from './services/model-refresh-scheduler.mjs';
|
|
77
80
|
import { createClaudeBillingSwitchService, normalizeClaudeBilling } from './services/claude-billing-switch-service.mjs';
|
|
78
81
|
import { createClaudeBillingSwapService } from './services/claude-billing-swap-service.mjs';
|
|
79
82
|
import { createWorkerReplacement, createWorkerReplacementFence } from './services/worker-replacement.mjs';
|
|
@@ -88,6 +91,10 @@ import { createClaudeCloudRepoService, listRecentCloudSourcesFromStatements } fr
|
|
|
88
91
|
import { createClaudeAccountUsageService } from './services/claude-account-usage-service.mjs';
|
|
89
92
|
import { createGitRemoteService } from './services/git-remote-service.mjs';
|
|
90
93
|
import { createCliInstallService, writeCliBinariesToConfigFile } from './services/cli-install-service.mjs';
|
|
94
|
+
import { createAgentSdkStore } from './services/agent-sdk-store.mjs';
|
|
95
|
+
import { createAgentSdkLaunchGate, staleWorkerLosesAgentSdk } from './services/agent-sdk-launch-gate.mjs';
|
|
96
|
+
import { agentSdkFromCheckout } from './services/agent-sdk-paths.mjs';
|
|
97
|
+
import { agentSdkDirEnvName, AgentSdkMissingError, loadAgentSdk, resolveAgentSdkSource, setAgentSdkStore } from './agent-sdk-loader.mjs';
|
|
91
98
|
import { createGrokAuthService } from './services/grok-auth-service.mjs';
|
|
92
99
|
import { sweepUnreferencedUploads, UNREFERENCED_UPLOADS_QUERY } from './services/upload-sweep.mjs';
|
|
93
100
|
import webpush from 'web-push';
|
|
@@ -123,6 +130,7 @@ import { createActiveDeviceTracker } from './services/push-active-device-service
|
|
|
123
130
|
import { registerPushRoutes } from './routes/push-routes.mjs';
|
|
124
131
|
import { registerAboutRoutes } from './routes/about-routes.mjs';
|
|
125
132
|
import { registerRelayRestartRoutes } from './routes/relay-restart-routes.mjs';
|
|
133
|
+
import { registerAgentSdkRoutes } from './routes/agent-sdk-routes.mjs';
|
|
126
134
|
import { createAboutService } from './services/about-service.mjs';
|
|
127
135
|
import { resolveBuildId } from '../shared/build-id.mjs';
|
|
128
136
|
import { createImageOperationService } from './services/image-operation-service.mjs';
|
|
@@ -137,7 +145,7 @@ import { createPlanUsageService } from './services/plan-usage-service.mjs';
|
|
|
137
145
|
import { splitFreeAndBufferTokens } from './services/context-usage-view.mjs';
|
|
138
146
|
import { normalizeCursorAllowanceSettings } from './services/plan-usage-cursor.mjs';
|
|
139
147
|
import { normalizeGrokAllowanceSettings } from './services/plan-usage-grok.mjs';
|
|
140
|
-
import { createGrokBillingUsageFetcher } from './services/grok-billing-usage.mjs';
|
|
148
|
+
import { createGrokBillingUsageFetcher, readGrokCliAuthKey } from './services/grok-billing-usage.mjs';
|
|
141
149
|
import { createCursorDashboardUsageFetcher, readCursorIdeSessionToken } from './services/cursor-dashboard-usage.mjs';
|
|
142
150
|
import { fetchPersonalBillingUsage } from './services/github-billing-usage.mjs';
|
|
143
151
|
import { createSessionTranscriptService } from './services/session-transcript-service.mjs';
|
|
@@ -163,7 +171,7 @@ import { createRelayCliLauncherService } from './services/relay-cli-launcher-ser
|
|
|
163
171
|
import { createSessionWorkerRegistry, normalizeWorkerKind } from './services/session-worker-registry-service.mjs';
|
|
164
172
|
import { createSessionWorkerSupervisor } from './services/session-worker-supervisor-service.mjs';
|
|
165
173
|
import { createSessionWorkerProcessInspector } from './services/session-worker-process-service.mjs';
|
|
166
|
-
import { applyClaudeApiProviderEnvironment, applyClaudeCloudProviderEnvironment, applyClaudeProviderEnvironment, applyCopilotSdkProviderEnvironment, applyCursorProviderEnvironment, applyDeepSeekProviderEnvironment, applyGrokProviderEnvironment, applyOpenAIProviderEnvironment, copilotSdkEngineUnavailableReason, killTmuxSession, launchSessionCli } from './services/session-worker-launch-service.mjs';
|
|
174
|
+
import { agentSdkForWorkerKind, applyClaudeApiProviderEnvironment, applyClaudeCloudProviderEnvironment, applyClaudeProviderEnvironment, applyCopilotSdkProviderEnvironment, applyCursorProviderEnvironment, applyDeepSeekProviderEnvironment, applyGrokProviderEnvironment, applyOpenAIProviderEnvironment, copilotSdkEngineUnavailableReason, killTmuxSession, launchSessionCli, resolveWorkerKind } from './services/session-worker-launch-service.mjs';
|
|
167
175
|
import { createSshTunnelManager } from './services/ssh-tunnel-manager-service.mjs';
|
|
168
176
|
import { createCloudflaredTunnelManager } from './services/cloudflared-tunnel-service.mjs';
|
|
169
177
|
import {
|
|
@@ -196,8 +204,8 @@ import {
|
|
|
196
204
|
resolveDefaultSessionWorkspaceRootState as resolveDefaultSessionWorkspaceRootStateFromService,
|
|
197
205
|
resolveLaunchWorkspaceRootPath,
|
|
198
206
|
} from './services/workspace-root-defaults-service.mjs';
|
|
199
|
-
import { maybeStartTtyConsole } from './tty-console-bootstrap.mjs';
|
|
200
207
|
import { installRelayConsoleLog, resolveRelayConsoleLogPath } from './services/relay-console-log.mjs';
|
|
208
|
+
import { isMissingSqliteBindingError, missingSqliteBindingLines } from './services/native-addon-build-service.mjs';
|
|
201
209
|
import {
|
|
202
210
|
computeFeatureFlagState,
|
|
203
211
|
FEATURE_REGISTRY,
|
|
@@ -327,13 +335,15 @@ const PWA_VERSION_PLACEHOLDER = /const __PWA_VERSION = '[^']*';/;
|
|
|
327
335
|
// after a restart or an incident.
|
|
328
336
|
const relayConsoleLogPath = resolveRelayConsoleLogPath({ env: process.env, serverDir: __dirname });
|
|
329
337
|
const relayConsoleLog = installRelayConsoleLog({ logPath: relayConsoleLogPath });
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
})
|
|
335
|
-
|
|
336
|
-
|
|
338
|
+
// Where better-sqlite3 ships no binary (Termux, 32-bit ARM) the binding is
|
|
339
|
+
// built by `oar build-native`; until then say so instead of a stack trace.
|
|
340
|
+
try {
|
|
341
|
+
new Database(':memory:').close();
|
|
342
|
+
} catch (error) {
|
|
343
|
+
if (!isMissingSqliteBindingError(error)) throw error;
|
|
344
|
+
for (const line of missingSqliteBindingLines({ error })) console.error(`[startup] ${line}`);
|
|
345
|
+
process.exit(1);
|
|
346
|
+
}
|
|
337
347
|
// TEMP and TMP for this relay and everything it starts (normalizeTempEnv).
|
|
338
348
|
for (const change of normalizeTempEnv()) console.log(`[startup] ${change} (quotes taken off, or the folder is not there)`);
|
|
339
349
|
|
|
@@ -373,6 +383,29 @@ for (const dir of UPLOAD_READ_DIRS.slice(1)) {
|
|
|
373
383
|
} catch {}
|
|
374
384
|
}
|
|
375
385
|
|
|
386
|
+
// Which models of OpenAI, Claude, Cursor and Grok are on (the DeepSeek and
|
|
387
|
+
// Claude API services keep their maps the same way).
|
|
388
|
+
const providerModelState = createProviderModelStateStore({
|
|
389
|
+
readSetting: readAppSettingValue,
|
|
390
|
+
writeSetting: (key, value, stamp) => stmts.upsertAppSetting.run(key, value, stamp),
|
|
391
|
+
});
|
|
392
|
+
// When each provider's models were last refreshed, by anything, and the new
|
|
393
|
+
// models the next models_updated announces (provider-model-refresh.mjs).
|
|
394
|
+
const modelRefreshLedger = createModelRefreshLedger({
|
|
395
|
+
readSetting: readAppSettingValue,
|
|
396
|
+
writeSetting: (key, value, stamp) => stmts.upsertAppSetting.run(key, value, stamp),
|
|
397
|
+
// What a provider's refresh error may echo: its key or token, cut out
|
|
398
|
+
// before the error is stored, shown in Settings or logged.
|
|
399
|
+
secretsFor: (provider) => ({
|
|
400
|
+
openai: () => [readAppSettingValue(OPENAI_API_KEY_SETTING_KEY)],
|
|
401
|
+
cursor: () => [readAppSettingValue(CURSOR_API_KEY_SETTING_KEY), readCursorDashboardSessionToken()],
|
|
402
|
+
deepseek: () => [readAppSettingValue(DEEPSEEK_API_KEY_SETTING_KEY)],
|
|
403
|
+
'claude-api': () => [readAppSettingValue(CLAUDE_API_KEY_SETTING_KEY)],
|
|
404
|
+
grok: () => [process.env.XAI_API_KEY, readGrokCliAuthKey()?.key],
|
|
405
|
+
claude: () => [process.env.ANTHROPIC_API_KEY],
|
|
406
|
+
}[provider]?.() || []),
|
|
407
|
+
});
|
|
408
|
+
|
|
376
409
|
async function refreshOpenAIProviderModels() {
|
|
377
410
|
const settings = getOpenAIProviderSettings();
|
|
378
411
|
if (!settings.enabled) return { ok: false, models: [], error: 'OpenAI API key is not configured' };
|
|
@@ -408,7 +441,8 @@ async function refreshOpenAIProviderModels() {
|
|
|
408
441
|
.filter((modelId) => isOpenAIModelId(modelId)),
|
|
409
442
|
])).filter(Boolean);
|
|
410
443
|
stmts.upsertAppSetting.run(OPENAI_MODELS_SETTING_KEY, JSON.stringify(models), new Date().toISOString());
|
|
411
|
-
|
|
444
|
+
const { added } = providerModelState.reconcile('openai', { availableModels: getOpenAIProviderSettings().availableModels });
|
|
445
|
+
return { ok: true, models, newModels: added, error: null };
|
|
412
446
|
}
|
|
413
447
|
|
|
414
448
|
function getOpenAIProviderSettings() {
|
|
@@ -425,12 +459,21 @@ function getOpenAIProviderSettings() {
|
|
|
425
459
|
} catch {
|
|
426
460
|
models = [];
|
|
427
461
|
}
|
|
462
|
+
const availableModels = Array.from(new Set([model, ...models]));
|
|
463
|
+
// Every discovered model was offered before the model map: an upgrade
|
|
464
|
+
// keeps them all on.
|
|
465
|
+
const enabledModels = providerModelState.resolve('openai', { availableModels, defaultModel: model });
|
|
428
466
|
return {
|
|
429
467
|
configured: !!apiKey,
|
|
430
468
|
enabled,
|
|
431
469
|
apiKey,
|
|
432
470
|
model,
|
|
433
|
-
models:
|
|
471
|
+
models: enabledModels.models,
|
|
472
|
+
availableModels,
|
|
473
|
+
enabledModels: enabledModels.models,
|
|
474
|
+
autoEnableNew: enabledModels.autoEnableNew,
|
|
475
|
+
fallback: enabledModels.fallback,
|
|
476
|
+
unseenModels: enabledModels.unseen,
|
|
434
477
|
baseUrl: readAppSettingValue(OPENAI_BASE_URL_SETTING_KEY) || DEFAULT_OPENAI_BASE_URL,
|
|
435
478
|
providerType: 'openai',
|
|
436
479
|
};
|
|
@@ -441,6 +484,7 @@ function setOpenAIProviderSettings(update = {}) {
|
|
|
441
484
|
const apiKey = payload.apiKey;
|
|
442
485
|
const model = payload.model;
|
|
443
486
|
const enabled = payload.enabled;
|
|
487
|
+
const enabledModels = payload.enabledModels;
|
|
444
488
|
const remove = payload.remove === true;
|
|
445
489
|
const hasBaseUrl = Object.prototype.hasOwnProperty.call(payload, 'baseUrl');
|
|
446
490
|
const baseUrl = hasBaseUrl ? payload.baseUrl : '';
|
|
@@ -479,6 +523,7 @@ function setOpenAIProviderSettings(update = {}) {
|
|
|
479
523
|
activeQueueConversationCount,
|
|
480
524
|
};
|
|
481
525
|
}
|
|
526
|
+
// The model map stays: a key added again finds its models as they were.
|
|
482
527
|
stmts.deleteAppSetting.run(OPENAI_API_KEY_SETTING_KEY);
|
|
483
528
|
stmts.deleteAppSetting.run(OPENAI_MODELS_SETTING_KEY);
|
|
484
529
|
stmts.upsertAppSetting.run(OPENAI_ENABLED_SETTING_KEY, 'false', nowIso);
|
|
@@ -502,6 +547,21 @@ function setOpenAIProviderSettings(update = {}) {
|
|
|
502
547
|
stmts.deleteAppSetting.run(OPENAI_BASE_URL_SETTING_KEY);
|
|
503
548
|
}
|
|
504
549
|
}
|
|
550
|
+
if (Array.isArray(enabledModels) || typeof payload.autoEnableNew === 'boolean') {
|
|
551
|
+
// The models Select Models showed are set as ticked; an empty list
|
|
552
|
+
// leaves the default model alone, never all.
|
|
553
|
+
providerModelState.select('openai', {
|
|
554
|
+
enabledModels: Array.isArray(enabledModels)
|
|
555
|
+
? enabledModels.map((value) => String(value || '').trim()).filter((value) => value && isSafeProviderModelId(value))
|
|
556
|
+
: undefined,
|
|
557
|
+
// The rows the browser drew; an older page sends none.
|
|
558
|
+
shownModels: Array.isArray(payload.shownModels)
|
|
559
|
+
? payload.shownModels.map((value) => String(value || '').trim()).filter((value) => value && isSafeProviderModelId(value))
|
|
560
|
+
: getOpenAIProviderSettings().availableModels,
|
|
561
|
+
defaultModel: normalizedModel,
|
|
562
|
+
autoEnableNew: payload.autoEnableNew,
|
|
563
|
+
});
|
|
564
|
+
}
|
|
505
565
|
}
|
|
506
566
|
const current = getOpenAIProviderSettings();
|
|
507
567
|
return {
|
|
@@ -509,6 +569,9 @@ function setOpenAIProviderSettings(update = {}) {
|
|
|
509
569
|
configured: current.configured,
|
|
510
570
|
enabled: current.enabled,
|
|
511
571
|
model: current.model,
|
|
572
|
+
models: current.models,
|
|
573
|
+
availableModels: current.availableModels,
|
|
574
|
+
autoEnableNew: current.autoEnableNew,
|
|
512
575
|
baseUrl: current.baseUrl,
|
|
513
576
|
};
|
|
514
577
|
}
|
|
@@ -552,15 +615,14 @@ function getClaudeProviderSettings() {
|
|
|
552
615
|
model,
|
|
553
616
|
...(discovered.length ? discovered : DEFAULT_CLAUDE_MODELS),
|
|
554
617
|
])).filter(Boolean);
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
])).filter(Boolean);
|
|
618
|
+
// The models that are on (Select Models and the model map) drive the
|
|
619
|
+
// catalog and composers; the default model always is.
|
|
620
|
+
const enabledModels = providerModelState.resolve('claude', {
|
|
621
|
+
availableModels,
|
|
622
|
+
defaultModel: model,
|
|
623
|
+
legacyAllowlist: () => readClaudeModelListSetting(CLAUDE_ENABLED_MODELS_SETTING_KEY),
|
|
624
|
+
});
|
|
625
|
+
const models = enabledModels.models;
|
|
564
626
|
const storedEfforts = readClaudeModelEffortsSetting();
|
|
565
627
|
const effortsByModel = {};
|
|
566
628
|
for (const availableModel of availableModels) {
|
|
@@ -584,6 +646,9 @@ function getClaudeProviderSettings() {
|
|
|
584
646
|
models,
|
|
585
647
|
availableModels,
|
|
586
648
|
enabledModels: models,
|
|
649
|
+
autoEnableNew: enabledModels.autoEnableNew,
|
|
650
|
+
fallback: enabledModels.fallback,
|
|
651
|
+
unseenModels: enabledModels.unseen,
|
|
587
652
|
effortsByModel,
|
|
588
653
|
modelsDiscovered,
|
|
589
654
|
attributionMode: normalizeClaudeAttributionMode(readAppSettingValue(CLAUDE_ATTRIBUTION_SETTING_KEY)) || DEFAULT_CLAUDE_ATTRIBUTION_MODE,
|
|
@@ -642,15 +707,20 @@ function setClaudeProviderSettings(update = {}) {
|
|
|
642
707
|
stmts.deleteAppSetting.run(CLAUDE_MODELS_SETTING_KEY);
|
|
643
708
|
}
|
|
644
709
|
}
|
|
645
|
-
if (Array.isArray(enabledModels)) {
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
710
|
+
if (Array.isArray(enabledModels) || typeof payload.autoEnableNew === 'boolean') {
|
|
711
|
+
// The models Select Models showed are set as ticked; an empty list
|
|
712
|
+
// leaves the default model alone, never all.
|
|
713
|
+
providerModelState.select('claude', {
|
|
714
|
+
enabledModels: Array.isArray(enabledModels)
|
|
715
|
+
? enabledModels.map((value) => String(value || '').trim()).filter((value) => value && isSafeClaudeModelId(value))
|
|
716
|
+
: undefined,
|
|
717
|
+
// The rows the browser drew; an older page sends none.
|
|
718
|
+
shownModels: Array.isArray(payload.shownModels)
|
|
719
|
+
? payload.shownModels.map((value) => String(value || '').trim()).filter((value) => value && isSafeClaudeModelId(value))
|
|
720
|
+
: getClaudeProviderSettings().availableModels,
|
|
721
|
+
defaultModel: normalizedModel,
|
|
722
|
+
autoEnableNew: payload.autoEnableNew,
|
|
723
|
+
});
|
|
654
724
|
}
|
|
655
725
|
const current = getClaudeProviderSettings();
|
|
656
726
|
return {
|
|
@@ -660,6 +730,7 @@ function setClaudeProviderSettings(update = {}) {
|
|
|
660
730
|
model: current.model,
|
|
661
731
|
models: current.models,
|
|
662
732
|
availableModels: current.availableModels,
|
|
733
|
+
autoEnableNew: current.autoEnableNew,
|
|
663
734
|
attributionMode: current.attributionMode,
|
|
664
735
|
};
|
|
665
736
|
}
|
|
@@ -707,7 +778,7 @@ async function refreshClaudeProviderModels() {
|
|
|
707
778
|
let claudeQuery = null;
|
|
708
779
|
let releaseInputGate = () => {};
|
|
709
780
|
try {
|
|
710
|
-
const { query } = await
|
|
781
|
+
const { query } = await loadAgentSdk('claude');
|
|
711
782
|
const inputGate = new Promise((resolve) => { releaseInputGate = resolve; });
|
|
712
783
|
claudeQuery = query({
|
|
713
784
|
// Streaming-input mode with a gated (never-yielding) prompt keeps the
|
|
@@ -760,8 +831,13 @@ async function refreshClaudeProviderModels() {
|
|
|
760
831
|
const mergedEfforts = mergeClaudeModelEfforts(readClaudeModelEffortsSetting(), effortsByModel);
|
|
761
832
|
stmts.upsertAppSetting.run(CLAUDE_MODELS_SETTING_KEY, JSON.stringify(models), nowIso);
|
|
762
833
|
stmts.upsertAppSetting.run(CLAUDE_MODEL_EFFORTS_SETTING_KEY, JSON.stringify(mergedEfforts), nowIso);
|
|
763
|
-
|
|
834
|
+
const { added } = providerModelState.reconcile('claude', { availableModels: getClaudeProviderSettings().availableModels });
|
|
835
|
+
return { ok: true, models, newModels: added, error: null };
|
|
764
836
|
} catch (error) {
|
|
837
|
+
// No SDK installed yet: nothing is asked, the stored catalogue stays.
|
|
838
|
+
if (error instanceof AgentSdkMissingError) {
|
|
839
|
+
return { ok: false, models: settings.models, sdkMissing: true, error: `Claude model discovery waits for the SDK: ${error.message}` };
|
|
840
|
+
}
|
|
765
841
|
return {
|
|
766
842
|
ok: false,
|
|
767
843
|
models: settings.models,
|
|
@@ -825,24 +901,19 @@ function getCursorProviderSettings() {
|
|
|
825
901
|
models = [];
|
|
826
902
|
}
|
|
827
903
|
const availableModels = Array.from(new Set([model, ...(models.length ? models : DEFAULT_CURSOR_MODELS)]));
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
// catalog and composers; an empty selection means "all available".
|
|
842
|
-
const resolvedModels = Array.from(new Set([
|
|
843
|
-
model,
|
|
844
|
-
...(enabledSelection.length ? enabledSelection : availableModels),
|
|
845
|
-
])).filter(Boolean);
|
|
904
|
+
// The models that are on (Select Models and the model map) drive the
|
|
905
|
+
// catalog and composers; the default model always is.
|
|
906
|
+
const enabledModels = providerModelState.resolve('cursor', {
|
|
907
|
+
availableModels,
|
|
908
|
+
defaultModel: model,
|
|
909
|
+
legacyAllowlist: () => {
|
|
910
|
+
const parsed = JSON.parse(readAppSettingValue(CURSOR_ENABLED_MODELS_SETTING_KEY) || '[]');
|
|
911
|
+
return Array.isArray(parsed)
|
|
912
|
+
? parsed.map((value) => String(value || '').trim()).filter((modelId) => isSafeCursorModelId(modelId))
|
|
913
|
+
: [];
|
|
914
|
+
},
|
|
915
|
+
});
|
|
916
|
+
const resolvedModels = enabledModels.models;
|
|
846
917
|
const storedEfforts = readCursorModelEffortsSetting();
|
|
847
918
|
const storedReasoningOff = readCursorModelReasoningOffSetting();
|
|
848
919
|
const effortsByModel = {};
|
|
@@ -866,6 +937,9 @@ function getCursorProviderSettings() {
|
|
|
866
937
|
models: resolvedModels,
|
|
867
938
|
availableModels,
|
|
868
939
|
enabledModels: resolvedModels,
|
|
940
|
+
autoEnableNew: enabledModels.autoEnableNew,
|
|
941
|
+
fallback: enabledModels.fallback,
|
|
942
|
+
unseenModels: enabledModels.unseen,
|
|
869
943
|
effortsByModel,
|
|
870
944
|
reasoningOffByModel,
|
|
871
945
|
// Pre-effort installs have models but no efforts setting; the settings
|
|
@@ -1107,9 +1181,9 @@ function setCursorProviderSettings(update = {}) {
|
|
|
1107
1181
|
activeQueueConversationCount,
|
|
1108
1182
|
};
|
|
1109
1183
|
}
|
|
1184
|
+
// The model map stays: a key added again finds its models as they were.
|
|
1110
1185
|
stmts.deleteAppSetting.run(CURSOR_API_KEY_SETTING_KEY);
|
|
1111
1186
|
stmts.deleteAppSetting.run(CURSOR_MODELS_SETTING_KEY);
|
|
1112
|
-
stmts.deleteAppSetting.run(CURSOR_ENABLED_MODELS_SETTING_KEY);
|
|
1113
1187
|
stmts.deleteAppSetting.run(CURSOR_MODEL_EFFORTS_SETTING_KEY);
|
|
1114
1188
|
stmts.deleteAppSetting.run(CURSOR_MODEL_REASONING_OFF_SETTING_KEY);
|
|
1115
1189
|
stmts.upsertAppSetting.run(CURSOR_ENABLED_SETTING_KEY, 'false', nowIso);
|
|
@@ -1127,15 +1201,20 @@ function setCursorProviderSettings(update = {}) {
|
|
|
1127
1201
|
if (nextEnabled && !hasApiKey) return { ok: false, error: 'Cursor API key is not configured' };
|
|
1128
1202
|
stmts.upsertAppSetting.run(CURSOR_ENABLED_SETTING_KEY, nextEnabled ? 'true' : 'false', nowIso);
|
|
1129
1203
|
stmts.upsertAppSetting.run(CURSOR_MODEL_SETTING_KEY, normalizedModel, nowIso);
|
|
1130
|
-
if (Array.isArray(enabledModels)) {
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1204
|
+
if (Array.isArray(enabledModels) || typeof payload.autoEnableNew === 'boolean') {
|
|
1205
|
+
// The models Select Models showed are set as ticked; an empty list
|
|
1206
|
+
// leaves the default model alone, never all.
|
|
1207
|
+
providerModelState.select('cursor', {
|
|
1208
|
+
enabledModels: Array.isArray(enabledModels)
|
|
1209
|
+
? enabledModels.map((value) => String(value || '').trim()).filter((value) => value && isSafeCursorModelId(value))
|
|
1210
|
+
: undefined,
|
|
1211
|
+
// The rows the browser drew; an older page sends none.
|
|
1212
|
+
shownModels: Array.isArray(payload.shownModels)
|
|
1213
|
+
? payload.shownModels.map((value) => String(value || '').trim()).filter((value) => value && isSafeCursorModelId(value))
|
|
1214
|
+
: getCursorProviderSettings().availableModels,
|
|
1215
|
+
defaultModel: normalizedModel,
|
|
1216
|
+
autoEnableNew: payload.autoEnableNew,
|
|
1217
|
+
});
|
|
1139
1218
|
}
|
|
1140
1219
|
}
|
|
1141
1220
|
const current = getCursorProviderSettings();
|
|
@@ -1146,6 +1225,7 @@ function setCursorProviderSettings(update = {}) {
|
|
|
1146
1225
|
model: current.model,
|
|
1147
1226
|
models: current.models,
|
|
1148
1227
|
availableModels: current.availableModels,
|
|
1228
|
+
autoEnableNew: current.autoEnableNew,
|
|
1149
1229
|
};
|
|
1150
1230
|
}
|
|
1151
1231
|
|
|
@@ -1202,7 +1282,7 @@ async function refreshCursorProviderModels() {
|
|
|
1202
1282
|
return { ok: false, models: settings.models, error: 'Cursor API key is not configured' };
|
|
1203
1283
|
}
|
|
1204
1284
|
try {
|
|
1205
|
-
const mod = await
|
|
1285
|
+
const mod = await loadAgentSdk('cursor');
|
|
1206
1286
|
// The namespace form is the only working call shape: `Cursor` has a
|
|
1207
1287
|
// private constructor and `models` is static, so the old client-instance
|
|
1208
1288
|
// fallback could only throw a TypeError that replaced the real error.
|
|
@@ -1241,8 +1321,12 @@ async function refreshCursorProviderModels() {
|
|
|
1241
1321
|
stmts.upsertAppSetting.run(CURSOR_MODELS_SETTING_KEY, JSON.stringify(models), nowIso);
|
|
1242
1322
|
stmts.upsertAppSetting.run(CURSOR_MODEL_EFFORTS_SETTING_KEY, JSON.stringify(effortsByModel), nowIso);
|
|
1243
1323
|
stmts.upsertAppSetting.run(CURSOR_MODEL_REASONING_OFF_SETTING_KEY, JSON.stringify(reasoningOffByModel), nowIso);
|
|
1244
|
-
|
|
1324
|
+
const { added } = providerModelState.reconcile('cursor', { availableModels: getCursorProviderSettings().availableModels });
|
|
1325
|
+
return { ok: true, models, newModels: added, error: null };
|
|
1245
1326
|
} catch (error) {
|
|
1327
|
+
if (error instanceof AgentSdkMissingError) {
|
|
1328
|
+
return { ok: false, models: settings.models, sdkMissing: true, error: `Cursor model discovery waits for the SDK: ${error.message}` };
|
|
1329
|
+
}
|
|
1246
1330
|
return {
|
|
1247
1331
|
ok: false,
|
|
1248
1332
|
models: settings.models,
|
|
@@ -1292,13 +1376,12 @@ function getGrokProviderSettings() {
|
|
|
1292
1376
|
model,
|
|
1293
1377
|
...(discovered.length ? discovered : DEFAULT_GROK_MODELS),
|
|
1294
1378
|
])).filter(Boolean);
|
|
1295
|
-
const
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
])).filter(Boolean);
|
|
1379
|
+
const enabledModels = providerModelState.resolve('grok', {
|
|
1380
|
+
availableModels,
|
|
1381
|
+
defaultModel: model,
|
|
1382
|
+
legacyAllowlist: () => readGrokModelListSetting(GROK_ENABLED_MODELS_SETTING_KEY),
|
|
1383
|
+
});
|
|
1384
|
+
const models = enabledModels.models;
|
|
1302
1385
|
const storedEfforts = readGrokModelEffortsSetting();
|
|
1303
1386
|
const effortsByModel = {};
|
|
1304
1387
|
for (const availableModel of availableModels) {
|
|
@@ -1317,6 +1400,9 @@ function getGrokProviderSettings() {
|
|
|
1317
1400
|
models,
|
|
1318
1401
|
availableModels,
|
|
1319
1402
|
enabledModels: models,
|
|
1403
|
+
autoEnableNew: enabledModels.autoEnableNew,
|
|
1404
|
+
fallback: enabledModels.fallback,
|
|
1405
|
+
unseenModels: enabledModels.unseen,
|
|
1320
1406
|
effortsByModel,
|
|
1321
1407
|
modelsDiscovered,
|
|
1322
1408
|
providerType: 'grok',
|
|
@@ -1351,15 +1437,20 @@ function setGrokProviderSettings(update = {}) {
|
|
|
1351
1437
|
stmts.deleteAppSetting?.run?.(GROK_MODELS_SETTING_KEY);
|
|
1352
1438
|
}
|
|
1353
1439
|
}
|
|
1354
|
-
if (Array.isArray(enabledModels)) {
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1440
|
+
if (Array.isArray(enabledModels) || typeof payload.autoEnableNew === 'boolean') {
|
|
1441
|
+
// The models Select Models showed are set as ticked; an empty list
|
|
1442
|
+
// leaves the default model alone, never all.
|
|
1443
|
+
providerModelState.select('grok', {
|
|
1444
|
+
enabledModels: Array.isArray(enabledModels)
|
|
1445
|
+
? enabledModels.map((value) => String(value || '').trim()).filter((value) => value && isSafeGrokModelId(value))
|
|
1446
|
+
: undefined,
|
|
1447
|
+
// The rows the browser drew; an older page sends none.
|
|
1448
|
+
shownModels: Array.isArray(payload.shownModels)
|
|
1449
|
+
? payload.shownModels.map((value) => String(value || '').trim()).filter((value) => value && isSafeGrokModelId(value))
|
|
1450
|
+
: getGrokProviderSettings().availableModels,
|
|
1451
|
+
defaultModel: normalizedModel,
|
|
1452
|
+
autoEnableNew: payload.autoEnableNew,
|
|
1453
|
+
});
|
|
1363
1454
|
}
|
|
1364
1455
|
const current = getGrokProviderSettings();
|
|
1365
1456
|
return {
|
|
@@ -1369,6 +1460,7 @@ function setGrokProviderSettings(update = {}) {
|
|
|
1369
1460
|
model: current.model,
|
|
1370
1461
|
models: current.models,
|
|
1371
1462
|
availableModels: current.availableModels,
|
|
1463
|
+
autoEnableNew: current.autoEnableNew,
|
|
1372
1464
|
};
|
|
1373
1465
|
}
|
|
1374
1466
|
|
|
@@ -1450,7 +1542,8 @@ async function refreshGrokProviderModels() {
|
|
|
1450
1542
|
const models = Array.from(new Set([settings.model, ...discovered])).filter(Boolean);
|
|
1451
1543
|
stmts.upsertAppSetting.run(GROK_MODELS_SETTING_KEY, JSON.stringify(models), nowIso);
|
|
1452
1544
|
stmts.upsertAppSetting.run(GROK_MODEL_EFFORTS_SETTING_KEY, JSON.stringify(effortsByModel), nowIso);
|
|
1453
|
-
|
|
1545
|
+
const { added } = providerModelState.reconcile('grok', { availableModels: getGrokProviderSettings().availableModels });
|
|
1546
|
+
return { ok: true, models, newModels: added, error: null };
|
|
1454
1547
|
} catch (error) {
|
|
1455
1548
|
return {
|
|
1456
1549
|
ok: false,
|
|
@@ -1507,6 +1600,8 @@ const FALLBACK_COPILOT_ENGINE = 'extension';
|
|
|
1507
1600
|
const CLAUDE_ENABLED_SETTING_KEY = 'claude_enabled';
|
|
1508
1601
|
const CLAUDE_MODEL_SETTING_KEY = 'claude_model';
|
|
1509
1602
|
const CLAUDE_MODELS_SETTING_KEY = 'claude_models';
|
|
1603
|
+
// The allowlists before 0.9.24 (Claude, Cursor, Grok): read once to build
|
|
1604
|
+
// the model map, then left as they are so an older version still finds them.
|
|
1510
1605
|
const CLAUDE_ENABLED_MODELS_SETTING_KEY = 'claude_enabled_models';
|
|
1511
1606
|
const CLAUDE_MODEL_EFFORTS_SETTING_KEY = 'claude_model_efforts';
|
|
1512
1607
|
// oar | vanilla | off — what commits and PRs made in Claude sessions say
|
|
@@ -2112,7 +2207,6 @@ function workspaceRootPayload() {
|
|
|
2112
2207
|
}
|
|
2113
2208
|
|
|
2114
2209
|
const ACTIVE_SESSION_CWD_STATUSES = new Set(['starting', 'ready', 'processing']);
|
|
2115
|
-
const ACTIVE_TTY_CLI_WORKER_STATUSES = new Set(['starting', 'ready', 'processing']);
|
|
2116
2210
|
|
|
2117
2211
|
function normalizeConversationWorkspaceRootPath(candidatePath) {
|
|
2118
2212
|
const normalized = normalizeWorkspaceRootPath(candidatePath);
|
|
@@ -2677,6 +2771,10 @@ const modelVariantCatalog = createModelVariantCatalogService({
|
|
|
2677
2771
|
curatedModelIds: CURATED_MODEL_IDS,
|
|
2678
2772
|
autoModelSentinel: AUTO_MODEL_SENTINEL,
|
|
2679
2773
|
runCopilotCliCommand,
|
|
2774
|
+
modelStateStore: providerModelState,
|
|
2775
|
+
onNewModels: (baseModelIds, on) => modelRefreshLedger.noteNewModels('github', baseModelIds.map((id) => ({ id, on }))),
|
|
2776
|
+
// A worker's snapshot read the runtime's list: a Copilot model refresh.
|
|
2777
|
+
onAuthoritativeSnapshot: () => modelRefreshLedger.record('github', { ok: true }),
|
|
2680
2778
|
});
|
|
2681
2779
|
const {
|
|
2682
2780
|
SUPPORTED_REASONING_EFFORTS,
|
|
@@ -2989,6 +3087,7 @@ const deepseekSettingsService = createDeepSeekSettingsService({
|
|
|
2989
3087
|
},
|
|
2990
3088
|
deleteSetting: (key) => stmts.deleteAppSetting.run(key),
|
|
2991
3089
|
listProviderCandidates: () => stmts.listRuntimeSessionProviderCandidates?.all?.() || [],
|
|
3090
|
+
onModelsRefreshed: (result) => modelRefreshLedger.record('deepseek', result),
|
|
2992
3091
|
});
|
|
2993
3092
|
function getDeepSeekProviderSettings() {
|
|
2994
3093
|
return deepseekSettingsService.getSettings();
|
|
@@ -3006,6 +3105,7 @@ const claudeApiSettingsService = createClaudeApiSettingsService({
|
|
|
3006
3105
|
listProviderCandidates: () => stmts.listRuntimeSessionProviderCandidates?.all?.() || [],
|
|
3007
3106
|
countLiveKeyWorkers: () => (sessionWorkerRegistry?.listWorkers?.() || []).filter((worker) => worker?.kind === 'claude-api').length,
|
|
3008
3107
|
getClaudeProviderSettings: () => getClaudeProviderSettings(),
|
|
3108
|
+
onModelsRefreshed: (result) => modelRefreshLedger.record('claude-api', result),
|
|
3009
3109
|
});
|
|
3010
3110
|
function getClaudeApiProviderSettings() {
|
|
3011
3111
|
return claudeApiSettingsService.getSettings();
|
|
@@ -3095,6 +3195,44 @@ try {
|
|
|
3095
3195
|
console.warn(`[cli-install] failed to apply persisted CLI bindings: ${error?.message || error}`);
|
|
3096
3196
|
}
|
|
3097
3197
|
|
|
3198
|
+
// The agent SDKs (and koffi on Windows) the relay installs on demand
|
|
3199
|
+
// (agent-sdk-store.mjs). Registered with the loader, so this process takes
|
|
3200
|
+
// them from the store, never from a worker folder in its environment.
|
|
3201
|
+
const agentSdkStore = createAgentSdkStore({
|
|
3202
|
+
fromCheckout: (sdk) => agentSdkFromCheckout(sdk),
|
|
3203
|
+
readSetting: readAppSettingValue,
|
|
3204
|
+
writeSetting: (key, value) => stmts.upsertAppSetting.run(key, value, new Date().toISOString()),
|
|
3205
|
+
});
|
|
3206
|
+
setAgentSdkStore(agentSdkStore);
|
|
3207
|
+
// A provider that was just set up gets its SDK at once, in the
|
|
3208
|
+
// background; a failure shows in its Settings tab and on the first chat.
|
|
3209
|
+
function installAgentSdkForSetup(sdk, reason) {
|
|
3210
|
+
if (resolveAgentSdkSource(sdk)) return;
|
|
3211
|
+
void agentSdkStore.ensure(sdk, { reason, retryFailed: true }).catch((error) => {
|
|
3212
|
+
console.warn(`[agent-sdk] ${sdk} install after ${reason} failed: ${error?.message || error}`);
|
|
3213
|
+
});
|
|
3214
|
+
}
|
|
3215
|
+
claudeAuthService.onLoginSuccess?.(() => installAgentSdkForSetup('claude', 'Claude login'));
|
|
3216
|
+
// The SDKs whose providers are in use: enabled, logged in, or with a
|
|
3217
|
+
// conversation that is not archived. koffi wherever the store supports it.
|
|
3218
|
+
function agentSdksInUse() {
|
|
3219
|
+
const inUse = new Set(['koffi']);
|
|
3220
|
+
const providerSdk = { claude: 'claude', 'claude-api': 'claude', deepseek: 'claude', cursor: 'cursor' };
|
|
3221
|
+
try {
|
|
3222
|
+
if (getClaudeProviderSettings()?.enabled === true || claudeAuthService.getCachedStatus?.()?.loggedIn === true) inUse.add('claude');
|
|
3223
|
+
if (claudeApiSettingsService.getSettings?.()?.enabled === true || deepseekSettingsService.getSettings?.()?.enabled === true) inUse.add('claude');
|
|
3224
|
+
if (getCursorProviderSettings()?.enabled === true) inUse.add('cursor');
|
|
3225
|
+
const rows = db.prepare(`SELECT DISTINCT r.provider_type AS provider FROM runtime_sessions r JOIN conversations c ON c.id = r.conversation_id WHERE c.archived = 0`).all();
|
|
3226
|
+
for (const row of rows) {
|
|
3227
|
+
const sdk = providerSdk[String(row?.provider || '').trim().toLowerCase()];
|
|
3228
|
+
if (sdk) inUse.add(sdk);
|
|
3229
|
+
}
|
|
3230
|
+
} catch (error) {
|
|
3231
|
+
console.warn(`[agent-sdk] could not tell which providers are in use: ${error?.message || error}`);
|
|
3232
|
+
}
|
|
3233
|
+
return [...inUse];
|
|
3234
|
+
}
|
|
3235
|
+
|
|
3098
3236
|
// ─── Web Push ─────────────────────────────────────────────────────────────────
|
|
3099
3237
|
// VAPID keys are generated once and persisted in app_settings; regenerating
|
|
3100
3238
|
// them would invalidate every stored subscription.
|
|
@@ -3382,6 +3520,20 @@ const relayCliLauncherService = createRelayCliLauncherService({
|
|
|
3382
3520
|
workerScope,
|
|
3383
3521
|
});
|
|
3384
3522
|
|
|
3523
|
+
// The launch gate (agent-sdk-launch-gate.mjs): never waits for an install.
|
|
3524
|
+
// A session it held back gets its launch back when the install ends or the
|
|
3525
|
+
// SDK turns ready later: the restart schedule cleared, the queue woken.
|
|
3526
|
+
const agentSdkLaunchGate = createAgentSdkLaunchGate({
|
|
3527
|
+
store: agentSdkStore,
|
|
3528
|
+
sdkForWorkerKind: agentSdkForWorkerKind,
|
|
3529
|
+
resolveSource: (sdk) => resolveAgentSdkSource(sdk),
|
|
3530
|
+
noteInstalling: (sdkSessionId, { label }) => workerLaunchNoteService.handle({ type: 'installing', sdkSessionId, lifecycle: { sdkLabel: label } }),
|
|
3531
|
+
onSessionsReleased: ({ sessions }) => {
|
|
3532
|
+
for (const sdkSessionId of sessions) sessionWorkerSupervisor.clearRestartSchedule?.(sdkSessionId);
|
|
3533
|
+
wakeSessionWorkerQueue('agent-sdk-ready');
|
|
3534
|
+
},
|
|
3535
|
+
});
|
|
3536
|
+
|
|
3385
3537
|
async function spawnSessionWorkerCli(targetSessionId, { allowProcessReuse = true } = {}) {
|
|
3386
3538
|
const normalizedTargetSessionId = String(targetSessionId || '').trim();
|
|
3387
3539
|
if (!normalizedTargetSessionId) {
|
|
@@ -3412,6 +3564,11 @@ async function spawnSessionWorkerCli(targetSessionId, { allowProcessReuse = true
|
|
|
3412
3564
|
}
|
|
3413
3565
|
const resolvedWorkspaceRoot = resolveLaunchWorkspaceRootForSession(normalizedTargetSessionId);
|
|
3414
3566
|
const workerLaunchEnv = buildSessionWorkerLaunchEnvForSession(normalizedTargetSessionId);
|
|
3567
|
+
// Synchronous: nothing between the gate and the launch can start a
|
|
3568
|
+
// second worker for the session.
|
|
3569
|
+
const agentSdkDir = agentSdkLaunchGate.check(normalizedTargetSessionId, resolveWorkerKind(workerLaunchEnv));
|
|
3570
|
+
if (agentSdkDir.deferred) return { deferred: agentSdkDir.deferred };
|
|
3571
|
+
if (agentSdkDir.dir) workerLaunchEnv[agentSdkDirEnvName(agentSdkDir.sdk)] = agentSdkDir.dir;
|
|
3415
3572
|
const launched = await launchSessionCli({
|
|
3416
3573
|
workerScope,
|
|
3417
3574
|
targetSessionId: normalizedTargetSessionId,
|
|
@@ -3426,6 +3583,9 @@ async function spawnSessionWorkerCli(targetSessionId, { allowProcessReuse = true
|
|
|
3426
3583
|
log: (message) => console.warn(`${runtimeLogPrefix()}${message}`),
|
|
3427
3584
|
});
|
|
3428
3585
|
const workerPid = Number.isInteger(Number(launched?.pid)) ? Number(launched.pid) : null;
|
|
3586
|
+
// The worker leases its folder when it loads the SDK; this covers the
|
|
3587
|
+
// seconds before, so no clean-up takes it in between.
|
|
3588
|
+
if (agentSdkDir.dir && workerPid) agentSdkStore.lease(agentSdkDir.dir, workerPid);
|
|
3429
3589
|
const workerId = `worker-${normalizedTargetSessionId.slice(0, 8)}`;
|
|
3430
3590
|
const launchMode = String(launched?.launchMode || 'detached').trim();
|
|
3431
3591
|
const tmuxSessionName = String(launched?.tmuxSessionName || '').trim();
|
|
@@ -3760,27 +3920,6 @@ function queueCounts() {
|
|
|
3760
3920
|
return { pendingCount: map.pending || 0, processingCount: map.processing || 0, parkedCount: map.parked || 0 };
|
|
3761
3921
|
}
|
|
3762
3922
|
|
|
3763
|
-
function countActiveCliWorkers() {
|
|
3764
|
-
const workers = sessionWorkerRegistry?.listWorkers?.() || [];
|
|
3765
|
-
return workers.reduce((acc, worker) => {
|
|
3766
|
-
const status = String(worker?.status || '').trim().toLowerCase();
|
|
3767
|
-
return ACTIVE_TTY_CLI_WORKER_STATUSES.has(status) ? acc + 1 : acc;
|
|
3768
|
-
}, 0);
|
|
3769
|
-
}
|
|
3770
|
-
|
|
3771
|
-
function attachTtyConsoleRuntimeTitle(runtime) {
|
|
3772
|
-
const addConsoleTitle = runtime?.tty?.addConsoleTitle;
|
|
3773
|
-
if (typeof addConsoleTitle !== 'function') return;
|
|
3774
|
-
addConsoleTitle(() => {
|
|
3775
|
-
const counts = queueCounts();
|
|
3776
|
-
const connectedClients = Number(io?.sockets?.sockets?.size || 0);
|
|
3777
|
-
const activeCliWorkers = countActiveCliWorkers();
|
|
3778
|
-
const cliState = cliOnline ? 'online' : 'offline';
|
|
3779
|
-
return `Clients: ${connectedClients} :: Pen: ${counts.pendingCount} :: Proc: ${counts.processingCount} :: parked: ${counts.parkedCount} :: CLIs: ${activeCliWorkers} :: CLI: ${cliState}`;
|
|
3780
|
-
});
|
|
3781
|
-
}
|
|
3782
|
-
attachTtyConsoleRuntimeTitle(ttyConsoleRuntime);
|
|
3783
|
-
|
|
3784
3923
|
// ─── Queued relay restart / shutdown ─────────────────────────────────────────
|
|
3785
3924
|
// A request waits until no turn is processing and holds new turns meanwhile
|
|
3786
3925
|
// (services/relay-shutdown-gate.mjs); past the maximum wait it asks instead
|
|
@@ -6279,6 +6418,7 @@ const copilotModelDiscoveryService = createCopilotModelDiscoveryService({
|
|
|
6279
6418
|
}),
|
|
6280
6419
|
resolveInstalledPaths: () => resolveInstalledCopilotPaths({ config }),
|
|
6281
6420
|
updateModelCatalog,
|
|
6421
|
+
onRefreshed: (result) => modelRefreshLedger.record('github', result),
|
|
6282
6422
|
logger: console,
|
|
6283
6423
|
});
|
|
6284
6424
|
const contextSnapshotService = createContextSnapshotService({
|
|
@@ -7105,9 +7245,51 @@ const workerRecycleService = createWorkerRecycleService({
|
|
|
7105
7245
|
// The stop and the launch under the per-session launch mutex the Change
|
|
7106
7246
|
// CWD relaunch and the launch routes take (sessions-routes.mjs).
|
|
7107
7247
|
runExclusive: (sid, fn) => sessionLaunchMutex.runExclusive(sid, fn),
|
|
7248
|
+
// The kinds the launch gate gives an agent SDK: once stale, recycled
|
|
7249
|
+
// without waiting for their CLI child, and fenced until then.
|
|
7250
|
+
isAgentSdkWorker: (worker) => staleWorkerLosesAgentSdk(worker?.kind, { sdkForWorkerKind: agentSdkForWorkerKind, fromCheckout: (sdk) => agentSdkFromCheckout(sdk) }),
|
|
7108
7251
|
logger: console,
|
|
7109
7252
|
});
|
|
7110
7253
|
|
|
7254
|
+
// A worker's hello, before the hello's own request for work: the registry
|
|
7255
|
+
// learns the process, its kind and its build id (a relay that just started
|
|
7256
|
+
// has no entry yet, and the delivery would register the process without its
|
|
7257
|
+
// build id), and the recycle sweeps, so a stale worker on an agent SDK is
|
|
7258
|
+
// fenced before anything is handed to it.
|
|
7259
|
+
function noteSessionWorkerHello({ sessionId, pid, kind = null, buildId = null } = {}) {
|
|
7260
|
+
if (featureFlags?.SESSION_WORKER_ROUTING_ENABLED !== true || runtimeShutdownStarted) return;
|
|
7261
|
+
const sid = String(sessionId || '').trim();
|
|
7262
|
+
if (!sid) return;
|
|
7263
|
+
// A killed session's worker is registered by nothing, as the delivery
|
|
7264
|
+
// leaves it unregistered.
|
|
7265
|
+
if (sessionWorkerSupervisor?.isKillBlocked?.(sid) !== true) {
|
|
7266
|
+
const statedPid = Number.isInteger(Number(pid)) && Number(pid) > 0 ? Number(pid) : null;
|
|
7267
|
+
let worker = sessionWorkerRegistry?.getWorker?.(sid) || null;
|
|
7268
|
+
// No entry, or one with no process behind it (a launch the agent SDK
|
|
7269
|
+
// gate deferred leaves 'new'): the process saying hello is the session's
|
|
7270
|
+
// worker, ready, as its first heartbeat would make it (/api/heartbeat).
|
|
7271
|
+
if (!worker || worker.status === 'new' || (!worker.workerId && !worker.pid)) {
|
|
7272
|
+
worker = sessionWorkerRegistry?.upsertWorker?.({
|
|
7273
|
+
...(worker || {}),
|
|
7274
|
+
sdkSessionId: sid,
|
|
7275
|
+
workerId: worker?.workerId || `worker-${sid.slice(0, 8)}`,
|
|
7276
|
+
status: 'ready',
|
|
7277
|
+
pid: statedPid || worker?.pid || null,
|
|
7278
|
+
queueDepth: Math.max(0, Number(queueCounts?.().pendingCount || 0)),
|
|
7279
|
+
}) || null;
|
|
7280
|
+
} else if (statedPid && worker.pid !== statedPid) {
|
|
7281
|
+
worker = sessionWorkerRegistry?.upsertWorker?.({ ...worker, sdkSessionId: sid, pid: statedPid, status: worker.status || 'ready' }) || null;
|
|
7282
|
+
}
|
|
7283
|
+
// Stated for the process just registered, in an upsert of its own: the
|
|
7284
|
+
// registry drops a build id that comes with a new pid and equals the old
|
|
7285
|
+
// entry's (the old process's, spread in).
|
|
7286
|
+
if (worker && ((kind && worker.kind !== kind) || (buildId && worker.buildId !== buildId))) {
|
|
7287
|
+
sessionWorkerRegistry?.upsertWorker?.({ ...worker, ...(kind ? { kind } : {}), ...(buildId ? { buildId } : {}) });
|
|
7288
|
+
}
|
|
7289
|
+
}
|
|
7290
|
+
void workerRecycleService.sweepNow();
|
|
7291
|
+
}
|
|
7292
|
+
|
|
7111
7293
|
// Once at start, after the restart's own worker reconnects settled: what
|
|
7112
7294
|
// still runs for an archived conversation is stopped and stays stopped.
|
|
7113
7295
|
// Through the recycle's replacement, one session at a time; never throws.
|
|
@@ -7342,6 +7524,9 @@ const sessionWorkerWebSocketService = createSessionWorkerWebSocketService({
|
|
|
7342
7524
|
const worker = sessionWorkerRegistry?.getWorker?.(sid) || null;
|
|
7343
7525
|
if (worker && worker.buildId !== buildId) sessionWorkerRegistry?.upsertWorker?.({ ...worker, buildId });
|
|
7344
7526
|
},
|
|
7527
|
+
// Registers the process and runs the recycle's sweep before the hello's
|
|
7528
|
+
// request for work (noteSessionWorkerHello).
|
|
7529
|
+
onWorkerHello: noteSessionWorkerHello,
|
|
7345
7530
|
// Sent in server.hello, so a worker on other code can say so in its log.
|
|
7346
7531
|
buildId: BUILD_ID,
|
|
7347
7532
|
isWorkerProcessAlive: (pid) => isProcessAlive(Number(pid)),
|
|
@@ -7430,6 +7615,12 @@ app.use(tunnelWorkerPathGuard.requestMiddleware);
|
|
|
7430
7615
|
// Uploads travel as raw bodies on POST /api/upload (express.raw, capped by
|
|
7431
7616
|
// MAX_UPLOAD_BYTES), so the JSON limit only bounds inline image data URLs.
|
|
7432
7617
|
app.use(express.json({ limit: '20mb' }));
|
|
7618
|
+
// Express 5 leaves req.body undefined when no parser read a body (a GET, a
|
|
7619
|
+
// POST without a JSON body); the routes read it as express 4 gave it, {}.
|
|
7620
|
+
app.use((req, _res, next) => {
|
|
7621
|
+
if (req.body === undefined) req.body = {};
|
|
7622
|
+
next();
|
|
7623
|
+
});
|
|
7433
7624
|
app.use((req, _res, next) => {
|
|
7434
7625
|
stripRequestPathPrefix(req, remotePath);
|
|
7435
7626
|
next();
|
|
@@ -7461,9 +7652,7 @@ app.use((req, res, next) => {
|
|
|
7461
7652
|
});
|
|
7462
7653
|
app.get('/socket.io/socket.io.js', (req, res, next) => {
|
|
7463
7654
|
res.setHeader('Cache-Control', 'no-store');
|
|
7464
|
-
res
|
|
7465
|
-
if (error) next(error);
|
|
7466
|
-
});
|
|
7655
|
+
sendOwnFile(res, SOCKET_IO_CLIENT_JS_PATH, next);
|
|
7467
7656
|
});
|
|
7468
7657
|
app.get('/manifest.webmanifest', (req, res, next) => {
|
|
7469
7658
|
try {
|
|
@@ -7477,9 +7666,7 @@ app.get('/manifest.webmanifest', (req, res, next) => {
|
|
|
7477
7666
|
});
|
|
7478
7667
|
app.get('/sw.js', (req, res, next) => {
|
|
7479
7668
|
res.setHeader('Cache-Control', 'no-store');
|
|
7480
|
-
res
|
|
7481
|
-
if (error) next(error);
|
|
7482
|
-
});
|
|
7669
|
+
sendOwnFile(res, PWA_SW_PATH, next);
|
|
7483
7670
|
});
|
|
7484
7671
|
app.get(['/', '/index.html'], (req, res, next) => {
|
|
7485
7672
|
try {
|
|
@@ -7554,7 +7741,6 @@ const runtimeState = {
|
|
|
7554
7741
|
// Resumed: what was queued while paused goes out now.
|
|
7555
7742
|
if (wasPaused && !relayPaused) wakeSessionWorkerQueue('relay-resumed');
|
|
7556
7743
|
},
|
|
7557
|
-
get ttyConsoleActive() { return Boolean(ttyConsoleRuntime?.tty); },
|
|
7558
7744
|
get relayShutdown() { return relayRestartQueue.getState(); },
|
|
7559
7745
|
// A shutdown or restart waits for the processing turns: no pending row
|
|
7560
7746
|
// goes out meanwhile (services/relay-restart-queue.mjs).
|
|
@@ -7729,6 +7915,85 @@ const remoteRelayDispatcher = createRemoteRelayDispatcher({
|
|
|
7729
7915
|
logger: console,
|
|
7730
7916
|
});
|
|
7731
7917
|
|
|
7918
|
+
// Every refresh of a provider's models resets its clock in the ledger,
|
|
7919
|
+
// whoever asks; the background check refreshes what is due through the same
|
|
7920
|
+
// table as the Select Models Refresh button.
|
|
7921
|
+
const trackedModelRefreshers = {
|
|
7922
|
+
refreshOpenAIProviderModels: modelRefreshLedger.track('openai', refreshOpenAIProviderModels),
|
|
7923
|
+
refreshClaudeProviderModels: modelRefreshLedger.track('claude', refreshClaudeProviderModels),
|
|
7924
|
+
refreshCursorProviderModels: modelRefreshLedger.track('cursor', refreshCursorProviderModels),
|
|
7925
|
+
refreshGrokProviderModels: modelRefreshLedger.track('grok', refreshGrokProviderModels),
|
|
7926
|
+
};
|
|
7927
|
+
const modelRefreshSettings = createModelRefreshSettings({
|
|
7928
|
+
readSetting: readAppSettingValue,
|
|
7929
|
+
writeSetting: (key, value, stamp) => stmts.upsertAppSetting.run(key, value, stamp),
|
|
7930
|
+
});
|
|
7931
|
+
// Claude and Grok are set up when their CLI is installed or their login is
|
|
7932
|
+
// there: a file lookup and the last status, never a CLI started to ask.
|
|
7933
|
+
function isClaudeModelRefreshSetUp() {
|
|
7934
|
+
// Discovery runs on the SDK: none installed yet, nothing to ask.
|
|
7935
|
+
if (!resolveAgentSdkSource('claude')) return false;
|
|
7936
|
+
return !!cliInstallService.resolveCliBinary('claude')
|
|
7937
|
+
|| claudeCloudCredentials.describe().hasToken === true
|
|
7938
|
+
|| claudeAuthService.getCachedStatus?.()?.loggedIn === true;
|
|
7939
|
+
}
|
|
7940
|
+
function isGrokModelRefreshSetUp() {
|
|
7941
|
+
return !!String(process.env.GROK_CLI_COMMAND || process.env.GROK_COMMAND || '').trim()
|
|
7942
|
+
|| !!cliInstallService.resolveCliBinary('grok')
|
|
7943
|
+
|| !!readGrokCliAuthKey();
|
|
7944
|
+
}
|
|
7945
|
+
const modelRefreshTable = buildProviderRefreshTable({
|
|
7946
|
+
getOpenAIProviderSettings,
|
|
7947
|
+
getClaudeProviderSettings,
|
|
7948
|
+
getCursorProviderSettings,
|
|
7949
|
+
getGrokProviderSettings,
|
|
7950
|
+
...trackedModelRefreshers,
|
|
7951
|
+
deepseekSettingsService,
|
|
7952
|
+
claudeApiSettingsService,
|
|
7953
|
+
copilotModelDiscoveryService,
|
|
7954
|
+
isClaudeSetUp: isClaudeModelRefreshSetUp,
|
|
7955
|
+
isGrokSetUp: isGrokModelRefreshSetUp,
|
|
7956
|
+
});
|
|
7957
|
+
// A freshly installed SDK fills its provider's catalogue at once, as a
|
|
7958
|
+
// fresh CLI install does.
|
|
7959
|
+
agentSdkStore.onReady(({ id } = {}) => {
|
|
7960
|
+
// A session the gate held back, or whose launch gave up on a failed
|
|
7961
|
+
// install, delivers its queued message now.
|
|
7962
|
+
agentSdkLaunchGate.onReady(id);
|
|
7963
|
+
const providers = { claude: ['claude'], cursor: ['cursor'] }[id];
|
|
7964
|
+
if (!providers) return;
|
|
7965
|
+
void refreshProviderModels({ table: modelRefreshTable, providers, reason: 'agent-sdk-installed', gate: 'isSetUp' }).catch((error) => {
|
|
7966
|
+
console.warn(`[agent-sdk] model refresh after the ${id} install failed: ${error?.message || error}`);
|
|
7967
|
+
});
|
|
7968
|
+
});
|
|
7969
|
+
const modelRefreshScheduler = createModelRefreshScheduler({
|
|
7970
|
+
getSettings: () => modelRefreshSettings.get(),
|
|
7971
|
+
getLast: () => modelRefreshLedger.readLast(),
|
|
7972
|
+
getTable: () => modelRefreshTable,
|
|
7973
|
+
refreshProviders: ({ providers, reason }) => refreshProviderModels({ table: modelRefreshTable, providers, reason, gate: 'isSetUp' }),
|
|
7974
|
+
logger: console,
|
|
7975
|
+
});
|
|
7976
|
+
|
|
7977
|
+
// The same for the settings routes: a save that leaves the provider enabled
|
|
7978
|
+
// installs its SDK in the background.
|
|
7979
|
+
function withAgentSdkSetup(setter, sdk, isEnabled, reason) {
|
|
7980
|
+
return (...args) => {
|
|
7981
|
+
const result = setter(...args);
|
|
7982
|
+
try { if (result?.ok !== false && isEnabled()) installAgentSdkForSetup(sdk, reason); } catch {}
|
|
7983
|
+
return result;
|
|
7984
|
+
};
|
|
7985
|
+
}
|
|
7986
|
+
function withAgentSdkUpdate(service, reason) {
|
|
7987
|
+
if (!service) return service;
|
|
7988
|
+
return Object.assign(Object.create(service), {
|
|
7989
|
+
update: (...args) => {
|
|
7990
|
+
const result = service.update(...args);
|
|
7991
|
+
try { if (result?.ok !== false && service.getSettings?.()?.enabled === true) installAgentSdkForSetup('claude', reason); } catch {}
|
|
7992
|
+
return result;
|
|
7993
|
+
},
|
|
7994
|
+
});
|
|
7995
|
+
}
|
|
7996
|
+
|
|
7732
7997
|
const sharedRouteDeps = {
|
|
7733
7998
|
auth,
|
|
7734
7999
|
isTrulyLocalRequest: localRequestCheck.isTrulyLocal,
|
|
@@ -7785,27 +8050,27 @@ const sharedRouteDeps = {
|
|
|
7785
8050
|
setDefaultSessionWorkspaceRootPath,
|
|
7786
8051
|
getOpenAIProviderSettings,
|
|
7787
8052
|
setOpenAIProviderSettings,
|
|
7788
|
-
refreshOpenAIProviderModels,
|
|
8053
|
+
refreshOpenAIProviderModels: trackedModelRefreshers.refreshOpenAIProviderModels,
|
|
7789
8054
|
getCopilotProviderSettings,
|
|
7790
8055
|
setCopilotProviderSettings,
|
|
7791
8056
|
getClaudeProviderSettings,
|
|
7792
|
-
setClaudeProviderSettings,
|
|
7793
|
-
refreshClaudeProviderModels,
|
|
8057
|
+
setClaudeProviderSettings: withAgentSdkSetup(setClaudeProviderSettings, 'claude', () => getClaudeProviderSettings()?.enabled === true, 'Claude enabled'),
|
|
8058
|
+
refreshClaudeProviderModels: trackedModelRefreshers.refreshClaudeProviderModels,
|
|
7794
8059
|
getCursorProviderSettings,
|
|
7795
|
-
setCursorProviderSettings,
|
|
7796
|
-
refreshCursorProviderModels,
|
|
8060
|
+
setCursorProviderSettings: withAgentSdkSetup(setCursorProviderSettings, 'cursor', () => getCursorProviderSettings()?.enabled === true, 'Cursor key saved'),
|
|
8061
|
+
refreshCursorProviderModels: trackedModelRefreshers.refreshCursorProviderModels,
|
|
7797
8062
|
getGrokProviderSettings,
|
|
7798
8063
|
setGrokProviderSettings,
|
|
7799
|
-
refreshGrokProviderModels,
|
|
8064
|
+
refreshGrokProviderModels: trackedModelRefreshers.refreshGrokProviderModels,
|
|
7800
8065
|
getClaudeCloudProviderSettings,
|
|
7801
8066
|
claudeCloudSettingsService,
|
|
7802
8067
|
claudeCloudSessionService,
|
|
7803
8068
|
getDeepSeekProviderSettings,
|
|
7804
|
-
deepseekSettingsService,
|
|
8069
|
+
deepseekSettingsService: withAgentSdkUpdate(deepseekSettingsService, 'DeepSeek set up'),
|
|
7805
8070
|
deepseekBalanceService,
|
|
7806
8071
|
deepseekUsageImportService,
|
|
7807
8072
|
getClaudeApiProviderSettings,
|
|
7808
|
-
claudeApiSettingsService,
|
|
8073
|
+
claudeApiSettingsService: withAgentSdkUpdate(claudeApiSettingsService, 'Claude API set up'),
|
|
7809
8074
|
claudeCloudRepoService,
|
|
7810
8075
|
// The login's state for the workers' nudge route (never the token).
|
|
7811
8076
|
claudeCloudLoginStatus: { describe: () => claudeCloudCredentials.describe() },
|
|
@@ -7826,6 +8091,15 @@ const sharedRouteDeps = {
|
|
|
7826
8091
|
listModelVariantRows,
|
|
7827
8092
|
refreshModelVariantCatalogFromCli,
|
|
7828
8093
|
setEnabledModelVariants,
|
|
8094
|
+
getCopilotModelState: modelVariantCatalog.getCopilotModelState,
|
|
8095
|
+
setCopilotAutoEnableNew: modelVariantCatalog.setCopilotAutoEnableNew,
|
|
8096
|
+
markCopilotModelsSeen: modelVariantCatalog.markCopilotModelsSeen,
|
|
8097
|
+
markProviderModelsSeen: (provider) => providerModelState.markSeen(provider),
|
|
8098
|
+
modelRefreshLedger,
|
|
8099
|
+
modelRefreshSettings,
|
|
8100
|
+
modelRefreshScheduler,
|
|
8101
|
+
isClaudeModelRefreshSetUp,
|
|
8102
|
+
isGrokModelRefreshSetUp,
|
|
7829
8103
|
parseModelVariantSelection,
|
|
7830
8104
|
SUPPORTED_REASONING_EFFORTS,
|
|
7831
8105
|
buildRelayReadyBannerData,
|
|
@@ -7987,6 +8261,7 @@ const sharedRouteDeps = {
|
|
|
7987
8261
|
shareRepository,
|
|
7988
8262
|
};
|
|
7989
8263
|
registerMessagesRoutes(app, sharedRouteDeps);
|
|
8264
|
+
registerAgentSdkRoutes(app, { auth, io, agentSdkStore });
|
|
7990
8265
|
registerSessionsRoutes(app, sharedRouteDeps);
|
|
7991
8266
|
registerConversationGroupRoutes(app, sharedRouteDeps);
|
|
7992
8267
|
registerAskUserRoutes(app, sharedRouteDeps);
|
|
@@ -8073,6 +8348,7 @@ registerAboutRoutes(app, {
|
|
|
8073
8348
|
isRelayCliOnline: () => runtimeState.cliOnline === true,
|
|
8074
8349
|
listWorkers: () => sessionWorkerRegistry.listWorkers(),
|
|
8075
8350
|
getUpdateState: () => buildUpdateStatePayload(),
|
|
8351
|
+
getAgentSdkStatus: () => agentSdkStore.snapshot(),
|
|
8076
8352
|
describeBackups: () => describeBackups({ layout: STATE_LAYOUT, serverDir: __dirname, dataDir: DATA_DIR, config, sizes: false }),
|
|
8077
8353
|
}),
|
|
8078
8354
|
});
|
|
@@ -8219,7 +8495,7 @@ if (runtimeBindingsBootstrapped > 0) {
|
|
|
8219
8495
|
|
|
8220
8496
|
function ts() { return new Date().toISOString().slice(11, 23); }
|
|
8221
8497
|
function runtimeLogPrefix() {
|
|
8222
|
-
return
|
|
8498
|
+
return `[${ts()}] `;
|
|
8223
8499
|
}
|
|
8224
8500
|
|
|
8225
8501
|
// ─── Auth Middleware ──────────────────────────────────────────────────────────
|
|
@@ -8719,12 +8995,17 @@ function shutdownRuntime(reason = 'unknown', { exitCode = 0 } = {}) {
|
|
|
8719
8995
|
const discoveryShutdown = copilotModelDiscoveryService.dispose().catch((error) => {
|
|
8720
8996
|
console.warn(`${runtimeLogPrefix()}Copilot model discovery shutdown failed: ${error?.message || error}`);
|
|
8721
8997
|
});
|
|
8998
|
+
// Stops the timer at once; a check in flight ends with the discoveries above.
|
|
8999
|
+
void modelRefreshScheduler.dispose();
|
|
8722
9000
|
try { claudeAuthService.dispose(); } catch (error) {
|
|
8723
9001
|
console.warn(`${runtimeLogPrefix()}Claude auth service shutdown failed: ${error?.message || error}`);
|
|
8724
9002
|
}
|
|
8725
9003
|
try { cliInstallService.dispose(); } catch (error) {
|
|
8726
9004
|
console.warn(`${runtimeLogPrefix()}CLI install service shutdown failed: ${error?.message || error}`);
|
|
8727
9005
|
}
|
|
9006
|
+
try { agentSdkStore.dispose(); } catch (error) {
|
|
9007
|
+
console.warn(`${runtimeLogPrefix()}agent SDK store shutdown failed: ${error?.message || error}`);
|
|
9008
|
+
}
|
|
8728
9009
|
try { grokAuthService.dispose(); } catch (error) {
|
|
8729
9010
|
console.warn(`${runtimeLogPrefix()}Grok auth service shutdown failed: ${error?.message || error}`);
|
|
8730
9011
|
}
|
|
@@ -8899,6 +9180,20 @@ httpServer.listen(config.port, listenHost, () => {
|
|
|
8899
9180
|
// Deferred and unref'd (a few seconds behind the startup import's own CLI
|
|
8900
9181
|
// spawn); a relay without an installed Copilot CLI skips it entirely.
|
|
8901
9182
|
copilotModelDiscoveryService.scheduleStartupRefresh();
|
|
9183
|
+
// "Check for new models": a first look a minute in (overdue providers),
|
|
9184
|
+
// then every quarter of an hour; unref'd.
|
|
9185
|
+
modelRefreshScheduler.start();
|
|
9186
|
+
// At start: the SDKs of the providers in use, and koffi on
|
|
9187
|
+
// Windows, installed in the background when their pinned folder is
|
|
9188
|
+
// missing (after an update: the new pins); the start never waits for it.
|
|
9189
|
+
// A git checkout runs the devDependencies it has installed and leaves
|
|
9190
|
+
// the store alone for those.
|
|
9191
|
+
const storeSdks = agentSdkStore.ids().filter((sdk) => !agentSdkFromCheckout(sdk));
|
|
9192
|
+
if (storeSdks.length) {
|
|
9193
|
+
void agentSdkStore.reconcile({ inUse: agentSdksInUse().filter((sdk) => storeSdks.includes(sdk)) }).catch((error) => {
|
|
9194
|
+
console.warn(`[agent-sdk] start-up check failed: ${error?.message || error}`);
|
|
9195
|
+
});
|
|
9196
|
+
}
|
|
8902
9197
|
// Archived conversations' workers: stopped once, 30 s in, so the workers
|
|
8903
9198
|
// that reconnect after the restart and the first prime have settled. With
|
|
8904
9199
|
// routing off the relay launches no workers, so there is nothing to sweep.
|