@oar-sh/oar 0.9.17 → 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/skills/tool-activity.mjs +5 -4
- package/CHANGELOG.md +192 -0
- package/README.md +13 -12
- package/package.json +1 -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-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 +35 -9
- package/server/claude-worker/claude-session-process.mjs +166 -21
- package/server/claude-worker/claude-session-worker-main.mjs +3 -0
- package/server/claude-worker/claude-subscription-key-check.mjs +64 -0
- package/server/claude-worker/claude-turn-publisher.mjs +117 -5
- package/server/claude-worker/claude-usage-totals.mjs +53 -0
- package/server/claude-worker/sdk-message-normalizer.mjs +1 -1
- package/server/copilot-worker/copilot-remote-relay-tool.mjs +2 -2
- package/server/copilot-worker/copilot-sdk-event-normalizer.mjs +1 -1
- package/server/copilot-worker/copilot-sdk-session-process.mjs +9 -9
- 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 +33 -1
- package/server/deepseek-worker/deepseek-claude-backend.mjs +14 -130
- 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/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 +52 -0
- package/server/public/app/bootstrap.js +153 -27
- 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 +6 -2
- package/server/public/app/context-usage-view.mjs +1 -1
- package/server/public/app/conversation-provider-indicator.mjs +11 -0
- package/server/public/app/conversation-view.js +29 -3
- package/server/public/app/inbox-view.mjs +1 -1
- package/server/public/app/journal-view.js +127 -10
- package/server/public/app/model-selector-options.mjs +17 -1
- package/server/public/app/new-conversation-model-choice.mjs +21 -0
- package/server/public/app/provider-readiness.mjs +5 -2
- package/server/public/app/relay-error-ctas.mjs +50 -0
- package/server/public/app/relay-schedules-ui.js +1 -1
- package/server/public/app/settings-modal.js +11 -0
- package/server/public/app/settings-tabs.js +1 -1
- package/server/public/app/socket-handlers.js +18 -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 +189 -3
- package/server/relay-tools.md +4 -4
- package/server/repositories/conversation-group-repository.mjs +1 -1
- 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 +1 -1
- package/server/routes/messages-routes.mjs +645 -27
- 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 +327 -29
- package/server/server-runtime.mjs +363 -26
- package/server/services/about-service.mjs +1 -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/plan-usage-claude-api.mjs +445 -0
- package/server/services/plan-usage-service.mjs +5 -0
- package/server/services/provider-model-selection.mjs +3 -2
- package/server/services/provider-reasoning-effort.mjs +9 -0
- 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 +83 -14
- package/server/services/remote-relay-registry-service.mjs +1 -1
- package/server/services/session-worker-launch-service.mjs +61 -2
- 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 +2 -0
- package/shared/anthropic-api.mjs +202 -0
- 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 +1 -0
- package/shared/worker-runtime/api-client.mjs +2 -2
- package/shared/worker-runtime/heartbeat.mjs +5 -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';
|
|
@@ -70,6 +70,11 @@ import { createWorkerPostRedaction } from './services/worker-secret-redaction.mj
|
|
|
70
70
|
import { createDeepSeekBalanceService } from './services/deepseek-balance-service.mjs';
|
|
71
71
|
import { deepseekCardCurrency } from './services/plan-usage-deepseek.mjs';
|
|
72
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';
|
|
73
78
|
import { DEEPSEEK_DEFAULT_AUTO_COMPACT_WINDOW, DEFAULT_DEEPSEEK_MODEL, deepseekModelLabel } from '../shared/deepseek-models.mjs';
|
|
74
79
|
import { createClaudeCloudSessionService } from './services/claude-cloud-session-service.mjs';
|
|
75
80
|
import { createClaudeCloudRepoService, listRecentCloudSourcesFromStatements } from './services/claude-cloud-repo-service.mjs';
|
|
@@ -142,10 +147,10 @@ import { mergeStrayMigratedUploads } from './services/oar-state-migration-servic
|
|
|
142
147
|
import { createRelayRestartOrchestrator } from './services/relay-restart-orchestrator-service.mjs';
|
|
143
148
|
import { createRelayBridgeOwnerService } from './services/relay-bridge-owner-service.mjs';
|
|
144
149
|
import { createRelayCliLauncherService } from './services/relay-cli-launcher-service.mjs';
|
|
145
|
-
import { createSessionWorkerRegistry } from './services/session-worker-registry-service.mjs';
|
|
150
|
+
import { createSessionWorkerRegistry, normalizeWorkerKind } from './services/session-worker-registry-service.mjs';
|
|
146
151
|
import { createSessionWorkerSupervisor } from './services/session-worker-supervisor-service.mjs';
|
|
147
152
|
import { createSessionWorkerProcessInspector } from './services/session-worker-process-service.mjs';
|
|
148
|
-
import { applyClaudeCloudProviderEnvironment, applyClaudeProviderEnvironment, applyCopilotSdkProviderEnvironment, applyCursorProviderEnvironment, applyDeepSeekProviderEnvironment, 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';
|
|
149
154
|
import { createSshTunnelManager } from './services/ssh-tunnel-manager-service.mjs';
|
|
150
155
|
import { createCloudflaredTunnelManager } from './services/cloudflared-tunnel-service.mjs';
|
|
151
156
|
import {
|
|
@@ -1943,6 +1948,7 @@ const runtimeTimers = {
|
|
|
1943
1948
|
pendingWorkerPrime: null,
|
|
1944
1949
|
sharedViewerPrune: null,
|
|
1945
1950
|
restoredTaskSettle: null,
|
|
1951
|
+
claudeApiCapClock: null,
|
|
1946
1952
|
};
|
|
1947
1953
|
|
|
1948
1954
|
function normalizeRemotePath(value) {
|
|
@@ -2839,7 +2845,8 @@ const claudeCloudRepoService = createClaudeCloudRepoService({
|
|
|
2839
2845
|
});
|
|
2840
2846
|
// What a session worker posts loses every key a provider saved in Settings
|
|
2841
2847
|
// (worker-secret-redaction.mjs). Built here, at the start, so it reads the
|
|
2842
|
-
// saved keys now, and the DeepSeek
|
|
2848
|
+
// saved keys now, and the DeepSeek and Claude API saves below note each key
|
|
2849
|
+
// they write: one
|
|
2843
2850
|
// changed before any worker has posted is still redacted.
|
|
2844
2851
|
const workerPostRedaction = createWorkerPostRedaction({ readSetting: readAppSettingValue });
|
|
2845
2852
|
// DeepSeek: one API key per relay, its models and its endpoint. The key stays
|
|
@@ -2856,6 +2863,23 @@ const deepseekSettingsService = createDeepSeekSettingsService({
|
|
|
2856
2863
|
function getDeepSeekProviderSettings() {
|
|
2857
2864
|
return deepseekSettingsService.getSettings();
|
|
2858
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
|
+
}
|
|
2859
2883
|
// The DeepSeek balance for Check Usage: read with the saved key only while
|
|
2860
2884
|
// DeepSeek is on, kept for a minute.
|
|
2861
2885
|
const deepseekBalanceService = createDeepSeekBalanceService({ getSettings: getDeepSeekProviderSettings });
|
|
@@ -3117,7 +3141,7 @@ function buildSessionWorkerLaunchEnvForSession(targetSessionId) {
|
|
|
3117
3141
|
const providerType = String(runtimeSession?.provider_type || 'github').trim().toLowerCase();
|
|
3118
3142
|
// Clear every provider's variables first (the appliers' kind-guarded deletes
|
|
3119
3143
|
// keep the chained clears order-independent), then apply only the bound one.
|
|
3120
|
-
const cleared = applyDeepSeekProviderEnvironment(applyClaudeCloudProviderEnvironment(applyCopilotSdkProviderEnvironment(applyGrokProviderEnvironment(applyCursorProviderEnvironment(applyClaudeProviderEnvironment(applyOpenAIProviderEnvironment(sessionWorkerLaunchEnv)))))));
|
|
3144
|
+
const cleared = applyClaudeApiProviderEnvironment(applyDeepSeekProviderEnvironment(applyClaudeCloudProviderEnvironment(applyCopilotSdkProviderEnvironment(applyGrokProviderEnvironment(applyCursorProviderEnvironment(applyClaudeProviderEnvironment(applyOpenAIProviderEnvironment(sessionWorkerLaunchEnv))))))));
|
|
3121
3145
|
// A Claude Cloud conversation gets its own worker and nothing else: it must
|
|
3122
3146
|
// never reach the Copilot CLI fall-through at the end of this function. The
|
|
3123
3147
|
// environment names the worker and the model, no credential.
|
|
@@ -3160,6 +3184,19 @@ function buildSessionWorkerLaunchEnvForSession(targetSessionId) {
|
|
|
3160
3184
|
configDir: DEEPSEEK_CLAUDE_CONFIG_DIR,
|
|
3161
3185
|
});
|
|
3162
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
|
+
}
|
|
3163
3200
|
if (providerType === 'grok') {
|
|
3164
3201
|
const grokSettings = getGrokProviderSettings();
|
|
3165
3202
|
const grokModel = String(runtimeSession?.provider_model || runtimeSession?.model || grokSettings.model).trim();
|
|
@@ -3388,14 +3425,16 @@ async function stopSessionWorkerForProviderRebind(sdkSessionId) {
|
|
|
3388
3425
|
|
|
3389
3426
|
async function reconcileUnstartedConversationProviders({ enabled, model, provider = 'openai' } = {}) {
|
|
3390
3427
|
const normalizedProvider = String(provider || 'openai').trim().toLowerCase();
|
|
3391
|
-
const managedProvider = ['claude', 'cursor', 'grok', 'deepseek'].includes(normalizedProvider) ? normalizedProvider : 'openai';
|
|
3392
|
-
const managedDefaultModel =
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
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];
|
|
3399
3438
|
const providerType = enabled === true ? managedProvider : 'github';
|
|
3400
3439
|
const providerModel = enabled === true
|
|
3401
3440
|
? (String(model || '').trim() || managedDefaultModel)
|
|
@@ -3470,7 +3509,9 @@ async function reconcileUnstartedConversationProviders({ enabled, model, provide
|
|
|
3470
3509
|
? getGrokProviderSettings().enabled === true
|
|
3471
3510
|
: (currentProvider === 'deepseek'
|
|
3472
3511
|
? getDeepSeekProviderSettings().configured === true
|
|
3473
|
-
: (currentProvider
|
|
3512
|
+
: (currentProvider === 'claude-api'
|
|
3513
|
+
? getClaudeApiProviderSettings().enabled === true
|
|
3514
|
+
: (currentProvider !== 'openai' || getOpenAIProviderSettings().configured === true)))));
|
|
3474
3515
|
if (providerWasUpdated && canRestorePreviousProvider) {
|
|
3475
3516
|
stmts.updateRuntimeSessionProvider.run(
|
|
3476
3517
|
currentProvider,
|
|
@@ -5205,7 +5246,18 @@ function createCompactedConversation(sourceConversationId) {
|
|
|
5205
5246
|
// The new conversation stays in the old one's session group, and heads
|
|
5206
5247
|
// it if the old one did; the archived source stays a member.
|
|
5207
5248
|
conversationGroups.carryToCompacted(sourceConversationId, targetConversationId, now);
|
|
5208
|
-
|
|
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
|
+
: {});
|
|
5209
5261
|
});
|
|
5210
5262
|
tx();
|
|
5211
5263
|
// Its schedules go on in the new conversation, both the ones that fire
|
|
@@ -5491,6 +5543,67 @@ const usageLimitPauseService = createUsageLimitPauseService({
|
|
|
5491
5543
|
logger: console,
|
|
5492
5544
|
});
|
|
5493
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
|
+
|
|
5494
5607
|
// Turns a restart cut off for good (their worker went with the relay, and a
|
|
5495
5608
|
// second delivery would run the prompt twice): with the setting on, their
|
|
5496
5609
|
// session gets one note to carry on. The rows processing right now are the
|
|
@@ -5544,7 +5657,7 @@ relayEventService = createRelayEventService({
|
|
|
5544
5657
|
});
|
|
5545
5658
|
|
|
5546
5659
|
// ─── Relay schedules ──────────────────────────────────────────────────────────
|
|
5547
|
-
// Prompts agents scheduled with
|
|
5660
|
+
// Prompts agents scheduled with relay_tool's `schedule`: kept in the
|
|
5548
5661
|
// database, their timers rebuilt at boot (services/relay-schedule-service.mjs).
|
|
5549
5662
|
const relayScheduleService = createRelayScheduleService({
|
|
5550
5663
|
db,
|
|
@@ -5562,7 +5675,7 @@ const relayScheduleService = createRelayScheduleService({
|
|
|
5562
5675
|
});
|
|
5563
5676
|
|
|
5564
5677
|
// ─── Questions that don't block ───────────────────────────────────────────────
|
|
5565
|
-
//
|
|
5678
|
+
// relay_tool's `ask`: a card in the session and the Inbox; the answer
|
|
5566
5679
|
// reaches the askers as a relay event (services/relay-ask-service.mjs).
|
|
5567
5680
|
const relayAskService = createRelayAskService({
|
|
5568
5681
|
db,
|
|
@@ -6309,7 +6422,7 @@ function markSharedViewerPresence({ conversationId, token, viewerId } = {}) {
|
|
|
6309
6422
|
}
|
|
6310
6423
|
return { ok: true, watcherCount, capped: false };
|
|
6311
6424
|
}
|
|
6312
|
-
async function requestSessionWorkerSocketDelivery({ sessionId, pid, reason = 'worker-ready' } = {}) {
|
|
6425
|
+
async function requestSessionWorkerSocketDelivery({ sessionId, pid, reason = 'worker-ready', workerKind = null } = {}) {
|
|
6313
6426
|
const requesterSessionId = String(sessionId || '').trim();
|
|
6314
6427
|
if (!requesterSessionId) return { message: null, reason: 'missing-session-id' };
|
|
6315
6428
|
touchCli();
|
|
@@ -6367,6 +6480,10 @@ async function requestSessionWorkerSocketDelivery({ sessionId, pid, reason = 'wo
|
|
|
6367
6480
|
|
|
6368
6481
|
const counts = queueCounts();
|
|
6369
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);
|
|
6370
6487
|
if (!existingWorker) {
|
|
6371
6488
|
sessionWorkerRegistry?.upsertWorker?.({
|
|
6372
6489
|
sdkSessionId: requesterSessionId,
|
|
@@ -6374,6 +6491,7 @@ async function requestSessionWorkerSocketDelivery({ sessionId, pid, reason = 'wo
|
|
|
6374
6491
|
status: 'ready',
|
|
6375
6492
|
pid: pid || null,
|
|
6376
6493
|
queueDepth: Math.max(0, Number(counts.pendingCount || 0)),
|
|
6494
|
+
...(statedKind ? { kind: statedKind } : {}),
|
|
6377
6495
|
});
|
|
6378
6496
|
} else if (pid && existingWorker.pid !== pid) {
|
|
6379
6497
|
sessionWorkerRegistry?.upsertWorker?.({
|
|
@@ -6382,7 +6500,10 @@ async function requestSessionWorkerSocketDelivery({ sessionId, pid, reason = 'wo
|
|
|
6382
6500
|
pid,
|
|
6383
6501
|
status: existingWorker.status || 'ready',
|
|
6384
6502
|
queueDepth: Math.max(0, Number(counts.pendingCount || 0)),
|
|
6503
|
+
kind: statedKind,
|
|
6385
6504
|
});
|
|
6505
|
+
} else if (statedKind && existingWorker.kind !== statedKind) {
|
|
6506
|
+
sessionWorkerRegistry?.upsertWorker?.({ ...existingWorker, kind: statedKind });
|
|
6386
6507
|
}
|
|
6387
6508
|
|
|
6388
6509
|
const dequeueResult = await dequeuePendingMessageForWorkerLoop({
|
|
@@ -6396,6 +6517,11 @@ async function requestSessionWorkerSocketDelivery({ sessionId, pid, reason = 'wo
|
|
|
6396
6517
|
sessionWorkerSupervisor,
|
|
6397
6518
|
relayRestartOrchestrator,
|
|
6398
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,
|
|
6399
6525
|
});
|
|
6400
6526
|
if (!dequeueResult?.message) {
|
|
6401
6527
|
return {
|
|
@@ -6439,11 +6565,14 @@ async function requestSessionWorkerSocketDelivery({ sessionId, pid, reason = 'wo
|
|
|
6439
6565
|
}
|
|
6440
6566
|
console.log(`${runtimeLogPrefix()}WS DEQUEUE ${out.id.slice(0, 8)} session=${requesterSessionId.slice(0, 8)} reason=${String(reason || 'worker-ready')}`);
|
|
6441
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;
|
|
6442
6570
|
return {
|
|
6443
6571
|
message: out,
|
|
6444
6572
|
// Piggybacked settings so workers pick up slider changes on the next
|
|
6445
6573
|
// delivery without a restart.
|
|
6446
6574
|
settings: {
|
|
6575
|
+
...(billingSwap ? { billingSwap } : {}),
|
|
6447
6576
|
backgroundTaskTimeoutMs: backgroundTaskTimeoutMinutesToMs(getBackgroundTaskTimeoutMinutes()),
|
|
6448
6577
|
// The user's max-turn-duration ceiling (0 = no limit): worker-local
|
|
6449
6578
|
// watchdogs (Grok's prompt ceiling) must honor it rather than imposing
|
|
@@ -6479,15 +6608,18 @@ async function requestSessionWorkerSocketDelivery({ sessionId, pid, reason = 'wo
|
|
|
6479
6608
|
claudeBackgroundCommandLimitMs: claudeBackgroundCommandLimitMinutesToMs(getClaudeBackgroundCommandLimitMinutes()),
|
|
6480
6609
|
};
|
|
6481
6610
|
}
|
|
6482
|
-
|
|
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 {};
|
|
6483
6615
|
return {
|
|
6484
6616
|
attribution: claudeAttributionForConversation(conv, out.providerModel || out.model, {
|
|
6485
|
-
relayFolderFallback:
|
|
6617
|
+
relayFolderFallback: localClaude,
|
|
6486
6618
|
}),
|
|
6487
6619
|
// The Claude tab's background command limit; the worker
|
|
6488
6620
|
// hands it to the CLI it spawns next. Claude Cloud runs no
|
|
6489
6621
|
// local CLI, so it does not get one.
|
|
6490
|
-
...(
|
|
6622
|
+
...(localClaude
|
|
6491
6623
|
? { claudeBackgroundCommandLimitMs: claudeBackgroundCommandLimitMinutesToMs(getClaudeBackgroundCommandLimitMinutes()) }
|
|
6492
6624
|
: {}),
|
|
6493
6625
|
};
|
|
@@ -6699,6 +6831,177 @@ async function sweepDeadWorkerProcessingRowsOnce() {
|
|
|
6699
6831
|
return recovered;
|
|
6700
6832
|
}
|
|
6701
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
|
+
|
|
6702
7005
|
// A worker alive without its socket while a message waits: replaced after a
|
|
6703
7006
|
// grace time (see the service). Stops the way the kill route does, but only
|
|
6704
7007
|
// names the registry's pid when the process list confirms it is the
|
|
@@ -6769,6 +7072,14 @@ const sessionWorkerWebSocketService = createSessionWorkerWebSocketService({
|
|
|
6769
7072
|
onDeliverySendFailed: recoverUndeliveredSessionWorkerMessage,
|
|
6770
7073
|
onWorkerSocketClosed: handleWorkerSocketClosed,
|
|
6771
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
|
+
},
|
|
6772
7083
|
isWorkerProcessAlive: (pid) => isProcessAlive(Number(pid)),
|
|
6773
7084
|
pathPrefix: remotePath,
|
|
6774
7085
|
pollIntervalMs: 500,
|
|
@@ -6811,6 +7122,14 @@ async function primePendingSessionWorkers(reason = 'pending-worker-prime') {
|
|
|
6811
7122
|
const sdkSessionId = String(row?.sdk_session_id || '').trim();
|
|
6812
7123
|
if (!sdkSessionId) continue;
|
|
6813
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
|
+
}
|
|
6814
7133
|
requested += 1;
|
|
6815
7134
|
try {
|
|
6816
7135
|
const result = await sessionWorkerSupervisor.ensureWorker(sdkSessionId);
|
|
@@ -7057,7 +7376,7 @@ const remoteRelayInbound = {
|
|
|
7057
7376
|
},
|
|
7058
7377
|
repository: remoteRelayRepository,
|
|
7059
7378
|
};
|
|
7060
|
-
// The
|
|
7379
|
+
// The relay_tool: who is calling (live turn, provider, model, mode,
|
|
7061
7380
|
// hops) and the Allow / Deny card write actions need in ask/plan mode.
|
|
7062
7381
|
const remoteRelayCallerContext = createRemoteRelayCallerContext({
|
|
7063
7382
|
db,
|
|
@@ -7175,6 +7494,8 @@ const sharedRouteDeps = {
|
|
|
7175
7494
|
deepseekSettingsService,
|
|
7176
7495
|
deepseekBalanceService,
|
|
7177
7496
|
deepseekUsageImportService,
|
|
7497
|
+
getClaudeApiProviderSettings,
|
|
7498
|
+
claudeApiSettingsService,
|
|
7178
7499
|
claudeCloudRepoService,
|
|
7179
7500
|
// The login's state for the workers' nudge route (never the token).
|
|
7180
7501
|
claudeCloudLoginStatus: { describe: () => claudeCloudCredentials.describe() },
|
|
@@ -7321,6 +7642,14 @@ const sharedRouteDeps = {
|
|
|
7321
7642
|
hostSuspendService,
|
|
7322
7643
|
collectHostActivity,
|
|
7323
7644
|
usageLimitPauseService,
|
|
7645
|
+
claudeBillingSwitchService,
|
|
7646
|
+
claudeApiUsageService,
|
|
7647
|
+
claudeApiClaimCheck,
|
|
7648
|
+
onClaudeBillingFence: handleClaudeBillingFence,
|
|
7649
|
+
claudeApiPlanUsage,
|
|
7650
|
+
getClaudeApiCapState,
|
|
7651
|
+
checkClaudeApiCap: (reason) => claudeApiCapWatch.check(reason),
|
|
7652
|
+
claudeApiStopLedger,
|
|
7324
7653
|
relayEventService,
|
|
7325
7654
|
relayScheduleService,
|
|
7326
7655
|
relayAskService,
|
|
@@ -7420,6 +7749,7 @@ registerAboutRoutes(app, {
|
|
|
7420
7749
|
'claude-cloud': { enabled: getClaudeCloudProviderSettings()?.enabled === true },
|
|
7421
7750
|
cursor: { enabled: getCursorProviderSettings()?.enabled === true },
|
|
7422
7751
|
grok: { enabled: getGrokProviderSettings()?.enabled === true },
|
|
7752
|
+
'claude-api': { enabled: getClaudeApiProviderSettings()?.enabled === true },
|
|
7423
7753
|
}),
|
|
7424
7754
|
getCachedCliProviders: () => cliInstallService.getCachedStatusSnapshot()?.providers || {},
|
|
7425
7755
|
isRelayCliOnline: () => runtimeState.cliOnline === true,
|
|
@@ -7665,10 +7995,17 @@ function ensureRuntimeSessionBinding(
|
|
|
7665
7995
|
? 'claude-cloud'
|
|
7666
7996
|
: (normalizedRequestedProviderType === 'deepseek'
|
|
7667
7997
|
? 'deepseek'
|
|
7668
|
-
|
|
7669
|
-
|
|
7670
|
-
|
|
7671
|
-
|
|
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
|
+
)
|
|
7672
8009
|
? (String(providerModel || normalizedModel || '').trim() || null)
|
|
7673
8010
|
: null;
|
|
7674
8011
|
try {
|
|
@@ -25,7 +25,8 @@ import {
|
|
|
25
25
|
// Providers whose worker lists every running background sub-agent in its
|
|
26
26
|
// task set, by the run's id. Others may run background agents no set lists
|
|
27
27
|
// (their run rows are their only trace), so a set says nothing about a row.
|
|
28
|
-
|
|
28
|
+
// The Claude API worker is the Claude one on an API key.
|
|
29
|
+
const RUN_LISTING_PROVIDERS = new Set(['claude', 'claude-api']);
|
|
29
30
|
|
|
30
31
|
function toText(value) {
|
|
31
32
|
return String(value ?? '').trim();
|