@oar-sh/oar 0.9.16 → 0.9.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/extensions/web-relay/polling/polling-loop.mjs +14 -87
- package/.github/extensions/web-relay/skills/tool-activity.mjs +5 -4
- package/CHANGELOG.md +412 -0
- package/README.md +15 -13
- package/package.json +2 -1
- package/server/claude-api-worker/claude-api-backend.mjs +345 -0
- package/server/claude-api-worker/claude-api-session-worker.mjs +29 -0
- package/server/claude-worker/claude-attachments.mjs +7 -4
- package/server/claude-worker/claude-cli-pinned-env.mjs +150 -0
- package/server/claude-worker/claude-remote-relay-tool.mjs +3 -3
- package/server/claude-worker/claude-sdk-adapter.mjs +199 -15
- package/server/claude-worker/claude-session-process.mjs +276 -33
- package/server/claude-worker/claude-session-worker-main.mjs +269 -0
- package/server/claude-worker/claude-session-worker.mjs +2 -253
- package/server/claude-worker/claude-subscription-key-check.mjs +64 -0
- package/server/claude-worker/claude-turn-failure.mjs +8 -0
- package/server/claude-worker/claude-turn-publisher.mjs +394 -13
- package/server/claude-worker/claude-usage-totals.mjs +161 -0
- package/server/claude-worker/sdk-message-normalizer.mjs +56 -1
- package/server/copilot-worker/copilot-remote-relay-tool.mjs +2 -2
- package/server/copilot-worker/copilot-sdk-adapter.mjs +18 -4
- package/server/copilot-worker/copilot-sdk-event-normalizer.mjs +1 -1
- package/server/copilot-worker/copilot-sdk-session-process.mjs +80 -14
- package/server/cursor-worker/cursor-remote-relay-tool.mjs +3 -3
- package/server/cursor-worker/cursor-turn-runner.mjs +4 -4
- package/server/cursor-worker/sdk-message-normalizer.mjs +2 -2
- package/server/db-schema.mjs +96 -1
- package/server/deepseek-worker/deepseek-claude-backend.mjs +317 -0
- package/server/deepseek-worker/deepseek-session-worker.mjs +37 -0
- package/server/grok-worker/acp-client.mjs +1 -1
- package/server/grok-worker/grok-turn-runner.mjs +4 -4
- package/server/grok-worker/sdk-message-normalizer.mjs +1 -1
- package/server/mcp/oar-mcp-server.mjs +11 -5
- package/server/migrations/0024-deepseek-usage-totals.mjs +151 -0
- package/server/migrations/0025-deepseek-usage-requests-daily.mjs +179 -0
- package/server/migrations/0026-deepseek-usage-served-models.mjs +222 -0
- package/server/migrations/0027-deepseek-usage-checkpoint-runs.mjs +101 -0
- package/server/migrations/0028-deepseek-usage-checkpoint-request-runs.mjs +99 -0
- package/server/migrations/0029-deepseek-balance-rises.mjs +99 -0
- package/server/migrations/0030-deepseek-usage-actuals.mjs +123 -0
- package/server/migrations/0031-queue-provider.mjs +143 -0
- package/server/migrations/0032-runtime-session-billing-switch.mjs +94 -0
- package/server/migrations/0033-claude-api-usage-checkpoints.mjs +138 -0
- package/server/migrations/0034-claude-api-usage-totals.mjs +112 -0
- package/server/public/app/activity-replay-state.mjs +4 -3
- package/server/public/app/agent-sessions-settings-ui.js +1 -1
- package/server/public/app/agent-sessions-settings.mjs +1 -1
- package/server/public/app/api-client.js +105 -0
- package/server/public/app/bootstrap.js +243 -29
- package/server/public/app/claude-api-settings-ui.js +362 -0
- package/server/public/app/claude-billing-toggle-ui.js +224 -0
- package/server/public/app/claude-billing-toggle.mjs +112 -0
- package/server/public/app/context-tier-options.mjs +10 -2
- package/server/public/app/context-usage-view.mjs +1 -1
- package/server/public/app/conversation-provider-indicator.mjs +18 -0
- package/server/public/app/conversation-view.js +31 -3
- package/server/public/app/deepseek-settings-ui.js +342 -0
- package/server/public/app/inbox-ui.js +14 -3
- package/server/public/app/inbox-view.mjs +1 -1
- package/server/public/app/journal-view.js +156 -11
- package/server/public/app/model-selector-options.mjs +28 -1
- package/server/public/app/model-vision-support.mjs +2 -0
- package/server/public/app/new-conversation-model-choice.mjs +21 -0
- package/server/public/app/no-conversation-state.mjs +1 -1
- package/server/public/app/plan-usage-view.mjs +165 -13
- package/server/public/app/provider-readiness.mjs +8 -2
- package/server/public/app/relay-error-ctas.mjs +58 -0
- package/server/public/app/relay-schedules-ui.js +1 -1
- package/server/public/app/settings-modal.js +22 -0
- package/server/public/app/settings-tabs.js +1 -1
- package/server/public/app/socket-handlers.js +28 -1
- package/server/public/app/tool-activity-emoji.mjs +4 -3
- package/server/public/app/usage-limit-ui.js +136 -5
- package/server/public/index.html +596 -35
- package/server/relay-tools.md +4 -4
- package/server/repositories/conversation-group-repository.mjs +14 -7
- package/server/repositories/message-repository.mjs +7 -0
- package/server/repositories/session-repository.mjs +8 -1
- package/server/routes/ask-user-routes.mjs +1 -1
- package/server/routes/conversation-group-routes.mjs +3 -2
- package/server/routes/deepseek-usage-import-routes.mjs +72 -0
- package/server/routes/messages-routes.mjs +831 -36
- package/server/routes/relay-ask-routes.mjs +3 -3
- package/server/routes/relay-event-routes.mjs +2 -2
- package/server/routes/relay-schedule-routes.mjs +1 -1
- package/server/routes/remote-relay-routes.mjs +3 -3
- package/server/routes/sessions-routes.mjs +670 -63
- package/server/server-runtime.mjs +476 -26
- package/server/services/about-service.mjs +2 -0
- package/server/services/background-work-view.mjs +2 -1
- package/server/services/claude-api-settings-service.mjs +575 -0
- package/server/services/claude-api-usage-service.mjs +361 -0
- package/server/services/claude-billing-swap-service.mjs +274 -0
- package/server/services/claude-billing-switch-service.mjs +343 -0
- package/server/services/claude-session-leftovers.mjs +226 -0
- package/server/services/deepseek-balance-service.mjs +102 -0
- package/server/services/deepseek-settings-service.mjs +380 -0
- package/server/services/deepseek-usage-import.mjs +471 -0
- package/server/services/model-variant-catalog-service.mjs +1 -0
- package/server/services/plan-usage-claude-api.mjs +445 -0
- package/server/services/plan-usage-contract.mjs +100 -5
- package/server/services/plan-usage-deepseek.mjs +931 -0
- package/server/services/plan-usage-service.mjs +436 -0
- package/server/services/provider-model-selection.mjs +4 -2
- package/server/services/provider-reasoning-effort.mjs +24 -2
- package/server/services/relay-ask-service.mjs +1 -1
- package/server/services/relay-event-service.mjs +3 -2
- package/server/services/relay-schedule-service.mjs +1 -1
- package/server/services/remote-relay-caller-context.mjs +7 -0
- package/server/services/remote-relay-dispatcher.mjs +84 -14
- package/server/services/remote-relay-registry-service.mjs +1 -1
- package/server/services/session-worker-launch-service.mjs +294 -84
- package/server/services/session-worker-registry-service.mjs +28 -1
- package/server/services/session-worker-websocket-service.mjs +17 -0
- package/server/services/usage-limit-pause-service.mjs +55 -4
- package/server/services/worker-secret-redaction.mjs +320 -0
- package/shared/anthropic-api.mjs +202 -0
- package/shared/context-window-fallbacks.mjs +3 -0
- package/shared/deepseek-api.mjs +137 -0
- package/shared/deepseek-models.mjs +114 -0
- package/shared/deepseek-pricing.mjs +146 -0
- package/shared/host-claude-session-env.mjs +101 -0
- package/shared/model-id.mjs +7 -0
- package/shared/preview-tool-core.mjs +1 -1
- package/shared/provider-routing.mjs +5 -3
- package/shared/relay-asks.mjs +11 -5
- package/shared/relay-events.mjs +2 -2
- package/shared/relay-schedules.mjs +2 -2
- package/shared/remote-relay-contract.mjs +29 -9
- package/shared/remote-relay-tool-core.mjs +23 -6
- package/shared/steer-settle-failure.mjs +2 -0
- package/shared/worker-runtime/api-client.mjs +2 -2
- package/shared/worker-runtime/direct-openai-image.mjs +128 -0
- package/shared/worker-runtime/heartbeat.mjs +5 -0
- package/shared/worker-runtime/worker-log-file.mjs +60 -0
- package/shared/worker-runtime/worker-websocket-link.mjs +6 -0
|
@@ -135,7 +135,7 @@ export function isBackgroundLaunch(block, toolUseResult = null) {
|
|
|
135
135
|
|
|
136
136
|
export function summarizeToolInput(toolName, input) {
|
|
137
137
|
if (!input || typeof input !== 'object') return '';
|
|
138
|
-
// `
|
|
138
|
+
// `mcp__relay__relay_tool` reads as what it does on which relay
|
|
139
139
|
// (`send → linux-test session 01234567: “…”`), not as its first argument.
|
|
140
140
|
if (isRemoteRelayToolName(toolName)) {
|
|
141
141
|
const summary = remoteRelayActivitySummary(input);
|
|
@@ -198,6 +198,57 @@ export function createSdkMessageNormalizer() {
|
|
|
198
198
|
// The `error` of the turn's last main-thread assistant message: the SDK's
|
|
199
199
|
// name for the failure a refused turn ended on.
|
|
200
200
|
let lastAssistantError = '';
|
|
201
|
+
// The turn's web searches and fetches, per API message id: the API counts
|
|
202
|
+
// them in each message's `usage.server_tool_use`, and the SDK repeats a
|
|
203
|
+
// message's usage with every content block it delivers, so a message is
|
|
204
|
+
// counted once. Summed per model on the result; DeepSeek bills them per
|
|
205
|
+
// request (shared/deepseek-pricing.mjs).
|
|
206
|
+
const serverToolUse = new Map(); // message id -> { model, webSearchRequests, webFetchRequests }
|
|
207
|
+
let unnamedServerToolUseMessages = 0;
|
|
208
|
+
|
|
209
|
+
function requestCount(value) {
|
|
210
|
+
const number = Number(value ?? 0);
|
|
211
|
+
return Number.isFinite(number) && number > 0 ? Math.round(number) : 0;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function noteServerToolUse(messageId, message) {
|
|
215
|
+
const use = message?.usage?.server_tool_use;
|
|
216
|
+
if (!use || typeof use !== 'object') return;
|
|
217
|
+
const webSearchRequests = requestCount(use.web_search_requests);
|
|
218
|
+
const webFetchRequests = requestCount(use.web_fetch_requests);
|
|
219
|
+
if (!webSearchRequests && !webFetchRequests) return;
|
|
220
|
+
const key = messageId || `unnamed-${++unnamedServerToolUseMessages}`;
|
|
221
|
+
const current = serverToolUse.get(key)
|
|
222
|
+
|| { model: String(message?.model || '').trim() || initModel, webSearchRequests: 0, webFetchRequests: 0 };
|
|
223
|
+
// A message's usage is the message's total, so repeats do not add up.
|
|
224
|
+
current.webSearchRequests = Math.max(current.webSearchRequests, webSearchRequests);
|
|
225
|
+
current.webFetchRequests = Math.max(current.webFetchRequests, webFetchRequests);
|
|
226
|
+
serverToolUse.set(key, current);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** The turn's `[{ model, webSearchRequests, webFetchRequests }]`, and the count starts over. */
|
|
230
|
+
function takeServerToolUse(resultUsage) {
|
|
231
|
+
const byModel = new Map();
|
|
232
|
+
const add = (model, webSearchRequests, webFetchRequests) => {
|
|
233
|
+
const key = String(model || '').trim() || initModel;
|
|
234
|
+
const current = byModel.get(key) || { model: key, webSearchRequests: 0, webFetchRequests: 0 };
|
|
235
|
+
current.webSearchRequests += webSearchRequests;
|
|
236
|
+
current.webFetchRequests += webFetchRequests;
|
|
237
|
+
byModel.set(key, current);
|
|
238
|
+
};
|
|
239
|
+
for (const entry of serverToolUse.values()) add(entry.model, entry.webSearchRequests, entry.webFetchRequests);
|
|
240
|
+
serverToolUse.clear();
|
|
241
|
+
unnamedServerToolUseMessages = 0;
|
|
242
|
+
// A result without assistant messages seen still carries the turn's
|
|
243
|
+
// counts in its own usage, under the turn's model.
|
|
244
|
+
const use = resultUsage?.server_tool_use;
|
|
245
|
+
if (!byModel.size && use && typeof use === 'object') {
|
|
246
|
+
const webSearchRequests = requestCount(use.web_search_requests);
|
|
247
|
+
const webFetchRequests = requestCount(use.web_fetch_requests);
|
|
248
|
+
if (webSearchRequests || webFetchRequests) add(initModel, webSearchRequests, webFetchRequests);
|
|
249
|
+
}
|
|
250
|
+
return [...byModel.values()];
|
|
251
|
+
}
|
|
201
252
|
|
|
202
253
|
function threadKeyFor(parentToolUseId) {
|
|
203
254
|
return String(parentToolUseId || '').trim() || 'main';
|
|
@@ -592,6 +643,7 @@ export function createSdkMessageNormalizer() {
|
|
|
592
643
|
const content = Array.isArray(sdkMessage?.message?.content) ? sdkMessage.message.content : [];
|
|
593
644
|
const messageId = String(sdkMessage?.message?.id || '').trim();
|
|
594
645
|
if (!parentToolUseId) lastAssistantError = String(sdkMessage?.error || '').trim();
|
|
646
|
+
noteServerToolUse(messageId, sdkMessage?.message);
|
|
595
647
|
return actionsForAssistantMessage(content, parentToolUseId, messageId);
|
|
596
648
|
}
|
|
597
649
|
|
|
@@ -683,6 +735,9 @@ export function createSdkMessageNormalizer() {
|
|
|
683
735
|
totalCostUsd: Number.isFinite(Number(sdkMessage.total_cost_usd))
|
|
684
736
|
? Number(sdkMessage.total_cost_usd)
|
|
685
737
|
: null,
|
|
738
|
+
// This turn's web searches and fetches per model (not running
|
|
739
|
+
// totals): `[{ model, webSearchRequests, webFetchRequests }]`.
|
|
740
|
+
serverToolUse: takeServerToolUse(sdkMessage.usage),
|
|
686
741
|
},
|
|
687
742
|
}];
|
|
688
743
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// The relay's `
|
|
1
|
+
// The relay's `relay_tool` tool for Copilot SDK sessions: a plain SDK
|
|
2
2
|
// `Tool` (`{ name, description, parameters, skipPermission, handler }`) built
|
|
3
3
|
// from the shared contract and forwarded through the shared tool core. The
|
|
4
4
|
// module never imports the SDK; the session process hands the definition to
|
|
@@ -50,7 +50,7 @@ export function buildCopilotRemoteRelayTool({
|
|
|
50
50
|
}));
|
|
51
51
|
} catch (error) {
|
|
52
52
|
const message = error?.message || String(error);
|
|
53
|
-
dbg('
|
|
53
|
+
dbg('relay_tool failed', message);
|
|
54
54
|
result = { ok: false, code: REMOTE_RELAY_CALL_FAILED, error: message };
|
|
55
55
|
}
|
|
56
56
|
return formatRemoteRelayToolResult(result);
|
|
@@ -392,8 +392,11 @@ export function isCopilotQuotaError(data) {
|
|
|
392
392
|
* "Open Check Usage" pointer shipped in 37d2899. The structured quota verdict
|
|
393
393
|
* is passed to that classifier as a hint rather than smuggled in as forged
|
|
394
394
|
* prose, so a quota error is non-retryable however GitHub worded it.
|
|
395
|
+
*
|
|
396
|
+
* `provider` is the worker's provider (`openai` for a BYOK worker): the note
|
|
397
|
+
* names it, so an OpenAI turn does not read as a Copilot one.
|
|
395
398
|
*/
|
|
396
|
-
export function classifyCopilotSessionError(data) {
|
|
399
|
+
export function classifyCopilotSessionError(data, { provider = '' } = {}) {
|
|
397
400
|
const detail = String(data?.message || '').trim() || 'the Copilot runtime reported a session error';
|
|
398
401
|
const quota = isCopilotQuotaError(data);
|
|
399
402
|
const probe = new Error(detail);
|
|
@@ -420,7 +423,7 @@ export function classifyCopilotSessionError(data) {
|
|
|
420
423
|
return {
|
|
421
424
|
code,
|
|
422
425
|
stableCode: `copilot.${code}`,
|
|
423
|
-
text:
|
|
426
|
+
text: turnFailedText(detail, provider),
|
|
424
427
|
// Without one the relay adds its default, which advises a retry.
|
|
425
428
|
guidance: COPILOT_TURN_FAILURE_GUIDANCE,
|
|
426
429
|
detail,
|
|
@@ -490,6 +493,17 @@ export function isCopilotAuthError(error) {
|
|
|
490
493
|
return AUTH_ERROR_RE.test(String(error?.message || error || ''));
|
|
491
494
|
}
|
|
492
495
|
|
|
496
|
+
/**
|
|
497
|
+
* The generic failure note. On the SDK engine the provider is GitHub Copilot or,
|
|
498
|
+
* for a BYOK worker, OpenAI; the note names the one the turn ran on.
|
|
499
|
+
*/
|
|
500
|
+
function turnFailedText(detail, provider) {
|
|
501
|
+
const subject = String(provider || '').trim().toLowerCase() === 'openai'
|
|
502
|
+
? 'the OpenAI turn on the Copilot SDK engine'
|
|
503
|
+
: 'the Copilot turn';
|
|
504
|
+
return `System note: ${subject} failed (${detail}). Retry or send a new message.`;
|
|
505
|
+
}
|
|
506
|
+
|
|
493
507
|
/**
|
|
494
508
|
* What the relay appends to a failed Copilot turn in place of its own default
|
|
495
509
|
* (which advises a retry). The worker deals with its runtime itself: a delivered
|
|
@@ -509,7 +523,7 @@ export const COPILOT_TURN_CONTINUE_GUIDANCE =
|
|
|
509
523
|
* than a `session.error` event. Auth gets its own branch because the fix is a
|
|
510
524
|
* user action on the relay host, not a retry.
|
|
511
525
|
*/
|
|
512
|
-
export function classifyCopilotTurnException(error) {
|
|
526
|
+
export function classifyCopilotTurnException(error, { provider = '' } = {}) {
|
|
513
527
|
const detail = String(error?.message || error || '').trim() || 'unknown error';
|
|
514
528
|
// The conversation's folder is gone; the message names it.
|
|
515
529
|
if (isWorkingDirectoryMissingError(error)) {
|
|
@@ -563,7 +577,7 @@ export function classifyCopilotTurnException(error) {
|
|
|
563
577
|
return {
|
|
564
578
|
code: 'turn-error',
|
|
565
579
|
stableCode: 'copilot.turn-error',
|
|
566
|
-
text:
|
|
580
|
+
text: turnFailedText(detail, provider),
|
|
567
581
|
guidance: COPILOT_TURN_FAILURE_GUIDANCE,
|
|
568
582
|
detail,
|
|
569
583
|
};
|
|
@@ -84,7 +84,7 @@ export function summarizeToolInput(input) {
|
|
|
84
84
|
|
|
85
85
|
export function formatToolActivityText(toolName, input) {
|
|
86
86
|
const rawName = String(toolName || '').trim() || 'tool';
|
|
87
|
-
// `
|
|
87
|
+
// `relay_tool` (bare, or namespaced by an MCP server) reads as what it
|
|
88
88
|
// does on which relay (`send → linux-test session 01234567: “…”`); its
|
|
89
89
|
// arguments may arrive as a JSON string.
|
|
90
90
|
const isRemoteRelay = isRemoteRelayToolName(rawName);
|
|
@@ -114,6 +114,11 @@ import { shouldEmitStreamUpdate } from '../../shared/stream-emit-gating.mjs';
|
|
|
114
114
|
import { withSteerNote } from '../../shared/steer-note.mjs';
|
|
115
115
|
import { isRelayAutomatedDelivery } from '../../shared/relay-events.mjs';
|
|
116
116
|
import { isTransientRelayError } from '../../shared/worker-runtime/relay-errors.mjs';
|
|
117
|
+
import {
|
|
118
|
+
isOpenAIImageTerminalError,
|
|
119
|
+
runDirectOpenAIImageTurn,
|
|
120
|
+
shouldUseDirectOpenAIImageApi,
|
|
121
|
+
} from '../../shared/worker-runtime/direct-openai-image.mjs';
|
|
117
122
|
import {
|
|
118
123
|
createTurnLiveness,
|
|
119
124
|
createTurnStalledError,
|
|
@@ -451,7 +456,7 @@ export function createCopilotSdkSessionRunner({
|
|
|
451
456
|
// runtime's built-in one. A runtime that refuses the override falls back
|
|
452
457
|
// to the built-in for the rest of this worker's life.
|
|
453
458
|
relayAskUserTool = true,
|
|
454
|
-
// Whether sessions carry the relay's `
|
|
459
|
+
// Whether sessions carry the relay's `relay_tool` tool
|
|
455
460
|
// (shared/remote-relay-tool-core.mjs). Default: ask the relay.
|
|
456
461
|
remoteRelayToolGate = null,
|
|
457
462
|
taskRefreshMs = DEFAULT_TASK_REFRESH_MS,
|
|
@@ -556,6 +561,12 @@ export function createCopilotSdkSessionRunner({
|
|
|
556
561
|
// terminal error until the publish lands, so recovery can never re-run them
|
|
557
562
|
// (see publishSettleMarker).
|
|
558
563
|
const settlingMessages = new Map();
|
|
564
|
+
// Image-model rows running on the relay's image routes (id → message). They
|
|
565
|
+
// never reach the runtime, so no turn owns them; the heartbeat and the crash
|
|
566
|
+
// guard still have to.
|
|
567
|
+
const directImageMessages = new Map();
|
|
568
|
+
// `openai` on a BYOK worker; the failure notes name the provider.
|
|
569
|
+
const workerProvider = String(env.COPILOT_PROVIDER_TYPE || '').trim().toLowerCase();
|
|
559
570
|
// Settle markers that could not be posted even terminally; the heartbeat
|
|
560
571
|
// hands them to the relay to fail (id → terminalError).
|
|
561
572
|
const settleFailedTerminals = new Map();
|
|
@@ -685,12 +696,12 @@ export function createCopilotSdkSessionRunner({
|
|
|
685
696
|
// the human's eventual answer to a runtime whose row is already settled.
|
|
686
697
|
// Same guard the Cursor worker's `hasPendingClientWork` provides.
|
|
687
698
|
let pendingHumanRequests = 0;
|
|
688
|
-
// Running `
|
|
699
|
+
// Running `relay_tool` calls. The runtime is just as silent while one
|
|
689
700
|
// runs (the relay may wait up to 600 s for a remote turn, plus an approval
|
|
690
701
|
// card), so they hold the stall watchdog and the idle shutdown too — but
|
|
691
702
|
// not steering: nobody is answering a card here.
|
|
692
703
|
let pendingRelayToolCalls = 0;
|
|
693
|
-
// `
|
|
704
|
+
// `relay_tool` is registered only while this relay has paired remotes.
|
|
694
705
|
// Asked once when the worker starts; a session build waits for that first
|
|
695
706
|
// answer (it is async anyway), and every runtime stop asks again so the
|
|
696
707
|
// next session picks up a remote paired in the meantime.
|
|
@@ -2295,7 +2306,7 @@ export function createCopilotSdkSessionRunner({
|
|
|
2295
2306
|
}
|
|
2296
2307
|
|
|
2297
2308
|
/**
|
|
2298
|
-
* The relay's `
|
|
2309
|
+
* The relay's `relay_tool` tool (copilot-remote-relay-tool.mjs). Every call
|
|
2299
2310
|
* is counted while it runs: the stall watchdog re-arms instead of failing the
|
|
2300
2311
|
* row, and the lifecycle keeps the runtime up. The clock restarts from the
|
|
2301
2312
|
* answer, like after a card. A call is cancelled by the runtime (its
|
|
@@ -2338,7 +2349,7 @@ export function createCopilotSdkSessionRunner({
|
|
|
2338
2349
|
* runtime refuses the override (an older runtime). The refusal is
|
|
2339
2350
|
* remembered until the runtime is stopped — the next runtime (possibly
|
|
2340
2351
|
* auto-updated under the relay) gets another try. Only the override goes:
|
|
2341
|
-
* the relay's other tools (`
|
|
2352
|
+
* the relay's other tools (`relay_tool`) override nothing and stay.
|
|
2342
2353
|
*/
|
|
2343
2354
|
async function openSession(open, config) {
|
|
2344
2355
|
try {
|
|
@@ -3187,7 +3198,7 @@ export function createCopilotSdkSessionRunner({
|
|
|
3187
3198
|
// Cancels any relay question card this turn is blocked on, so an aborted
|
|
3188
3199
|
// turn does not leave a human answering into the void.
|
|
3189
3200
|
abortController: new AbortController(),
|
|
3190
|
-
// Cancels the `
|
|
3201
|
+
// Cancels the `relay_tool` calls that started under this turn — on the
|
|
3191
3202
|
// user's Stop only. Not `abortController`: that one also fires when the
|
|
3192
3203
|
// turn settles normally, and a background agent's call must outlive that.
|
|
3193
3204
|
remoteRelayStop: new AbortController(),
|
|
@@ -3358,7 +3369,7 @@ export function createCopilotSdkSessionRunner({
|
|
|
3358
3369
|
if (turn.settled) return;
|
|
3359
3370
|
// A human staring at a question card is not a stalled runtime: the card
|
|
3360
3371
|
// has its own (much longer) timeout, and failing the row here would
|
|
3361
|
-
// settle it while the answer is still coming. A
|
|
3372
|
+
// settle it while the answer is still coming. A relay_tool call waiting
|
|
3362
3373
|
// on another relay is the same kind of silence; the relay bounds that
|
|
3363
3374
|
// wait itself. So is a compaction, bounded by its stale cap, and the wait
|
|
3364
3375
|
// for the previous runtime to stop, bounded by the teardown's own limits.
|
|
@@ -3728,7 +3739,7 @@ export function createCopilotSdkSessionRunner({
|
|
|
3728
3739
|
// afterwards: a runtime that died or went silent under a Stop used to put
|
|
3729
3740
|
// the watchdog's failure on every row instead.
|
|
3730
3741
|
if (failure && !turn.aborted) {
|
|
3731
|
-
const classified = classifyCopilotTurnException(failure);
|
|
3742
|
+
const classified = classifyCopilotTurnException(failure, { provider: workerProvider });
|
|
3732
3743
|
dbg('copilot turn failed', turn.message?.id || '(continuation)', classified.detail);
|
|
3733
3744
|
const running = turn.currentOwner || turn.primaryEntry;
|
|
3734
3745
|
for (const entry of pending) {
|
|
@@ -3747,7 +3758,7 @@ export function createCopilotSdkSessionRunner({
|
|
|
3747
3758
|
return;
|
|
3748
3759
|
}
|
|
3749
3760
|
if (result?.isError) {
|
|
3750
|
-
const classified = classifyCopilotSessionError(result.errorData || { message: result.errorMessage });
|
|
3761
|
+
const classified = classifyCopilotSessionError(result.errorData || { message: result.errorMessage }, { provider: workerProvider });
|
|
3751
3762
|
dbg('turn failed', turn.message?.id || '(continuation)', classified.stableCode);
|
|
3752
3763
|
const running = turn.currentOwner || turn.primaryEntry;
|
|
3753
3764
|
for (const entry of pending) {
|
|
@@ -3901,7 +3912,7 @@ export function createCopilotSdkSessionRunner({
|
|
|
3901
3912
|
}
|
|
3902
3913
|
turn.aborted = true;
|
|
3903
3914
|
turn.abortedRowId = rowId;
|
|
3904
|
-
// A pending `
|
|
3915
|
+
// A pending `relay_tool` call ends now rather than when the runtime gets
|
|
3905
3916
|
// round to it: the relay drops the request, withdraws its approval card
|
|
3906
3917
|
// and stops waiting on the remote, and the watchdog hold is released.
|
|
3907
3918
|
turn.remoteRelayStop?.abort();
|
|
@@ -4117,6 +4128,7 @@ export function createCopilotSdkSessionRunner({
|
|
|
4117
4128
|
dbg('continuation output dropped (no relay row)');
|
|
4118
4129
|
const classified = classifyCopilotTurnException(
|
|
4119
4130
|
turn.failure || new Error('the relay refused a continuation row for this turn'),
|
|
4131
|
+
{ provider: workerProvider },
|
|
4120
4132
|
);
|
|
4121
4133
|
for (const entry of turn.entries) {
|
|
4122
4134
|
if (entry.settled || entry.turn !== turn) continue;
|
|
@@ -4552,7 +4564,7 @@ export function createCopilotSdkSessionRunner({
|
|
|
4552
4564
|
* is not waiting for its tools, so a steer interrupts nothing. A
|
|
4553
4565
|
* subagent the main agent runs in the foreground is covered by the main
|
|
4554
4566
|
* agent's own tool call that started it.
|
|
4555
|
-
* - `
|
|
4567
|
+
* - `relay_tool`, which holds the stall watchdog but never held steering
|
|
4556
4568
|
* (see `pendingRelayToolCalls`).
|
|
4557
4569
|
*/
|
|
4558
4570
|
function mainToolsInFlight(turn = activeTurn) {
|
|
@@ -4821,6 +4833,9 @@ export function createCopilotSdkSessionRunner({
|
|
|
4821
4833
|
async function handlePendingPayload(pending) {
|
|
4822
4834
|
const message = pending?.message || null;
|
|
4823
4835
|
if (!message) return false;
|
|
4836
|
+
// An image model is no chat model: its row goes to the relay's image
|
|
4837
|
+
// routes, whatever the runtime is doing, and never into the runtime.
|
|
4838
|
+
if (shouldUseDirectOpenAIImageApi(message)) return runDirectImageTurn(message);
|
|
4824
4839
|
// Held (a card is open, a compaction is running, a tool of the main agent
|
|
4825
4840
|
// is running, the turn-opening prompt is not sent yet): hand the row back
|
|
4826
4841
|
// with no retry penalty; the relay re-delivers it when the hold ends. An
|
|
@@ -4850,7 +4865,7 @@ export function createCopilotSdkSessionRunner({
|
|
|
4850
4865
|
if (outcome !== NOT_STEERED) return outcome;
|
|
4851
4866
|
} catch (error) {
|
|
4852
4867
|
dbg('steering failed', message.id, error?.message || String(error));
|
|
4853
|
-
const classified = classifyCopilotTurnException(error);
|
|
4868
|
+
const classified = classifyCopilotTurnException(error, { provider: workerProvider });
|
|
4854
4869
|
await publishResponse(message, {
|
|
4855
4870
|
text: classified.text,
|
|
4856
4871
|
model: null,
|
|
@@ -4863,6 +4878,54 @@ export function createCopilotSdkSessionRunner({
|
|
|
4863
4878
|
return runTurn(turn);
|
|
4864
4879
|
}
|
|
4865
4880
|
|
|
4881
|
+
/**
|
|
4882
|
+
* One image-model row (shared/worker-runtime/direct-openai-image.mjs). With
|
|
4883
|
+
* an image operation the relay's execute route publishes the reply, the
|
|
4884
|
+
* failure or the requeue itself; without one the shared helper publishes
|
|
4885
|
+
* the reply, and a refusal by the provider is published here as the row's
|
|
4886
|
+
* failure. Anything else requeues the row, as on the extension engine.
|
|
4887
|
+
*/
|
|
4888
|
+
async function runDirectImageTurn(message) {
|
|
4889
|
+
const id = String(message.id || '');
|
|
4890
|
+
directImageMessages.set(id, message);
|
|
4891
|
+
touch();
|
|
4892
|
+
try {
|
|
4893
|
+
const result = await runDirectOpenAIImageTurn({
|
|
4894
|
+
api,
|
|
4895
|
+
message,
|
|
4896
|
+
// An edit can take minutes, longer than fetch waits for headers.
|
|
4897
|
+
requestOptions: { longCall: true },
|
|
4898
|
+
responseFields: attemptFields(message),
|
|
4899
|
+
});
|
|
4900
|
+
dbg('image turn', message.id, result.via, result.outcome);
|
|
4901
|
+
} catch (error) {
|
|
4902
|
+
if (isOpenAIImageTerminalError(error)) {
|
|
4903
|
+
dbg('image turn refused by OpenAI', message.id, error.message);
|
|
4904
|
+
await publishResponse(message, {
|
|
4905
|
+
text: error.message,
|
|
4906
|
+
model: null,
|
|
4907
|
+
terminalError: {
|
|
4908
|
+
kind: 'openai-image-failed',
|
|
4909
|
+
// The code the relay's image operation uses for the same refusal.
|
|
4910
|
+
code: 'image-provider-rejected',
|
|
4911
|
+
stableCode: 'image-provider-rejected',
|
|
4912
|
+
message: error.message,
|
|
4913
|
+
...(error.detail ? { detail: String(error.detail) } : {}),
|
|
4914
|
+
failedAt: new Date().toISOString(),
|
|
4915
|
+
queueMessageId: id || null,
|
|
4916
|
+
},
|
|
4917
|
+
});
|
|
4918
|
+
} else {
|
|
4919
|
+
dbg('image turn failed; requeueing', message.id, error?.message || String(error));
|
|
4920
|
+
await api('POST', '/api/requeue', { messageId: message.id, ...attemptFields(message) }).catch(() => {});
|
|
4921
|
+
}
|
|
4922
|
+
} finally {
|
|
4923
|
+
directImageMessages.delete(id);
|
|
4924
|
+
touch();
|
|
4925
|
+
}
|
|
4926
|
+
return true;
|
|
4927
|
+
}
|
|
4928
|
+
|
|
4866
4929
|
// --------------------------------------------------------------- teardown --
|
|
4867
4930
|
|
|
4868
4931
|
function getActiveQueueMessageId() {
|
|
@@ -4872,6 +4935,7 @@ export function createCopilotSdkSessionRunner({
|
|
|
4872
4935
|
const id = String(entry.message?.id || '');
|
|
4873
4936
|
if (id && !entry.released) return id;
|
|
4874
4937
|
}
|
|
4938
|
+
for (const id of directImageMessages.keys()) return id;
|
|
4875
4939
|
return '';
|
|
4876
4940
|
}
|
|
4877
4941
|
|
|
@@ -4911,6 +4975,7 @@ export function createCopilotSdkSessionRunner({
|
|
|
4911
4975
|
for (const { message, variant } of settlingMessages.values()) {
|
|
4912
4976
|
push(message, buildSteerSettleFailure(message, { variant, agentLabel: SETTLE_AGENT_LABEL }));
|
|
4913
4977
|
}
|
|
4978
|
+
for (const message of directImageMessages.values()) push(message);
|
|
4914
4979
|
return entries;
|
|
4915
4980
|
}
|
|
4916
4981
|
|
|
@@ -4949,8 +5014,9 @@ export function createCopilotSdkSessionRunner({
|
|
|
4949
5014
|
// registry and refuses ids it does not know.
|
|
4950
5015
|
stopBackgroundTask,
|
|
4951
5016
|
// "Active" spans both ownerships: a settled turn still publishing must
|
|
4952
|
-
// keep the worker's idle/shutdown gates closed just like a running one
|
|
4953
|
-
|
|
5017
|
+
// keep the worker's idle/shutdown gates closed just like a running one,
|
|
5018
|
+
// and so must an image row on the relay's image routes.
|
|
5019
|
+
isTurnActive: () => !!activeTurn || publishingTurns.size > 0 || directImageMessages.size > 0,
|
|
4954
5020
|
dispose,
|
|
4955
5021
|
// The turn's tokens/cost/TTFT, as posted to `/api/copilot-plan-usage`.
|
|
4956
5022
|
getLastTurnUsage: () => lastTurnUsage,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The relay's `
|
|
2
|
+
* The relay's `relay_tool` custom tool for the Cursor worker. SDK-free: the
|
|
3
3
|
* turn runner passes the returned `{ description, inputSchema, execute }` into
|
|
4
|
-
* `customTools.
|
|
4
|
+
* `customTools.relay_tool`, and every call goes through the shared tool core
|
|
5
5
|
* to the local relay endpoint.
|
|
6
6
|
*
|
|
7
7
|
* A call can legitimately run for many minutes (a remote turn plus an approval
|
|
@@ -46,7 +46,7 @@ export function createRemoteRelayTool({
|
|
|
46
46
|
} catch (error) {
|
|
47
47
|
// A tool exception must never kill the run.
|
|
48
48
|
const message = error?.message || String(error);
|
|
49
|
-
dbg('
|
|
49
|
+
dbg('relay_tool failed', message);
|
|
50
50
|
const result = { ok: false, code: REMOTE_RELAY_CALL_FAILED, error: message };
|
|
51
51
|
return {
|
|
52
52
|
structuredContent: result,
|
|
@@ -126,7 +126,7 @@ export function createCursorTurnRunner({
|
|
|
126
126
|
// The error an agent started in `cwd` fails with up front, or null
|
|
127
127
|
// (shared/worker-runtime/working-directory.mjs).
|
|
128
128
|
checkWorkingDirectoryImpl = missingWorkingDirectoryError,
|
|
129
|
-
// Whether agent handles carry the relay's `
|
|
129
|
+
// Whether agent handles carry the relay's `relay_tool` tool
|
|
130
130
|
// (shared/remote-relay-tool-core.mjs). Default: ask the relay.
|
|
131
131
|
remoteRelayToolGate = null,
|
|
132
132
|
dbg = () => {},
|
|
@@ -149,7 +149,7 @@ export function createCursorTurnRunner({
|
|
|
149
149
|
// Open ask_user round-trips: the merged-stream stall watchdog defers while
|
|
150
150
|
// a human is thinking about a question card.
|
|
151
151
|
let pendingAskUserCalls = 0;
|
|
152
|
-
// Running
|
|
152
|
+
// Running relay_tool calls, same family: the relay may be waiting up to
|
|
153
153
|
// 600 s for a remote turn (plus an approval card) with no SDK traffic.
|
|
154
154
|
let pendingRemoteRelayCalls = 0;
|
|
155
155
|
// Mirrors the Copilot extension's lastPromptedRelayMode: full mode
|
|
@@ -178,7 +178,7 @@ export function createCursorTurnRunner({
|
|
|
178
178
|
// plain refusal the model can relay instead of a failed turn.
|
|
179
179
|
const previewTool = createPreviewTool({
|
|
180
180
|
api,
|
|
181
|
-
// Between turns, the worker's own session (as
|
|
181
|
+
// Between turns, the worker's own session (as relay_tool below): list
|
|
182
182
|
// and close are scoped to a conversation and refused without one.
|
|
183
183
|
getConversationId: () => activeMessage?.conversationId || sdkSessionId || '',
|
|
184
184
|
dbg,
|
|
@@ -202,7 +202,7 @@ export function createCursorTurnRunner({
|
|
|
202
202
|
execute: previewTool.execute,
|
|
203
203
|
},
|
|
204
204
|
};
|
|
205
|
-
// `
|
|
205
|
+
// `relay_tool` joins the set only while this relay has paired remotes.
|
|
206
206
|
// Asked once when the worker starts; the first handle waits for the answer
|
|
207
207
|
// (it is created asynchronously anyway), and every handle creation refreshes
|
|
208
208
|
// it so a later rebuild picks up a remote paired in the meantime.
|
|
@@ -47,8 +47,8 @@ export function summarizeToolInput(toolName, input) {
|
|
|
47
47
|
/**
|
|
48
48
|
* The relay's custom tools reach the stream as the SDK's generic MCP call
|
|
49
49
|
* (`name: 'mcp'`, args `{ providerIdentifier: 'custom-user-tools', toolName,
|
|
50
|
-
* args }`). `
|
|
51
|
-
* summary, so the transcript reads `Tool (
|
|
50
|
+
* args }`). `relay_tool` is shown as itself with the contract's one-line
|
|
51
|
+
* summary, so the transcript reads `Tool (relay_tool): send → …` like every
|
|
52
52
|
* other provider's; null for any other tool.
|
|
53
53
|
*/
|
|
54
54
|
function remoteRelayCallArgs(toolName, input) {
|
package/server/db-schema.mjs
CHANGED
|
@@ -29,6 +29,17 @@ import { migrateConversationMayCreateSessions } from './migrations/0020-conversa
|
|
|
29
29
|
import { migrateConversationGroupAgentMarks } from './migrations/0021-conversation-group-agent-marks.mjs';
|
|
30
30
|
import { migrateRelaySchedules } from './migrations/0022-relay-schedules.mjs';
|
|
31
31
|
import { migrateRelayAsks } from './migrations/0023-relay-asks.mjs';
|
|
32
|
+
import { migrateDeepSeekUsageTotals } from './migrations/0024-deepseek-usage-totals.mjs';
|
|
33
|
+
import { migrateDeepSeekUsageRequestsDaily } from './migrations/0025-deepseek-usage-requests-daily.mjs';
|
|
34
|
+
import { migrateDeepSeekUsageServedModels } from './migrations/0026-deepseek-usage-served-models.mjs';
|
|
35
|
+
import { migrateDeepSeekUsageCheckpointRuns } from './migrations/0027-deepseek-usage-checkpoint-runs.mjs';
|
|
36
|
+
import { migrateDeepSeekUsageCheckpointRequestRuns } from './migrations/0028-deepseek-usage-checkpoint-request-runs.mjs';
|
|
37
|
+
import { migrateDeepSeekBalanceRises } from './migrations/0029-deepseek-balance-rises.mjs';
|
|
38
|
+
import { migrateDeepSeekUsageActuals } from './migrations/0030-deepseek-usage-actuals.mjs';
|
|
39
|
+
import { migrateQueueProvider } from './migrations/0031-queue-provider.mjs';
|
|
40
|
+
import { migrateRuntimeSessionBillingSwitch } from './migrations/0032-runtime-session-billing-switch.mjs';
|
|
41
|
+
import { migrateClaudeApiUsageCheckpoints } from './migrations/0033-claude-api-usage-checkpoints.mjs';
|
|
42
|
+
import { migrateClaudeApiUsageTotals } from './migrations/0034-claude-api-usage-totals.mjs';
|
|
32
43
|
import { migrateImageConversationSchema } from './repositories/image-conversation-repository.mjs';
|
|
33
44
|
|
|
34
45
|
// Mirrors DEFAULT_RELAY_MODE in server-runtime.mjs; used only by the one-time
|
|
@@ -898,7 +909,7 @@ try {
|
|
|
898
909
|
console.warn(`[relay-schedules] migration 0022 failed; retrying on next boot: ${error?.message || error}`);
|
|
899
910
|
}
|
|
900
911
|
|
|
901
|
-
// Questions that don't block (
|
|
912
|
+
// Questions that don't block (relay_tool ask): one new table. The ask
|
|
902
913
|
// service checks for it and refuses the ask actions without it, so a failure
|
|
903
914
|
// is logged and retried next boot like 0022.
|
|
904
915
|
try {
|
|
@@ -907,6 +918,62 @@ try {
|
|
|
907
918
|
console.warn(`[relay-asks] migration 0023 failed; retrying on next boot: ${error?.message || error}`);
|
|
908
919
|
}
|
|
909
920
|
|
|
921
|
+
// DeepSeek usage totals: one new table. The plan usage service creates it
|
|
922
|
+
// itself as well, so a failure here is logged and retried next boot like 0023.
|
|
923
|
+
try {
|
|
924
|
+
migrateDeepSeekUsageTotals(db);
|
|
925
|
+
} catch (error) {
|
|
926
|
+
console.warn(`[deepseek-usage] migration 0024 failed; retrying on next boot: ${error?.message || error}`);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// DeepSeek search/fetch counts, daily spend and balance reference: four
|
|
930
|
+
// columns on 0024's table and two tables, handled like 0024.
|
|
931
|
+
try {
|
|
932
|
+
migrateDeepSeekUsageRequestsDaily(db);
|
|
933
|
+
} catch (error) {
|
|
934
|
+
console.warn(`[deepseek-usage] migration 0025 failed; retrying on next boot: ${error?.message || error}`);
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// DeepSeek usage booked under a Claude model name is folded into the DeepSeek
|
|
938
|
+
// model that served it, handled like 0024.
|
|
939
|
+
try {
|
|
940
|
+
migrateDeepSeekUsageServedModels(db);
|
|
941
|
+
} catch (error) {
|
|
942
|
+
console.warn(`[deepseek-usage] migration 0026 failed; retrying on next boot: ${error?.message || error}`);
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// The DeepSeek usage checkpoint learns the worker process's run id and its
|
|
946
|
+
// search and fetch counts: three columns on 0024's table, handled like 0024.
|
|
947
|
+
try {
|
|
948
|
+
migrateDeepSeekUsageCheckpointRuns(db);
|
|
949
|
+
} catch (error) {
|
|
950
|
+
console.warn(`[deepseek-usage] migration 0027 failed; retrying on next boot: ${error?.message || error}`);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// The checkpoint's search and fetch counts get a run id of their own: one
|
|
954
|
+
// column on 0024's table, handled like 0024.
|
|
955
|
+
try {
|
|
956
|
+
migrateDeepSeekUsageCheckpointRequestRuns(db);
|
|
957
|
+
} catch (error) {
|
|
958
|
+
console.warn(`[deepseek-usage] migration 0028 failed; retrying on next boot: ${error?.message || error}`);
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// The balance reference notes when the balance last rose: one column on
|
|
962
|
+
// 0025's table, handled like 0025.
|
|
963
|
+
try {
|
|
964
|
+
migrateDeepSeekBalanceRises(db);
|
|
965
|
+
} catch (error) {
|
|
966
|
+
console.warn(`[deepseek-usage] migration 0029 failed; retrying on next boot: ${error?.message || error}`);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// The imported figures of DeepSeek's usage export: a table of its own, handled
|
|
970
|
+
// like 0024.
|
|
971
|
+
try {
|
|
972
|
+
migrateDeepSeekUsageActuals(db);
|
|
973
|
+
} catch (error) {
|
|
974
|
+
console.warn(`[deepseek-usage] migration 0030 failed; retrying on next boot: ${error?.message || error}`);
|
|
975
|
+
}
|
|
976
|
+
|
|
910
977
|
const runtimeSessionColumns = db.prepare(`PRAGMA table_info(runtime_sessions)`).all().map((c) => c.name);
|
|
911
978
|
if (runtimeSessionColumns.length) {
|
|
912
979
|
if (!runtimeSessionColumns.includes('strategy')) {
|
|
@@ -964,6 +1031,34 @@ if (runtimeSessionColumns.length) {
|
|
|
964
1031
|
}
|
|
965
1032
|
}
|
|
966
1033
|
|
|
1034
|
+
// The Claude billing switch: the billing a queue row runs on, and the chat's
|
|
1035
|
+
// last switch. After runtime_sessions has provider_type: 0031's back-fill
|
|
1036
|
+
// reads it. Every reader and writer of these columns checks for them, so a
|
|
1037
|
+
// failure is logged and retried next boot like 0004.
|
|
1038
|
+
try {
|
|
1039
|
+
migrateQueueProvider(db);
|
|
1040
|
+
} catch (error) {
|
|
1041
|
+
console.warn(`[claude-billing] migration 0031 failed; retrying on next boot: ${error?.message || error}`);
|
|
1042
|
+
}
|
|
1043
|
+
try {
|
|
1044
|
+
migrateRuntimeSessionBillingSwitch(db);
|
|
1045
|
+
} catch (error) {
|
|
1046
|
+
console.warn(`[claude-billing] migration 0032 failed; retrying on next boot: ${error?.message || error}`);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
// Claude API usage: the checkpoints across both Claude billings and the
|
|
1050
|
+
// booked totals per day and model, handled like 0024.
|
|
1051
|
+
try {
|
|
1052
|
+
migrateClaudeApiUsageCheckpoints(db);
|
|
1053
|
+
} catch (error) {
|
|
1054
|
+
console.warn(`[claude-api-usage] migration 0033 failed; retrying on next boot: ${error?.message || error}`);
|
|
1055
|
+
}
|
|
1056
|
+
try {
|
|
1057
|
+
migrateClaudeApiUsageTotals(db);
|
|
1058
|
+
} catch (error) {
|
|
1059
|
+
console.warn(`[claude-api-usage] migration 0034 failed; retrying on next boot: ${error?.message || error}`);
|
|
1060
|
+
}
|
|
1061
|
+
|
|
967
1062
|
const conversationColumns = db.prepare(`PRAGMA table_info(conversations)`).all().map((c) => c.name);
|
|
968
1063
|
if (!conversationColumns.includes('archived')) {
|
|
969
1064
|
db.exec(`ALTER TABLE conversations ADD COLUMN archived INTEGER NOT NULL DEFAULT 0`);
|