@oar-sh/oar 0.9.16 → 0.9.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/extensions/web-relay/polling/polling-loop.mjs +14 -87
- package/.github/extensions/web-relay/skills/tool-activity.mjs +5 -4
- package/CHANGELOG.md +412 -0
- package/README.md +15 -13
- package/package.json +2 -1
- package/server/claude-api-worker/claude-api-backend.mjs +345 -0
- package/server/claude-api-worker/claude-api-session-worker.mjs +29 -0
- package/server/claude-worker/claude-attachments.mjs +7 -4
- package/server/claude-worker/claude-cli-pinned-env.mjs +150 -0
- package/server/claude-worker/claude-remote-relay-tool.mjs +3 -3
- package/server/claude-worker/claude-sdk-adapter.mjs +199 -15
- package/server/claude-worker/claude-session-process.mjs +276 -33
- package/server/claude-worker/claude-session-worker-main.mjs +269 -0
- package/server/claude-worker/claude-session-worker.mjs +2 -253
- package/server/claude-worker/claude-subscription-key-check.mjs +64 -0
- package/server/claude-worker/claude-turn-failure.mjs +8 -0
- package/server/claude-worker/claude-turn-publisher.mjs +394 -13
- package/server/claude-worker/claude-usage-totals.mjs +161 -0
- package/server/claude-worker/sdk-message-normalizer.mjs +56 -1
- package/server/copilot-worker/copilot-remote-relay-tool.mjs +2 -2
- package/server/copilot-worker/copilot-sdk-adapter.mjs +18 -4
- package/server/copilot-worker/copilot-sdk-event-normalizer.mjs +1 -1
- package/server/copilot-worker/copilot-sdk-session-process.mjs +80 -14
- package/server/cursor-worker/cursor-remote-relay-tool.mjs +3 -3
- package/server/cursor-worker/cursor-turn-runner.mjs +4 -4
- package/server/cursor-worker/sdk-message-normalizer.mjs +2 -2
- package/server/db-schema.mjs +96 -1
- package/server/deepseek-worker/deepseek-claude-backend.mjs +317 -0
- package/server/deepseek-worker/deepseek-session-worker.mjs +37 -0
- package/server/grok-worker/acp-client.mjs +1 -1
- package/server/grok-worker/grok-turn-runner.mjs +4 -4
- package/server/grok-worker/sdk-message-normalizer.mjs +1 -1
- package/server/mcp/oar-mcp-server.mjs +11 -5
- package/server/migrations/0024-deepseek-usage-totals.mjs +151 -0
- package/server/migrations/0025-deepseek-usage-requests-daily.mjs +179 -0
- package/server/migrations/0026-deepseek-usage-served-models.mjs +222 -0
- package/server/migrations/0027-deepseek-usage-checkpoint-runs.mjs +101 -0
- package/server/migrations/0028-deepseek-usage-checkpoint-request-runs.mjs +99 -0
- package/server/migrations/0029-deepseek-balance-rises.mjs +99 -0
- package/server/migrations/0030-deepseek-usage-actuals.mjs +123 -0
- package/server/migrations/0031-queue-provider.mjs +143 -0
- package/server/migrations/0032-runtime-session-billing-switch.mjs +94 -0
- package/server/migrations/0033-claude-api-usage-checkpoints.mjs +138 -0
- package/server/migrations/0034-claude-api-usage-totals.mjs +112 -0
- package/server/public/app/activity-replay-state.mjs +4 -3
- package/server/public/app/agent-sessions-settings-ui.js +1 -1
- package/server/public/app/agent-sessions-settings.mjs +1 -1
- package/server/public/app/api-client.js +105 -0
- package/server/public/app/bootstrap.js +243 -29
- package/server/public/app/claude-api-settings-ui.js +362 -0
- package/server/public/app/claude-billing-toggle-ui.js +224 -0
- package/server/public/app/claude-billing-toggle.mjs +112 -0
- package/server/public/app/context-tier-options.mjs +10 -2
- package/server/public/app/context-usage-view.mjs +1 -1
- package/server/public/app/conversation-provider-indicator.mjs +18 -0
- package/server/public/app/conversation-view.js +31 -3
- package/server/public/app/deepseek-settings-ui.js +342 -0
- package/server/public/app/inbox-ui.js +14 -3
- package/server/public/app/inbox-view.mjs +1 -1
- package/server/public/app/journal-view.js +156 -11
- package/server/public/app/model-selector-options.mjs +28 -1
- package/server/public/app/model-vision-support.mjs +2 -0
- package/server/public/app/new-conversation-model-choice.mjs +21 -0
- package/server/public/app/no-conversation-state.mjs +1 -1
- package/server/public/app/plan-usage-view.mjs +165 -13
- package/server/public/app/provider-readiness.mjs +8 -2
- package/server/public/app/relay-error-ctas.mjs +58 -0
- package/server/public/app/relay-schedules-ui.js +1 -1
- package/server/public/app/settings-modal.js +22 -0
- package/server/public/app/settings-tabs.js +1 -1
- package/server/public/app/socket-handlers.js +28 -1
- package/server/public/app/tool-activity-emoji.mjs +4 -3
- package/server/public/app/usage-limit-ui.js +136 -5
- package/server/public/index.html +596 -35
- package/server/relay-tools.md +4 -4
- package/server/repositories/conversation-group-repository.mjs +14 -7
- package/server/repositories/message-repository.mjs +7 -0
- package/server/repositories/session-repository.mjs +8 -1
- package/server/routes/ask-user-routes.mjs +1 -1
- package/server/routes/conversation-group-routes.mjs +3 -2
- package/server/routes/deepseek-usage-import-routes.mjs +72 -0
- package/server/routes/messages-routes.mjs +831 -36
- package/server/routes/relay-ask-routes.mjs +3 -3
- package/server/routes/relay-event-routes.mjs +2 -2
- package/server/routes/relay-schedule-routes.mjs +1 -1
- package/server/routes/remote-relay-routes.mjs +3 -3
- package/server/routes/sessions-routes.mjs +670 -63
- package/server/server-runtime.mjs +476 -26
- package/server/services/about-service.mjs +2 -0
- package/server/services/background-work-view.mjs +2 -1
- package/server/services/claude-api-settings-service.mjs +575 -0
- package/server/services/claude-api-usage-service.mjs +361 -0
- package/server/services/claude-billing-swap-service.mjs +274 -0
- package/server/services/claude-billing-switch-service.mjs +343 -0
- package/server/services/claude-session-leftovers.mjs +226 -0
- package/server/services/deepseek-balance-service.mjs +102 -0
- package/server/services/deepseek-settings-service.mjs +380 -0
- package/server/services/deepseek-usage-import.mjs +471 -0
- package/server/services/model-variant-catalog-service.mjs +1 -0
- package/server/services/plan-usage-claude-api.mjs +445 -0
- package/server/services/plan-usage-contract.mjs +100 -5
- package/server/services/plan-usage-deepseek.mjs +931 -0
- package/server/services/plan-usage-service.mjs +436 -0
- package/server/services/provider-model-selection.mjs +4 -2
- package/server/services/provider-reasoning-effort.mjs +24 -2
- package/server/services/relay-ask-service.mjs +1 -1
- package/server/services/relay-event-service.mjs +3 -2
- package/server/services/relay-schedule-service.mjs +1 -1
- package/server/services/remote-relay-caller-context.mjs +7 -0
- package/server/services/remote-relay-dispatcher.mjs +84 -14
- package/server/services/remote-relay-registry-service.mjs +1 -1
- package/server/services/session-worker-launch-service.mjs +294 -84
- package/server/services/session-worker-registry-service.mjs +28 -1
- package/server/services/session-worker-websocket-service.mjs +17 -0
- package/server/services/usage-limit-pause-service.mjs +55 -4
- package/server/services/worker-secret-redaction.mjs +320 -0
- package/shared/anthropic-api.mjs +202 -0
- package/shared/context-window-fallbacks.mjs +3 -0
- package/shared/deepseek-api.mjs +137 -0
- package/shared/deepseek-models.mjs +114 -0
- package/shared/deepseek-pricing.mjs +146 -0
- package/shared/host-claude-session-env.mjs +101 -0
- package/shared/model-id.mjs +7 -0
- package/shared/preview-tool-core.mjs +1 -1
- package/shared/provider-routing.mjs +5 -3
- package/shared/relay-asks.mjs +11 -5
- package/shared/relay-events.mjs +2 -2
- package/shared/relay-schedules.mjs +2 -2
- package/shared/remote-relay-contract.mjs +29 -9
- package/shared/remote-relay-tool-core.mjs +23 -6
- package/shared/steer-settle-failure.mjs +2 -0
- package/shared/worker-runtime/api-client.mjs +2 -2
- package/shared/worker-runtime/direct-openai-image.mjs +128 -0
- package/shared/worker-runtime/heartbeat.mjs +5 -0
- package/shared/worker-runtime/worker-log-file.mjs +60 -0
- package/shared/worker-runtime/worker-websocket-link.mjs +6 -0
|
@@ -36,7 +36,7 @@ import { createMessageRepository } from './repositories/message-repository.mjs';
|
|
|
36
36
|
import { createQuestionRepository } from './repositories/question-repository.mjs';
|
|
37
37
|
import { createImageConversationRepository } from './repositories/image-conversation-repository.mjs';
|
|
38
38
|
import { registerSessionsRoutes } from './routes/sessions-routes.mjs';
|
|
39
|
-
import { buildDequeuedRelayMessage, dequeuePendingMessageForWorkerLoop, registerMessagesRoutes } from './routes/messages-routes.mjs';
|
|
39
|
+
import { buildDequeuedRelayMessage, claudeApiRemedyText, dequeuePendingMessageForWorkerLoop, registerMessagesRoutes } from './routes/messages-routes.mjs';
|
|
40
40
|
import { registerAskUserRoutes } from './routes/ask-user-routes.mjs';
|
|
41
41
|
import { registerRelayBoardRoutes } from './routes/relay-board-routes.mjs';
|
|
42
42
|
import { registerCacheRoutes } from './routes/cache-routes.mjs';
|
|
@@ -46,6 +46,7 @@ import { createPreviewScopeResolver, registerPreviewRoutes } from './routes/prev
|
|
|
46
46
|
import { registerRemoteRelayRoutes } from './routes/remote-relay-routes.mjs';
|
|
47
47
|
import { registerRelayEventRoutes } from './routes/relay-event-routes.mjs';
|
|
48
48
|
import { registerRelayScheduleRoutes } from './routes/relay-schedule-routes.mjs';
|
|
49
|
+
import { registerDeepSeekUsageImportRoutes } from './routes/deepseek-usage-import-routes.mjs';
|
|
49
50
|
import { registerRelayAskRoutes } from './routes/relay-ask-routes.mjs';
|
|
50
51
|
import { createRemoteRelayRepository } from './repositories/remote-relay-repository.mjs';
|
|
51
52
|
import { createConversationGroupRepository } from './repositories/conversation-group-repository.mjs';
|
|
@@ -64,6 +65,17 @@ import { createClaudeCloudCredentials } from '../shared/claude-cloud/credentials
|
|
|
64
65
|
import { createClaudeCloudClient } from '../shared/claude-cloud/api-client.mjs';
|
|
65
66
|
import { resolveClaudeCloudBaseUrl } from '../shared/claude-cloud/base-url.mjs';
|
|
66
67
|
import { createClaudeCloudSettingsService } from './services/claude-cloud-settings-service.mjs';
|
|
68
|
+
import { createDeepSeekSettingsService } from './services/deepseek-settings-service.mjs';
|
|
69
|
+
import { createWorkerPostRedaction } from './services/worker-secret-redaction.mjs';
|
|
70
|
+
import { createDeepSeekBalanceService } from './services/deepseek-balance-service.mjs';
|
|
71
|
+
import { deepseekCardCurrency } from './services/plan-usage-deepseek.mjs';
|
|
72
|
+
import { createDeepSeekUsageImportService } from './services/deepseek-usage-import.mjs';
|
|
73
|
+
import { createClaudeApiSettingsService } from './services/claude-api-settings-service.mjs';
|
|
74
|
+
import { createClaudeBillingSwitchService, normalizeClaudeBilling } from './services/claude-billing-switch-service.mjs';
|
|
75
|
+
import { createClaudeBillingSwapService } from './services/claude-billing-swap-service.mjs';
|
|
76
|
+
import { createClaudeApiUsageService } from './services/claude-api-usage-service.mjs';
|
|
77
|
+
import { createClaudeApiCapWatch, createClaudeApiPlanUsage, createClaudeApiStopNoteLedger } from './services/plan-usage-claude-api.mjs';
|
|
78
|
+
import { DEEPSEEK_DEFAULT_AUTO_COMPACT_WINDOW, DEFAULT_DEEPSEEK_MODEL, deepseekModelLabel } from '../shared/deepseek-models.mjs';
|
|
67
79
|
import { createClaudeCloudSessionService } from './services/claude-cloud-session-service.mjs';
|
|
68
80
|
import { createClaudeCloudRepoService, listRecentCloudSourcesFromStatements } from './services/claude-cloud-repo-service.mjs';
|
|
69
81
|
import { createClaudeAccountUsageService } from './services/claude-account-usage-service.mjs';
|
|
@@ -135,10 +147,10 @@ import { mergeStrayMigratedUploads } from './services/oar-state-migration-servic
|
|
|
135
147
|
import { createRelayRestartOrchestrator } from './services/relay-restart-orchestrator-service.mjs';
|
|
136
148
|
import { createRelayBridgeOwnerService } from './services/relay-bridge-owner-service.mjs';
|
|
137
149
|
import { createRelayCliLauncherService } from './services/relay-cli-launcher-service.mjs';
|
|
138
|
-
import { createSessionWorkerRegistry } from './services/session-worker-registry-service.mjs';
|
|
150
|
+
import { createSessionWorkerRegistry, normalizeWorkerKind } from './services/session-worker-registry-service.mjs';
|
|
139
151
|
import { createSessionWorkerSupervisor } from './services/session-worker-supervisor-service.mjs';
|
|
140
152
|
import { createSessionWorkerProcessInspector } from './services/session-worker-process-service.mjs';
|
|
141
|
-
import { applyClaudeCloudProviderEnvironment, applyClaudeProviderEnvironment, applyCopilotSdkProviderEnvironment, applyCursorProviderEnvironment, applyGrokProviderEnvironment, applyOpenAIProviderEnvironment, copilotSdkEngineUnavailableReason, killTmuxSession, launchSessionCli } from './services/session-worker-launch-service.mjs';
|
|
153
|
+
import { applyClaudeApiProviderEnvironment, applyClaudeCloudProviderEnvironment, applyClaudeProviderEnvironment, applyCopilotSdkProviderEnvironment, applyCursorProviderEnvironment, applyDeepSeekProviderEnvironment, applyGrokProviderEnvironment, applyOpenAIProviderEnvironment, copilotSdkEngineUnavailableReason, killTmuxSession, launchSessionCli } from './services/session-worker-launch-service.mjs';
|
|
142
154
|
import { createSshTunnelManager } from './services/ssh-tunnel-manager-service.mjs';
|
|
143
155
|
import { createCloudflaredTunnelManager } from './services/cloudflared-tunnel-service.mjs';
|
|
144
156
|
import {
|
|
@@ -1936,6 +1948,7 @@ const runtimeTimers = {
|
|
|
1936
1948
|
pendingWorkerPrime: null,
|
|
1937
1949
|
sharedViewerPrune: null,
|
|
1938
1950
|
restoredTaskSettle: null,
|
|
1951
|
+
claudeApiCapClock: null,
|
|
1939
1952
|
};
|
|
1940
1953
|
|
|
1941
1954
|
function normalizeRemotePath(value) {
|
|
@@ -2830,6 +2843,76 @@ const claudeCloudRepoService = createClaudeCloudRepoService({
|
|
|
2830
2843
|
isEnabled: () => claudeCloudSettingsService.getSettings().enabled === true,
|
|
2831
2844
|
listRecentCloudSources: () => listRecentCloudSourcesFromStatements(stmts),
|
|
2832
2845
|
});
|
|
2846
|
+
// What a session worker posts loses every key a provider saved in Settings
|
|
2847
|
+
// (worker-secret-redaction.mjs). Built here, at the start, so it reads the
|
|
2848
|
+
// saved keys now, and the DeepSeek and Claude API saves below note each key
|
|
2849
|
+
// they write: one
|
|
2850
|
+
// changed before any worker has posted is still redacted.
|
|
2851
|
+
const workerPostRedaction = createWorkerPostRedaction({ readSetting: readAppSettingValue });
|
|
2852
|
+
// DeepSeek: one API key per relay, its models and its endpoint. The key stays
|
|
2853
|
+
// in the settings table; GET routes answer `configured`, never the key.
|
|
2854
|
+
const deepseekSettingsService = createDeepSeekSettingsService({
|
|
2855
|
+
readSetting: readAppSettingValue,
|
|
2856
|
+
writeSetting: (key, value) => {
|
|
2857
|
+
stmts.upsertAppSetting.run(key, String(value), new Date().toISOString());
|
|
2858
|
+
workerPostRedaction.noteSavedSetting(key, value);
|
|
2859
|
+
},
|
|
2860
|
+
deleteSetting: (key) => stmts.deleteAppSetting.run(key),
|
|
2861
|
+
listProviderCandidates: () => stmts.listRuntimeSessionProviderCandidates?.all?.() || [],
|
|
2862
|
+
});
|
|
2863
|
+
function getDeepSeekProviderSettings() {
|
|
2864
|
+
return deepseekSettingsService.getSettings();
|
|
2865
|
+
}
|
|
2866
|
+
// Claude API: Claude chats on a Console API key, one key per relay. The key
|
|
2867
|
+
// stays in the settings table; GET routes answer `configured`, never the key.
|
|
2868
|
+
// Before its own discovery the Claude provider's models stand in.
|
|
2869
|
+
const claudeApiSettingsService = createClaudeApiSettingsService({
|
|
2870
|
+
readSetting: readAppSettingValue,
|
|
2871
|
+
writeSetting: (key, value) => {
|
|
2872
|
+
stmts.upsertAppSetting.run(key, String(value), new Date().toISOString());
|
|
2873
|
+
workerPostRedaction.noteSavedSetting(key, value);
|
|
2874
|
+
},
|
|
2875
|
+
deleteSetting: (key) => stmts.deleteAppSetting.run(key),
|
|
2876
|
+
listProviderCandidates: () => stmts.listRuntimeSessionProviderCandidates?.all?.() || [],
|
|
2877
|
+
countLiveKeyWorkers: () => (sessionWorkerRegistry?.listWorkers?.() || []).filter((worker) => worker?.kind === 'claude-api').length,
|
|
2878
|
+
getClaudeProviderSettings: () => getClaudeProviderSettings(),
|
|
2879
|
+
});
|
|
2880
|
+
function getClaudeApiProviderSettings() {
|
|
2881
|
+
return claudeApiSettingsService.getSettings();
|
|
2882
|
+
}
|
|
2883
|
+
// The DeepSeek balance for Check Usage: read with the saved key only while
|
|
2884
|
+
// DeepSeek is on, kept for a minute.
|
|
2885
|
+
const deepseekBalanceService = createDeepSeekBalanceService({ getSettings: getDeepSeekProviderSettings });
|
|
2886
|
+
// DeepSeek's own usage export, imported in Settings (or with
|
|
2887
|
+
// `npm run deepseek:import`): the usage card shows its figures for the days
|
|
2888
|
+
// it covers, for the saved key only, and the estimate after.
|
|
2889
|
+
const deepseekUsageImportService = createDeepSeekUsageImportService({
|
|
2890
|
+
db,
|
|
2891
|
+
getApiKey: () => getDeepSeekProviderSettings()?.apiKey || '',
|
|
2892
|
+
});
|
|
2893
|
+
// Where DeepSeek workers keep their Claude Code state (sessions, transcripts,
|
|
2894
|
+
// settings): a directory of the relay's own, so no Claude login is in reach
|
|
2895
|
+
// of a CLI that talks to DeepSeek and their sessions stay out of Claude's.
|
|
2896
|
+
const DEEPSEEK_CLAUDE_CONFIG_DIR = path.join(DATA_DIR, 'deepseek-claude');
|
|
2897
|
+
|
|
2898
|
+
/**
|
|
2899
|
+
* What a DeepSeek session's commits and PRs say: the DeepSeek tab's mode
|
|
2900
|
+
* (OAR or off), the folder override on top, the DeepSeek model's name in the
|
|
2901
|
+
* trailer. A folder set to vanilla gets OAR's: Claude Code's own lines would
|
|
2902
|
+
* credit Claude for DeepSeek's work.
|
|
2903
|
+
*/
|
|
2904
|
+
function deepseekAttributionForConversation(conv, modelId) {
|
|
2905
|
+
const root = String(conv?.runtime_workspace_root_path || conv?.configured_workspace_root_path || '').trim()
|
|
2906
|
+
|| currentWorkspaceRootPath();
|
|
2907
|
+
const resolved = resolveClaudeAttributionMode({
|
|
2908
|
+
providerMode: getDeepSeekProviderSettings().attribution,
|
|
2909
|
+
folderMode: workspaceRootAttributionMode(root),
|
|
2910
|
+
});
|
|
2911
|
+
return buildClaudeAttributionSettings({
|
|
2912
|
+
mode: resolved === 'vanilla' ? 'oar' : resolved,
|
|
2913
|
+
modelLabel: deepseekModelLabel(modelId),
|
|
2914
|
+
});
|
|
2915
|
+
}
|
|
2833
2916
|
// The account's live usage for the Check Usage modal. The Claude Cloud switch
|
|
2834
2917
|
// is the consent to use the Claude CLI's login, so nothing is read while it
|
|
2835
2918
|
// is off.
|
|
@@ -3058,7 +3141,7 @@ function buildSessionWorkerLaunchEnvForSession(targetSessionId) {
|
|
|
3058
3141
|
const providerType = String(runtimeSession?.provider_type || 'github').trim().toLowerCase();
|
|
3059
3142
|
// Clear every provider's variables first (the appliers' kind-guarded deletes
|
|
3060
3143
|
// keep the chained clears order-independent), then apply only the bound one.
|
|
3061
|
-
const cleared = applyClaudeCloudProviderEnvironment(applyCopilotSdkProviderEnvironment(applyGrokProviderEnvironment(applyCursorProviderEnvironment(applyClaudeProviderEnvironment(applyOpenAIProviderEnvironment(sessionWorkerLaunchEnv))))));
|
|
3144
|
+
const cleared = applyClaudeApiProviderEnvironment(applyDeepSeekProviderEnvironment(applyClaudeCloudProviderEnvironment(applyCopilotSdkProviderEnvironment(applyGrokProviderEnvironment(applyCursorProviderEnvironment(applyClaudeProviderEnvironment(applyOpenAIProviderEnvironment(sessionWorkerLaunchEnv))))))));
|
|
3062
3145
|
// A Claude Cloud conversation gets its own worker and nothing else: it must
|
|
3063
3146
|
// never reach the Copilot CLI fall-through at the end of this function. The
|
|
3064
3147
|
// environment names the worker and the model, no credential.
|
|
@@ -3088,6 +3171,32 @@ function buildSessionWorkerLaunchEnvForSession(targetSessionId) {
|
|
|
3088
3171
|
dataDir: DATA_DIR,
|
|
3089
3172
|
});
|
|
3090
3173
|
}
|
|
3174
|
+
// A DeepSeek conversation gets its own worker and must never reach the
|
|
3175
|
+
// Copilot fall-through below. Without a key the applier throws, and the
|
|
3176
|
+
// conversation gets the launch note.
|
|
3177
|
+
if (providerType === 'deepseek') {
|
|
3178
|
+
const deepseekSettings = getDeepSeekProviderSettings();
|
|
3179
|
+
return applyDeepSeekProviderEnvironment(cleared, {
|
|
3180
|
+
enabled: true,
|
|
3181
|
+
model: String(runtimeSession?.provider_model || runtimeSession?.model || deepseekSettings.model).trim(),
|
|
3182
|
+
apiKey: deepseekSettings.apiKey,
|
|
3183
|
+
baseUrl: deepseekSettings.baseUrl,
|
|
3184
|
+
configDir: DEEPSEEK_CLAUDE_CONFIG_DIR,
|
|
3185
|
+
});
|
|
3186
|
+
}
|
|
3187
|
+
// A Claude API conversation gets its own worker, never the Copilot
|
|
3188
|
+
// fall-through below nor a Claude one on the host's subscription login.
|
|
3189
|
+
// Without a key the applier throws, and the conversation gets the launch
|
|
3190
|
+
// note. No config dir of its own: the chat is a Claude chat in the host's
|
|
3191
|
+
// Claude Code state, billed to the key.
|
|
3192
|
+
if (providerType === 'claude-api') {
|
|
3193
|
+
const claudeApiSettings = getClaudeApiProviderSettings();
|
|
3194
|
+
return applyClaudeApiProviderEnvironment(cleared, {
|
|
3195
|
+
enabled: true,
|
|
3196
|
+
model: String(runtimeSession?.provider_model || runtimeSession?.model || claudeApiSettings.model).trim(),
|
|
3197
|
+
apiKey: claudeApiSettings.apiKey,
|
|
3198
|
+
});
|
|
3199
|
+
}
|
|
3091
3200
|
if (providerType === 'grok') {
|
|
3092
3201
|
const grokSettings = getGrokProviderSettings();
|
|
3093
3202
|
const grokModel = String(runtimeSession?.provider_model || runtimeSession?.model || grokSettings.model).trim();
|
|
@@ -3316,13 +3425,16 @@ async function stopSessionWorkerForProviderRebind(sdkSessionId) {
|
|
|
3316
3425
|
|
|
3317
3426
|
async function reconcileUnstartedConversationProviders({ enabled, model, provider = 'openai' } = {}) {
|
|
3318
3427
|
const normalizedProvider = String(provider || 'openai').trim().toLowerCase();
|
|
3319
|
-
const managedProvider = ['claude', 'cursor', 'grok'].includes(normalizedProvider) ? normalizedProvider : 'openai';
|
|
3320
|
-
const managedDefaultModel =
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3428
|
+
const managedProvider = ['claude', 'cursor', 'grok', 'deepseek', 'claude-api'].includes(normalizedProvider) ? normalizedProvider : 'openai';
|
|
3429
|
+
const managedDefaultModel = managedProvider === 'claude-api'
|
|
3430
|
+
? getClaudeApiProviderSettings().model
|
|
3431
|
+
: {
|
|
3432
|
+
openai: DEFAULT_OPENAI_MODEL,
|
|
3433
|
+
claude: DEFAULT_CLAUDE_MODEL,
|
|
3434
|
+
cursor: DEFAULT_CURSOR_MODEL,
|
|
3435
|
+
grok: DEFAULT_GROK_MODEL,
|
|
3436
|
+
deepseek: DEFAULT_DEEPSEEK_MODEL,
|
|
3437
|
+
}[managedProvider];
|
|
3326
3438
|
const providerType = enabled === true ? managedProvider : 'github';
|
|
3327
3439
|
const providerModel = enabled === true
|
|
3328
3440
|
? (String(model || '').trim() || managedDefaultModel)
|
|
@@ -3395,7 +3507,11 @@ async function reconcileUnstartedConversationProviders({ enabled, model, provide
|
|
|
3395
3507
|
? getCursorProviderSettings().configured === true
|
|
3396
3508
|
: (currentProvider === 'grok'
|
|
3397
3509
|
? getGrokProviderSettings().enabled === true
|
|
3398
|
-
: (currentProvider
|
|
3510
|
+
: (currentProvider === 'deepseek'
|
|
3511
|
+
? getDeepSeekProviderSettings().configured === true
|
|
3512
|
+
: (currentProvider === 'claude-api'
|
|
3513
|
+
? getClaudeApiProviderSettings().enabled === true
|
|
3514
|
+
: (currentProvider !== 'openai' || getOpenAIProviderSettings().configured === true)))));
|
|
3399
3515
|
if (providerWasUpdated && canRestorePreviousProvider) {
|
|
3400
3516
|
stmts.updateRuntimeSessionProvider.run(
|
|
3401
3517
|
currentProvider,
|
|
@@ -5130,7 +5246,18 @@ function createCompactedConversation(sourceConversationId) {
|
|
|
5130
5246
|
// The new conversation stays in the old one's session group, and heads
|
|
5131
5247
|
// it if the old one did; the archived source stays a member.
|
|
5132
5248
|
conversationGroups.carryToCompacted(sourceConversationId, targetConversationId, now);
|
|
5133
|
-
|
|
5249
|
+
// A Claude chat keeps its billing (the subscription or the API key, a
|
|
5250
|
+
// pending swap's included): the new chat's first worker launches on it,
|
|
5251
|
+
// and it is never guessed again from a Claude model id. Other providers
|
|
5252
|
+
// bind as before.
|
|
5253
|
+
const sourceRuntime = stmts.getRuntimeSessionByConversation.get(sourceConversationId) || null;
|
|
5254
|
+
const sourceBilling = normalizeClaudeBilling(sourceRuntime?.provider_type);
|
|
5255
|
+
targetRuntimeSession = ensureRuntimeSessionBinding(targetConversationId, null, now, null, sourceBilling
|
|
5256
|
+
? {
|
|
5257
|
+
providerType: sourceBilling,
|
|
5258
|
+
providerModel: String(sourceRuntime?.provider_model || sourceRuntime?.model || '').trim() || null,
|
|
5259
|
+
}
|
|
5260
|
+
: {});
|
|
5134
5261
|
});
|
|
5135
5262
|
tx();
|
|
5136
5263
|
// Its schedules go on in the new conversation, both the ones that fire
|
|
@@ -5416,6 +5543,67 @@ const usageLimitPauseService = createUsageLimitPauseService({
|
|
|
5416
5543
|
logger: console,
|
|
5417
5544
|
});
|
|
5418
5545
|
|
|
5546
|
+
// What turns on the saved Claude API key cost, booked from both Claude
|
|
5547
|
+
// workers' reports (services/claude-api-usage-service.mjs); the Claude API
|
|
5548
|
+
// card and its cap read it.
|
|
5549
|
+
const claudeApiUsageService = createClaudeApiUsageService({ db, logger: console });
|
|
5550
|
+
// This month's spend on the key against the cap, and the Check Usage card
|
|
5551
|
+
// (services/plan-usage-claude-api.mjs), over the same local days.
|
|
5552
|
+
const claudeApiPlanUsage = createClaudeApiPlanUsage({
|
|
5553
|
+
usageService: claudeApiUsageService,
|
|
5554
|
+
getSettings: () => getClaudeApiProviderSettings(),
|
|
5555
|
+
});
|
|
5556
|
+
|
|
5557
|
+
// A Claude chat's billing: the subscription or the saved API key, switched
|
|
5558
|
+
// between turns (services/claude-billing-switch-service.mjs). A switch to the
|
|
5559
|
+
// key releases a turn held at the subscription's usage limit.
|
|
5560
|
+
const claudeBillingSwitchService = createClaudeBillingSwitchService({
|
|
5561
|
+
db,
|
|
5562
|
+
stmts,
|
|
5563
|
+
emit: (event, payload) => io.emit(event, payload),
|
|
5564
|
+
getClaudeProviderSettings: () => getClaudeProviderSettings(),
|
|
5565
|
+
getClaudeApiProviderSettings: () => getClaudeApiProviderSettings(),
|
|
5566
|
+
getClaudeApiCapState: () => getClaudeApiCapState(),
|
|
5567
|
+
releaseUsageLimitPause: (args) => usageLimitPauseService.releaseForBillingSwitch(args),
|
|
5568
|
+
// What the swap waits for: a turn, a question, a compaction, background tasks.
|
|
5569
|
+
getPending: (runtime) => claudeBillingSwapService.pendingFor(runtime?.sdk_session_id || runtime?.conversation_id),
|
|
5570
|
+
onQueueChanged: (reason) => {
|
|
5571
|
+
void primePendingSessionWorkers(reason);
|
|
5572
|
+
sessionWorkerWebSocketService?.emitQueueChanged?.(reason);
|
|
5573
|
+
},
|
|
5574
|
+
logger: console,
|
|
5575
|
+
});
|
|
5576
|
+
|
|
5577
|
+
// When the Claude API cap flips (a booked report, the cap setting, a new
|
|
5578
|
+
// month), every open Claude chat's billing state is announced again: the
|
|
5579
|
+
// composer's toggle and the usage-limit banner's switch button follow it.
|
|
5580
|
+
const listOpenClaudeChats = db.prepare(`
|
|
5581
|
+
SELECT rs.conversation_id AS id FROM runtime_sessions rs
|
|
5582
|
+
JOIN conversations c ON c.id = rs.conversation_id
|
|
5583
|
+
WHERE rs.provider_type IN ('claude', 'claude-api') AND COALESCE(c.archived, 0) = 0
|
|
5584
|
+
AND COALESCE(c.status, '') != 'deleted'
|
|
5585
|
+
`);
|
|
5586
|
+
// The notes for self-started turns stopped on the key, once per chat per
|
|
5587
|
+
// episode: a cap that clears ends every chat's episode (the settings route
|
|
5588
|
+
// clears it on a save, a claimed turn on the key for its chat).
|
|
5589
|
+
const claudeApiStopLedger = createClaudeApiStopNoteLedger();
|
|
5590
|
+
const claudeApiCapWatch = createClaudeApiCapWatch({
|
|
5591
|
+
getCapState: () => getClaudeApiCapState(),
|
|
5592
|
+
onChange: ({ reached, reason }) => {
|
|
5593
|
+
if (!reached) claudeApiStopLedger.clearAll();
|
|
5594
|
+
const chats = listOpenClaudeChats.all();
|
|
5595
|
+
console.log(`[claude-api-cap] ${reached ? 'reached' : 'cleared'} (${reason}): announcing ${chats.length} Claude chat(s)`);
|
|
5596
|
+
for (const { id } of chats) {
|
|
5597
|
+
const billing = claudeBillingSwitchService.billingState(id);
|
|
5598
|
+
if (billing) io.emit('conversation_billing', { conversationId: id, billing });
|
|
5599
|
+
}
|
|
5600
|
+
},
|
|
5601
|
+
logger: console,
|
|
5602
|
+
});
|
|
5603
|
+
claudeApiCapWatch.check('boot');
|
|
5604
|
+
runtimeTimers.claudeApiCapClock = setInterval(() => claudeApiCapWatch.check('clock'), 5 * 60 * 1000);
|
|
5605
|
+
runtimeTimers.claudeApiCapClock.unref?.();
|
|
5606
|
+
|
|
5419
5607
|
// Turns a restart cut off for good (their worker went with the relay, and a
|
|
5420
5608
|
// second delivery would run the prompt twice): with the setting on, their
|
|
5421
5609
|
// session gets one note to carry on. The rows processing right now are the
|
|
@@ -5469,7 +5657,7 @@ relayEventService = createRelayEventService({
|
|
|
5469
5657
|
});
|
|
5470
5658
|
|
|
5471
5659
|
// ─── Relay schedules ──────────────────────────────────────────────────────────
|
|
5472
|
-
// Prompts agents scheduled with
|
|
5660
|
+
// Prompts agents scheduled with relay_tool's `schedule`: kept in the
|
|
5473
5661
|
// database, their timers rebuilt at boot (services/relay-schedule-service.mjs).
|
|
5474
5662
|
const relayScheduleService = createRelayScheduleService({
|
|
5475
5663
|
db,
|
|
@@ -5487,7 +5675,7 @@ const relayScheduleService = createRelayScheduleService({
|
|
|
5487
5675
|
});
|
|
5488
5676
|
|
|
5489
5677
|
// ─── Questions that don't block ───────────────────────────────────────────────
|
|
5490
|
-
//
|
|
5678
|
+
// relay_tool's `ask`: a card in the session and the Inbox; the answer
|
|
5491
5679
|
// reaches the askers as a relay event (services/relay-ask-service.mjs).
|
|
5492
5680
|
const relayAskService = createRelayAskService({
|
|
5493
5681
|
db,
|
|
@@ -5984,6 +6172,14 @@ const readContextFromSessionEvents = contextSnapshotService.readContextFromSessi
|
|
|
5984
6172
|
// Claude-provider sessions live under the Agent SDK's project layout rather than
|
|
5985
6173
|
// ~/.copilot/session-state, so their browsable session folder resolves separately.
|
|
5986
6174
|
const claudeSessionRootResolver = createClaudeSessionRootResolver({ fs, path });
|
|
6175
|
+
// DeepSeek chats run Claude Code with a config dir of their own: only that
|
|
6176
|
+
// root, never the host's ~/.claude.
|
|
6177
|
+
const deepseekSessionRootResolver = createClaudeSessionRootResolver({
|
|
6178
|
+
fs,
|
|
6179
|
+
path,
|
|
6180
|
+
env: { CLAUDE_CONFIG_DIR: DEEPSEEK_CLAUDE_CONFIG_DIR },
|
|
6181
|
+
homedir: () => '',
|
|
6182
|
+
});
|
|
5987
6183
|
const cursorSessionRootResolver = createCursorSessionRootResolver({ fs, path, serverDir: __dirname, dataDir: DATA_DIR });
|
|
5988
6184
|
|
|
5989
6185
|
// ─── Express + Socket.io Setup ────────────────────────────────────────────────
|
|
@@ -6226,7 +6422,7 @@ function markSharedViewerPresence({ conversationId, token, viewerId } = {}) {
|
|
|
6226
6422
|
}
|
|
6227
6423
|
return { ok: true, watcherCount, capped: false };
|
|
6228
6424
|
}
|
|
6229
|
-
async function requestSessionWorkerSocketDelivery({ sessionId, pid, reason = 'worker-ready' } = {}) {
|
|
6425
|
+
async function requestSessionWorkerSocketDelivery({ sessionId, pid, reason = 'worker-ready', workerKind = null } = {}) {
|
|
6230
6426
|
const requesterSessionId = String(sessionId || '').trim();
|
|
6231
6427
|
if (!requesterSessionId) return { message: null, reason: 'missing-session-id' };
|
|
6232
6428
|
touchCli();
|
|
@@ -6284,6 +6480,10 @@ async function requestSessionWorkerSocketDelivery({ sessionId, pid, reason = 'wo
|
|
|
6284
6480
|
|
|
6285
6481
|
const counts = queueCounts();
|
|
6286
6482
|
const existingWorker = sessionWorkerRegistry?.getWorker?.(requesterSessionId) || null;
|
|
6483
|
+
// The kind the socket's worker stated is registered with it, so what a
|
|
6484
|
+
// response ran on is known from the first turn on (not from its first
|
|
6485
|
+
// heartbeat).
|
|
6486
|
+
const statedKind = normalizeWorkerKind(workerKind);
|
|
6287
6487
|
if (!existingWorker) {
|
|
6288
6488
|
sessionWorkerRegistry?.upsertWorker?.({
|
|
6289
6489
|
sdkSessionId: requesterSessionId,
|
|
@@ -6291,6 +6491,7 @@ async function requestSessionWorkerSocketDelivery({ sessionId, pid, reason = 'wo
|
|
|
6291
6491
|
status: 'ready',
|
|
6292
6492
|
pid: pid || null,
|
|
6293
6493
|
queueDepth: Math.max(0, Number(counts.pendingCount || 0)),
|
|
6494
|
+
...(statedKind ? { kind: statedKind } : {}),
|
|
6294
6495
|
});
|
|
6295
6496
|
} else if (pid && existingWorker.pid !== pid) {
|
|
6296
6497
|
sessionWorkerRegistry?.upsertWorker?.({
|
|
@@ -6299,7 +6500,10 @@ async function requestSessionWorkerSocketDelivery({ sessionId, pid, reason = 'wo
|
|
|
6299
6500
|
pid,
|
|
6300
6501
|
status: existingWorker.status || 'ready',
|
|
6301
6502
|
queueDepth: Math.max(0, Number(counts.pendingCount || 0)),
|
|
6503
|
+
kind: statedKind,
|
|
6302
6504
|
});
|
|
6505
|
+
} else if (statedKind && existingWorker.kind !== statedKind) {
|
|
6506
|
+
sessionWorkerRegistry?.upsertWorker?.({ ...existingWorker, kind: statedKind });
|
|
6303
6507
|
}
|
|
6304
6508
|
|
|
6305
6509
|
const dequeueResult = await dequeuePendingMessageForWorkerLoop({
|
|
@@ -6313,6 +6517,11 @@ async function requestSessionWorkerSocketDelivery({ sessionId, pid, reason = 'wo
|
|
|
6313
6517
|
sessionWorkerSupervisor,
|
|
6314
6518
|
relayRestartOrchestrator,
|
|
6315
6519
|
inFlightProcessingCount: Number(counts.processingCount || 0),
|
|
6520
|
+
// A Claude chat's row goes only to a worker of its billing: the socket's
|
|
6521
|
+
// own word, else what the worker last said in a heartbeat.
|
|
6522
|
+
workerKind: workerKind || sessionWorkerRegistry?.getWorker?.(requesterSessionId)?.kind || null,
|
|
6523
|
+
claudeApiClaimCheck,
|
|
6524
|
+
onBillingFence: handleClaudeBillingFence,
|
|
6316
6525
|
});
|
|
6317
6526
|
if (!dequeueResult?.message) {
|
|
6318
6527
|
return {
|
|
@@ -6356,11 +6565,14 @@ async function requestSessionWorkerSocketDelivery({ sessionId, pid, reason = 'wo
|
|
|
6356
6565
|
}
|
|
6357
6566
|
console.log(`${runtimeLogPrefix()}WS DEQUEUE ${out.id.slice(0, 8)} session=${requesterSessionId.slice(0, 8)} reason=${String(reason || 'worker-ready')}`);
|
|
6358
6567
|
io.emit('message_status', { messageId: out.id, conversationId: out.conversationId, status: 'processing' });
|
|
6568
|
+
// The first message after a billing swap tells the new worker so.
|
|
6569
|
+
const billingSwap = out?.ownerSessionId ? claudeBillingSwapService.takeSwapNote(out.ownerSessionId) : null;
|
|
6359
6570
|
return {
|
|
6360
6571
|
message: out,
|
|
6361
6572
|
// Piggybacked settings so workers pick up slider changes on the next
|
|
6362
6573
|
// delivery without a restart.
|
|
6363
6574
|
settings: {
|
|
6575
|
+
...(billingSwap ? { billingSwap } : {}),
|
|
6364
6576
|
backgroundTaskTimeoutMs: backgroundTaskTimeoutMinutesToMs(getBackgroundTaskTimeoutMinutes()),
|
|
6365
6577
|
// The user's max-turn-duration ceiling (0 = no limit): worker-local
|
|
6366
6578
|
// watchdogs (Grok's prompt ceiling) must honor it rather than imposing
|
|
@@ -6371,8 +6583,12 @@ async function requestSessionWorkerSocketDelivery({ sessionId, pid, reason = 'wo
|
|
|
6371
6583
|
// without a new queue column or push channel. null = Auto.
|
|
6372
6584
|
...((() => {
|
|
6373
6585
|
const conv = out.conversationId ? stmts.getConvAnyStatus.get(out.conversationId) : null;
|
|
6586
|
+
const deliveredProvider = String(out.providerType || '').trim().toLowerCase();
|
|
6374
6587
|
return {
|
|
6375
|
-
|
|
6588
|
+
// DeepSeek's window is 1M; its sessions compact at 400K unless the
|
|
6589
|
+
// conversation pins another window.
|
|
6590
|
+
autoCompactWindow: parseAutoCompactWindow(conv?.auto_compact_window)
|
|
6591
|
+
?? (deliveredProvider === 'deepseek' ? DEEPSEEK_DEFAULT_AUTO_COMPACT_WINDOW : null),
|
|
6376
6592
|
// Per-conversation thinking control, same delivery scoping. An
|
|
6377
6593
|
// explicit null on thinkingEnabled means Host default — the worker
|
|
6378
6594
|
// distinguishes it from an absent key (an older relay), so both
|
|
@@ -6384,16 +6600,26 @@ async function requestSessionWorkerSocketDelivery({ sessionId, pid, reason = 'wo
|
|
|
6384
6600
|
// attribution. Always present on such a delivery, so the worker can
|
|
6385
6601
|
// tell "left as it was" (absent) from "vanilla" (null).
|
|
6386
6602
|
...((() => {
|
|
6387
|
-
|
|
6388
|
-
|
|
6603
|
+
// A DeepSeek session runs Claude Code on the host like a Claude
|
|
6604
|
+
// one: the same two settings, with DeepSeek's attribution.
|
|
6605
|
+
if (deliveredProvider === 'deepseek') {
|
|
6606
|
+
return {
|
|
6607
|
+
attribution: deepseekAttributionForConversation(conv, out.providerModel || out.model),
|
|
6608
|
+
claudeBackgroundCommandLimitMs: claudeBackgroundCommandLimitMinutesToMs(getClaudeBackgroundCommandLimitMinutes()),
|
|
6609
|
+
};
|
|
6610
|
+
}
|
|
6611
|
+
// A Claude API session is a Claude session on another bill:
|
|
6612
|
+
// Claude's attribution and background command limit.
|
|
6613
|
+
const localClaude = deliveredProvider === 'claude' || deliveredProvider === 'claude-api';
|
|
6614
|
+
if (!localClaude && deliveredProvider !== 'claude-cloud') return {};
|
|
6389
6615
|
return {
|
|
6390
6616
|
attribution: claudeAttributionForConversation(conv, out.providerModel || out.model, {
|
|
6391
|
-
relayFolderFallback:
|
|
6617
|
+
relayFolderFallback: localClaude,
|
|
6392
6618
|
}),
|
|
6393
6619
|
// The Claude tab's background command limit; the worker
|
|
6394
6620
|
// hands it to the CLI it spawns next. Claude Cloud runs no
|
|
6395
6621
|
// local CLI, so it does not get one.
|
|
6396
|
-
...(
|
|
6622
|
+
...(localClaude
|
|
6397
6623
|
? { claudeBackgroundCommandLimitMs: claudeBackgroundCommandLimitMinutesToMs(getClaudeBackgroundCommandLimitMinutes()) }
|
|
6398
6624
|
: {}),
|
|
6399
6625
|
};
|
|
@@ -6605,6 +6831,177 @@ async function sweepDeadWorkerProcessingRowsOnce() {
|
|
|
6605
6831
|
return recovered;
|
|
6606
6832
|
}
|
|
6607
6833
|
|
|
6834
|
+
// The Claude billing switch's worker swap (services/
|
|
6835
|
+
// claude-billing-swap-service.mjs): a Claude chat's next row of the other
|
|
6836
|
+
// billing is refused to the worker it has, which is replaced, once idle, by
|
|
6837
|
+
// one of the row's kind. The old one is stopped like the kill route does, and
|
|
6838
|
+
// the new one launched only once every process of the session is gone.
|
|
6839
|
+
const claudeBillingSwapService = createClaudeBillingSwapService({
|
|
6840
|
+
getWorker: (sid) => sessionWorkerRegistry?.getWorker?.(sid) || null,
|
|
6841
|
+
// A row the session claimed and has not finished: never stopped under it.
|
|
6842
|
+
hasRunningTurn: (sid) => (stmts.listProcessingRowsForOwner?.all?.(sid) || []).length > 0,
|
|
6843
|
+
stopWorker: async (sid) => {
|
|
6844
|
+
const worker = sessionWorkerRegistry?.getWorker?.(sid) || null;
|
|
6845
|
+
// Nothing may start the session while it is being stopped: a launch then
|
|
6846
|
+
// would adopt the old process, kind and all.
|
|
6847
|
+
sessionWorkerSupervisor?.markKilled?.(sid);
|
|
6848
|
+
// A stop that fails backs off (the swap service retries it): the killed
|
|
6849
|
+
// marker goes with it, so the session's worker, alive or relaunched,
|
|
6850
|
+
// keeps asking for work meanwhile. The fence still keeps the other
|
|
6851
|
+
// billing's rows from it.
|
|
6852
|
+
const backOff = (error, remainingPids = []) => {
|
|
6853
|
+
sessionWorkerSupervisor?.clearRestartSchedule?.(sid, { resetKilledMarker: true });
|
|
6854
|
+
console.warn(`${runtimeLogPrefix()}BILLING SWAP STOP FAILED session=${sid.slice(0, 8)} ${error}${remainingPids.length ? ` remaining=${remainingPids.join(',')}` : ''} — kill marker cleared, retrying with a backoff`);
|
|
6855
|
+
return { ok: false, error, remainingPids };
|
|
6856
|
+
};
|
|
6857
|
+
let stopped = null;
|
|
6858
|
+
try {
|
|
6859
|
+
await sessionWorkerSupervisor?.cancelPendingStart?.(sid, { wait: true });
|
|
6860
|
+
stopped = await stopSessionWorkerProcesses({
|
|
6861
|
+
sdkSessionId: sid,
|
|
6862
|
+
worker,
|
|
6863
|
+
processInspector: sessionWorkerProcessInspector,
|
|
6864
|
+
isPidAliveImpl: isProcessAlive,
|
|
6865
|
+
killTmuxSessionImpl: killTmuxSession,
|
|
6866
|
+
});
|
|
6867
|
+
} catch (error) {
|
|
6868
|
+
return backOff(String(error?.message || error || 'worker-stop-failed'));
|
|
6869
|
+
}
|
|
6870
|
+
if (!stopped.ok) return backOff(stopped.error || 'worker-survived-stop', stopped.remainingPids || []);
|
|
6871
|
+
sessionWorkerRegistry?.removeWorker?.(sid);
|
|
6872
|
+
sessionWorkerSupervisor?.resetHealth?.(sid, { clearFailureCount: false });
|
|
6873
|
+
return { ok: true };
|
|
6874
|
+
},
|
|
6875
|
+
launchWorker: async (sid) => {
|
|
6876
|
+
sessionWorkerSupervisor?.clearRestartSchedule?.(sid, { resetKilledMarker: true });
|
|
6877
|
+
const launched = await sessionWorkerSupervisor?.ensureWorker?.(sid, { allowProcessReuse: false });
|
|
6878
|
+
return { ok: launched?.ok === true, error: launched?.error || null };
|
|
6879
|
+
},
|
|
6880
|
+
onStuck: ({ conversationId, rowBilling, attempts, reason }) => {
|
|
6881
|
+
if (!conversationId) return;
|
|
6882
|
+
const at = new Date().toISOString();
|
|
6883
|
+
const messageId = uuidv4();
|
|
6884
|
+
const cause = reason === 'probe'
|
|
6885
|
+
? `the relay cannot tell whether the old Claude process still runs a turn (${attempts} tries)`
|
|
6886
|
+
: `the old Claude process did not stop (${attempts} tries)`;
|
|
6887
|
+
const text = `⚠️ Could not restart the session on the ${rowBilling === 'claude-api' ? 'Claude API key' : 'Claude subscription'}: `
|
|
6888
|
+
+ `${cause}. The relay keeps trying; switching the billing back runs the queued messages on the old process.`;
|
|
6889
|
+
try {
|
|
6890
|
+
db.transaction(() => {
|
|
6891
|
+
stmts.insertMsg.run(messageId, conversationId, 'assistant', text, null, null, null, at, null, null, null);
|
|
6892
|
+
stmts.setMessageKind?.run?.('billing-switch-stuck', messageId);
|
|
6893
|
+
stmts.updateConvTime?.run?.(at, conversationId);
|
|
6894
|
+
})();
|
|
6895
|
+
io.emit('assistant_message', {
|
|
6896
|
+
conversationId,
|
|
6897
|
+
messageId,
|
|
6898
|
+
message: { id: messageId, role: 'assistant', text, kind: 'billing-switch-stuck', model: null, mode: null, attachments: [], timestamp: at },
|
|
6899
|
+
});
|
|
6900
|
+
} catch (error) {
|
|
6901
|
+
console.warn(`[claude-billing] stuck note failed: ${error?.message || error}`);
|
|
6902
|
+
}
|
|
6903
|
+
},
|
|
6904
|
+
onPendingChange: ({ conversationId }) => {
|
|
6905
|
+
if (!conversationId) return;
|
|
6906
|
+
io.emit('conversation_billing', { conversationId, billing: claudeBillingSwitchService.billingState(conversationId) });
|
|
6907
|
+
},
|
|
6908
|
+
logger: console,
|
|
6909
|
+
});
|
|
6910
|
+
|
|
6911
|
+
// The Claude API monthly cap (plan decision 12): `{ reached, error? }`. The
|
|
6912
|
+
// one seam the claim check below, the billing switch, an agent's send and a
|
|
6913
|
+
// new Claude API chat ask: this month's priced estimate in
|
|
6914
|
+
// claude_api_usage_totals (relay local days, claudeApiUsageService.localDay)
|
|
6915
|
+
// against claude_api_monthly_cap_usd (services/plan-usage-claude-api.mjs).
|
|
6916
|
+
// It throws when the totals cannot be read; every caller counts that as reached.
|
|
6917
|
+
function getClaudeApiCapState() {
|
|
6918
|
+
return claudeApiPlanUsage.getCapState();
|
|
6919
|
+
}
|
|
6920
|
+
|
|
6921
|
+
// A Claude API row must not run while the provider is off, has no key, or
|
|
6922
|
+
// this month's cap is reached (getClaudeApiCapState): it is failed closed at
|
|
6923
|
+
// the claim, with the way out in its note.
|
|
6924
|
+
function claudeApiClaimCheck() {
|
|
6925
|
+
const settings = getClaudeApiProviderSettings();
|
|
6926
|
+
if (settings?.configured !== true) {
|
|
6927
|
+
return { code: 'CLAUDE_API_NOT_CONFIGURED', error: 'Claude API has no key, so this turn did not run.' };
|
|
6928
|
+
}
|
|
6929
|
+
if (settings?.enabled !== true) {
|
|
6930
|
+
return { code: 'CLAUDE_API_NOT_CONFIGURED', error: 'Claude API is switched off, so this turn did not run.', switchedOff: true };
|
|
6931
|
+
}
|
|
6932
|
+
let cap = null;
|
|
6933
|
+
try { cap = getClaudeApiCapState() || null; } catch { cap = { reached: true }; }
|
|
6934
|
+
if (cap?.reached === true) {
|
|
6935
|
+
return {
|
|
6936
|
+
code: 'CLAUDE_API_CAP_REACHED',
|
|
6937
|
+
error: `${String(cap.error || '').trim() || 'This month\'s Claude API spend has reached its cap.'} This turn did not run.`,
|
|
6938
|
+
};
|
|
6939
|
+
}
|
|
6940
|
+
return null;
|
|
6941
|
+
}
|
|
6942
|
+
|
|
6943
|
+
// A Claude API chat's worker is not launched while Claude API cannot run a
|
|
6944
|
+
// turn (no key, switched off, the cap): its waiting rows on the key are
|
|
6945
|
+
// failed closed here, as the claim would, instead of a spawn that could only
|
|
6946
|
+
// refuse them. True when it was so.
|
|
6947
|
+
const listPendingClaudeApiRowsForOwner = db.prepare(`
|
|
6948
|
+
SELECT id FROM queue
|
|
6949
|
+
WHERE status = 'pending' AND NULLIF(owner_sdk_session_id, '') = ?
|
|
6950
|
+
AND COALESCE(kind, '') != 'continuation'
|
|
6951
|
+
AND LOWER(COALESCE(${stmts.setQueueProvider ? 'provider' : 'NULL'}, ?)) = 'claude-api'
|
|
6952
|
+
`);
|
|
6953
|
+
function failClaudeApiRowsWithoutLaunch(sdkSessionId) {
|
|
6954
|
+
const runtime = stmts.getRuntimeSessionBySdkSessionId.get(sdkSessionId) || null;
|
|
6955
|
+
if (normalizeClaudeBilling(runtime?.provider_type) !== 'claude-api') return false;
|
|
6956
|
+
// A check that cannot answer fails this session's rows on the key closed,
|
|
6957
|
+
// as the claim does (messages-routes.mjs, claudeBillingFence).
|
|
6958
|
+
let refusal = null;
|
|
6959
|
+
try {
|
|
6960
|
+
refusal = claudeApiClaimCheck();
|
|
6961
|
+
} catch (error) {
|
|
6962
|
+
console.warn(`${runtimeLogPrefix()}Claude API claim check failed for session=${sdkSessionId.slice(0, 8)}: ${error?.message || error} — its rows on the key fail closed`);
|
|
6963
|
+
refusal = {
|
|
6964
|
+
code: 'CLAUDE_API_NOT_CONFIGURED',
|
|
6965
|
+
error: 'The relay could not check Claude API (key, switch, cap), so this turn did not run.',
|
|
6966
|
+
};
|
|
6967
|
+
}
|
|
6968
|
+
if (!refusal) return false;
|
|
6969
|
+
for (const row of listPendingClaudeApiRowsForOwner.all(sdkSessionId, 'claude-api')) {
|
|
6970
|
+
handleClaudeBillingFence({ reason: 'claude-api-unavailable', messageId: row.id, refusal });
|
|
6971
|
+
}
|
|
6972
|
+
console.warn(`${runtimeLogPrefix()}WORKER PRIME skipped session=${sdkSessionId.slice(0, 8)} reason=${refusal.code} — no Claude API worker while Claude API cannot run a turn`);
|
|
6973
|
+
return true;
|
|
6974
|
+
}
|
|
6975
|
+
|
|
6976
|
+
// The delivery refused a Claude chat's row to the worker that asked
|
|
6977
|
+
// (messages-routes.mjs, claudeBillingFence).
|
|
6978
|
+
function handleClaudeBillingFence(fence) {
|
|
6979
|
+
if (fence?.reason === 'claude-api-unavailable') {
|
|
6980
|
+
const row = stmts.findQById.get(fence.messageId);
|
|
6981
|
+
if (!row || String(row.status || '') !== 'pending') return;
|
|
6982
|
+
const code = String(fence.refusal?.code || 'CLAUDE_API_NOT_CONFIGURED');
|
|
6983
|
+
failRecoveredRowTerminally(row, 'claude-api-unavailable', {
|
|
6984
|
+
failureRecord: {
|
|
6985
|
+
kind: 'claude-api-unavailable',
|
|
6986
|
+
error: 'claude-api-unavailable',
|
|
6987
|
+
code,
|
|
6988
|
+
stableCode: `relay.${code.toLowerCase().replace(/_/g, '-')}`,
|
|
6989
|
+
failedAt: new Date().toISOString(),
|
|
6990
|
+
},
|
|
6991
|
+
// The code ends the note's paragraph, where the transcript's buttons
|
|
6992
|
+
// look for it (relay-error-ctas.mjs).
|
|
6993
|
+
failureText: `${fence.refusal?.error || 'Claude API is unavailable, so this turn did not run.'} `
|
|
6994
|
+
+ `${claudeApiRemedyText({ code, switchedOff: fence.refusal?.switchedOff === true })}, then send the message again. `
|
|
6995
|
+
+ `Error code: relay.${code.toLowerCase().replace(/_/g, '-')}.`,
|
|
6996
|
+
});
|
|
6997
|
+
return;
|
|
6998
|
+
}
|
|
6999
|
+
const result = claudeBillingSwapService.request(fence);
|
|
7000
|
+
if (result.state === 'swapping') {
|
|
7001
|
+
console.log(`${runtimeLogPrefix()}BILLING SWAP session=${String(fence.requesterSessionId || '').slice(0, 8)} conv=${String(fence.conversationId || '').slice(0, 8)} ${fence.workerKind || 'unknown'} → ${fence.rowBilling}`);
|
|
7002
|
+
}
|
|
7003
|
+
}
|
|
7004
|
+
|
|
6608
7005
|
// A worker alive without its socket while a message waits: replaced after a
|
|
6609
7006
|
// grace time (see the service). Stops the way the kill route does, but only
|
|
6610
7007
|
// names the registry's pid when the process list confirms it is the
|
|
@@ -6675,6 +7072,14 @@ const sessionWorkerWebSocketService = createSessionWorkerWebSocketService({
|
|
|
6675
7072
|
onDeliverySendFailed: recoverUndeliveredSessionWorkerMessage,
|
|
6676
7073
|
onWorkerSocketClosed: handleWorkerSocketClosed,
|
|
6677
7074
|
requestWork: requestSessionWorkerSocketDelivery,
|
|
7075
|
+
// What a worker says it is ("claude", "claude-api", ...): the registry
|
|
7076
|
+
// keeps it for the billing fence and for what a response ran on.
|
|
7077
|
+
onWorkerKind: (sessionId, kind) => {
|
|
7078
|
+
const sid = String(sessionId || '').trim();
|
|
7079
|
+
if (!sid) return;
|
|
7080
|
+
const worker = sessionWorkerRegistry?.getWorker?.(sid) || null;
|
|
7081
|
+
if (worker && worker.kind !== kind) sessionWorkerRegistry?.upsertWorker?.({ ...worker, kind });
|
|
7082
|
+
},
|
|
6678
7083
|
isWorkerProcessAlive: (pid) => isProcessAlive(Number(pid)),
|
|
6679
7084
|
pathPrefix: remotePath,
|
|
6680
7085
|
pollIntervalMs: 500,
|
|
@@ -6717,6 +7122,14 @@ async function primePendingSessionWorkers(reason = 'pending-worker-prime') {
|
|
|
6717
7122
|
const sdkSessionId = String(row?.sdk_session_id || '').trim();
|
|
6718
7123
|
if (!sdkSessionId) continue;
|
|
6719
7124
|
if (sessionWorkerSupervisor?.isKillBlocked?.(sdkSessionId) === true) continue;
|
|
7125
|
+
try {
|
|
7126
|
+
// Per session: a throw here costs this session its launch, never the
|
|
7127
|
+
// rest of the pass.
|
|
7128
|
+
if (failClaudeApiRowsWithoutLaunch(sdkSessionId)) continue;
|
|
7129
|
+
} catch (error) {
|
|
7130
|
+
console.warn(`${runtimeLogPrefix()}WORKER PRIME skipped session=${sdkSessionId.slice(0, 8)} err=${error?.message || error}`);
|
|
7131
|
+
continue;
|
|
7132
|
+
}
|
|
6720
7133
|
requested += 1;
|
|
6721
7134
|
try {
|
|
6722
7135
|
const result = await sessionWorkerSupervisor.ensureWorker(sdkSessionId);
|
|
@@ -6740,6 +7153,9 @@ app.use((req, _res, next) => {
|
|
|
6740
7153
|
stripRequestPathPrefix(req, remotePath);
|
|
6741
7154
|
next();
|
|
6742
7155
|
});
|
|
7156
|
+
// What a session worker posts loses every key a provider saved in Settings
|
|
7157
|
+
// before any route stores, broadcasts or logs it (worker-secret-redaction.mjs).
|
|
7158
|
+
app.use(workerPostRedaction);
|
|
6743
7159
|
function isApiRequestPath(requestPath) {
|
|
6744
7160
|
return /^\/api(?:\/|$)/.test(String(requestPath || ''));
|
|
6745
7161
|
}
|
|
@@ -6960,7 +7376,7 @@ const remoteRelayInbound = {
|
|
|
6960
7376
|
},
|
|
6961
7377
|
repository: remoteRelayRepository,
|
|
6962
7378
|
};
|
|
6963
|
-
// The
|
|
7379
|
+
// The relay_tool: who is calling (live turn, provider, model, mode,
|
|
6964
7380
|
// hops) and the Allow / Deny card write actions need in ask/plan mode.
|
|
6965
7381
|
const remoteRelayCallerContext = createRemoteRelayCallerContext({
|
|
6966
7382
|
db,
|
|
@@ -7074,6 +7490,12 @@ const sharedRouteDeps = {
|
|
|
7074
7490
|
getClaudeCloudProviderSettings,
|
|
7075
7491
|
claudeCloudSettingsService,
|
|
7076
7492
|
claudeCloudSessionService,
|
|
7493
|
+
getDeepSeekProviderSettings,
|
|
7494
|
+
deepseekSettingsService,
|
|
7495
|
+
deepseekBalanceService,
|
|
7496
|
+
deepseekUsageImportService,
|
|
7497
|
+
getClaudeApiProviderSettings,
|
|
7498
|
+
claudeApiSettingsService,
|
|
7077
7499
|
claudeCloudRepoService,
|
|
7078
7500
|
// The login's state for the workers' nudge route (never the token).
|
|
7079
7501
|
claudeCloudLoginStatus: { describe: () => claudeCloudCredentials.describe() },
|
|
@@ -7198,6 +7620,8 @@ const sharedRouteDeps = {
|
|
|
7198
7620
|
relayRestartOrchestrator,
|
|
7199
7621
|
resolveSessionStateRoot,
|
|
7200
7622
|
resolveClaudeSessionRoot: claudeSessionRootResolver.resolveClaudeSessionRoot,
|
|
7623
|
+
resolveDeepSeekSessionRoot: deepseekSessionRootResolver.resolveClaudeSessionRoot,
|
|
7624
|
+
deepseekClaudeConfigDir: DEEPSEEK_CLAUDE_CONFIG_DIR,
|
|
7201
7625
|
resolveCursorSessionRoot: cursorSessionRootResolver.resolveCursorSessionRoot,
|
|
7202
7626
|
getTurnCeilingMinutes,
|
|
7203
7627
|
setTurnCeilingMinutes,
|
|
@@ -7218,6 +7642,14 @@ const sharedRouteDeps = {
|
|
|
7218
7642
|
hostSuspendService,
|
|
7219
7643
|
collectHostActivity,
|
|
7220
7644
|
usageLimitPauseService,
|
|
7645
|
+
claudeBillingSwitchService,
|
|
7646
|
+
claudeApiUsageService,
|
|
7647
|
+
claudeApiClaimCheck,
|
|
7648
|
+
onClaudeBillingFence: handleClaudeBillingFence,
|
|
7649
|
+
claudeApiPlanUsage,
|
|
7650
|
+
getClaudeApiCapState,
|
|
7651
|
+
checkClaudeApiCap: (reason) => claudeApiCapWatch.check(reason),
|
|
7652
|
+
claudeApiStopLedger,
|
|
7221
7653
|
relayEventService,
|
|
7222
7654
|
relayScheduleService,
|
|
7223
7655
|
relayAskService,
|
|
@@ -7271,6 +7703,14 @@ registerRelayScheduleRoutes(app, {
|
|
|
7271
7703
|
relayScheduleService,
|
|
7272
7704
|
remoteRelayInbound,
|
|
7273
7705
|
});
|
|
7706
|
+
registerDeepSeekUsageImportRoutes(app, {
|
|
7707
|
+
auth,
|
|
7708
|
+
deepseekUsageImportService,
|
|
7709
|
+
remoteRelayInbound,
|
|
7710
|
+
// The usage card's currency (the balance's), so the import's answer can
|
|
7711
|
+
// say whether the card uses the export's rows.
|
|
7712
|
+
getCardCurrency: async () => deepseekCardCurrency(await deepseekBalanceService.getBalance()),
|
|
7713
|
+
});
|
|
7274
7714
|
registerRelayAskRoutes(app, {
|
|
7275
7715
|
auth,
|
|
7276
7716
|
db,
|
|
@@ -7309,6 +7749,7 @@ registerAboutRoutes(app, {
|
|
|
7309
7749
|
'claude-cloud': { enabled: getClaudeCloudProviderSettings()?.enabled === true },
|
|
7310
7750
|
cursor: { enabled: getCursorProviderSettings()?.enabled === true },
|
|
7311
7751
|
grok: { enabled: getGrokProviderSettings()?.enabled === true },
|
|
7752
|
+
'claude-api': { enabled: getClaudeApiProviderSettings()?.enabled === true },
|
|
7312
7753
|
}),
|
|
7313
7754
|
getCachedCliProviders: () => cliInstallService.getCachedStatusSnapshot()?.providers || {},
|
|
7314
7755
|
isRelayCliOnline: () => runtimeState.cliOnline === true,
|
|
@@ -7552,10 +7993,19 @@ function ensureRuntimeSessionBinding(
|
|
|
7552
7993
|
// default below and bind a GitHub (Copilot CLI) session.
|
|
7553
7994
|
: (normalizedRequestedProviderType === 'claude-cloud'
|
|
7554
7995
|
? 'claude-cloud'
|
|
7555
|
-
: (normalizedRequestedProviderType === '
|
|
7556
|
-
? '
|
|
7557
|
-
|
|
7558
|
-
|
|
7996
|
+
: (normalizedRequestedProviderType === 'deepseek'
|
|
7997
|
+
? 'deepseek'
|
|
7998
|
+
// Never folded into `claude`: the two bill different accounts.
|
|
7999
|
+
: (normalizedRequestedProviderType === 'claude-api'
|
|
8000
|
+
? 'claude-api'
|
|
8001
|
+
: (normalizedRequestedProviderType === 'github'
|
|
8002
|
+
? 'github'
|
|
8003
|
+
: (openAISettings?.enabled ? 'openai' : 'github'))))))));
|
|
8004
|
+
const resolvedProviderModel = (
|
|
8005
|
+
resolvedProviderType === 'openai' || resolvedProviderType === 'claude' || resolvedProviderType === 'cursor'
|
|
8006
|
+
|| resolvedProviderType === 'grok' || resolvedProviderType === 'claude-cloud' || resolvedProviderType === 'deepseek'
|
|
8007
|
+
|| resolvedProviderType === 'claude-api'
|
|
8008
|
+
)
|
|
7559
8009
|
? (String(providerModel || normalizedModel || '').trim() || null)
|
|
7560
8010
|
: null;
|
|
7561
8011
|
try {
|