@oar-sh/oar 0.9.23 → 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 +165 -0
- package/README.md +9 -9
- package/bin/oar.js +54 -4
- package/package.json +9 -7
- 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-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 +91 -17
- package/server/routes/sessions-routes.mjs +235 -109
- package/server/server-runtime.mjs +395 -81
- 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 +4 -2
- package/server/services/model-refresh-scheduler.mjs +153 -0
- package/server/services/model-variant-catalog-service.mjs +141 -9
- package/server/services/oar-cli-helpers.mjs +72 -9
- 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-singleton-guard.mjs +72 -10
- 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 +2 -2
- 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
|
@@ -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 {
|
|
@@ -375,6 +383,29 @@ for (const dir of UPLOAD_READ_DIRS.slice(1)) {
|
|
|
375
383
|
} catch {}
|
|
376
384
|
}
|
|
377
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
|
+
|
|
378
409
|
async function refreshOpenAIProviderModels() {
|
|
379
410
|
const settings = getOpenAIProviderSettings();
|
|
380
411
|
if (!settings.enabled) return { ok: false, models: [], error: 'OpenAI API key is not configured' };
|
|
@@ -410,7 +441,8 @@ async function refreshOpenAIProviderModels() {
|
|
|
410
441
|
.filter((modelId) => isOpenAIModelId(modelId)),
|
|
411
442
|
])).filter(Boolean);
|
|
412
443
|
stmts.upsertAppSetting.run(OPENAI_MODELS_SETTING_KEY, JSON.stringify(models), new Date().toISOString());
|
|
413
|
-
|
|
444
|
+
const { added } = providerModelState.reconcile('openai', { availableModels: getOpenAIProviderSettings().availableModels });
|
|
445
|
+
return { ok: true, models, newModels: added, error: null };
|
|
414
446
|
}
|
|
415
447
|
|
|
416
448
|
function getOpenAIProviderSettings() {
|
|
@@ -427,12 +459,21 @@ function getOpenAIProviderSettings() {
|
|
|
427
459
|
} catch {
|
|
428
460
|
models = [];
|
|
429
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 });
|
|
430
466
|
return {
|
|
431
467
|
configured: !!apiKey,
|
|
432
468
|
enabled,
|
|
433
469
|
apiKey,
|
|
434
470
|
model,
|
|
435
|
-
models:
|
|
471
|
+
models: enabledModels.models,
|
|
472
|
+
availableModels,
|
|
473
|
+
enabledModels: enabledModels.models,
|
|
474
|
+
autoEnableNew: enabledModels.autoEnableNew,
|
|
475
|
+
fallback: enabledModels.fallback,
|
|
476
|
+
unseenModels: enabledModels.unseen,
|
|
436
477
|
baseUrl: readAppSettingValue(OPENAI_BASE_URL_SETTING_KEY) || DEFAULT_OPENAI_BASE_URL,
|
|
437
478
|
providerType: 'openai',
|
|
438
479
|
};
|
|
@@ -443,6 +484,7 @@ function setOpenAIProviderSettings(update = {}) {
|
|
|
443
484
|
const apiKey = payload.apiKey;
|
|
444
485
|
const model = payload.model;
|
|
445
486
|
const enabled = payload.enabled;
|
|
487
|
+
const enabledModels = payload.enabledModels;
|
|
446
488
|
const remove = payload.remove === true;
|
|
447
489
|
const hasBaseUrl = Object.prototype.hasOwnProperty.call(payload, 'baseUrl');
|
|
448
490
|
const baseUrl = hasBaseUrl ? payload.baseUrl : '';
|
|
@@ -481,6 +523,7 @@ function setOpenAIProviderSettings(update = {}) {
|
|
|
481
523
|
activeQueueConversationCount,
|
|
482
524
|
};
|
|
483
525
|
}
|
|
526
|
+
// The model map stays: a key added again finds its models as they were.
|
|
484
527
|
stmts.deleteAppSetting.run(OPENAI_API_KEY_SETTING_KEY);
|
|
485
528
|
stmts.deleteAppSetting.run(OPENAI_MODELS_SETTING_KEY);
|
|
486
529
|
stmts.upsertAppSetting.run(OPENAI_ENABLED_SETTING_KEY, 'false', nowIso);
|
|
@@ -504,6 +547,21 @@ function setOpenAIProviderSettings(update = {}) {
|
|
|
504
547
|
stmts.deleteAppSetting.run(OPENAI_BASE_URL_SETTING_KEY);
|
|
505
548
|
}
|
|
506
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
|
+
}
|
|
507
565
|
}
|
|
508
566
|
const current = getOpenAIProviderSettings();
|
|
509
567
|
return {
|
|
@@ -511,6 +569,9 @@ function setOpenAIProviderSettings(update = {}) {
|
|
|
511
569
|
configured: current.configured,
|
|
512
570
|
enabled: current.enabled,
|
|
513
571
|
model: current.model,
|
|
572
|
+
models: current.models,
|
|
573
|
+
availableModels: current.availableModels,
|
|
574
|
+
autoEnableNew: current.autoEnableNew,
|
|
514
575
|
baseUrl: current.baseUrl,
|
|
515
576
|
};
|
|
516
577
|
}
|
|
@@ -554,15 +615,14 @@ function getClaudeProviderSettings() {
|
|
|
554
615
|
model,
|
|
555
616
|
...(discovered.length ? discovered : DEFAULT_CLAUDE_MODELS),
|
|
556
617
|
])).filter(Boolean);
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
])).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;
|
|
566
626
|
const storedEfforts = readClaudeModelEffortsSetting();
|
|
567
627
|
const effortsByModel = {};
|
|
568
628
|
for (const availableModel of availableModels) {
|
|
@@ -586,6 +646,9 @@ function getClaudeProviderSettings() {
|
|
|
586
646
|
models,
|
|
587
647
|
availableModels,
|
|
588
648
|
enabledModels: models,
|
|
649
|
+
autoEnableNew: enabledModels.autoEnableNew,
|
|
650
|
+
fallback: enabledModels.fallback,
|
|
651
|
+
unseenModels: enabledModels.unseen,
|
|
589
652
|
effortsByModel,
|
|
590
653
|
modelsDiscovered,
|
|
591
654
|
attributionMode: normalizeClaudeAttributionMode(readAppSettingValue(CLAUDE_ATTRIBUTION_SETTING_KEY)) || DEFAULT_CLAUDE_ATTRIBUTION_MODE,
|
|
@@ -644,15 +707,20 @@ function setClaudeProviderSettings(update = {}) {
|
|
|
644
707
|
stmts.deleteAppSetting.run(CLAUDE_MODELS_SETTING_KEY);
|
|
645
708
|
}
|
|
646
709
|
}
|
|
647
|
-
if (Array.isArray(enabledModels)) {
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
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
|
+
});
|
|
656
724
|
}
|
|
657
725
|
const current = getClaudeProviderSettings();
|
|
658
726
|
return {
|
|
@@ -662,6 +730,7 @@ function setClaudeProviderSettings(update = {}) {
|
|
|
662
730
|
model: current.model,
|
|
663
731
|
models: current.models,
|
|
664
732
|
availableModels: current.availableModels,
|
|
733
|
+
autoEnableNew: current.autoEnableNew,
|
|
665
734
|
attributionMode: current.attributionMode,
|
|
666
735
|
};
|
|
667
736
|
}
|
|
@@ -709,7 +778,7 @@ async function refreshClaudeProviderModels() {
|
|
|
709
778
|
let claudeQuery = null;
|
|
710
779
|
let releaseInputGate = () => {};
|
|
711
780
|
try {
|
|
712
|
-
const { query } = await
|
|
781
|
+
const { query } = await loadAgentSdk('claude');
|
|
713
782
|
const inputGate = new Promise((resolve) => { releaseInputGate = resolve; });
|
|
714
783
|
claudeQuery = query({
|
|
715
784
|
// Streaming-input mode with a gated (never-yielding) prompt keeps the
|
|
@@ -762,8 +831,13 @@ async function refreshClaudeProviderModels() {
|
|
|
762
831
|
const mergedEfforts = mergeClaudeModelEfforts(readClaudeModelEffortsSetting(), effortsByModel);
|
|
763
832
|
stmts.upsertAppSetting.run(CLAUDE_MODELS_SETTING_KEY, JSON.stringify(models), nowIso);
|
|
764
833
|
stmts.upsertAppSetting.run(CLAUDE_MODEL_EFFORTS_SETTING_KEY, JSON.stringify(mergedEfforts), nowIso);
|
|
765
|
-
|
|
834
|
+
const { added } = providerModelState.reconcile('claude', { availableModels: getClaudeProviderSettings().availableModels });
|
|
835
|
+
return { ok: true, models, newModels: added, error: null };
|
|
766
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
|
+
}
|
|
767
841
|
return {
|
|
768
842
|
ok: false,
|
|
769
843
|
models: settings.models,
|
|
@@ -827,24 +901,19 @@ function getCursorProviderSettings() {
|
|
|
827
901
|
models = [];
|
|
828
902
|
}
|
|
829
903
|
const availableModels = Array.from(new Set([model, ...(models.length ? models : DEFAULT_CURSOR_MODELS)]));
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
// catalog and composers; an empty selection means "all available".
|
|
844
|
-
const resolvedModels = Array.from(new Set([
|
|
845
|
-
model,
|
|
846
|
-
...(enabledSelection.length ? enabledSelection : availableModels),
|
|
847
|
-
])).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;
|
|
848
917
|
const storedEfforts = readCursorModelEffortsSetting();
|
|
849
918
|
const storedReasoningOff = readCursorModelReasoningOffSetting();
|
|
850
919
|
const effortsByModel = {};
|
|
@@ -868,6 +937,9 @@ function getCursorProviderSettings() {
|
|
|
868
937
|
models: resolvedModels,
|
|
869
938
|
availableModels,
|
|
870
939
|
enabledModels: resolvedModels,
|
|
940
|
+
autoEnableNew: enabledModels.autoEnableNew,
|
|
941
|
+
fallback: enabledModels.fallback,
|
|
942
|
+
unseenModels: enabledModels.unseen,
|
|
871
943
|
effortsByModel,
|
|
872
944
|
reasoningOffByModel,
|
|
873
945
|
// Pre-effort installs have models but no efforts setting; the settings
|
|
@@ -1109,9 +1181,9 @@ function setCursorProviderSettings(update = {}) {
|
|
|
1109
1181
|
activeQueueConversationCount,
|
|
1110
1182
|
};
|
|
1111
1183
|
}
|
|
1184
|
+
// The model map stays: a key added again finds its models as they were.
|
|
1112
1185
|
stmts.deleteAppSetting.run(CURSOR_API_KEY_SETTING_KEY);
|
|
1113
1186
|
stmts.deleteAppSetting.run(CURSOR_MODELS_SETTING_KEY);
|
|
1114
|
-
stmts.deleteAppSetting.run(CURSOR_ENABLED_MODELS_SETTING_KEY);
|
|
1115
1187
|
stmts.deleteAppSetting.run(CURSOR_MODEL_EFFORTS_SETTING_KEY);
|
|
1116
1188
|
stmts.deleteAppSetting.run(CURSOR_MODEL_REASONING_OFF_SETTING_KEY);
|
|
1117
1189
|
stmts.upsertAppSetting.run(CURSOR_ENABLED_SETTING_KEY, 'false', nowIso);
|
|
@@ -1129,15 +1201,20 @@ function setCursorProviderSettings(update = {}) {
|
|
|
1129
1201
|
if (nextEnabled && !hasApiKey) return { ok: false, error: 'Cursor API key is not configured' };
|
|
1130
1202
|
stmts.upsertAppSetting.run(CURSOR_ENABLED_SETTING_KEY, nextEnabled ? 'true' : 'false', nowIso);
|
|
1131
1203
|
stmts.upsertAppSetting.run(CURSOR_MODEL_SETTING_KEY, normalizedModel, nowIso);
|
|
1132
|
-
if (Array.isArray(enabledModels)) {
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
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
|
+
});
|
|
1141
1218
|
}
|
|
1142
1219
|
}
|
|
1143
1220
|
const current = getCursorProviderSettings();
|
|
@@ -1148,6 +1225,7 @@ function setCursorProviderSettings(update = {}) {
|
|
|
1148
1225
|
model: current.model,
|
|
1149
1226
|
models: current.models,
|
|
1150
1227
|
availableModels: current.availableModels,
|
|
1228
|
+
autoEnableNew: current.autoEnableNew,
|
|
1151
1229
|
};
|
|
1152
1230
|
}
|
|
1153
1231
|
|
|
@@ -1204,7 +1282,7 @@ async function refreshCursorProviderModels() {
|
|
|
1204
1282
|
return { ok: false, models: settings.models, error: 'Cursor API key is not configured' };
|
|
1205
1283
|
}
|
|
1206
1284
|
try {
|
|
1207
|
-
const mod = await
|
|
1285
|
+
const mod = await loadAgentSdk('cursor');
|
|
1208
1286
|
// The namespace form is the only working call shape: `Cursor` has a
|
|
1209
1287
|
// private constructor and `models` is static, so the old client-instance
|
|
1210
1288
|
// fallback could only throw a TypeError that replaced the real error.
|
|
@@ -1243,8 +1321,12 @@ async function refreshCursorProviderModels() {
|
|
|
1243
1321
|
stmts.upsertAppSetting.run(CURSOR_MODELS_SETTING_KEY, JSON.stringify(models), nowIso);
|
|
1244
1322
|
stmts.upsertAppSetting.run(CURSOR_MODEL_EFFORTS_SETTING_KEY, JSON.stringify(effortsByModel), nowIso);
|
|
1245
1323
|
stmts.upsertAppSetting.run(CURSOR_MODEL_REASONING_OFF_SETTING_KEY, JSON.stringify(reasoningOffByModel), nowIso);
|
|
1246
|
-
|
|
1324
|
+
const { added } = providerModelState.reconcile('cursor', { availableModels: getCursorProviderSettings().availableModels });
|
|
1325
|
+
return { ok: true, models, newModels: added, error: null };
|
|
1247
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
|
+
}
|
|
1248
1330
|
return {
|
|
1249
1331
|
ok: false,
|
|
1250
1332
|
models: settings.models,
|
|
@@ -1294,13 +1376,12 @@ function getGrokProviderSettings() {
|
|
|
1294
1376
|
model,
|
|
1295
1377
|
...(discovered.length ? discovered : DEFAULT_GROK_MODELS),
|
|
1296
1378
|
])).filter(Boolean);
|
|
1297
|
-
const
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
])).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;
|
|
1304
1385
|
const storedEfforts = readGrokModelEffortsSetting();
|
|
1305
1386
|
const effortsByModel = {};
|
|
1306
1387
|
for (const availableModel of availableModels) {
|
|
@@ -1319,6 +1400,9 @@ function getGrokProviderSettings() {
|
|
|
1319
1400
|
models,
|
|
1320
1401
|
availableModels,
|
|
1321
1402
|
enabledModels: models,
|
|
1403
|
+
autoEnableNew: enabledModels.autoEnableNew,
|
|
1404
|
+
fallback: enabledModels.fallback,
|
|
1405
|
+
unseenModels: enabledModels.unseen,
|
|
1322
1406
|
effortsByModel,
|
|
1323
1407
|
modelsDiscovered,
|
|
1324
1408
|
providerType: 'grok',
|
|
@@ -1353,15 +1437,20 @@ function setGrokProviderSettings(update = {}) {
|
|
|
1353
1437
|
stmts.deleteAppSetting?.run?.(GROK_MODELS_SETTING_KEY);
|
|
1354
1438
|
}
|
|
1355
1439
|
}
|
|
1356
|
-
if (Array.isArray(enabledModels)) {
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
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
|
+
});
|
|
1365
1454
|
}
|
|
1366
1455
|
const current = getGrokProviderSettings();
|
|
1367
1456
|
return {
|
|
@@ -1371,6 +1460,7 @@ function setGrokProviderSettings(update = {}) {
|
|
|
1371
1460
|
model: current.model,
|
|
1372
1461
|
models: current.models,
|
|
1373
1462
|
availableModels: current.availableModels,
|
|
1463
|
+
autoEnableNew: current.autoEnableNew,
|
|
1374
1464
|
};
|
|
1375
1465
|
}
|
|
1376
1466
|
|
|
@@ -1452,7 +1542,8 @@ async function refreshGrokProviderModels() {
|
|
|
1452
1542
|
const models = Array.from(new Set([settings.model, ...discovered])).filter(Boolean);
|
|
1453
1543
|
stmts.upsertAppSetting.run(GROK_MODELS_SETTING_KEY, JSON.stringify(models), nowIso);
|
|
1454
1544
|
stmts.upsertAppSetting.run(GROK_MODEL_EFFORTS_SETTING_KEY, JSON.stringify(effortsByModel), nowIso);
|
|
1455
|
-
|
|
1545
|
+
const { added } = providerModelState.reconcile('grok', { availableModels: getGrokProviderSettings().availableModels });
|
|
1546
|
+
return { ok: true, models, newModels: added, error: null };
|
|
1456
1547
|
} catch (error) {
|
|
1457
1548
|
return {
|
|
1458
1549
|
ok: false,
|
|
@@ -1509,6 +1600,8 @@ const FALLBACK_COPILOT_ENGINE = 'extension';
|
|
|
1509
1600
|
const CLAUDE_ENABLED_SETTING_KEY = 'claude_enabled';
|
|
1510
1601
|
const CLAUDE_MODEL_SETTING_KEY = 'claude_model';
|
|
1511
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.
|
|
1512
1605
|
const CLAUDE_ENABLED_MODELS_SETTING_KEY = 'claude_enabled_models';
|
|
1513
1606
|
const CLAUDE_MODEL_EFFORTS_SETTING_KEY = 'claude_model_efforts';
|
|
1514
1607
|
// oar | vanilla | off — what commits and PRs made in Claude sessions say
|
|
@@ -2678,6 +2771,10 @@ const modelVariantCatalog = createModelVariantCatalogService({
|
|
|
2678
2771
|
curatedModelIds: CURATED_MODEL_IDS,
|
|
2679
2772
|
autoModelSentinel: AUTO_MODEL_SENTINEL,
|
|
2680
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 }),
|
|
2681
2778
|
});
|
|
2682
2779
|
const {
|
|
2683
2780
|
SUPPORTED_REASONING_EFFORTS,
|
|
@@ -2990,6 +3087,7 @@ const deepseekSettingsService = createDeepSeekSettingsService({
|
|
|
2990
3087
|
},
|
|
2991
3088
|
deleteSetting: (key) => stmts.deleteAppSetting.run(key),
|
|
2992
3089
|
listProviderCandidates: () => stmts.listRuntimeSessionProviderCandidates?.all?.() || [],
|
|
3090
|
+
onModelsRefreshed: (result) => modelRefreshLedger.record('deepseek', result),
|
|
2993
3091
|
});
|
|
2994
3092
|
function getDeepSeekProviderSettings() {
|
|
2995
3093
|
return deepseekSettingsService.getSettings();
|
|
@@ -3007,6 +3105,7 @@ const claudeApiSettingsService = createClaudeApiSettingsService({
|
|
|
3007
3105
|
listProviderCandidates: () => stmts.listRuntimeSessionProviderCandidates?.all?.() || [],
|
|
3008
3106
|
countLiveKeyWorkers: () => (sessionWorkerRegistry?.listWorkers?.() || []).filter((worker) => worker?.kind === 'claude-api').length,
|
|
3009
3107
|
getClaudeProviderSettings: () => getClaudeProviderSettings(),
|
|
3108
|
+
onModelsRefreshed: (result) => modelRefreshLedger.record('claude-api', result),
|
|
3010
3109
|
});
|
|
3011
3110
|
function getClaudeApiProviderSettings() {
|
|
3012
3111
|
return claudeApiSettingsService.getSettings();
|
|
@@ -3096,6 +3195,44 @@ try {
|
|
|
3096
3195
|
console.warn(`[cli-install] failed to apply persisted CLI bindings: ${error?.message || error}`);
|
|
3097
3196
|
}
|
|
3098
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
|
+
|
|
3099
3236
|
// ─── Web Push ─────────────────────────────────────────────────────────────────
|
|
3100
3237
|
// VAPID keys are generated once and persisted in app_settings; regenerating
|
|
3101
3238
|
// them would invalidate every stored subscription.
|
|
@@ -3383,6 +3520,20 @@ const relayCliLauncherService = createRelayCliLauncherService({
|
|
|
3383
3520
|
workerScope,
|
|
3384
3521
|
});
|
|
3385
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
|
+
|
|
3386
3537
|
async function spawnSessionWorkerCli(targetSessionId, { allowProcessReuse = true } = {}) {
|
|
3387
3538
|
const normalizedTargetSessionId = String(targetSessionId || '').trim();
|
|
3388
3539
|
if (!normalizedTargetSessionId) {
|
|
@@ -3413,6 +3564,11 @@ async function spawnSessionWorkerCli(targetSessionId, { allowProcessReuse = true
|
|
|
3413
3564
|
}
|
|
3414
3565
|
const resolvedWorkspaceRoot = resolveLaunchWorkspaceRootForSession(normalizedTargetSessionId);
|
|
3415
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;
|
|
3416
3572
|
const launched = await launchSessionCli({
|
|
3417
3573
|
workerScope,
|
|
3418
3574
|
targetSessionId: normalizedTargetSessionId,
|
|
@@ -3427,6 +3583,9 @@ async function spawnSessionWorkerCli(targetSessionId, { allowProcessReuse = true
|
|
|
3427
3583
|
log: (message) => console.warn(`${runtimeLogPrefix()}${message}`),
|
|
3428
3584
|
});
|
|
3429
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);
|
|
3430
3589
|
const workerId = `worker-${normalizedTargetSessionId.slice(0, 8)}`;
|
|
3431
3590
|
const launchMode = String(launched?.launchMode || 'detached').trim();
|
|
3432
3591
|
const tmuxSessionName = String(launched?.tmuxSessionName || '').trim();
|
|
@@ -6259,6 +6418,7 @@ const copilotModelDiscoveryService = createCopilotModelDiscoveryService({
|
|
|
6259
6418
|
}),
|
|
6260
6419
|
resolveInstalledPaths: () => resolveInstalledCopilotPaths({ config }),
|
|
6261
6420
|
updateModelCatalog,
|
|
6421
|
+
onRefreshed: (result) => modelRefreshLedger.record('github', result),
|
|
6262
6422
|
logger: console,
|
|
6263
6423
|
});
|
|
6264
6424
|
const contextSnapshotService = createContextSnapshotService({
|
|
@@ -7085,9 +7245,51 @@ const workerRecycleService = createWorkerRecycleService({
|
|
|
7085
7245
|
// The stop and the launch under the per-session launch mutex the Change
|
|
7086
7246
|
// CWD relaunch and the launch routes take (sessions-routes.mjs).
|
|
7087
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) }),
|
|
7088
7251
|
logger: console,
|
|
7089
7252
|
});
|
|
7090
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
|
+
|
|
7091
7293
|
// Once at start, after the restart's own worker reconnects settled: what
|
|
7092
7294
|
// still runs for an archived conversation is stopped and stays stopped.
|
|
7093
7295
|
// Through the recycle's replacement, one session at a time; never throws.
|
|
@@ -7322,6 +7524,9 @@ const sessionWorkerWebSocketService = createSessionWorkerWebSocketService({
|
|
|
7322
7524
|
const worker = sessionWorkerRegistry?.getWorker?.(sid) || null;
|
|
7323
7525
|
if (worker && worker.buildId !== buildId) sessionWorkerRegistry?.upsertWorker?.({ ...worker, buildId });
|
|
7324
7526
|
},
|
|
7527
|
+
// Registers the process and runs the recycle's sweep before the hello's
|
|
7528
|
+
// request for work (noteSessionWorkerHello).
|
|
7529
|
+
onWorkerHello: noteSessionWorkerHello,
|
|
7325
7530
|
// Sent in server.hello, so a worker on other code can say so in its log.
|
|
7326
7531
|
buildId: BUILD_ID,
|
|
7327
7532
|
isWorkerProcessAlive: (pid) => isProcessAlive(Number(pid)),
|
|
@@ -7710,6 +7915,85 @@ const remoteRelayDispatcher = createRemoteRelayDispatcher({
|
|
|
7710
7915
|
logger: console,
|
|
7711
7916
|
});
|
|
7712
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
|
+
|
|
7713
7997
|
const sharedRouteDeps = {
|
|
7714
7998
|
auth,
|
|
7715
7999
|
isTrulyLocalRequest: localRequestCheck.isTrulyLocal,
|
|
@@ -7766,27 +8050,27 @@ const sharedRouteDeps = {
|
|
|
7766
8050
|
setDefaultSessionWorkspaceRootPath,
|
|
7767
8051
|
getOpenAIProviderSettings,
|
|
7768
8052
|
setOpenAIProviderSettings,
|
|
7769
|
-
refreshOpenAIProviderModels,
|
|
8053
|
+
refreshOpenAIProviderModels: trackedModelRefreshers.refreshOpenAIProviderModels,
|
|
7770
8054
|
getCopilotProviderSettings,
|
|
7771
8055
|
setCopilotProviderSettings,
|
|
7772
8056
|
getClaudeProviderSettings,
|
|
7773
|
-
setClaudeProviderSettings,
|
|
7774
|
-
refreshClaudeProviderModels,
|
|
8057
|
+
setClaudeProviderSettings: withAgentSdkSetup(setClaudeProviderSettings, 'claude', () => getClaudeProviderSettings()?.enabled === true, 'Claude enabled'),
|
|
8058
|
+
refreshClaudeProviderModels: trackedModelRefreshers.refreshClaudeProviderModels,
|
|
7775
8059
|
getCursorProviderSettings,
|
|
7776
|
-
setCursorProviderSettings,
|
|
7777
|
-
refreshCursorProviderModels,
|
|
8060
|
+
setCursorProviderSettings: withAgentSdkSetup(setCursorProviderSettings, 'cursor', () => getCursorProviderSettings()?.enabled === true, 'Cursor key saved'),
|
|
8061
|
+
refreshCursorProviderModels: trackedModelRefreshers.refreshCursorProviderModels,
|
|
7778
8062
|
getGrokProviderSettings,
|
|
7779
8063
|
setGrokProviderSettings,
|
|
7780
|
-
refreshGrokProviderModels,
|
|
8064
|
+
refreshGrokProviderModels: trackedModelRefreshers.refreshGrokProviderModels,
|
|
7781
8065
|
getClaudeCloudProviderSettings,
|
|
7782
8066
|
claudeCloudSettingsService,
|
|
7783
8067
|
claudeCloudSessionService,
|
|
7784
8068
|
getDeepSeekProviderSettings,
|
|
7785
|
-
deepseekSettingsService,
|
|
8069
|
+
deepseekSettingsService: withAgentSdkUpdate(deepseekSettingsService, 'DeepSeek set up'),
|
|
7786
8070
|
deepseekBalanceService,
|
|
7787
8071
|
deepseekUsageImportService,
|
|
7788
8072
|
getClaudeApiProviderSettings,
|
|
7789
|
-
claudeApiSettingsService,
|
|
8073
|
+
claudeApiSettingsService: withAgentSdkUpdate(claudeApiSettingsService, 'Claude API set up'),
|
|
7790
8074
|
claudeCloudRepoService,
|
|
7791
8075
|
// The login's state for the workers' nudge route (never the token).
|
|
7792
8076
|
claudeCloudLoginStatus: { describe: () => claudeCloudCredentials.describe() },
|
|
@@ -7807,6 +8091,15 @@ const sharedRouteDeps = {
|
|
|
7807
8091
|
listModelVariantRows,
|
|
7808
8092
|
refreshModelVariantCatalogFromCli,
|
|
7809
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,
|
|
7810
8103
|
parseModelVariantSelection,
|
|
7811
8104
|
SUPPORTED_REASONING_EFFORTS,
|
|
7812
8105
|
buildRelayReadyBannerData,
|
|
@@ -7968,6 +8261,7 @@ const sharedRouteDeps = {
|
|
|
7968
8261
|
shareRepository,
|
|
7969
8262
|
};
|
|
7970
8263
|
registerMessagesRoutes(app, sharedRouteDeps);
|
|
8264
|
+
registerAgentSdkRoutes(app, { auth, io, agentSdkStore });
|
|
7971
8265
|
registerSessionsRoutes(app, sharedRouteDeps);
|
|
7972
8266
|
registerConversationGroupRoutes(app, sharedRouteDeps);
|
|
7973
8267
|
registerAskUserRoutes(app, sharedRouteDeps);
|
|
@@ -8054,6 +8348,7 @@ registerAboutRoutes(app, {
|
|
|
8054
8348
|
isRelayCliOnline: () => runtimeState.cliOnline === true,
|
|
8055
8349
|
listWorkers: () => sessionWorkerRegistry.listWorkers(),
|
|
8056
8350
|
getUpdateState: () => buildUpdateStatePayload(),
|
|
8351
|
+
getAgentSdkStatus: () => agentSdkStore.snapshot(),
|
|
8057
8352
|
describeBackups: () => describeBackups({ layout: STATE_LAYOUT, serverDir: __dirname, dataDir: DATA_DIR, config, sizes: false }),
|
|
8058
8353
|
}),
|
|
8059
8354
|
});
|
|
@@ -8700,12 +8995,17 @@ function shutdownRuntime(reason = 'unknown', { exitCode = 0 } = {}) {
|
|
|
8700
8995
|
const discoveryShutdown = copilotModelDiscoveryService.dispose().catch((error) => {
|
|
8701
8996
|
console.warn(`${runtimeLogPrefix()}Copilot model discovery shutdown failed: ${error?.message || error}`);
|
|
8702
8997
|
});
|
|
8998
|
+
// Stops the timer at once; a check in flight ends with the discoveries above.
|
|
8999
|
+
void modelRefreshScheduler.dispose();
|
|
8703
9000
|
try { claudeAuthService.dispose(); } catch (error) {
|
|
8704
9001
|
console.warn(`${runtimeLogPrefix()}Claude auth service shutdown failed: ${error?.message || error}`);
|
|
8705
9002
|
}
|
|
8706
9003
|
try { cliInstallService.dispose(); } catch (error) {
|
|
8707
9004
|
console.warn(`${runtimeLogPrefix()}CLI install service shutdown failed: ${error?.message || error}`);
|
|
8708
9005
|
}
|
|
9006
|
+
try { agentSdkStore.dispose(); } catch (error) {
|
|
9007
|
+
console.warn(`${runtimeLogPrefix()}agent SDK store shutdown failed: ${error?.message || error}`);
|
|
9008
|
+
}
|
|
8709
9009
|
try { grokAuthService.dispose(); } catch (error) {
|
|
8710
9010
|
console.warn(`${runtimeLogPrefix()}Grok auth service shutdown failed: ${error?.message || error}`);
|
|
8711
9011
|
}
|
|
@@ -8880,6 +9180,20 @@ httpServer.listen(config.port, listenHost, () => {
|
|
|
8880
9180
|
// Deferred and unref'd (a few seconds behind the startup import's own CLI
|
|
8881
9181
|
// spawn); a relay without an installed Copilot CLI skips it entirely.
|
|
8882
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
|
+
}
|
|
8883
9197
|
// Archived conversations' workers: stopped once, 30 s in, so the workers
|
|
8884
9198
|
// that reconnect after the restart and the first prime have settled. With
|
|
8885
9199
|
// routing off the relay launches no workers, so there is nothing to sweep.
|