@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
|
@@ -68,6 +68,11 @@ import {
|
|
|
68
68
|
outputDirOf,
|
|
69
69
|
usableOutputPath,
|
|
70
70
|
} from './task-output-discovery.mjs';
|
|
71
|
+
import {
|
|
72
|
+
checkClaudeSubscriptionInit,
|
|
73
|
+
describeClaudeSubscriptionFailure,
|
|
74
|
+
isKnownApiKeySource,
|
|
75
|
+
} from './claude-subscription-key-check.mjs';
|
|
71
76
|
|
|
72
77
|
/**
|
|
73
78
|
* Which system-prompt append a relay mode gets (claude-sdk-adapter's
|
|
@@ -448,6 +453,13 @@ function isPhantomResult(sdkMessage) {
|
|
|
448
453
|
* every live task is stopped via the SDK and the wind-down proceeds through
|
|
449
454
|
* the normal notification/continuation path.
|
|
450
455
|
*/
|
|
456
|
+
|
|
457
|
+
// A Claude chat's billing named by a delivered row or a worker kind: the
|
|
458
|
+
// subscription ('claude') or the saved API key ('claude-api'); '' otherwise.
|
|
459
|
+
function claudeBillingOf(value) {
|
|
460
|
+
const billing = String(value || '').trim().toLowerCase();
|
|
461
|
+
return billing === 'claude' || billing === 'claude-api' ? billing : '';
|
|
462
|
+
}
|
|
451
463
|
export function createClaudeSessionRunner({
|
|
452
464
|
api,
|
|
453
465
|
sdkSessionId,
|
|
@@ -532,6 +544,9 @@ export function createClaudeSessionRunner({
|
|
|
532
544
|
// by the first turn to open regardless.
|
|
533
545
|
compactReplayAdoptionMs = 60_000,
|
|
534
546
|
continuationRetryDelayMs = 500,
|
|
547
|
+
// How long a delivered turn that opens over a finished continuation waits
|
|
548
|
+
// for that continuation's row to register, so its answer publishes first.
|
|
549
|
+
continuationRegistrationWaitMs = 10_000,
|
|
535
550
|
// Backoff between attempts to save a consumed steer's settle marker (~31 s
|
|
536
551
|
// in all); after the last one the row fails terminally instead.
|
|
537
552
|
settleRetryDelaysMs = [1_000, 2_000, 4_000, 8_000, 8_000, 8_000],
|
|
@@ -568,21 +583,56 @@ export function createClaudeSessionRunner({
|
|
|
568
583
|
// worker asks for a heartbeat, on which the relay learns which process
|
|
569
584
|
// holds the session and asks for the set again.
|
|
570
585
|
onBackgroundTaskSetStale = () => {},
|
|
571
|
-
// Whether CLI processes carry the relay's `
|
|
586
|
+
// Whether CLI processes carry the relay's `relay_tool` tool
|
|
572
587
|
// (shared/remote-relay-tool-core.mjs). Default: ask the relay.
|
|
573
588
|
remoteRelayToolGate = null,
|
|
574
589
|
// Records every sub-agent's frames as its timeline, run-scoped (see
|
|
575
590
|
// subagent-timeline-recorder.mjs). Injectable for tests.
|
|
576
591
|
subagentTimelineRecorder = null,
|
|
592
|
+
// Another endpoint for the CLI (../deepseek-worker/deepseek-claude-backend.mjs):
|
|
593
|
+
// its spawn environment, model ids and effort, and what it reports. null is
|
|
594
|
+
// Claude through the host's login.
|
|
595
|
+
backend = null,
|
|
577
596
|
dbg = () => {},
|
|
578
597
|
} = {}) {
|
|
598
|
+
// The backend that maps models, effort, thinking and images into its own
|
|
599
|
+
// terms. A backend on Claude's own models (`claudeModels`, the Claude API
|
|
600
|
+
// backend) brings only its environment, its failures and what it reports:
|
|
601
|
+
// everything model-shaped stays exactly as the Claude worker has it.
|
|
602
|
+
const modelBackend = backend && backend.claudeModels !== true ? backend : null;
|
|
603
|
+
// The kind of worker this runner is part of: 'claude' on the host's login,
|
|
604
|
+
// else the backend's provider ('claude-api', 'deepseek').
|
|
605
|
+
const workerKind = String(backend?.providerType || backend?.id || 'claude').trim().toLowerCase();
|
|
606
|
+
// A relay-announced billing swap is noted once per worker.
|
|
607
|
+
let billingSwapNoted = false;
|
|
608
|
+
// Neither Claude's plan windows nor its usage limit exist on another backend.
|
|
609
|
+
const readPlanUsageForTurn = backend && !backend.reportsPlanUsage ? async () => null : readPlanUsageImpl;
|
|
579
610
|
const timelineRecorder = subagentTimelineRecorder || createSubagentTimelineRecorder({ api, conversationId: sdkSessionId, dbg });
|
|
580
611
|
const publisher = createClaudeTurnPublisher({
|
|
581
612
|
api,
|
|
582
613
|
dbg,
|
|
583
614
|
takeWorkflowRuns: () => drainSettledWorkflowRuns(),
|
|
615
|
+
// Both Claude billings report to the Claude API booking; DeepSeek has
|
|
616
|
+
// its own.
|
|
617
|
+
...(claudeBillingOf(workerKind) ? { billingUsageRoute: '/api/claude-api-usage', billingKind: workerKind } : {}),
|
|
618
|
+
...(backend
|
|
619
|
+
? {
|
|
620
|
+
describeFailure: backend.describeFailure,
|
|
621
|
+
runtimeLabel: backend.label || 'DeepSeek',
|
|
622
|
+
failureCodePrefix: backend.id || 'deepseek',
|
|
623
|
+
reportsPlanUsage: backend.reportsPlanUsage !== false,
|
|
624
|
+
usageRoute: backend.usageRoute || null,
|
|
625
|
+
...(modelBackend
|
|
626
|
+
? {
|
|
627
|
+
usageModelKey: modelBackend.relayModel,
|
|
628
|
+
relayModel: (value) => (value ? modelBackend.relayModel(value) : value),
|
|
629
|
+
}
|
|
630
|
+
: {}),
|
|
631
|
+
}
|
|
632
|
+
// The subscription worker's own refusal: a turn that came up on an API key.
|
|
633
|
+
: { describeFailure: describeClaudeSubscriptionFailure }),
|
|
584
634
|
});
|
|
585
|
-
// `
|
|
635
|
+
// `relay_tool` is registered only while this relay has paired remotes.
|
|
586
636
|
// Asked once when the worker starts, and again whenever a CLI process winds
|
|
587
637
|
// down, so a remote paired later reaches the next process. A cold spawn
|
|
588
638
|
// waits for the pending answer (handlePendingPayload): the tool list is
|
|
@@ -591,7 +641,9 @@ export function createClaudeSessionRunner({
|
|
|
591
641
|
void remoteRelayGate.refresh();
|
|
592
642
|
// The subscription's usage limit as the CLI reports it. Runner-level: the
|
|
593
643
|
// limit is the account's, not the CLI process's.
|
|
594
|
-
const usageLimit =
|
|
644
|
+
const usageLimit = backend && !backend.reportsUsageLimit
|
|
645
|
+
? { observe: () => null, classifyResult: () => null, classifyException: () => null }
|
|
646
|
+
: createUsageLimitTracker();
|
|
595
647
|
const resolveWorkflowSessionDir = resolveWorkflowSessionDirImpl || createDefaultWorkflowSessionDirResolver();
|
|
596
648
|
let claudeNativeSessionId = '';
|
|
597
649
|
let proc = null;
|
|
@@ -657,13 +709,19 @@ export function createClaudeSessionRunner({
|
|
|
657
709
|
// Row id → the latest in-flight /api/queue-consumed POST touching it.
|
|
658
710
|
const consumedMarkChains = new Map();
|
|
659
711
|
|
|
660
|
-
|
|
712
|
+
/**
|
|
713
|
+
* `cliStartedAt` (ms) is a backend CLI's spawn time, sent at its first
|
|
714
|
+
* init even when the session id is the known one: the relay records each
|
|
715
|
+
* CLI start of a DeepSeek session to find its shell snapshot on delete.
|
|
716
|
+
*/
|
|
717
|
+
async function persistNativeSessionId(conversationId, sessionId, { cliStartedAt = null } = {}) {
|
|
661
718
|
const normalized = String(sessionId || '').trim();
|
|
662
|
-
if (!normalized || normalized === claudeNativeSessionId) return;
|
|
719
|
+
if (!normalized || (normalized === claudeNativeSessionId && !cliStartedAt)) return;
|
|
663
720
|
try {
|
|
664
721
|
await api('POST', '/api/claude-native-session', {
|
|
665
722
|
conversationId,
|
|
666
723
|
claudeNativeSessionId: normalized,
|
|
724
|
+
...(cliStartedAt ? { cliStartedAt } : {}),
|
|
667
725
|
});
|
|
668
726
|
// Only cache after the server accepted it, so a failed persist is
|
|
669
727
|
// retried on the next init — resume across worker restarts depends on
|
|
@@ -678,7 +736,8 @@ export function createClaudeSessionRunner({
|
|
|
678
736
|
// Per-message model wins so the composer can switch Claude models between
|
|
679
737
|
// turns; the conversation's provider model and worker default are fallbacks.
|
|
680
738
|
const requestedModel = String(message.model || '').trim();
|
|
681
|
-
const
|
|
739
|
+
const ownModel = modelBackend ? modelBackend.isModel(requestedModel) : requestedModel.toLowerCase().startsWith('claude-');
|
|
740
|
+
const perTurnModel = requestedModel.toLowerCase() !== 'auto' && ownModel
|
|
682
741
|
? requestedModel
|
|
683
742
|
: '';
|
|
684
743
|
return perTurnModel
|
|
@@ -813,6 +872,12 @@ export function createClaudeSessionRunner({
|
|
|
813
872
|
modelUsage: null,
|
|
814
873
|
},
|
|
815
874
|
planBoardPosted: false,
|
|
875
|
+
// The CLI process the turn ran in, as its usage report names it: taken
|
|
876
|
+
// from the process here and again when the context is made active, and
|
|
877
|
+
// read from the context at finalize — a late continuation flush
|
|
878
|
+
// finalizes after the process died, when `proc` is null or a newer one.
|
|
879
|
+
runId: proc?.runId || '',
|
|
880
|
+
resumedSessionId: proc?.resumedSessionId || '',
|
|
816
881
|
// Set only on a context attached by post-compaction adoption, and only
|
|
817
882
|
// until that turn produces output — see handBackAdoptedContext.
|
|
818
883
|
adoptedFromCompaction: false,
|
|
@@ -846,6 +911,8 @@ export function createClaudeSessionRunner({
|
|
|
846
911
|
|
|
847
912
|
function activateContext(ctx) {
|
|
848
913
|
proc.activeCtx = ctx;
|
|
914
|
+
ctx.runId = proc.runId || '';
|
|
915
|
+
ctx.resumedSessionId = proc.resumedSessionId || '';
|
|
849
916
|
proc.lastBoundary = null;
|
|
850
917
|
proc.continuationInitPending = false;
|
|
851
918
|
proc.notificationPendingAt = 0;
|
|
@@ -1173,7 +1240,7 @@ export function createClaudeSessionRunner({
|
|
|
1173
1240
|
}
|
|
1174
1241
|
|
|
1175
1242
|
/** Wait until a continuation context has its relay queue row (or gave up). */
|
|
1176
|
-
async function waitForContextRegistration(ctx, timeoutMs =
|
|
1243
|
+
async function waitForContextRegistration(ctx, timeoutMs = continuationRegistrationWaitMs) {
|
|
1177
1244
|
const deadline = Date.now() + timeoutMs;
|
|
1178
1245
|
while (!ctx.registered && !ctx.discarded && Date.now() < deadline) {
|
|
1179
1246
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
@@ -1432,9 +1499,13 @@ export function createClaudeSessionRunner({
|
|
|
1432
1499
|
ctx.finalized = true;
|
|
1433
1500
|
controlPoller?.stop?.(ctx.controlState);
|
|
1434
1501
|
// A late continuation flush can land after the process died; usage reads
|
|
1435
|
-
// and model fallbacks must survive that.
|
|
1502
|
+
// and model fallbacks must survive that. The process the report names is
|
|
1503
|
+
// the context's own (set when it was made active), never the one that
|
|
1504
|
+
// happens to be current now.
|
|
1436
1505
|
const turnRef = proc?.turn || null;
|
|
1437
1506
|
const procModel = proc?.model || null;
|
|
1507
|
+
const runId = ctx.runId || '';
|
|
1508
|
+
const resumedSessionId = ctx.resumedSessionId || '';
|
|
1438
1509
|
// Steers pushed into this turn and still unsettled: the CLI has their
|
|
1439
1510
|
// prompts, so the relay marks them consumed with this turn's commit and
|
|
1440
1511
|
// never re-runs them even if this worker dies before settling them.
|
|
@@ -1461,12 +1532,12 @@ export function createClaudeSessionRunner({
|
|
|
1461
1532
|
await publisher.publishFinalStream(ctx.message, ctx.state.lastStreamedText);
|
|
1462
1533
|
const [contextUsage, planUsage] = await Promise.all([
|
|
1463
1534
|
readContextUsageImpl(turnRef, dbg),
|
|
1464
|
-
|
|
1535
|
+
readPlanUsageForTurn(turnRef, dbg),
|
|
1465
1536
|
]);
|
|
1466
1537
|
ctx.state.contextUsage = contextUsage;
|
|
1467
1538
|
ctx.state.planUsage = planUsage;
|
|
1468
1539
|
await publisher.publishContextUsage({ message: ctx.message, state: ctx.state, model: procModel, sdkSessionId });
|
|
1469
|
-
await publisher.publishPlanUsage({ message: ctx.message, state: ctx.state, sdkSessionId });
|
|
1540
|
+
await publisher.publishPlanUsage({ message: ctx.message, state: ctx.state, sdkSessionId, runId, resumedSessionId });
|
|
1470
1541
|
})().catch((error) => dbg('stopped-turn publish failed', error?.message || String(error)));
|
|
1471
1542
|
lingerStoppedRow(ctx, publishChain);
|
|
1472
1543
|
closeContext(ctx, true);
|
|
@@ -1478,13 +1549,13 @@ export function createClaudeSessionRunner({
|
|
|
1478
1549
|
// indicator updates with each reply.
|
|
1479
1550
|
const [contextUsage, planUsage] = await Promise.all([
|
|
1480
1551
|
readContextUsageImpl(turnRef, dbg),
|
|
1481
|
-
|
|
1552
|
+
readPlanUsageForTurn(turnRef, dbg),
|
|
1482
1553
|
]);
|
|
1483
1554
|
ctx.state.contextUsage = contextUsage;
|
|
1484
1555
|
ctx.state.planUsage = planUsage;
|
|
1485
1556
|
const responseModel = ctx.state.responseModel || procModel || null;
|
|
1486
1557
|
await publisher.publishContextUsage({ message: ctx.message, state: ctx.state, model: procModel, sdkSessionId });
|
|
1487
|
-
await publisher.publishPlanUsage({ message: ctx.message, state: ctx.state, sdkSessionId });
|
|
1558
|
+
await publisher.publishPlanUsage({ message: ctx.message, state: ctx.state, sdkSessionId, runId, resumedSessionId });
|
|
1488
1559
|
|
|
1489
1560
|
if (ctx.state.result?.isError) {
|
|
1490
1561
|
await publisher.publishErrorResult({ message: ctx.message, state: ctx.state, responseModel, consumedSteerIds });
|
|
@@ -1511,10 +1582,17 @@ export function createClaudeSessionRunner({
|
|
|
1511
1582
|
}
|
|
1512
1583
|
// The transport is gone, so nothing new can be read — but usage already
|
|
1513
1584
|
// captured on this context (a result seen before the stream died) still
|
|
1514
|
-
// reaches the relay. Best-effort by
|
|
1585
|
+
// reaches the relay, under the process the context ran in. Best-effort by
|
|
1586
|
+
// definition of this path.
|
|
1515
1587
|
if (ctx.state.contextUsage || ctx.state.planUsage) {
|
|
1516
1588
|
await publisher.publishContextUsage({ message: ctx.message, state: ctx.state, model, sdkSessionId }).catch(() => {});
|
|
1517
|
-
await publisher.publishPlanUsage({
|
|
1589
|
+
await publisher.publishPlanUsage({
|
|
1590
|
+
message: ctx.message,
|
|
1591
|
+
state: ctx.state,
|
|
1592
|
+
sdkSessionId,
|
|
1593
|
+
runId: ctx.runId || '',
|
|
1594
|
+
resumedSessionId: ctx.resumedSessionId || '',
|
|
1595
|
+
}).catch(() => {});
|
|
1518
1596
|
}
|
|
1519
1597
|
if (ctx.interrupted || aborted) {
|
|
1520
1598
|
await publisher.publishFinalStream(ctx.message, ctx.state.lastStreamedText);
|
|
@@ -1769,7 +1847,13 @@ export function createClaudeSessionRunner({
|
|
|
1769
1847
|
const agentLike = SUBAGENT_TASK_TYPES.has(String(task.taskType || ''));
|
|
1770
1848
|
const pinnedModel = (task.toolUseId && processRef.subagentSpawns.get(task.toolUseId)) || '';
|
|
1771
1849
|
const activity = (task.toolUseId && processRef.subagentActivity.get(task.toolUseId)) || {};
|
|
1772
|
-
const
|
|
1850
|
+
const knownModel = agentLike ? (activity.model || pinnedModel || processRef.initModel || null) : null;
|
|
1851
|
+
// A backend's CLI names a sub-agent's model in Claude's terms (the
|
|
1852
|
+
// spawn's "opus", a Claude id of a CLI without the alias pins): the
|
|
1853
|
+
// badge shows the backend's model that runs it.
|
|
1854
|
+
const model = knownModel && modelBackend?.subagentModel
|
|
1855
|
+
? (modelBackend.subagentModel(knownModel, processRef.initModel || processRef.model) || knownModel)
|
|
1856
|
+
: knownModel;
|
|
1773
1857
|
return {
|
|
1774
1858
|
taskId,
|
|
1775
1859
|
taskType: task.taskType,
|
|
@@ -2393,7 +2477,9 @@ export function createClaudeSessionRunner({
|
|
|
2393
2477
|
// init lands, and skipping that init settled the message with a pointer
|
|
2394
2478
|
// and dropped its real answer.
|
|
2395
2479
|
if (!firstInit && proc.pendingDelivered.length) turnStartedForWaitingDeliveries();
|
|
2396
|
-
persistNativeSessionId(sdkSessionId, sdkMessage.session_id
|
|
2480
|
+
persistNativeSessionId(sdkSessionId, sdkMessage.session_id, {
|
|
2481
|
+
cliStartedAt: backend && firstInit ? proc.spawnedAt : null,
|
|
2482
|
+
}).catch(() => {});
|
|
2397
2483
|
// Tracked on the process too (not only through the persist round-trip):
|
|
2398
2484
|
// the workflow poller needs the native id to find the session dir even
|
|
2399
2485
|
// when the persist call is still in flight or failed.
|
|
@@ -2402,7 +2488,9 @@ export function createClaudeSessionRunner({
|
|
|
2402
2488
|
// normalizer never sees it — capture the CLI's actual model here and
|
|
2403
2489
|
// seed it into every context at activation. Without this, a composer
|
|
2404
2490
|
// set to `auto` published responses with model: null.
|
|
2405
|
-
|
|
2491
|
+
// A backend's CLI names the model in its own terms ("deepseek-flash[1m]").
|
|
2492
|
+
const initModel = String(sdkMessage.model || '').trim();
|
|
2493
|
+
proc.initModel = (modelBackend && initModel ? modelBackend.relayModel(initModel) : initModel) || proc.initModel;
|
|
2406
2494
|
// A fresh init with no active turn AND nothing queued to deliver is the CLI
|
|
2407
2495
|
// opening a turn on its own — a background-task continuation ("you will be
|
|
2408
2496
|
// notified"). The real SDK does NOT emit a user-message replay for these
|
|
@@ -2541,7 +2629,8 @@ export function createClaudeSessionRunner({
|
|
|
2541
2629
|
// the composer pinned a model (adaptProcess repins via setModel). An
|
|
2542
2630
|
// `auto` composer keeps the CLI's own behavior — only the recorded
|
|
2543
2631
|
// model follows the switch.
|
|
2544
|
-
const
|
|
2632
|
+
const reportedFallback = String(sdkMessage.fallbackModel || '').trim();
|
|
2633
|
+
const fallbackModel = modelBackend && reportedFallback ? modelBackend.relayModel(reportedFallback) : reportedFallback;
|
|
2545
2634
|
if (fallbackModel) {
|
|
2546
2635
|
proc.initModel = fallbackModel;
|
|
2547
2636
|
if (String(sdkMessage.scope || '').trim().toLowerCase() === 'session') {
|
|
@@ -3522,10 +3611,7 @@ export function createClaudeSessionRunner({
|
|
|
3522
3611
|
}
|
|
3523
3612
|
continue;
|
|
3524
3613
|
}
|
|
3525
|
-
await finalizeContextOnStreamEnd(ctx, {
|
|
3526
|
-
aborted: processRef.aborted,
|
|
3527
|
-
model: processRef.model,
|
|
3528
|
-
}).catch(() => {});
|
|
3614
|
+
await finalizeContextOnStreamEnd(ctx, { aborted: processRef.aborted, model: processRef.model }).catch(() => {});
|
|
3529
3615
|
}
|
|
3530
3616
|
for (const entry of stoppedSteers) {
|
|
3531
3617
|
await settleFoldedSteeredEntry(entry).catch((error) => {
|
|
@@ -3546,10 +3632,12 @@ export function createClaudeSessionRunner({
|
|
|
3546
3632
|
if (missingFolder) throw missingFolder;
|
|
3547
3633
|
const relayMode = message.relayMode || 'agent';
|
|
3548
3634
|
const model = resolvePerTurnModel(message);
|
|
3549
|
-
|
|
3635
|
+
// A backend's effort is its own (DeepSeek: low/high/max), and so is its
|
|
3636
|
+
// thinking switch (DeepSeek: always on).
|
|
3637
|
+
const effort = modelBackend ? modelBackend.effortFor(message.reasoningEffort) : normalizeClaudeEffort(message.reasoningEffort);
|
|
3550
3638
|
const autoCompactWindow = normalizeAutoCompactWindow(getAutoCompactWindow());
|
|
3551
3639
|
const thinkingState = getThinking() || {};
|
|
3552
|
-
const thinkingEnabled = parseThinkingEnabled(thinkingState.enabled);
|
|
3640
|
+
const thinkingEnabled = modelBackend ? modelBackend.thinkingEnabled(effort) : parseThinkingEnabled(thinkingState.enabled);
|
|
3553
3641
|
const thinkingDisplay = parseThinkingDisplay(thinkingState.display);
|
|
3554
3642
|
const attribution = getAttribution() || null;
|
|
3555
3643
|
const backgroundCommandLimitMs = getBackgroundCommandLimitMs() ?? null;
|
|
@@ -3557,6 +3645,13 @@ export function createClaudeSessionRunner({
|
|
|
3557
3645
|
const processRef = {
|
|
3558
3646
|
turn: null,
|
|
3559
3647
|
abortController,
|
|
3648
|
+
// Names this CLI process in its usage reports: the relay tells a
|
|
3649
|
+
// report that fell behind one of the same process from a process
|
|
3650
|
+
// that started counting over.
|
|
3651
|
+
runId: randomUUID(),
|
|
3652
|
+
// When the CLI was spawned, reported with its first init (see
|
|
3653
|
+
// persistNativeSessionId).
|
|
3654
|
+
spawnedAt: Date.now(),
|
|
3560
3655
|
model,
|
|
3561
3656
|
relayMode,
|
|
3562
3657
|
effort,
|
|
@@ -3574,6 +3669,12 @@ export function createClaudeSessionRunner({
|
|
|
3574
3669
|
thinkingBudget: null,
|
|
3575
3670
|
appendClass: modeAppendClass(relayMode),
|
|
3576
3671
|
permissionMode: permissionModeForRelayMode(relayMode),
|
|
3672
|
+
// What only a fresh CLI of the backend takes (adaptProcess compares it).
|
|
3673
|
+
spawnKey: modelBackend ? modelBackend.spawnKey(message, { model }) : '',
|
|
3674
|
+
// Whether the API answered since the last result (noteApiTraffic), and
|
|
3675
|
+
// the error a backend's init check ended the process with.
|
|
3676
|
+
trafficSinceResult: false,
|
|
3677
|
+
failClosed: null,
|
|
3577
3678
|
liveTasks: new Map(),
|
|
3578
3679
|
knownTasks: new Set(),
|
|
3579
3680
|
// taskId → the row of a task that left the live set, until its
|
|
@@ -3602,6 +3703,8 @@ export function createClaudeSessionRunner({
|
|
|
3602
3703
|
// from `resume`, updated on init) — the workflow poller's key into
|
|
3603
3704
|
// `~/.claude/projects`.
|
|
3604
3705
|
nativeSessionId: '',
|
|
3706
|
+
// The session the process was spawned to resume ('' for a fresh one).
|
|
3707
|
+
resumedSessionId: '',
|
|
3605
3708
|
// tool_use id → explicitly pinned model of a subagent spawn block; the
|
|
3606
3709
|
// task panel's model column resolves through this at publish time.
|
|
3607
3710
|
subagentSpawns: new Map(),
|
|
@@ -3721,6 +3824,10 @@ export function createClaudeSessionRunner({
|
|
|
3721
3824
|
|
|
3722
3825
|
const resume = String(resumeOverride || message.claudeNativeSessionId || claudeNativeSessionId || '').trim();
|
|
3723
3826
|
processRef.nativeSessionId = resume;
|
|
3827
|
+
// The session this process continues, for its usage reports: the CLI's
|
|
3828
|
+
// first result then carries that session's earlier totals (see
|
|
3829
|
+
// claude-turn-publisher.mjs, runningCounts).
|
|
3830
|
+
processRef.resumedSessionId = resume;
|
|
3724
3831
|
// The CLI resolves `resume` inside the project directory for *this* CWD, so
|
|
3725
3832
|
// a session whose workspace root changed has to bring its transcript along
|
|
3726
3833
|
// or every turn from here on fails with "No conversation found".
|
|
@@ -3752,6 +3859,7 @@ export function createClaudeSessionRunner({
|
|
|
3752
3859
|
|| '',
|
|
3753
3860
|
),
|
|
3754
3861
|
remoteRelayTool: remoteRelayGate.isEnabled(),
|
|
3862
|
+
backend,
|
|
3755
3863
|
dbg,
|
|
3756
3864
|
});
|
|
3757
3865
|
processRef.lifecycleTimer = setInterval(() => evaluateLifecycle(), lifecyclePollMs);
|
|
@@ -3760,6 +3868,17 @@ export function createClaudeSessionRunner({
|
|
|
3760
3868
|
let streamError = null;
|
|
3761
3869
|
try {
|
|
3762
3870
|
for await (const sdkMessage of processRef.turn) {
|
|
3871
|
+
// A backend that checks the CLI's credential (the Claude API
|
|
3872
|
+
// backend: the key's source on every init) stops the process on a
|
|
3873
|
+
// wrong one before the init is handled: the open turns fail with
|
|
3874
|
+
// its note (cleanupProcess), and nothing more is read.
|
|
3875
|
+
const refused = refuseOnInit(processRef, sdkMessage);
|
|
3876
|
+
if (refused) {
|
|
3877
|
+
streamError = refused;
|
|
3878
|
+
abortController.abort();
|
|
3879
|
+
break;
|
|
3880
|
+
}
|
|
3881
|
+
noteApiTraffic(processRef, sdkMessage);
|
|
3763
3882
|
await onSdkMessage(processRef, sdkMessage);
|
|
3764
3883
|
syncDeliveryReadiness();
|
|
3765
3884
|
}
|
|
@@ -3773,6 +3892,11 @@ export function createClaudeSessionRunner({
|
|
|
3773
3892
|
try {
|
|
3774
3893
|
processRef.turn.endInput?.();
|
|
3775
3894
|
} catch {}
|
|
3895
|
+
// The CLI refused a backend's live flag settings (its key and
|
|
3896
|
+
// endpoint pins, claude-sdk-adapter.mjs): nothing was sent, and the
|
|
3897
|
+
// SDK aborted the query, which reads like a stop. The open turns fail
|
|
3898
|
+
// with that note instead.
|
|
3899
|
+
if (!streamError && processRef.turn?.liveFlagSettingsError) streamError = processRef.turn.liveFlagSettingsError;
|
|
3776
3900
|
await cleanupProcess(processRef, streamError);
|
|
3777
3901
|
}
|
|
3778
3902
|
})();
|
|
@@ -3780,6 +3904,42 @@ export function createClaudeSessionRunner({
|
|
|
3780
3904
|
return processRef;
|
|
3781
3905
|
}
|
|
3782
3906
|
|
|
3907
|
+
/**
|
|
3908
|
+
* The backend's verdict on a `system/init` (`checkInit`): null to go on, or
|
|
3909
|
+
* the Error that ends the process. `afterTraffic` says whether the turn had
|
|
3910
|
+
* already talked to the API since the last result.
|
|
3911
|
+
*/
|
|
3912
|
+
function refuseOnInit(processRef, sdkMessage) {
|
|
3913
|
+
// A backend checks its own credential; the subscription worker checks
|
|
3914
|
+
// that the CLI took no API key (claude-subscription-key-check.mjs).
|
|
3915
|
+
const checkInit = typeof backend?.checkInit === 'function'
|
|
3916
|
+
? backend.checkInit
|
|
3917
|
+
: (backend ? null : checkClaudeSubscriptionInit);
|
|
3918
|
+
if (typeof checkInit !== 'function') return null;
|
|
3919
|
+
if (sdkMessage?.type !== 'system' || sdkMessage?.subtype !== 'init') return null;
|
|
3920
|
+
if (!backend && sdkMessage.apiKeySource !== undefined && !isKnownApiKeySource(sdkMessage.apiKeySource) && !processRef.unknownKeySourceLogged) {
|
|
3921
|
+
processRef.unknownKeySourceLogged = true;
|
|
3922
|
+
dbg('init names an API key source this relay does not know; the subscription turn goes on', String(sdkMessage.apiKeySource).slice(0, 40));
|
|
3923
|
+
}
|
|
3924
|
+
let refused = null;
|
|
3925
|
+
try {
|
|
3926
|
+
refused = checkInit(sdkMessage, { afterTraffic: processRef.trafficSinceResult === true });
|
|
3927
|
+
} catch (error) {
|
|
3928
|
+
refused = error instanceof Error ? error : new Error(String(error));
|
|
3929
|
+
}
|
|
3930
|
+
if (!refused) return null;
|
|
3931
|
+
processRef.failClosed = refused;
|
|
3932
|
+
dbg('init refused by the backend', refused.code || '', refused.message);
|
|
3933
|
+
return refused;
|
|
3934
|
+
}
|
|
3935
|
+
|
|
3936
|
+
/** Whether the API has answered since the last result: a stream event or an assistant frame. */
|
|
3937
|
+
function noteApiTraffic(processRef, sdkMessage) {
|
|
3938
|
+
const type = sdkMessage?.type;
|
|
3939
|
+
if (type === 'stream_event' || type === 'assistant') processRef.trafficSinceResult = true;
|
|
3940
|
+
else if (type === 'result') processRef.trafficSinceResult = false;
|
|
3941
|
+
}
|
|
3942
|
+
|
|
3783
3943
|
function expectSettingsReinit() {
|
|
3784
3944
|
settingsReinitsOwed += 1;
|
|
3785
3945
|
settingsWindowOpen = true;
|
|
@@ -3798,6 +3958,24 @@ export function createClaudeSessionRunner({
|
|
|
3798
3958
|
return !proc.suppressingTurn;
|
|
3799
3959
|
}
|
|
3800
3960
|
|
|
3961
|
+
/**
|
|
3962
|
+
* What keeps the live process from being released now, for the relay's
|
|
3963
|
+
* billing swap ('' when nothing does, or there is no process): 'question',
|
|
3964
|
+
* 'compaction', 'turn' or 'background'. The same conditions as
|
|
3965
|
+
* canReleaseForRespawn, named.
|
|
3966
|
+
*/
|
|
3967
|
+
function releaseHold() {
|
|
3968
|
+
// A delivered message on its way in (waiting for a draining process or
|
|
3969
|
+
// a cold spawn): a turn, though no process may exist yet.
|
|
3970
|
+
if (admittingDeliveries > 0) return 'turn';
|
|
3971
|
+
if (!proc || proc.closing || proc.aborted) return '';
|
|
3972
|
+
if (proc.pendingControlRequests > 0) return 'question';
|
|
3973
|
+
if (isCompacting() || Date.now() < proc.compactReplayUntil) return 'compaction';
|
|
3974
|
+
if (proc.activeCtx || proc.pendingDelivered.length || proc.suppressingTurn) return 'turn';
|
|
3975
|
+
if (proc.liveTasks.size || isContinuationImminent()) return 'background';
|
|
3976
|
+
return '';
|
|
3977
|
+
}
|
|
3978
|
+
|
|
3801
3979
|
/**
|
|
3802
3980
|
* The running CLI's compaction window (the one it was spawned with) and
|
|
3803
3981
|
* whether background work would block the respawn that applies another
|
|
@@ -3821,7 +3999,18 @@ export function createClaudeSessionRunner({
|
|
|
3821
3999
|
async function adaptProcess(message, delivery = {}) {
|
|
3822
4000
|
const relayMode = message.relayMode || 'agent';
|
|
3823
4001
|
const model = resolvePerTurnModel(message);
|
|
3824
|
-
const effort = normalizeClaudeEffort(message.reasoningEffort);
|
|
4002
|
+
const effort = modelBackend ? modelBackend.effortFor(message.reasoningEffort) : normalizeClaudeEffort(message.reasoningEffort);
|
|
4003
|
+
|
|
4004
|
+
// A backend's effort, and the model its CLI pins Claude Code's aliases
|
|
4005
|
+
// to, are read by the CLI at spawn only: a change takes a fresh process
|
|
4006
|
+
// on the same session, when nothing lives in the old one. Busy, the
|
|
4007
|
+
// delivery runs on the old process (setModel below) and the next
|
|
4008
|
+
// turn-opening one tries again.
|
|
4009
|
+
if (modelBackend && modelBackend.spawnKey(message, { model }) !== proc.spawnKey && canReleaseForRespawn()) {
|
|
4010
|
+
gracefulShutdown('backend-spawn');
|
|
4011
|
+
await drainClosingProcess(10_000);
|
|
4012
|
+
return null;
|
|
4013
|
+
}
|
|
3825
4014
|
|
|
3826
4015
|
// A mode change that would alter the spawn-time system prompt append gets
|
|
3827
4016
|
// a fresh process — but only when nothing lives in the old one.
|
|
@@ -3874,13 +4063,14 @@ export function createClaudeSessionRunner({
|
|
|
3874
4063
|
const repinAfterFallback = Boolean(proc.modelFallback) && Boolean(model);
|
|
3875
4064
|
if (model !== proc.model || repinAfterFallback) {
|
|
3876
4065
|
expectSettingsReinit();
|
|
3877
|
-
|
|
4066
|
+
const cliModel = modelBackend && model ? modelBackend.cliModel(model) : model;
|
|
4067
|
+
await Promise.resolve(proc.turn.setModel?.(cliModel || undefined)).catch((error) => {
|
|
3878
4068
|
dbg('setModel failed', error?.message || String(error));
|
|
3879
4069
|
});
|
|
3880
4070
|
proc.model = model;
|
|
3881
4071
|
if (model) proc.modelFallback = '';
|
|
3882
4072
|
}
|
|
3883
|
-
if (effort !== proc.effort) {
|
|
4073
|
+
if (!modelBackend && effort !== proc.effort) {
|
|
3884
4074
|
// 'ultracode' is not an effortLevel the CLI accepts — it maps to the
|
|
3885
4075
|
// session-scoped ultracode/enableWorkflows flags (with the effort pinned
|
|
3886
4076
|
// to the xhigh the flag implies); every other change clears those flags
|
|
@@ -3922,7 +4112,8 @@ export function createClaudeSessionRunner({
|
|
|
3922
4112
|
const thinkingEnabled = parseThinkingEnabled(thinkingState.enabled);
|
|
3923
4113
|
const thinkingDisplay = parseThinkingDisplay(thinkingState.display);
|
|
3924
4114
|
let reassertDisplay = false;
|
|
3925
|
-
|
|
4115
|
+
// A backend's thinking is fixed at spawn (above).
|
|
4116
|
+
if (!modelBackend && thinkingEnabled !== proc.thinkingEnabled) {
|
|
3926
4117
|
if (thinkingEnabled === true) {
|
|
3927
4118
|
// Enabling applies live. The display MUST be re-sent afterwards, in
|
|
3928
4119
|
// this order: a session spawned with thinking off carries no display
|
|
@@ -4053,9 +4244,14 @@ export function createClaudeSessionRunner({
|
|
|
4053
4244
|
function steeringState() {
|
|
4054
4245
|
// A closing/aborted process is about to die — nothing about its last turn
|
|
4055
4246
|
// should hold the composer of the worker that replaces it.
|
|
4247
|
+
// `releasable`: the relay may replace this worker now (a billing swap):
|
|
4248
|
+
// no CLI process and no delivery on its way in, or a process
|
|
4249
|
+
// canReleaseForRespawn would let go of.
|
|
4056
4250
|
if (!proc || proc.closing || proc.aborted) {
|
|
4251
|
+
const admitting = admittingDeliveries > 0;
|
|
4057
4252
|
return {
|
|
4058
4253
|
turnActive: false, canSteer: false, holdReason: null, messageId: null, supported: true, autoCompactWindow: null,
|
|
4254
|
+
releasable: !admitting, releaseHold: admitting ? 'turn' : null, backgroundTaskCount: 0,
|
|
4059
4255
|
};
|
|
4060
4256
|
}
|
|
4061
4257
|
const ctx = proc.activeCtx || null;
|
|
@@ -4083,7 +4279,18 @@ export function createClaudeSessionRunner({
|
|
|
4083
4279
|
// `supported` is the worker-advertised opt-in the client's composer reads
|
|
4084
4280
|
// (the same flag the Copilot SDK worker publishes) — its only steering
|
|
4085
4281
|
// gate, so every snapshot carries it, idle ones included.
|
|
4086
|
-
|
|
4282
|
+
const hold = releaseHold();
|
|
4283
|
+
return {
|
|
4284
|
+
turnActive,
|
|
4285
|
+
canSteer,
|
|
4286
|
+
holdReason,
|
|
4287
|
+
messageId,
|
|
4288
|
+
supported: true,
|
|
4289
|
+
autoCompactWindow,
|
|
4290
|
+
releasable: !hold,
|
|
4291
|
+
releaseHold: hold || null,
|
|
4292
|
+
backgroundTaskCount: proc.liveTasks.size,
|
|
4293
|
+
};
|
|
4087
4294
|
}
|
|
4088
4295
|
|
|
4089
4296
|
/**
|
|
@@ -4108,6 +4315,20 @@ export function createClaudeSessionRunner({
|
|
|
4108
4315
|
async function handlePendingPayload(pending) {
|
|
4109
4316
|
const message = pending?.message || null;
|
|
4110
4317
|
if (!message) return false;
|
|
4318
|
+
// A Claude chat's row runs only on a worker of its billing: one of the
|
|
4319
|
+
// other kind (a relay that missed a switch) goes back to the queue, with
|
|
4320
|
+
// no retry penalty, for the worker the switch brings up.
|
|
4321
|
+
const deliveredBilling = claudeBillingOf(message.billing || message.providerType);
|
|
4322
|
+
if (deliveredBilling && claudeBillingOf(workerKind) && deliveredBilling !== workerKind) {
|
|
4323
|
+
dbg('delivery of another billing handed back', message.id || '(no id)', `${deliveredBilling} != ${workerKind}`);
|
|
4324
|
+
if (message.id) {
|
|
4325
|
+
await api('POST', '/api/requeue', { messageId: message.id, class: 'kind-mismatch', ...attemptFields(message) }).catch((error) => {
|
|
4326
|
+
if (isStaleAttemptError(error)) dbg('kind-mismatch hand-back refused as stale_attempt', message.id);
|
|
4327
|
+
else dbg('kind-mismatch hand-back failed', message.id, error?.message || String(error));
|
|
4328
|
+
});
|
|
4329
|
+
}
|
|
4330
|
+
return false;
|
|
4331
|
+
}
|
|
4111
4332
|
// An older relay (the worker updated on disk before the relay restarted)
|
|
4112
4333
|
// treats the hand-back as a failed delivery — retry, backoff, worker
|
|
4113
4334
|
// marked errored — so against one the message is pushed the legacy way.
|
|
@@ -4124,6 +4345,21 @@ export function createClaudeSessionRunner({
|
|
|
4124
4345
|
syncDeliveryReadiness();
|
|
4125
4346
|
};
|
|
4126
4347
|
syncDeliveryReadiness();
|
|
4348
|
+
// The relay replaced the previous worker for a billing switch: said once,
|
|
4349
|
+
// on the first message this worker runs.
|
|
4350
|
+
const billingSwap = pending?.settings?.billingSwap;
|
|
4351
|
+
if (billingSwap && typeof billingSwap === 'object' && !billingSwapNoted) {
|
|
4352
|
+
billingSwapNoted = true;
|
|
4353
|
+
const to = claudeBillingOf(billingSwap.to) || workerKind;
|
|
4354
|
+
publisher.postActivity(
|
|
4355
|
+
message,
|
|
4356
|
+
to === 'claude-api'
|
|
4357
|
+
? 'Restarted the session on the Claude API key (billing switch)'
|
|
4358
|
+
: 'Restarted the session on the Claude subscription (billing switch)',
|
|
4359
|
+
null,
|
|
4360
|
+
{ kind: 'billing_switch_respawn', from: claudeBillingOf(billingSwap.from) || null, to },
|
|
4361
|
+
);
|
|
4362
|
+
}
|
|
4127
4363
|
// This delivery's own record: a window respawn taken for it (one at most).
|
|
4128
4364
|
const delivery = { windowRespawn: null };
|
|
4129
4365
|
// Its note goes on this message's row, once, whichever delivery's spawn
|
|
@@ -4155,7 +4391,7 @@ export function createClaudeSessionRunner({
|
|
|
4155
4391
|
// Awaited only when there is one: a cold spawn with the decision
|
|
4156
4392
|
// already in pushes synchronously.
|
|
4157
4393
|
if (proc) await drainClosingProcess();
|
|
4158
|
-
// The tool set is fixed per CLI process: let the
|
|
4394
|
+
// The tool set is fixed per CLI process: let the relay_tool
|
|
4159
4395
|
// decision settle first. The worker's first delivery can arrive
|
|
4160
4396
|
// before its startup GET has answered (seen live: 180 ms after the
|
|
4161
4397
|
// spawn). ready() never rejects and is bounded by the gate's timeout.
|
|
@@ -4175,14 +4411,20 @@ export function createClaudeSessionRunner({
|
|
|
4175
4411
|
// seconds (transcript load + first request); say so instead of
|
|
4176
4412
|
// showing bare dots. Posted once here — not in spawnProcess — so a
|
|
4177
4413
|
// push-race respawn cannot duplicate the line.
|
|
4414
|
+
// In a DeepSeek chat the CLI is Claude Code talking to DeepSeek; the
|
|
4415
|
+
// line names the backend so the wait is not read as a Claude turn.
|
|
4416
|
+
const startingWhat = backend ? `Claude Code for ${backend.label || 'DeepSeek'}` : 'the Claude CLI';
|
|
4178
4417
|
publisher.postActivity(message, (respawnResume || message.claudeNativeSessionId || claudeNativeSessionId)
|
|
4179
|
-
?
|
|
4180
|
-
:
|
|
4418
|
+
? `Starting ${startingWhat} (cold start — resuming the session transcript)…`
|
|
4419
|
+
: `Starting ${startingWhat} (cold start)…`);
|
|
4181
4420
|
}
|
|
4182
4421
|
// Released for the window, then joined a process another delivery
|
|
4183
4422
|
// spawned in the meantime.
|
|
4184
4423
|
noteWindowRespawn();
|
|
4185
|
-
|
|
4424
|
+
// A model that takes no image input gets every image by its path.
|
|
4425
|
+
const content = buildClaudeUserContent(message, {
|
|
4426
|
+
embedImages: modelBackend ? modelBackend.acceptsImages(resolvePerTurnModel(message)) : true,
|
|
4427
|
+
});
|
|
4186
4428
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
4187
4429
|
const procRef = proc;
|
|
4188
4430
|
const ctx = createDeliveredContext(message);
|
|
@@ -4314,6 +4556,7 @@ export function createClaudeSessionRunner({
|
|
|
4314
4556
|
}
|
|
4315
4557
|
|
|
4316
4558
|
return {
|
|
4559
|
+
workerKind,
|
|
4317
4560
|
handlePendingPayload,
|
|
4318
4561
|
canAcceptSteering,
|
|
4319
4562
|
isDeliveryHeld,
|