@oar-sh/oar 0.9.17 → 0.9.19
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/server-lifecycle/managed-server.mjs +13 -1
- package/.github/extensions/web-relay/skills/tool-activity.mjs +5 -4
- package/CHANGELOG.md +234 -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 +5 -5
- 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-heap-guard.mjs +338 -0
- package/server/relay-self-restart.mjs +33 -7
- package/server/relay-tools.md +6 -6
- 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 +678 -31
- 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 +365 -53
- package/server/server-runtime.mjs +373 -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 +111 -17
- 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 +54 -12
- 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';
|
|
@@ -136,16 +141,17 @@ import { createClaudeSessionRootResolver } from './services/claude-session-root-
|
|
|
136
141
|
import { createCursorSessionRootResolver } from './services/cursor-session-root-service.mjs';
|
|
137
142
|
import { createGitChangesService } from './services/git-changes-service.mjs';
|
|
138
143
|
import { createRelaySingletonGuard } from './services/relay-singleton-guard.mjs';
|
|
144
|
+
import { startRelayMemoryLog } from './relay-heap-guard.mjs';
|
|
139
145
|
import { adoptEarlierDatabase, lostPackageDatabaseWarning, packageDatabaseEvidence } from './services/earlier-database-service.mjs';
|
|
140
146
|
import { ensureDataDirs, resolveDataDir, resolveUploadDir, resolveUploadReadDirs, uploadPathInDirs } from './services/data-dir-resolution.mjs';
|
|
141
147
|
import { mergeStrayMigratedUploads } from './services/oar-state-migration-service.mjs';
|
|
142
148
|
import { createRelayRestartOrchestrator } from './services/relay-restart-orchestrator-service.mjs';
|
|
143
149
|
import { createRelayBridgeOwnerService } from './services/relay-bridge-owner-service.mjs';
|
|
144
150
|
import { createRelayCliLauncherService } from './services/relay-cli-launcher-service.mjs';
|
|
145
|
-
import { createSessionWorkerRegistry } from './services/session-worker-registry-service.mjs';
|
|
151
|
+
import { createSessionWorkerRegistry, normalizeWorkerKind } from './services/session-worker-registry-service.mjs';
|
|
146
152
|
import { createSessionWorkerSupervisor } from './services/session-worker-supervisor-service.mjs';
|
|
147
153
|
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';
|
|
154
|
+
import { applyClaudeApiProviderEnvironment, applyClaudeCloudProviderEnvironment, applyClaudeProviderEnvironment, applyCopilotSdkProviderEnvironment, applyCursorProviderEnvironment, applyDeepSeekProviderEnvironment, applyGrokProviderEnvironment, applyOpenAIProviderEnvironment, copilotSdkEngineUnavailableReason, killTmuxSession, launchSessionCli } from './services/session-worker-launch-service.mjs';
|
|
149
155
|
import { createSshTunnelManager } from './services/ssh-tunnel-manager-service.mjs';
|
|
150
156
|
import { createCloudflaredTunnelManager } from './services/cloudflared-tunnel-service.mjs';
|
|
151
157
|
import {
|
|
@@ -1943,6 +1949,9 @@ const runtimeTimers = {
|
|
|
1943
1949
|
pendingWorkerPrime: null,
|
|
1944
1950
|
sharedViewerPrune: null,
|
|
1945
1951
|
restoredTaskSettle: null,
|
|
1952
|
+
claudeApiCapClock: null,
|
|
1953
|
+
memoryLog: null,
|
|
1954
|
+
memoryWatch: null,
|
|
1946
1955
|
};
|
|
1947
1956
|
|
|
1948
1957
|
function normalizeRemotePath(value) {
|
|
@@ -2839,7 +2848,8 @@ const claudeCloudRepoService = createClaudeCloudRepoService({
|
|
|
2839
2848
|
});
|
|
2840
2849
|
// What a session worker posts loses every key a provider saved in Settings
|
|
2841
2850
|
// (worker-secret-redaction.mjs). Built here, at the start, so it reads the
|
|
2842
|
-
// saved keys now, and the DeepSeek
|
|
2851
|
+
// saved keys now, and the DeepSeek and Claude API saves below note each key
|
|
2852
|
+
// they write: one
|
|
2843
2853
|
// changed before any worker has posted is still redacted.
|
|
2844
2854
|
const workerPostRedaction = createWorkerPostRedaction({ readSetting: readAppSettingValue });
|
|
2845
2855
|
// DeepSeek: one API key per relay, its models and its endpoint. The key stays
|
|
@@ -2856,6 +2866,23 @@ const deepseekSettingsService = createDeepSeekSettingsService({
|
|
|
2856
2866
|
function getDeepSeekProviderSettings() {
|
|
2857
2867
|
return deepseekSettingsService.getSettings();
|
|
2858
2868
|
}
|
|
2869
|
+
// Claude API: Claude chats on a Console API key, one key per relay. The key
|
|
2870
|
+
// stays in the settings table; GET routes answer `configured`, never the key.
|
|
2871
|
+
// Before its own discovery the Claude provider's models stand in.
|
|
2872
|
+
const claudeApiSettingsService = createClaudeApiSettingsService({
|
|
2873
|
+
readSetting: readAppSettingValue,
|
|
2874
|
+
writeSetting: (key, value) => {
|
|
2875
|
+
stmts.upsertAppSetting.run(key, String(value), new Date().toISOString());
|
|
2876
|
+
workerPostRedaction.noteSavedSetting(key, value);
|
|
2877
|
+
},
|
|
2878
|
+
deleteSetting: (key) => stmts.deleteAppSetting.run(key),
|
|
2879
|
+
listProviderCandidates: () => stmts.listRuntimeSessionProviderCandidates?.all?.() || [],
|
|
2880
|
+
countLiveKeyWorkers: () => (sessionWorkerRegistry?.listWorkers?.() || []).filter((worker) => worker?.kind === 'claude-api').length,
|
|
2881
|
+
getClaudeProviderSettings: () => getClaudeProviderSettings(),
|
|
2882
|
+
});
|
|
2883
|
+
function getClaudeApiProviderSettings() {
|
|
2884
|
+
return claudeApiSettingsService.getSettings();
|
|
2885
|
+
}
|
|
2859
2886
|
// The DeepSeek balance for Check Usage: read with the saved key only while
|
|
2860
2887
|
// DeepSeek is on, kept for a minute.
|
|
2861
2888
|
const deepseekBalanceService = createDeepSeekBalanceService({ getSettings: getDeepSeekProviderSettings });
|
|
@@ -3117,7 +3144,7 @@ function buildSessionWorkerLaunchEnvForSession(targetSessionId) {
|
|
|
3117
3144
|
const providerType = String(runtimeSession?.provider_type || 'github').trim().toLowerCase();
|
|
3118
3145
|
// Clear every provider's variables first (the appliers' kind-guarded deletes
|
|
3119
3146
|
// keep the chained clears order-independent), then apply only the bound one.
|
|
3120
|
-
const cleared = applyDeepSeekProviderEnvironment(applyClaudeCloudProviderEnvironment(applyCopilotSdkProviderEnvironment(applyGrokProviderEnvironment(applyCursorProviderEnvironment(applyClaudeProviderEnvironment(applyOpenAIProviderEnvironment(sessionWorkerLaunchEnv)))))));
|
|
3147
|
+
const cleared = applyClaudeApiProviderEnvironment(applyDeepSeekProviderEnvironment(applyClaudeCloudProviderEnvironment(applyCopilotSdkProviderEnvironment(applyGrokProviderEnvironment(applyCursorProviderEnvironment(applyClaudeProviderEnvironment(applyOpenAIProviderEnvironment(sessionWorkerLaunchEnv))))))));
|
|
3121
3148
|
// A Claude Cloud conversation gets its own worker and nothing else: it must
|
|
3122
3149
|
// never reach the Copilot CLI fall-through at the end of this function. The
|
|
3123
3150
|
// environment names the worker and the model, no credential.
|
|
@@ -3160,6 +3187,19 @@ function buildSessionWorkerLaunchEnvForSession(targetSessionId) {
|
|
|
3160
3187
|
configDir: DEEPSEEK_CLAUDE_CONFIG_DIR,
|
|
3161
3188
|
});
|
|
3162
3189
|
}
|
|
3190
|
+
// A Claude API conversation gets its own worker, never the Copilot
|
|
3191
|
+
// fall-through below nor a Claude one on the host's subscription login.
|
|
3192
|
+
// Without a key the applier throws, and the conversation gets the launch
|
|
3193
|
+
// note. No config dir of its own: the chat is a Claude chat in the host's
|
|
3194
|
+
// Claude Code state, billed to the key.
|
|
3195
|
+
if (providerType === 'claude-api') {
|
|
3196
|
+
const claudeApiSettings = getClaudeApiProviderSettings();
|
|
3197
|
+
return applyClaudeApiProviderEnvironment(cleared, {
|
|
3198
|
+
enabled: true,
|
|
3199
|
+
model: String(runtimeSession?.provider_model || runtimeSession?.model || claudeApiSettings.model).trim(),
|
|
3200
|
+
apiKey: claudeApiSettings.apiKey,
|
|
3201
|
+
});
|
|
3202
|
+
}
|
|
3163
3203
|
if (providerType === 'grok') {
|
|
3164
3204
|
const grokSettings = getGrokProviderSettings();
|
|
3165
3205
|
const grokModel = String(runtimeSession?.provider_model || runtimeSession?.model || grokSettings.model).trim();
|
|
@@ -3388,14 +3428,16 @@ async function stopSessionWorkerForProviderRebind(sdkSessionId) {
|
|
|
3388
3428
|
|
|
3389
3429
|
async function reconcileUnstartedConversationProviders({ enabled, model, provider = 'openai' } = {}) {
|
|
3390
3430
|
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
|
-
|
|
3431
|
+
const managedProvider = ['claude', 'cursor', 'grok', 'deepseek', 'claude-api'].includes(normalizedProvider) ? normalizedProvider : 'openai';
|
|
3432
|
+
const managedDefaultModel = managedProvider === 'claude-api'
|
|
3433
|
+
? getClaudeApiProviderSettings().model
|
|
3434
|
+
: {
|
|
3435
|
+
openai: DEFAULT_OPENAI_MODEL,
|
|
3436
|
+
claude: DEFAULT_CLAUDE_MODEL,
|
|
3437
|
+
cursor: DEFAULT_CURSOR_MODEL,
|
|
3438
|
+
grok: DEFAULT_GROK_MODEL,
|
|
3439
|
+
deepseek: DEFAULT_DEEPSEEK_MODEL,
|
|
3440
|
+
}[managedProvider];
|
|
3399
3441
|
const providerType = enabled === true ? managedProvider : 'github';
|
|
3400
3442
|
const providerModel = enabled === true
|
|
3401
3443
|
? (String(model || '').trim() || managedDefaultModel)
|
|
@@ -3470,7 +3512,9 @@ async function reconcileUnstartedConversationProviders({ enabled, model, provide
|
|
|
3470
3512
|
? getGrokProviderSettings().enabled === true
|
|
3471
3513
|
: (currentProvider === 'deepseek'
|
|
3472
3514
|
? getDeepSeekProviderSettings().configured === true
|
|
3473
|
-
: (currentProvider
|
|
3515
|
+
: (currentProvider === 'claude-api'
|
|
3516
|
+
? getClaudeApiProviderSettings().enabled === true
|
|
3517
|
+
: (currentProvider !== 'openai' || getOpenAIProviderSettings().configured === true)))));
|
|
3474
3518
|
if (providerWasUpdated && canRestorePreviousProvider) {
|
|
3475
3519
|
stmts.updateRuntimeSessionProvider.run(
|
|
3476
3520
|
currentProvider,
|
|
@@ -5205,7 +5249,18 @@ function createCompactedConversation(sourceConversationId) {
|
|
|
5205
5249
|
// The new conversation stays in the old one's session group, and heads
|
|
5206
5250
|
// it if the old one did; the archived source stays a member.
|
|
5207
5251
|
conversationGroups.carryToCompacted(sourceConversationId, targetConversationId, now);
|
|
5208
|
-
|
|
5252
|
+
// A Claude chat keeps its billing (the subscription or the API key, a
|
|
5253
|
+
// pending swap's included): the new chat's first worker launches on it,
|
|
5254
|
+
// and it is never guessed again from a Claude model id. Other providers
|
|
5255
|
+
// bind as before.
|
|
5256
|
+
const sourceRuntime = stmts.getRuntimeSessionByConversation.get(sourceConversationId) || null;
|
|
5257
|
+
const sourceBilling = normalizeClaudeBilling(sourceRuntime?.provider_type);
|
|
5258
|
+
targetRuntimeSession = ensureRuntimeSessionBinding(targetConversationId, null, now, null, sourceBilling
|
|
5259
|
+
? {
|
|
5260
|
+
providerType: sourceBilling,
|
|
5261
|
+
providerModel: String(sourceRuntime?.provider_model || sourceRuntime?.model || '').trim() || null,
|
|
5262
|
+
}
|
|
5263
|
+
: {});
|
|
5209
5264
|
});
|
|
5210
5265
|
tx();
|
|
5211
5266
|
// Its schedules go on in the new conversation, both the ones that fire
|
|
@@ -5491,6 +5546,67 @@ const usageLimitPauseService = createUsageLimitPauseService({
|
|
|
5491
5546
|
logger: console,
|
|
5492
5547
|
});
|
|
5493
5548
|
|
|
5549
|
+
// What turns on the saved Claude API key cost, booked from both Claude
|
|
5550
|
+
// workers' reports (services/claude-api-usage-service.mjs); the Claude API
|
|
5551
|
+
// card and its cap read it.
|
|
5552
|
+
const claudeApiUsageService = createClaudeApiUsageService({ db, logger: console });
|
|
5553
|
+
// This month's spend on the key against the cap, and the Check Usage card
|
|
5554
|
+
// (services/plan-usage-claude-api.mjs), over the same local days.
|
|
5555
|
+
const claudeApiPlanUsage = createClaudeApiPlanUsage({
|
|
5556
|
+
usageService: claudeApiUsageService,
|
|
5557
|
+
getSettings: () => getClaudeApiProviderSettings(),
|
|
5558
|
+
});
|
|
5559
|
+
|
|
5560
|
+
// A Claude chat's billing: the subscription or the saved API key, switched
|
|
5561
|
+
// between turns (services/claude-billing-switch-service.mjs). A switch to the
|
|
5562
|
+
// key releases a turn held at the subscription's usage limit.
|
|
5563
|
+
const claudeBillingSwitchService = createClaudeBillingSwitchService({
|
|
5564
|
+
db,
|
|
5565
|
+
stmts,
|
|
5566
|
+
emit: (event, payload) => io.emit(event, payload),
|
|
5567
|
+
getClaudeProviderSettings: () => getClaudeProviderSettings(),
|
|
5568
|
+
getClaudeApiProviderSettings: () => getClaudeApiProviderSettings(),
|
|
5569
|
+
getClaudeApiCapState: () => getClaudeApiCapState(),
|
|
5570
|
+
releaseUsageLimitPause: (args) => usageLimitPauseService.releaseForBillingSwitch(args),
|
|
5571
|
+
// What the swap waits for: a turn, a question, a compaction, background tasks.
|
|
5572
|
+
getPending: (runtime) => claudeBillingSwapService.pendingFor(runtime?.sdk_session_id || runtime?.conversation_id),
|
|
5573
|
+
onQueueChanged: (reason) => {
|
|
5574
|
+
void primePendingSessionWorkers(reason);
|
|
5575
|
+
sessionWorkerWebSocketService?.emitQueueChanged?.(reason);
|
|
5576
|
+
},
|
|
5577
|
+
logger: console,
|
|
5578
|
+
});
|
|
5579
|
+
|
|
5580
|
+
// When the Claude API cap flips (a booked report, the cap setting, a new
|
|
5581
|
+
// month), every open Claude chat's billing state is announced again: the
|
|
5582
|
+
// composer's toggle and the usage-limit banner's switch button follow it.
|
|
5583
|
+
const listOpenClaudeChats = db.prepare(`
|
|
5584
|
+
SELECT rs.conversation_id AS id FROM runtime_sessions rs
|
|
5585
|
+
JOIN conversations c ON c.id = rs.conversation_id
|
|
5586
|
+
WHERE rs.provider_type IN ('claude', 'claude-api') AND COALESCE(c.archived, 0) = 0
|
|
5587
|
+
AND COALESCE(c.status, '') != 'deleted'
|
|
5588
|
+
`);
|
|
5589
|
+
// The notes for self-started turns stopped on the key, once per chat per
|
|
5590
|
+
// episode: a cap that clears ends every chat's episode (the settings route
|
|
5591
|
+
// clears it on a save, a claimed turn on the key for its chat).
|
|
5592
|
+
const claudeApiStopLedger = createClaudeApiStopNoteLedger();
|
|
5593
|
+
const claudeApiCapWatch = createClaudeApiCapWatch({
|
|
5594
|
+
getCapState: () => getClaudeApiCapState(),
|
|
5595
|
+
onChange: ({ reached, reason }) => {
|
|
5596
|
+
if (!reached) claudeApiStopLedger.clearAll();
|
|
5597
|
+
const chats = listOpenClaudeChats.all();
|
|
5598
|
+
console.log(`[claude-api-cap] ${reached ? 'reached' : 'cleared'} (${reason}): announcing ${chats.length} Claude chat(s)`);
|
|
5599
|
+
for (const { id } of chats) {
|
|
5600
|
+
const billing = claudeBillingSwitchService.billingState(id);
|
|
5601
|
+
if (billing) io.emit('conversation_billing', { conversationId: id, billing });
|
|
5602
|
+
}
|
|
5603
|
+
},
|
|
5604
|
+
logger: console,
|
|
5605
|
+
});
|
|
5606
|
+
claudeApiCapWatch.check('boot');
|
|
5607
|
+
runtimeTimers.claudeApiCapClock = setInterval(() => claudeApiCapWatch.check('clock'), 5 * 60 * 1000);
|
|
5608
|
+
runtimeTimers.claudeApiCapClock.unref?.();
|
|
5609
|
+
|
|
5494
5610
|
// Turns a restart cut off for good (their worker went with the relay, and a
|
|
5495
5611
|
// second delivery would run the prompt twice): with the setting on, their
|
|
5496
5612
|
// session gets one note to carry on. The rows processing right now are the
|
|
@@ -5544,7 +5660,7 @@ relayEventService = createRelayEventService({
|
|
|
5544
5660
|
});
|
|
5545
5661
|
|
|
5546
5662
|
// ─── Relay schedules ──────────────────────────────────────────────────────────
|
|
5547
|
-
// Prompts agents scheduled with
|
|
5663
|
+
// Prompts agents scheduled with relay_tool's `schedule`: kept in the
|
|
5548
5664
|
// database, their timers rebuilt at boot (services/relay-schedule-service.mjs).
|
|
5549
5665
|
const relayScheduleService = createRelayScheduleService({
|
|
5550
5666
|
db,
|
|
@@ -5562,7 +5678,7 @@ const relayScheduleService = createRelayScheduleService({
|
|
|
5562
5678
|
});
|
|
5563
5679
|
|
|
5564
5680
|
// ─── Questions that don't block ───────────────────────────────────────────────
|
|
5565
|
-
//
|
|
5681
|
+
// relay_tool's `ask`: a card in the session and the Inbox; the answer
|
|
5566
5682
|
// reaches the askers as a relay event (services/relay-ask-service.mjs).
|
|
5567
5683
|
const relayAskService = createRelayAskService({
|
|
5568
5684
|
db,
|
|
@@ -6309,7 +6425,7 @@ function markSharedViewerPresence({ conversationId, token, viewerId } = {}) {
|
|
|
6309
6425
|
}
|
|
6310
6426
|
return { ok: true, watcherCount, capped: false };
|
|
6311
6427
|
}
|
|
6312
|
-
async function requestSessionWorkerSocketDelivery({ sessionId, pid, reason = 'worker-ready' } = {}) {
|
|
6428
|
+
async function requestSessionWorkerSocketDelivery({ sessionId, pid, reason = 'worker-ready', workerKind = null } = {}) {
|
|
6313
6429
|
const requesterSessionId = String(sessionId || '').trim();
|
|
6314
6430
|
if (!requesterSessionId) return { message: null, reason: 'missing-session-id' };
|
|
6315
6431
|
touchCli();
|
|
@@ -6367,6 +6483,10 @@ async function requestSessionWorkerSocketDelivery({ sessionId, pid, reason = 'wo
|
|
|
6367
6483
|
|
|
6368
6484
|
const counts = queueCounts();
|
|
6369
6485
|
const existingWorker = sessionWorkerRegistry?.getWorker?.(requesterSessionId) || null;
|
|
6486
|
+
// The kind the socket's worker stated is registered with it, so what a
|
|
6487
|
+
// response ran on is known from the first turn on (not from its first
|
|
6488
|
+
// heartbeat).
|
|
6489
|
+
const statedKind = normalizeWorkerKind(workerKind);
|
|
6370
6490
|
if (!existingWorker) {
|
|
6371
6491
|
sessionWorkerRegistry?.upsertWorker?.({
|
|
6372
6492
|
sdkSessionId: requesterSessionId,
|
|
@@ -6374,6 +6494,7 @@ async function requestSessionWorkerSocketDelivery({ sessionId, pid, reason = 'wo
|
|
|
6374
6494
|
status: 'ready',
|
|
6375
6495
|
pid: pid || null,
|
|
6376
6496
|
queueDepth: Math.max(0, Number(counts.pendingCount || 0)),
|
|
6497
|
+
...(statedKind ? { kind: statedKind } : {}),
|
|
6377
6498
|
});
|
|
6378
6499
|
} else if (pid && existingWorker.pid !== pid) {
|
|
6379
6500
|
sessionWorkerRegistry?.upsertWorker?.({
|
|
@@ -6382,7 +6503,10 @@ async function requestSessionWorkerSocketDelivery({ sessionId, pid, reason = 'wo
|
|
|
6382
6503
|
pid,
|
|
6383
6504
|
status: existingWorker.status || 'ready',
|
|
6384
6505
|
queueDepth: Math.max(0, Number(counts.pendingCount || 0)),
|
|
6506
|
+
kind: statedKind,
|
|
6385
6507
|
});
|
|
6508
|
+
} else if (statedKind && existingWorker.kind !== statedKind) {
|
|
6509
|
+
sessionWorkerRegistry?.upsertWorker?.({ ...existingWorker, kind: statedKind });
|
|
6386
6510
|
}
|
|
6387
6511
|
|
|
6388
6512
|
const dequeueResult = await dequeuePendingMessageForWorkerLoop({
|
|
@@ -6396,6 +6520,11 @@ async function requestSessionWorkerSocketDelivery({ sessionId, pid, reason = 'wo
|
|
|
6396
6520
|
sessionWorkerSupervisor,
|
|
6397
6521
|
relayRestartOrchestrator,
|
|
6398
6522
|
inFlightProcessingCount: Number(counts.processingCount || 0),
|
|
6523
|
+
// A Claude chat's row goes only to a worker of its billing: the socket's
|
|
6524
|
+
// own word, else what the worker last said in a heartbeat.
|
|
6525
|
+
workerKind: workerKind || sessionWorkerRegistry?.getWorker?.(requesterSessionId)?.kind || null,
|
|
6526
|
+
claudeApiClaimCheck,
|
|
6527
|
+
onBillingFence: handleClaudeBillingFence,
|
|
6399
6528
|
});
|
|
6400
6529
|
if (!dequeueResult?.message) {
|
|
6401
6530
|
return {
|
|
@@ -6439,11 +6568,14 @@ async function requestSessionWorkerSocketDelivery({ sessionId, pid, reason = 'wo
|
|
|
6439
6568
|
}
|
|
6440
6569
|
console.log(`${runtimeLogPrefix()}WS DEQUEUE ${out.id.slice(0, 8)} session=${requesterSessionId.slice(0, 8)} reason=${String(reason || 'worker-ready')}`);
|
|
6441
6570
|
io.emit('message_status', { messageId: out.id, conversationId: out.conversationId, status: 'processing' });
|
|
6571
|
+
// The first message after a billing swap tells the new worker so.
|
|
6572
|
+
const billingSwap = out?.ownerSessionId ? claudeBillingSwapService.takeSwapNote(out.ownerSessionId) : null;
|
|
6442
6573
|
return {
|
|
6443
6574
|
message: out,
|
|
6444
6575
|
// Piggybacked settings so workers pick up slider changes on the next
|
|
6445
6576
|
// delivery without a restart.
|
|
6446
6577
|
settings: {
|
|
6578
|
+
...(billingSwap ? { billingSwap } : {}),
|
|
6447
6579
|
backgroundTaskTimeoutMs: backgroundTaskTimeoutMinutesToMs(getBackgroundTaskTimeoutMinutes()),
|
|
6448
6580
|
// The user's max-turn-duration ceiling (0 = no limit): worker-local
|
|
6449
6581
|
// watchdogs (Grok's prompt ceiling) must honor it rather than imposing
|
|
@@ -6479,15 +6611,18 @@ async function requestSessionWorkerSocketDelivery({ sessionId, pid, reason = 'wo
|
|
|
6479
6611
|
claudeBackgroundCommandLimitMs: claudeBackgroundCommandLimitMinutesToMs(getClaudeBackgroundCommandLimitMinutes()),
|
|
6480
6612
|
};
|
|
6481
6613
|
}
|
|
6482
|
-
|
|
6614
|
+
// A Claude API session is a Claude session on another bill:
|
|
6615
|
+
// Claude's attribution and background command limit.
|
|
6616
|
+
const localClaude = deliveredProvider === 'claude' || deliveredProvider === 'claude-api';
|
|
6617
|
+
if (!localClaude && deliveredProvider !== 'claude-cloud') return {};
|
|
6483
6618
|
return {
|
|
6484
6619
|
attribution: claudeAttributionForConversation(conv, out.providerModel || out.model, {
|
|
6485
|
-
relayFolderFallback:
|
|
6620
|
+
relayFolderFallback: localClaude,
|
|
6486
6621
|
}),
|
|
6487
6622
|
// The Claude tab's background command limit; the worker
|
|
6488
6623
|
// hands it to the CLI it spawns next. Claude Cloud runs no
|
|
6489
6624
|
// local CLI, so it does not get one.
|
|
6490
|
-
...(
|
|
6625
|
+
...(localClaude
|
|
6491
6626
|
? { claudeBackgroundCommandLimitMs: claudeBackgroundCommandLimitMinutesToMs(getClaudeBackgroundCommandLimitMinutes()) }
|
|
6492
6627
|
: {}),
|
|
6493
6628
|
};
|
|
@@ -6699,6 +6834,177 @@ async function sweepDeadWorkerProcessingRowsOnce() {
|
|
|
6699
6834
|
return recovered;
|
|
6700
6835
|
}
|
|
6701
6836
|
|
|
6837
|
+
// The Claude billing switch's worker swap (services/
|
|
6838
|
+
// claude-billing-swap-service.mjs): a Claude chat's next row of the other
|
|
6839
|
+
// billing is refused to the worker it has, which is replaced, once idle, by
|
|
6840
|
+
// one of the row's kind. The old one is stopped like the kill route does, and
|
|
6841
|
+
// the new one launched only once every process of the session is gone.
|
|
6842
|
+
const claudeBillingSwapService = createClaudeBillingSwapService({
|
|
6843
|
+
getWorker: (sid) => sessionWorkerRegistry?.getWorker?.(sid) || null,
|
|
6844
|
+
// A row the session claimed and has not finished: never stopped under it.
|
|
6845
|
+
hasRunningTurn: (sid) => (stmts.listProcessingRowsForOwner?.all?.(sid) || []).length > 0,
|
|
6846
|
+
stopWorker: async (sid) => {
|
|
6847
|
+
const worker = sessionWorkerRegistry?.getWorker?.(sid) || null;
|
|
6848
|
+
// Nothing may start the session while it is being stopped: a launch then
|
|
6849
|
+
// would adopt the old process, kind and all.
|
|
6850
|
+
sessionWorkerSupervisor?.markKilled?.(sid);
|
|
6851
|
+
// A stop that fails backs off (the swap service retries it): the killed
|
|
6852
|
+
// marker goes with it, so the session's worker, alive or relaunched,
|
|
6853
|
+
// keeps asking for work meanwhile. The fence still keeps the other
|
|
6854
|
+
// billing's rows from it.
|
|
6855
|
+
const backOff = (error, remainingPids = []) => {
|
|
6856
|
+
sessionWorkerSupervisor?.clearRestartSchedule?.(sid, { resetKilledMarker: true });
|
|
6857
|
+
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`);
|
|
6858
|
+
return { ok: false, error, remainingPids };
|
|
6859
|
+
};
|
|
6860
|
+
let stopped = null;
|
|
6861
|
+
try {
|
|
6862
|
+
await sessionWorkerSupervisor?.cancelPendingStart?.(sid, { wait: true });
|
|
6863
|
+
stopped = await stopSessionWorkerProcesses({
|
|
6864
|
+
sdkSessionId: sid,
|
|
6865
|
+
worker,
|
|
6866
|
+
processInspector: sessionWorkerProcessInspector,
|
|
6867
|
+
isPidAliveImpl: isProcessAlive,
|
|
6868
|
+
killTmuxSessionImpl: killTmuxSession,
|
|
6869
|
+
});
|
|
6870
|
+
} catch (error) {
|
|
6871
|
+
return backOff(String(error?.message || error || 'worker-stop-failed'));
|
|
6872
|
+
}
|
|
6873
|
+
if (!stopped.ok) return backOff(stopped.error || 'worker-survived-stop', stopped.remainingPids || []);
|
|
6874
|
+
sessionWorkerRegistry?.removeWorker?.(sid);
|
|
6875
|
+
sessionWorkerSupervisor?.resetHealth?.(sid, { clearFailureCount: false });
|
|
6876
|
+
return { ok: true };
|
|
6877
|
+
},
|
|
6878
|
+
launchWorker: async (sid) => {
|
|
6879
|
+
sessionWorkerSupervisor?.clearRestartSchedule?.(sid, { resetKilledMarker: true });
|
|
6880
|
+
const launched = await sessionWorkerSupervisor?.ensureWorker?.(sid, { allowProcessReuse: false });
|
|
6881
|
+
return { ok: launched?.ok === true, error: launched?.error || null };
|
|
6882
|
+
},
|
|
6883
|
+
onStuck: ({ conversationId, rowBilling, attempts, reason }) => {
|
|
6884
|
+
if (!conversationId) return;
|
|
6885
|
+
const at = new Date().toISOString();
|
|
6886
|
+
const messageId = uuidv4();
|
|
6887
|
+
const cause = reason === 'probe'
|
|
6888
|
+
? `the relay cannot tell whether the old Claude process still runs a turn (${attempts} tries)`
|
|
6889
|
+
: `the old Claude process did not stop (${attempts} tries)`;
|
|
6890
|
+
const text = `⚠️ Could not restart the session on the ${rowBilling === 'claude-api' ? 'Claude API key' : 'Claude subscription'}: `
|
|
6891
|
+
+ `${cause}. The relay keeps trying; switching the billing back runs the queued messages on the old process.`;
|
|
6892
|
+
try {
|
|
6893
|
+
db.transaction(() => {
|
|
6894
|
+
stmts.insertMsg.run(messageId, conversationId, 'assistant', text, null, null, null, at, null, null, null);
|
|
6895
|
+
stmts.setMessageKind?.run?.('billing-switch-stuck', messageId);
|
|
6896
|
+
stmts.updateConvTime?.run?.(at, conversationId);
|
|
6897
|
+
})();
|
|
6898
|
+
io.emit('assistant_message', {
|
|
6899
|
+
conversationId,
|
|
6900
|
+
messageId,
|
|
6901
|
+
message: { id: messageId, role: 'assistant', text, kind: 'billing-switch-stuck', model: null, mode: null, attachments: [], timestamp: at },
|
|
6902
|
+
});
|
|
6903
|
+
} catch (error) {
|
|
6904
|
+
console.warn(`[claude-billing] stuck note failed: ${error?.message || error}`);
|
|
6905
|
+
}
|
|
6906
|
+
},
|
|
6907
|
+
onPendingChange: ({ conversationId }) => {
|
|
6908
|
+
if (!conversationId) return;
|
|
6909
|
+
io.emit('conversation_billing', { conversationId, billing: claudeBillingSwitchService.billingState(conversationId) });
|
|
6910
|
+
},
|
|
6911
|
+
logger: console,
|
|
6912
|
+
});
|
|
6913
|
+
|
|
6914
|
+
// The Claude API monthly cap (plan decision 12): `{ reached, error? }`. The
|
|
6915
|
+
// one seam the claim check below, the billing switch, an agent's send and a
|
|
6916
|
+
// new Claude API chat ask: this month's priced estimate in
|
|
6917
|
+
// claude_api_usage_totals (relay local days, claudeApiUsageService.localDay)
|
|
6918
|
+
// against claude_api_monthly_cap_usd (services/plan-usage-claude-api.mjs).
|
|
6919
|
+
// It throws when the totals cannot be read; every caller counts that as reached.
|
|
6920
|
+
function getClaudeApiCapState() {
|
|
6921
|
+
return claudeApiPlanUsage.getCapState();
|
|
6922
|
+
}
|
|
6923
|
+
|
|
6924
|
+
// A Claude API row must not run while the provider is off, has no key, or
|
|
6925
|
+
// this month's cap is reached (getClaudeApiCapState): it is failed closed at
|
|
6926
|
+
// the claim, with the way out in its note.
|
|
6927
|
+
function claudeApiClaimCheck() {
|
|
6928
|
+
const settings = getClaudeApiProviderSettings();
|
|
6929
|
+
if (settings?.configured !== true) {
|
|
6930
|
+
return { code: 'CLAUDE_API_NOT_CONFIGURED', error: 'Claude API has no key, so this turn did not run.' };
|
|
6931
|
+
}
|
|
6932
|
+
if (settings?.enabled !== true) {
|
|
6933
|
+
return { code: 'CLAUDE_API_NOT_CONFIGURED', error: 'Claude API is switched off, so this turn did not run.', switchedOff: true };
|
|
6934
|
+
}
|
|
6935
|
+
let cap = null;
|
|
6936
|
+
try { cap = getClaudeApiCapState() || null; } catch { cap = { reached: true }; }
|
|
6937
|
+
if (cap?.reached === true) {
|
|
6938
|
+
return {
|
|
6939
|
+
code: 'CLAUDE_API_CAP_REACHED',
|
|
6940
|
+
error: `${String(cap.error || '').trim() || 'This month\'s Claude API spend has reached its cap.'} This turn did not run.`,
|
|
6941
|
+
};
|
|
6942
|
+
}
|
|
6943
|
+
return null;
|
|
6944
|
+
}
|
|
6945
|
+
|
|
6946
|
+
// A Claude API chat's worker is not launched while Claude API cannot run a
|
|
6947
|
+
// turn (no key, switched off, the cap): its waiting rows on the key are
|
|
6948
|
+
// failed closed here, as the claim would, instead of a spawn that could only
|
|
6949
|
+
// refuse them. True when it was so.
|
|
6950
|
+
const listPendingClaudeApiRowsForOwner = db.prepare(`
|
|
6951
|
+
SELECT id FROM queue
|
|
6952
|
+
WHERE status = 'pending' AND NULLIF(owner_sdk_session_id, '') = ?
|
|
6953
|
+
AND COALESCE(kind, '') != 'continuation'
|
|
6954
|
+
AND LOWER(COALESCE(${stmts.setQueueProvider ? 'provider' : 'NULL'}, ?)) = 'claude-api'
|
|
6955
|
+
`);
|
|
6956
|
+
function failClaudeApiRowsWithoutLaunch(sdkSessionId) {
|
|
6957
|
+
const runtime = stmts.getRuntimeSessionBySdkSessionId.get(sdkSessionId) || null;
|
|
6958
|
+
if (normalizeClaudeBilling(runtime?.provider_type) !== 'claude-api') return false;
|
|
6959
|
+
// A check that cannot answer fails this session's rows on the key closed,
|
|
6960
|
+
// as the claim does (messages-routes.mjs, claudeBillingFence).
|
|
6961
|
+
let refusal = null;
|
|
6962
|
+
try {
|
|
6963
|
+
refusal = claudeApiClaimCheck();
|
|
6964
|
+
} catch (error) {
|
|
6965
|
+
console.warn(`${runtimeLogPrefix()}Claude API claim check failed for session=${sdkSessionId.slice(0, 8)}: ${error?.message || error} — its rows on the key fail closed`);
|
|
6966
|
+
refusal = {
|
|
6967
|
+
code: 'CLAUDE_API_NOT_CONFIGURED',
|
|
6968
|
+
error: 'The relay could not check Claude API (key, switch, cap), so this turn did not run.',
|
|
6969
|
+
};
|
|
6970
|
+
}
|
|
6971
|
+
if (!refusal) return false;
|
|
6972
|
+
for (const row of listPendingClaudeApiRowsForOwner.all(sdkSessionId, 'claude-api')) {
|
|
6973
|
+
handleClaudeBillingFence({ reason: 'claude-api-unavailable', messageId: row.id, refusal });
|
|
6974
|
+
}
|
|
6975
|
+
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`);
|
|
6976
|
+
return true;
|
|
6977
|
+
}
|
|
6978
|
+
|
|
6979
|
+
// The delivery refused a Claude chat's row to the worker that asked
|
|
6980
|
+
// (messages-routes.mjs, claudeBillingFence).
|
|
6981
|
+
function handleClaudeBillingFence(fence) {
|
|
6982
|
+
if (fence?.reason === 'claude-api-unavailable') {
|
|
6983
|
+
const row = stmts.findQById.get(fence.messageId);
|
|
6984
|
+
if (!row || String(row.status || '') !== 'pending') return;
|
|
6985
|
+
const code = String(fence.refusal?.code || 'CLAUDE_API_NOT_CONFIGURED');
|
|
6986
|
+
failRecoveredRowTerminally(row, 'claude-api-unavailable', {
|
|
6987
|
+
failureRecord: {
|
|
6988
|
+
kind: 'claude-api-unavailable',
|
|
6989
|
+
error: 'claude-api-unavailable',
|
|
6990
|
+
code,
|
|
6991
|
+
stableCode: `relay.${code.toLowerCase().replace(/_/g, '-')}`,
|
|
6992
|
+
failedAt: new Date().toISOString(),
|
|
6993
|
+
},
|
|
6994
|
+
// The code ends the note's paragraph, where the transcript's buttons
|
|
6995
|
+
// look for it (relay-error-ctas.mjs).
|
|
6996
|
+
failureText: `${fence.refusal?.error || 'Claude API is unavailable, so this turn did not run.'} `
|
|
6997
|
+
+ `${claudeApiRemedyText({ code, switchedOff: fence.refusal?.switchedOff === true })}, then send the message again. `
|
|
6998
|
+
+ `Error code: relay.${code.toLowerCase().replace(/_/g, '-')}.`,
|
|
6999
|
+
});
|
|
7000
|
+
return;
|
|
7001
|
+
}
|
|
7002
|
+
const result = claudeBillingSwapService.request(fence);
|
|
7003
|
+
if (result.state === 'swapping') {
|
|
7004
|
+
console.log(`${runtimeLogPrefix()}BILLING SWAP session=${String(fence.requesterSessionId || '').slice(0, 8)} conv=${String(fence.conversationId || '').slice(0, 8)} ${fence.workerKind || 'unknown'} → ${fence.rowBilling}`);
|
|
7005
|
+
}
|
|
7006
|
+
}
|
|
7007
|
+
|
|
6702
7008
|
// A worker alive without its socket while a message waits: replaced after a
|
|
6703
7009
|
// grace time (see the service). Stops the way the kill route does, but only
|
|
6704
7010
|
// names the registry's pid when the process list confirms it is the
|
|
@@ -6769,6 +7075,14 @@ const sessionWorkerWebSocketService = createSessionWorkerWebSocketService({
|
|
|
6769
7075
|
onDeliverySendFailed: recoverUndeliveredSessionWorkerMessage,
|
|
6770
7076
|
onWorkerSocketClosed: handleWorkerSocketClosed,
|
|
6771
7077
|
requestWork: requestSessionWorkerSocketDelivery,
|
|
7078
|
+
// What a worker says it is ("claude", "claude-api", ...): the registry
|
|
7079
|
+
// keeps it for the billing fence and for what a response ran on.
|
|
7080
|
+
onWorkerKind: (sessionId, kind) => {
|
|
7081
|
+
const sid = String(sessionId || '').trim();
|
|
7082
|
+
if (!sid) return;
|
|
7083
|
+
const worker = sessionWorkerRegistry?.getWorker?.(sid) || null;
|
|
7084
|
+
if (worker && worker.kind !== kind) sessionWorkerRegistry?.upsertWorker?.({ ...worker, kind });
|
|
7085
|
+
},
|
|
6772
7086
|
isWorkerProcessAlive: (pid) => isProcessAlive(Number(pid)),
|
|
6773
7087
|
pathPrefix: remotePath,
|
|
6774
7088
|
pollIntervalMs: 500,
|
|
@@ -6811,6 +7125,14 @@ async function primePendingSessionWorkers(reason = 'pending-worker-prime') {
|
|
|
6811
7125
|
const sdkSessionId = String(row?.sdk_session_id || '').trim();
|
|
6812
7126
|
if (!sdkSessionId) continue;
|
|
6813
7127
|
if (sessionWorkerSupervisor?.isKillBlocked?.(sdkSessionId) === true) continue;
|
|
7128
|
+
try {
|
|
7129
|
+
// Per session: a throw here costs this session its launch, never the
|
|
7130
|
+
// rest of the pass.
|
|
7131
|
+
if (failClaudeApiRowsWithoutLaunch(sdkSessionId)) continue;
|
|
7132
|
+
} catch (error) {
|
|
7133
|
+
console.warn(`${runtimeLogPrefix()}WORKER PRIME skipped session=${sdkSessionId.slice(0, 8)} err=${error?.message || error}`);
|
|
7134
|
+
continue;
|
|
7135
|
+
}
|
|
6814
7136
|
requested += 1;
|
|
6815
7137
|
try {
|
|
6816
7138
|
const result = await sessionWorkerSupervisor.ensureWorker(sdkSessionId);
|
|
@@ -7057,7 +7379,7 @@ const remoteRelayInbound = {
|
|
|
7057
7379
|
},
|
|
7058
7380
|
repository: remoteRelayRepository,
|
|
7059
7381
|
};
|
|
7060
|
-
// The
|
|
7382
|
+
// The relay_tool: who is calling (live turn, provider, model, mode,
|
|
7061
7383
|
// hops) and the Allow / Deny card write actions need in ask/plan mode.
|
|
7062
7384
|
const remoteRelayCallerContext = createRemoteRelayCallerContext({
|
|
7063
7385
|
db,
|
|
@@ -7175,6 +7497,8 @@ const sharedRouteDeps = {
|
|
|
7175
7497
|
deepseekSettingsService,
|
|
7176
7498
|
deepseekBalanceService,
|
|
7177
7499
|
deepseekUsageImportService,
|
|
7500
|
+
getClaudeApiProviderSettings,
|
|
7501
|
+
claudeApiSettingsService,
|
|
7178
7502
|
claudeCloudRepoService,
|
|
7179
7503
|
// The login's state for the workers' nudge route (never the token).
|
|
7180
7504
|
claudeCloudLoginStatus: { describe: () => claudeCloudCredentials.describe() },
|
|
@@ -7321,6 +7645,14 @@ const sharedRouteDeps = {
|
|
|
7321
7645
|
hostSuspendService,
|
|
7322
7646
|
collectHostActivity,
|
|
7323
7647
|
usageLimitPauseService,
|
|
7648
|
+
claudeBillingSwitchService,
|
|
7649
|
+
claudeApiUsageService,
|
|
7650
|
+
claudeApiClaimCheck,
|
|
7651
|
+
onClaudeBillingFence: handleClaudeBillingFence,
|
|
7652
|
+
claudeApiPlanUsage,
|
|
7653
|
+
getClaudeApiCapState,
|
|
7654
|
+
checkClaudeApiCap: (reason) => claudeApiCapWatch.check(reason),
|
|
7655
|
+
claudeApiStopLedger,
|
|
7324
7656
|
relayEventService,
|
|
7325
7657
|
relayScheduleService,
|
|
7326
7658
|
relayAskService,
|
|
@@ -7420,6 +7752,7 @@ registerAboutRoutes(app, {
|
|
|
7420
7752
|
'claude-cloud': { enabled: getClaudeCloudProviderSettings()?.enabled === true },
|
|
7421
7753
|
cursor: { enabled: getCursorProviderSettings()?.enabled === true },
|
|
7422
7754
|
grok: { enabled: getGrokProviderSettings()?.enabled === true },
|
|
7755
|
+
'claude-api': { enabled: getClaudeApiProviderSettings()?.enabled === true },
|
|
7423
7756
|
}),
|
|
7424
7757
|
getCachedCliProviders: () => cliInstallService.getCachedStatusSnapshot()?.providers || {},
|
|
7425
7758
|
isRelayCliOnline: () => runtimeState.cliOnline === true,
|
|
@@ -7526,6 +7859,13 @@ runtimeTimers.questionExpiry = setInterval(expirePendingQuestions, 10_000);
|
|
|
7526
7859
|
expirePendingQuestions();
|
|
7527
7860
|
runtimeTimers.sharedViewerPrune = setInterval(pruneSharedViewerPresence, 5_000);
|
|
7528
7861
|
if (typeof runtimeTimers.sharedViewerPrune.unref === 'function') runtimeTimers.sharedViewerPrune.unref();
|
|
7862
|
+
// Memory in the status log: now, every 10 minutes, and once per crossing of
|
|
7863
|
+
// 80 % of the heap limit (relay-heap-guard.mjs).
|
|
7864
|
+
{
|
|
7865
|
+
const memoryTimers = startRelayMemoryLog({ logger: console });
|
|
7866
|
+
runtimeTimers.memoryLog = memoryTimers.log;
|
|
7867
|
+
runtimeTimers.memoryWatch = memoryTimers.check;
|
|
7868
|
+
}
|
|
7529
7869
|
const sweptUploads = sweepUnreferencedUploadBlobs();
|
|
7530
7870
|
if (sweptUploads > 0) {
|
|
7531
7871
|
console.log(`${runtimeLogPrefix()}Reclaimed ${sweptUploads} unreferenced upload(s)`);
|
|
@@ -7665,10 +8005,17 @@ function ensureRuntimeSessionBinding(
|
|
|
7665
8005
|
? 'claude-cloud'
|
|
7666
8006
|
: (normalizedRequestedProviderType === 'deepseek'
|
|
7667
8007
|
? 'deepseek'
|
|
7668
|
-
|
|
7669
|
-
|
|
7670
|
-
|
|
7671
|
-
|
|
8008
|
+
// Never folded into `claude`: the two bill different accounts.
|
|
8009
|
+
: (normalizedRequestedProviderType === 'claude-api'
|
|
8010
|
+
? 'claude-api'
|
|
8011
|
+
: (normalizedRequestedProviderType === 'github'
|
|
8012
|
+
? 'github'
|
|
8013
|
+
: (openAISettings?.enabled ? 'openai' : 'github'))))))));
|
|
8014
|
+
const resolvedProviderModel = (
|
|
8015
|
+
resolvedProviderType === 'openai' || resolvedProviderType === 'claude' || resolvedProviderType === 'cursor'
|
|
8016
|
+
|| resolvedProviderType === 'grok' || resolvedProviderType === 'claude-cloud' || resolvedProviderType === 'deepseek'
|
|
8017
|
+
|| resolvedProviderType === 'claude-api'
|
|
8018
|
+
)
|
|
7672
8019
|
? (String(providerModel || normalizedModel || '').trim() || null)
|
|
7673
8020
|
: null;
|
|
7674
8021
|
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();
|