@oar-sh/oar 0.9.5 → 0.9.7
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 +12 -32
- package/.github/extensions/web-relay/skills/question-bridge.mjs +14 -39
- package/CHANGELOG.md +340 -0
- package/README.md +196 -26
- package/package.json +2 -1
- package/server/README.md +174 -7
- package/server/claude-cloud-worker/claude-cloud-event-normalizer.mjs +340 -0
- package/server/claude-cloud-worker/claude-cloud-session-process.mjs +1325 -0
- package/server/claude-cloud-worker/claude-cloud-session-worker.mjs +155 -0
- package/server/claude-worker/claude-remote-relay-tool.mjs +2 -0
- package/server/claude-worker/claude-sdk-adapter.mjs +18 -2
- package/server/claude-worker/claude-session-process.mjs +151 -1
- package/server/claude-worker/claude-session-worker.mjs +6 -0
- package/server/claude-worker/claude-turn-failure.mjs +92 -0
- package/server/claude-worker/claude-turn-publisher.mjs +52 -3
- package/server/claude-worker/sdk-message-normalizer.mjs +13 -1
- package/server/copilot-worker/copilot-model-switch.mjs +29 -3
- package/server/copilot-worker/copilot-question-bridge.mjs +2 -4
- package/server/copilot-worker/copilot-runtime-tree.mjs +91 -0
- package/server/copilot-worker/copilot-sdk-adapter.mjs +48 -8
- package/server/copilot-worker/copilot-sdk-session-process.mjs +897 -166
- package/server/cursor-worker/cursor-sdk-adapter.mjs +80 -13
- package/server/cursor-worker/cursor-turn-runner.mjs +5 -2
- package/server/db-schema.mjs +24 -0
- package/server/grok-worker/acp-client.mjs +68 -12
- package/server/grok-worker/grok-sdk-adapter.mjs +4 -0
- package/server/grok-worker/grok-turn-runner.mjs +6 -2
- package/server/migrations/0006-workspace-root-settings.mjs +67 -0
- package/server/migrations/0007-claude-cloud.mjs +82 -0
- package/server/public/app/agent-sessions-settings-ui.js +197 -0
- package/server/public/app/agent-sessions-settings.mjs +134 -0
- package/server/public/app/api-client.js +160 -6
- package/server/public/app/attachments-view.js +20 -1
- package/server/public/app/attribution-control.mjs +46 -0
- package/server/public/app/bootstrap.js +347 -31
- package/server/public/app/claude-cloud-conversation-ui.js +110 -0
- package/server/public/app/claude-cloud-settings-ui.js +374 -0
- package/server/public/app/claude-cloud-ui.mjs +611 -0
- package/server/public/app/composer-control-state.mjs +15 -3
- package/server/public/app/context-menu.mjs +140 -0
- package/server/public/app/context-tier-options.mjs +4 -0
- package/server/public/app/conversation-origin-ui.js +27 -0
- package/server/public/app/conversation-provider-indicator.mjs +22 -4
- package/server/public/app/conversation-view.js +178 -8
- package/server/public/app/cwd-picker.js +12 -0
- package/server/public/app/journal-view.js +704 -28
- package/server/public/app/markdown-strikethrough.mjs +31 -0
- package/server/public/app/math-dollars.mjs +89 -0
- package/server/public/app/model-selector-options.mjs +1 -1
- package/server/public/app/plan-usage-view.mjs +36 -8
- package/server/public/app/relay-board-inline.mjs +70 -0
- package/server/public/app/relay-board-view.js +157 -61
- package/server/public/app/relay-error-ctas.mjs +28 -5
- package/server/public/app/remote-relay-origin-view.mjs +120 -4
- package/server/public/app/router.js +49 -12
- package/server/public/app/settings-modal.js +33 -2
- package/server/public/app/settings-tabs.js +1 -1
- package/server/public/app/socket-handlers.js +60 -0
- package/server/public/app/store.js +4 -1
- package/server/public/app/subagent-fold.mjs +61 -0
- package/server/public/app/usage-limit-ui.js +225 -0
- package/server/public/index.html +471 -8
- package/server/relay-tools.md +13 -1
- package/server/repositories/message-repository.mjs +10 -1
- package/server/repositories/question-repository.mjs +1 -0
- package/server/repositories/remote-relay-repository.mjs +72 -1
- package/server/repositories/session-repository.mjs +47 -1
- package/server/routes/claude-cloud-routes.mjs +140 -0
- package/server/routes/git-routes.mjs +37 -0
- package/server/routes/messages-routes.mjs +202 -26
- package/server/routes/relay-board-routes.mjs +56 -2
- package/server/routes/remote-relay-routes.mjs +15 -1
- package/server/routes/sessions-routes.mjs +538 -72
- package/server/server-runtime.mjs +291 -27
- package/server/services/claude-account-usage-service.mjs +176 -0
- package/server/services/claude-cloud-repo-service.mjs +408 -0
- package/server/services/claude-cloud-session-service.mjs +326 -0
- package/server/services/claude-cloud-settings-service.mjs +269 -0
- package/server/services/git-remote-service.mjs +115 -0
- package/server/services/plan-usage-claude-cloud.mjs +211 -0
- package/server/services/plan-usage-claude.mjs +202 -40
- package/server/services/plan-usage-contract.mjs +39 -3
- package/server/services/plan-usage-service.mjs +82 -2
- package/server/services/preview-proxy-server.mjs +64 -36
- package/server/services/process-list-service.mjs +122 -0
- package/server/services/provider-reasoning-effort.mjs +3 -0
- package/server/services/relay-console-log.mjs +199 -0
- package/server/services/remote-relay-caller-context.mjs +78 -19
- package/server/services/remote-relay-client.mjs +9 -0
- package/server/services/remote-relay-dispatcher.mjs +451 -39
- package/server/services/remote-relay-inbound.mjs +83 -1
- package/server/services/remote-relay-loopback.mjs +245 -0
- package/server/services/remote-relay-registry-service.mjs +52 -0
- package/server/services/session-worker-launch-service.mjs +100 -15
- package/server/services/session-worker-process-service.mjs +410 -138
- package/server/services/session-worker-registry-service.mjs +1 -1
- package/server/services/session-worker-stop-service.mjs +24 -13
- package/server/services/session-worker-supervisor-service.mjs +89 -2
- package/server/services/usage-limit-pause-service.mjs +443 -0
- package/server/services/worker-launch-note-service.mjs +151 -0
- package/shared/ask-user-bridge.mjs +14 -39
- package/shared/claude-attribution.mjs +117 -0
- package/shared/claude-cloud/api-client.mjs +809 -0
- package/shared/claude-cloud/base-url.mjs +47 -0
- package/shared/claude-cloud/credentials.mjs +221 -0
- package/shared/claude-cloud/repo-url.mjs +111 -0
- package/shared/claude-usage-limit.mjs +257 -0
- package/shared/control-poller.mjs +69 -20
- package/shared/failure-note-text.mjs +38 -0
- package/shared/provider-routing.mjs +4 -2
- package/shared/question-wait.mjs +108 -0
- package/shared/remote-relay-contract.mjs +167 -22
- package/shared/remote-relay-tool-core.mjs +11 -8
- package/shared/steer-settle-failure.mjs +17 -0
- package/shared/steer-settle-markers.mjs +8 -0
- package/shared/worker-runtime/process-tree.mjs +483 -0
- package/shared/worker-runtime/relay-errors.mjs +31 -0
- package/shared/worker-runtime/send-and-wait-errors.mjs +4 -4
- package/shared/worker-runtime/turn-liveness.mjs +249 -0
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
STEER_SETTLE_FAILED_CODE,
|
|
23
23
|
STEER_SETTLE_FAILED_STABLE_CODE,
|
|
24
24
|
} from '../../shared/steer-settle-failure.mjs';
|
|
25
|
+
import { matchFailureNote } from '../../shared/failure-note-text.mjs';
|
|
25
26
|
import { buildRelayStopFailure } from '../../shared/relay-stop-failure.mjs';
|
|
26
27
|
import { stopSessionWorkerProcesses } from '../services/session-worker-stop-service.mjs';
|
|
27
28
|
import {
|
|
@@ -31,6 +32,7 @@ import {
|
|
|
31
32
|
usageSnapshotFromSummary,
|
|
32
33
|
} from '../services/usage-snapshot-helpers.mjs';
|
|
33
34
|
import { claudePlanUsageFromResult, normalizeClaudePlanUsage } from '../services/plan-usage-claude.mjs';
|
|
35
|
+
import { isUsageLimitTerminalError } from '../services/usage-limit-pause-service.mjs';
|
|
34
36
|
import { normalizeCopilotWorkerUsage } from '../services/plan-usage-copilot.mjs';
|
|
35
37
|
import { normalizeGrokTurnUsage } from '../services/plan-usage-grok.mjs';
|
|
36
38
|
import { openAIReasoningEffortsForModel } from '../../shared/openai-reasoning.mjs';
|
|
@@ -41,9 +43,13 @@ import { ANSWERED_ELSEWHERE_KIND, STEER_FOLDED_TEXT } from '../../shared/steer-s
|
|
|
41
43
|
import { isLiveResend } from '../services/steer-resend.mjs';
|
|
42
44
|
import { resolveProviderModelSelection } from '../services/provider-model-selection.mjs';
|
|
43
45
|
import { stripRemotePromptHeader } from '../../shared/remote-relay-contract.mjs';
|
|
46
|
+
import { stripModelTierSuffix } from '../../shared/claude-cloud/repo-url.mjs';
|
|
47
|
+
import { buildClaudeCloudDelivery, isClaudeCloudProviderType } from '../services/claude-cloud-session-service.mjs';
|
|
44
48
|
import {
|
|
45
49
|
admitRemoteRelayRequest,
|
|
46
50
|
findMentionedRemoteRelays,
|
|
51
|
+
findOriginRemoteRelay,
|
|
52
|
+
formatRemoteRelayOriginHint,
|
|
47
53
|
formatRemoteRelayMentionHint,
|
|
48
54
|
recordRemoteRelayUnlocks,
|
|
49
55
|
withRemoteRelayMentionHint,
|
|
@@ -110,6 +116,8 @@ function normalizeRequestedProviderType(value = '') {
|
|
|
110
116
|
// "no provider stated" and the guards below could never fire.
|
|
111
117
|
if (normalized === 'grok' || normalized === 'xai' || normalized === 'xai-grok') return 'grok';
|
|
112
118
|
if (normalized === 'claude' || normalized === 'anthropic') return 'claude';
|
|
119
|
+
// Its own provider, never read as `claude`.
|
|
120
|
+
if (normalized === 'claude-cloud') return 'claude-cloud';
|
|
113
121
|
return '';
|
|
114
122
|
}
|
|
115
123
|
|
|
@@ -117,6 +125,7 @@ const BOOTSTRAP_ONLY_PROVIDER_LABELS = Object.freeze({
|
|
|
117
125
|
cursor: 'Cursor',
|
|
118
126
|
grok: 'Grok',
|
|
119
127
|
claude: 'Claude',
|
|
128
|
+
'claude-cloud': 'Claude Cloud',
|
|
120
129
|
});
|
|
121
130
|
|
|
122
131
|
function deriveModelOrigin(model) {
|
|
@@ -450,15 +459,19 @@ function extractTerminalId(text, patterns = []) {
|
|
|
450
459
|
return null;
|
|
451
460
|
}
|
|
452
461
|
|
|
462
|
+
/**
|
|
463
|
+
* Reads a failure a sender reported as text only, without a `terminalError`
|
|
464
|
+
* record. The text has to BE a failure note, not merely contain one
|
|
465
|
+
* (shared/failure-note-text.mjs): a reply that quoted a note was stored as a
|
|
466
|
+
* failed turn, cut off at the quoted code.
|
|
467
|
+
*/
|
|
453
468
|
export function parseTerminalFailureText(text) {
|
|
454
469
|
const raw = String(text || '').trim();
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
const stableCode = String(stableCodeMatch[1] || '').trim().toLowerCase();
|
|
459
|
-
const code = normalizeTerminalErrorCode(stableCode);
|
|
470
|
+
const note = matchFailureNote(raw);
|
|
471
|
+
if (!note) return null;
|
|
472
|
+
const code = normalizeTerminalErrorCode(note.stableCode);
|
|
460
473
|
if (!code) return null;
|
|
461
|
-
const message = normalizeTerminalErrorText(
|
|
474
|
+
const message = normalizeTerminalErrorText(note.lead.trim().replace(/\s+$/, '.'));
|
|
462
475
|
const detailMatch = raw.match(/details:\s*(.+)$/i);
|
|
463
476
|
return {
|
|
464
477
|
terminal: true,
|
|
@@ -478,7 +491,7 @@ export function parseTerminalFailureText(text) {
|
|
|
478
491
|
};
|
|
479
492
|
}
|
|
480
493
|
|
|
481
|
-
function buildTerminalFailureTextForChat(terminalFailure, fallbackMessage = null) {
|
|
494
|
+
export function buildTerminalFailureTextForChat(terminalFailure, fallbackMessage = null) {
|
|
482
495
|
const failure = terminalFailure && typeof terminalFailure === 'object' ? terminalFailure : {};
|
|
483
496
|
const code = normalizeTerminalErrorCode(failure.code || failure.stableCode) || 'unknown-terminal';
|
|
484
497
|
const stableCode = `relay.${code}`;
|
|
@@ -486,7 +499,7 @@ function buildTerminalFailureTextForChat(terminalFailure, fallbackMessage = null
|
|
|
486
499
|
|| normalizeTerminalErrorText(fallbackMessage)
|
|
487
500
|
|| 'The relay runtime hit a terminal error and could not complete this turn.';
|
|
488
501
|
const guidance = normalizeTerminalErrorText(failure.guidance)
|
|
489
|
-
|| '
|
|
502
|
+
|| 'Send the message again to retry. If this keeps failing, include the error code when you report it.';
|
|
490
503
|
const detail = normalizeTerminalErrorText(failure.detail);
|
|
491
504
|
const ids = [
|
|
492
505
|
normalizeTerminalErrorText(failure.functionCallId) ? `functionCallId=${failure.functionCallId}` : null,
|
|
@@ -581,9 +594,17 @@ async function resolveRelayResponseText({
|
|
|
581
594
|
}
|
|
582
595
|
}
|
|
583
596
|
|
|
584
|
-
|
|
597
|
+
/**
|
|
598
|
+
* `inferFromText`: whether a text shaped like a failure note counts as one
|
|
599
|
+
* when no record came with it. A session worker always sends the record
|
|
600
|
+
* (`terminalError`), so for its publishes (an `attemptId` names the attempt)
|
|
601
|
+
* the text is a reply whatever it says — a reply that names a code after
|
|
602
|
+
* fixing a failed test is a reply. Senders without an attempt (the Copilot
|
|
603
|
+
* extension, a note read from another relay) still report as text.
|
|
604
|
+
*/
|
|
605
|
+
export function resolveTerminalFailurePayload(payload = {}, { fallbackText = null, inferFromText = true } = {}) {
|
|
585
606
|
const body = payload && typeof payload === 'object' ? payload : {};
|
|
586
|
-
const parsedTextFailure = parseTerminalFailureText(body.text);
|
|
607
|
+
const parsedTextFailure = inferFromText ? parseTerminalFailureText(body.text) : null;
|
|
587
608
|
const direct = body.terminalError && typeof body.terminalError === 'object'
|
|
588
609
|
? body.terminalError
|
|
589
610
|
: null;
|
|
@@ -628,8 +649,13 @@ export function resolveTerminalFailurePayload(payload = {}, { fallbackText = nul
|
|
|
628
649
|
const code = explicitCode
|
|
629
650
|
|| normalizeTerminalErrorCode(parsedTextFailure?.code)
|
|
630
651
|
|| 'unknown-terminal';
|
|
652
|
+
// Known from the text alone: nothing but the shape of the text says that
|
|
653
|
+
// this is a failure, so the text is kept as it was sent.
|
|
654
|
+
const fromText = !!parsedTextFailure && !direct && !explicitCode
|
|
655
|
+
&& body.terminal !== true && body.isTerminal !== true && body.errorKind !== 'terminal';
|
|
631
656
|
return {
|
|
632
657
|
terminal: true,
|
|
658
|
+
...(fromText ? { fromText: true } : {}),
|
|
633
659
|
code,
|
|
634
660
|
stableCode: `relay.${code}`,
|
|
635
661
|
message: explicitMessage || parsedTextFailure?.message || normalizeTerminalErrorText(fallbackText),
|
|
@@ -1108,6 +1134,7 @@ export function buildDequeuedRelayMessage({
|
|
|
1108
1134
|
defaultRelayMode,
|
|
1109
1135
|
defaultModel,
|
|
1110
1136
|
getCursorProviderSettings = () => null,
|
|
1137
|
+
getClaudeCloudProviderSettings = () => null,
|
|
1111
1138
|
} = {}) {
|
|
1112
1139
|
if (!msg) return null;
|
|
1113
1140
|
const attachments = parseAttachments(msg.attachments).map(hydrateAttachment).filter(Boolean);
|
|
@@ -1137,6 +1164,17 @@ export function buildDequeuedRelayMessage({
|
|
|
1137
1164
|
.map((value) => String(value || '').trim())
|
|
1138
1165
|
.filter(Boolean);
|
|
1139
1166
|
}
|
|
1167
|
+
// A Claude Cloud worker creates its session from the first delivered
|
|
1168
|
+
// message and sends into it afterwards: the repository, the branch and the
|
|
1169
|
+
// binding it reported ride on every message, so a restarted worker needs
|
|
1170
|
+
// nothing but the delivery. Other providers never get the field.
|
|
1171
|
+
const claudeCloud = isClaudeCloudProviderType(providerType)
|
|
1172
|
+
? buildClaudeCloudDelivery({
|
|
1173
|
+
conversation: stmts.getConvAnyStatus?.get?.(msg.conversation_id) || null,
|
|
1174
|
+
runtimeSession,
|
|
1175
|
+
fallbackEnvironmentId: getClaudeCloudProviderSettings()?.environmentId || '',
|
|
1176
|
+
})
|
|
1177
|
+
: null;
|
|
1140
1178
|
return {
|
|
1141
1179
|
id: msg.id,
|
|
1142
1180
|
conversationId: msg.conversation_id,
|
|
@@ -1150,6 +1188,7 @@ export function buildDequeuedRelayMessage({
|
|
|
1150
1188
|
cursorAgentId: String(runtimeSession?.cursor_agent_id || '').trim() || null,
|
|
1151
1189
|
cursorSubagentModels,
|
|
1152
1190
|
grokNativeSessionId: String(runtimeSession?.grok_native_session_id || '').trim() || null,
|
|
1191
|
+
...(isClaudeCloudProviderType(providerType) ? { claudeCloud } : {}),
|
|
1153
1192
|
reasoningEffort: String(msg.reasoning_effort || '').trim() || null,
|
|
1154
1193
|
contextTier: String(msg.context_tier || '').trim() || 'default',
|
|
1155
1194
|
quality: normalizeOpenAIImageQuality(msg.reasoning_effort),
|
|
@@ -1770,6 +1809,7 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
1770
1809
|
getClaudeProviderSettings = () => ({ enabled: false, model: '', models: [] }),
|
|
1771
1810
|
getCursorProviderSettings = () => ({ enabled: false, model: '', models: [] }),
|
|
1772
1811
|
getGrokProviderSettings = () => ({ enabled: false, model: '', models: [] }),
|
|
1812
|
+
getClaudeCloudProviderSettings = () => ({ enabled: false, defaultModel: '', environmentId: '', models: [] }),
|
|
1773
1813
|
rebindUnstartedOpenAIConversationModel = null,
|
|
1774
1814
|
normalizeRelayMode,
|
|
1775
1815
|
DEFAULT_RELAY_MODE,
|
|
@@ -1804,6 +1844,7 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
1804
1844
|
resolveSessionStateRoot,
|
|
1805
1845
|
fetchUsageSummary = null,
|
|
1806
1846
|
planUsageService = null,
|
|
1847
|
+
usageLimitPauseService = null,
|
|
1807
1848
|
getCursorPlanAllowanceSettings = () => ({ cursorModelsUsd: null, otherModelsUsd: null, resetDay: 1 }),
|
|
1808
1849
|
getGrokPlanAllowanceSettings = () => ({ monthlyUsd: null, resetDay: 1 }),
|
|
1809
1850
|
opaqueResponseRecoveryWaitMs = OPAQUE_RESPONSE_RECOVERY_WAIT_MS,
|
|
@@ -4152,7 +4193,8 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
4152
4193
|
const runtimeBoundToExplicitProvider = runtimeProviderType === 'openai'
|
|
4153
4194
|
|| runtimeProviderType === 'claude'
|
|
4154
4195
|
|| runtimeProviderType === 'cursor'
|
|
4155
|
-
|| runtimeProviderType === 'grok'
|
|
4196
|
+
|| runtimeProviderType === 'grok'
|
|
4197
|
+
|| runtimeProviderType === 'claude-cloud';
|
|
4156
4198
|
const configuredOpenAIModel = String(configuredOpenAI?.model || '').trim();
|
|
4157
4199
|
const availableOpenAIModels = new Set(
|
|
4158
4200
|
(Array.isArray(configuredOpenAI?.models) ? configuredOpenAI.models : [])
|
|
@@ -4276,6 +4318,17 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
4276
4318
|
});
|
|
4277
4319
|
}
|
|
4278
4320
|
}
|
|
4321
|
+
// Claude Cloud conversations are bootstrap-created only as well: each
|
|
4322
|
+
// carries the repository its cloud session clones. Only an explicit
|
|
4323
|
+
// provider counts here — the cloud's model ids are the Claude provider's
|
|
4324
|
+
// own, so an id alone never means "cloud".
|
|
4325
|
+
const runtimeUsesClaudeCloud = runtimeProviderType === 'claude-cloud';
|
|
4326
|
+
if (!shouldCreateConversation && !runtimeUsesClaudeCloud && requestedProviderType === 'claude-cloud') {
|
|
4327
|
+
return res.status(409).json({
|
|
4328
|
+
error: 'Claude Cloud requires creating a new Claude Cloud conversation',
|
|
4329
|
+
code: 'CLAUDE_CLOUD_REQUIRES_NEW_CONVERSATION',
|
|
4330
|
+
});
|
|
4331
|
+
}
|
|
4279
4332
|
if (
|
|
4280
4333
|
useOpenAIProvider
|
|
4281
4334
|
&& requestedOpenAIModel
|
|
@@ -4408,6 +4461,32 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
4408
4461
|
const grokModel = runtimeUsesGrok
|
|
4409
4462
|
? (pinnedGrokModel || String(configuredGrok?.model || '').trim())
|
|
4410
4463
|
: '';
|
|
4464
|
+
// Claude Cloud conversations resolve models against the cloud provider's
|
|
4465
|
+
// own list (plain ids, no "[1m]" tier). The model is part of the cloud
|
|
4466
|
+
// session's configuration: it can still change before the first message
|
|
4467
|
+
// creates that session, and is locked afterwards.
|
|
4468
|
+
const configuredClaudeCloud = runtimeUsesClaudeCloud ? getClaudeCloudProviderSettings() : null;
|
|
4469
|
+
const pinnedClaudeCloudModel = runtimeUsesClaudeCloud
|
|
4470
|
+
? stripModelTierSuffix(existingRuntimeSession?.provider_model)
|
|
4471
|
+
: '';
|
|
4472
|
+
const claudeCloudModelSelection = runtimeUsesClaudeCloud
|
|
4473
|
+
? resolveProviderModelSelection({
|
|
4474
|
+
requestedModel: stripModelTierSuffix(model),
|
|
4475
|
+
configuredModel: pinnedClaudeCloudModel || String(configuredClaudeCloud?.defaultModel || '').trim(),
|
|
4476
|
+
availableModels: Array.isArray(configuredClaudeCloud?.models) ? configuredClaudeCloud.models : [],
|
|
4477
|
+
})
|
|
4478
|
+
: null;
|
|
4479
|
+
if (claudeCloudModelSelection && !claudeCloudModelSelection.ok) {
|
|
4480
|
+
return res.status(400).json({
|
|
4481
|
+
error: `Claude Cloud model "${claudeCloudModelSelection.requestedModel}" is not available`,
|
|
4482
|
+
code: 'CLAUDE_CLOUD_MODEL_UNAVAILABLE',
|
|
4483
|
+
supportedModels: claudeCloudModelSelection.availableModels || [],
|
|
4484
|
+
});
|
|
4485
|
+
}
|
|
4486
|
+
// A cloud session's model can be switched between turns (the worker
|
|
4487
|
+
// sends set_model before the message), so a changed model is taken as
|
|
4488
|
+
// for a local Claude chat.
|
|
4489
|
+
const claudeCloudModel = claudeCloudModelSelection ? claudeCloudModelSelection.model : '';
|
|
4411
4490
|
const modelResolution = useOpenAIProvider
|
|
4412
4491
|
? {
|
|
4413
4492
|
ok: !!openAIModel,
|
|
@@ -4440,7 +4519,15 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
4440
4519
|
reasoningEffort: null,
|
|
4441
4520
|
error: grokModel ? null : 'Grok model is not configured',
|
|
4442
4521
|
}
|
|
4443
|
-
:
|
|
4522
|
+
: (runtimeUsesClaudeCloud
|
|
4523
|
+
? {
|
|
4524
|
+
ok: !!claudeCloudModel,
|
|
4525
|
+
model: claudeCloudModel,
|
|
4526
|
+
modelVariantId: claudeCloudModel,
|
|
4527
|
+
reasoningEffort: null,
|
|
4528
|
+
error: claudeCloudModel ? null : 'Claude Cloud model is not configured',
|
|
4529
|
+
}
|
|
4530
|
+
: resolveRequestedModel(model)))));
|
|
4444
4531
|
if (!modelResolution.ok) return res.status(400).json({ error: modelResolution.error, supportedModels: modelResolution.available || [] });
|
|
4445
4532
|
const requestedModel = String(modelResolution.model || '').trim();
|
|
4446
4533
|
const requestedAutoModel = requestedModel.toLowerCase() === AUTO_MODEL_SENTINEL;
|
|
@@ -4509,10 +4596,13 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
4509
4596
|
? resolveCursorReasoningEffort()
|
|
4510
4597
|
: (runtimeUsesGrok
|
|
4511
4598
|
? resolveGrokReasoningEffort()
|
|
4512
|
-
:
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
4599
|
+
// A cloud session takes no effort: whatever was asked, it is none.
|
|
4600
|
+
: (runtimeUsesClaudeCloud
|
|
4601
|
+
? { ok: true, effort: 'none', supported: ['none'] }
|
|
4602
|
+
: resolveRequestedReasoningEffort(
|
|
4603
|
+
requestedModel,
|
|
4604
|
+
explicitReasoningEffort || modelResolution.reasoningEffort || null,
|
|
4605
|
+
)))));
|
|
4516
4606
|
if (!reasoningResolution?.ok && !explicitReasoningEffort) {
|
|
4517
4607
|
const supportedEfforts = Array.isArray(reasoningResolution?.supported)
|
|
4518
4608
|
? reasoningResolution.supported.map((value) => String(value || '').trim().toLowerCase()).filter(Boolean)
|
|
@@ -4539,6 +4629,9 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
4539
4629
|
}
|
|
4540
4630
|
} else if (!['default', 'long_context'].includes(requestedContextTier)) {
|
|
4541
4631
|
return res.status(400).json({ error: 'Unsupported context tier' });
|
|
4632
|
+
} else if (runtimeUsesClaudeCloud) {
|
|
4633
|
+
// The cloud has no long-context tier to pick.
|
|
4634
|
+
normalizedContextTier = 'default';
|
|
4542
4635
|
}
|
|
4543
4636
|
if (!requestedRelayMode) return res.status(400).json({ error: 'Unsupported relay mode' });
|
|
4544
4637
|
const conversationWorkspaceState = (!shouldCreateConversation && typeof resolveConversationWorkspaceState === 'function')
|
|
@@ -4685,13 +4778,19 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
4685
4778
|
trimmedText || '(User sent image attachments only.)',
|
|
4686
4779
|
].join('\n')
|
|
4687
4780
|
: trimmedText;
|
|
4688
|
-
// Only what a human typed unlocks a remote relay (decision 19)
|
|
4781
|
+
// Only what a human typed unlocks a remote relay by mention (decision 19);
|
|
4782
|
+
// a prompt from a relay's agent opens that one relay and no other. The hint
|
|
4689
4783
|
// rides in the queued prompt only, so the stored message and the user's
|
|
4690
4784
|
// bubble stay exactly what was typed.
|
|
4691
4785
|
const mentionedRemoteRelays = remoteRequest.remote
|
|
4692
4786
|
? []
|
|
4693
4787
|
: findMentionedRemoteRelays(remoteRelayInbound, trimmedText);
|
|
4694
|
-
|
|
4788
|
+
// A prompt from another relay's agent opens that relay for this
|
|
4789
|
+
// conversation (it may be answered): the agent is told where it came from.
|
|
4790
|
+
const remoteRelayHint = remoteRequest.remote
|
|
4791
|
+
? formatRemoteRelayOriginHint(findOriginRemoteRelay(remoteRelayInbound, remoteRequest.origin), remoteRequest.origin)
|
|
4792
|
+
: formatRemoteRelayMentionHint(mentionedRemoteRelays);
|
|
4793
|
+
const queueText = withRemoteRelayMentionHint(promptText, remoteRelayHint);
|
|
4695
4794
|
// A Resend never came from the composer, and neither did another relay's
|
|
4696
4795
|
// agent: whatever the user is typing there stays their draft.
|
|
4697
4796
|
const keepsComposerDraft = !!resendOriginId || remoteRequest.remote;
|
|
@@ -4764,6 +4863,17 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
4764
4863
|
) {
|
|
4765
4864
|
stmts.updateRuntimeSessionProvider.run('claude', claudeModel, claudeModel, now, runtimeSession.id);
|
|
4766
4865
|
}
|
|
4866
|
+
// A cloud conversation follows the composer like a Claude one: the
|
|
4867
|
+
// worker switches the sandbox's model before the message.
|
|
4868
|
+
if (
|
|
4869
|
+
runtimeUsesClaudeCloud
|
|
4870
|
+
&& claudeCloudModel
|
|
4871
|
+
&& runtimeSession?.id
|
|
4872
|
+
&& claudeCloudModel !== String(existingRuntimeSession?.provider_model || '').trim()
|
|
4873
|
+
&& typeof stmts.updateRuntimeSessionProvider?.run === 'function'
|
|
4874
|
+
) {
|
|
4875
|
+
stmts.updateRuntimeSessionProvider.run('claude-cloud', claudeCloudModel, claudeCloudModel, now, runtimeSession.id);
|
|
4876
|
+
}
|
|
4767
4877
|
// Another relay's agent picks the model and mode of its own turn; the
|
|
4768
4878
|
// composer keeps restoring the user's choice for this conversation.
|
|
4769
4879
|
conversationPreferences = remoteRequest.remote && !shouldCreateConversation
|
|
@@ -5265,7 +5375,10 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
5265
5375
|
if (!runtimeSession) {
|
|
5266
5376
|
return res.status(404).json({ error: 'Runtime session not found for conversation' });
|
|
5267
5377
|
}
|
|
5268
|
-
|
|
5378
|
+
// The Claude Cloud worker reports the same shape (the cloud session's
|
|
5379
|
+
// context and token usage), so both providers are taken here.
|
|
5380
|
+
const contextUsageProvider = String(runtimeSession.provider_type || 'github').trim().toLowerCase();
|
|
5381
|
+
if (contextUsageProvider !== 'claude' && contextUsageProvider !== 'claude-cloud') {
|
|
5269
5382
|
return res.status(409).json({ error: 'Conversation is not bound to the Claude provider' });
|
|
5270
5383
|
}
|
|
5271
5384
|
if (typeof stmts.updateRuntimeSessionContextUsage?.run !== 'function') {
|
|
@@ -5325,13 +5438,17 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
5325
5438
|
// Claude-bound conversation's worker may write the Claude card. The
|
|
5326
5439
|
// snapshot itself is global, so a misrouted poster would corrupt it for
|
|
5327
5440
|
// everyone.
|
|
5441
|
+
// A Claude Cloud worker posts here too; its report is kept under its own
|
|
5442
|
+
// snapshot key, so the Claude card stays what the Claude workers wrote.
|
|
5328
5443
|
const conversationId = String(req.body?.conversationId || '').trim();
|
|
5444
|
+
let snapshotProvider = 'claude';
|
|
5329
5445
|
if (conversationId) {
|
|
5330
5446
|
const runtimeSession = stmts.getRuntimeSessionByConversation?.get?.(conversationId) || null;
|
|
5331
5447
|
const boundProvider = String(runtimeSession?.provider_type || 'github').trim().toLowerCase();
|
|
5332
|
-
if (runtimeSession && boundProvider !== 'claude') {
|
|
5448
|
+
if (runtimeSession && boundProvider !== 'claude' && boundProvider !== 'claude-cloud') {
|
|
5333
5449
|
return res.status(409).json({ error: 'Conversation is not bound to the Claude provider' });
|
|
5334
5450
|
}
|
|
5451
|
+
if (runtimeSession && boundProvider === 'claude-cloud') snapshotProvider = 'claude-cloud';
|
|
5335
5452
|
}
|
|
5336
5453
|
const usage = normalizeClaudePlanUsage(req.body?.usage)
|
|
5337
5454
|
|| claudePlanUsageFromResult({
|
|
@@ -5339,13 +5456,34 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
5339
5456
|
totalCostUsd: req.body?.totalCostUsd,
|
|
5340
5457
|
});
|
|
5341
5458
|
if (!usage) return res.status(400).json({ error: 'Missing usable usage payload' });
|
|
5342
|
-
planUsageService.saveSnapshot(
|
|
5459
|
+
planUsageService.saveSnapshot(snapshotProvider, usage, {
|
|
5343
5460
|
source: 'worker',
|
|
5344
5461
|
error: String(req.body?.error || '').trim() || null,
|
|
5345
5462
|
});
|
|
5346
5463
|
res.json({ ok: true });
|
|
5347
5464
|
});
|
|
5348
5465
|
|
|
5466
|
+
// POST /api/claude-usage-limit — the Claude worker forwards what the CLI
|
|
5467
|
+
// reported about the subscription's usage limit (a `rate_limit_event`): the
|
|
5468
|
+
// clients warn before the limit and show the reset once it is reached.
|
|
5469
|
+
app.post('/api/claude-usage-limit', auth, (req, res) => {
|
|
5470
|
+
touchCli();
|
|
5471
|
+
if (!usageLimitPauseService) return res.status(501).json({ error: 'Usage limit handling is unavailable' });
|
|
5472
|
+
// The report is the account's, so only a Claude-bound conversation's
|
|
5473
|
+
// worker may write it (same validation as the plan usage card); a Claude
|
|
5474
|
+
// Cloud chat runs on the same account and reports the same limit.
|
|
5475
|
+
const conversationId = String(req.body?.conversationId || '').trim();
|
|
5476
|
+
if (conversationId) {
|
|
5477
|
+
const runtimeSession = stmts.getRuntimeSessionByConversation?.get?.(conversationId) || null;
|
|
5478
|
+
const boundProvider = String(runtimeSession?.provider_type || 'github').trim().toLowerCase();
|
|
5479
|
+
if (runtimeSession && boundProvider !== 'claude' && boundProvider !== 'claude-cloud') {
|
|
5480
|
+
return res.status(409).json({ error: 'Conversation is not bound to the Claude provider' });
|
|
5481
|
+
}
|
|
5482
|
+
}
|
|
5483
|
+
const state = usageLimitPauseService.recordReport({ report: req.body?.report });
|
|
5484
|
+
res.json({ ok: true, state });
|
|
5485
|
+
});
|
|
5486
|
+
|
|
5349
5487
|
// POST /api/copilot-plan-usage — the Copilot SDK worker reports the turn's
|
|
5350
5488
|
// billing/token detail.
|
|
5351
5489
|
//
|
|
@@ -5942,6 +6080,7 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
5942
6080
|
defaultRelayMode: DEFAULT_RELAY_MODE,
|
|
5943
6081
|
defaultModel: DEFAULT_MODEL,
|
|
5944
6082
|
getCursorProviderSettings,
|
|
6083
|
+
getClaudeCloudProviderSettings,
|
|
5945
6084
|
});
|
|
5946
6085
|
|
|
5947
6086
|
if (out.attachments.length) {
|
|
@@ -6535,8 +6674,11 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
6535
6674
|
});
|
|
6536
6675
|
}
|
|
6537
6676
|
};
|
|
6538
|
-
|
|
6539
|
-
|
|
6677
|
+
let trimmedText = String(text || '').trim();
|
|
6678
|
+
let terminalFailure = resolveTerminalFailurePayload(req.body, {
|
|
6679
|
+
fallbackText: trimmedText,
|
|
6680
|
+
inferFromText: !String(req.body?.attemptId || '').trim(),
|
|
6681
|
+
});
|
|
6540
6682
|
// Final digests of background workflows that settled during this turn —
|
|
6541
6683
|
// the transcript's "Finished background task" cards. Same structural
|
|
6542
6684
|
// sanitizer as the live panel rows; junk entries drop silently (this
|
|
@@ -6590,6 +6732,22 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
6590
6732
|
}
|
|
6591
6733
|
|
|
6592
6734
|
const relayMode = normalizeRelayMode(mode || q?.relay_mode) || DEFAULT_RELAY_MODE;
|
|
6735
|
+
// A Claude turn refused at the usage limit is paused, not failed: its row
|
|
6736
|
+
// ends with a note like any answered one, and a follow-up is queued that
|
|
6737
|
+
// is held until the reset (usage-limit-pause-service).
|
|
6738
|
+
const usageLimitPause = terminalFailure && q && isUsageLimitTerminalError(req.body?.terminalError)
|
|
6739
|
+
? (usageLimitPauseService?.planPause?.({
|
|
6740
|
+
terminalError: req.body.terminalError,
|
|
6741
|
+
conversationId: targetConversationId,
|
|
6742
|
+
providerType: stmts.getRuntimeSessionByConversation?.get(targetConversationId)?.provider_type,
|
|
6743
|
+
queueRow: q,
|
|
6744
|
+
partialText: req.body?.partialText,
|
|
6745
|
+
}) || null)
|
|
6746
|
+
: null;
|
|
6747
|
+
if (usageLimitPause) {
|
|
6748
|
+
trimmedText = usageLimitPause.noteText;
|
|
6749
|
+
terminalFailure = null;
|
|
6750
|
+
}
|
|
6593
6751
|
if (terminalFailure) {
|
|
6594
6752
|
// Terminal failures carry provenance too: the original hijack incident
|
|
6595
6753
|
// presented exactly as stolen turns dying on 402 through this path.
|
|
@@ -6615,14 +6773,22 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
6615
6773
|
if (terminalCrossConversation.crossConversation) {
|
|
6616
6774
|
console.warn(`[${ts()}] CONVERSATION MISMATCH ${messageId?.slice(0, 8)} conv=${targetConversationId?.slice(0, 8)} responderConv=${terminalCrossConversation.responderConversationId?.slice(0, 8)} — terminal failure posted by another conversation's worker`);
|
|
6617
6775
|
}
|
|
6618
|
-
|
|
6776
|
+
// A failure read from the text alone keeps that text, whole: it is the
|
|
6777
|
+
// note already, and should the reading be wrong, nothing of a reply is
|
|
6778
|
+
// lost.
|
|
6779
|
+
const failureText = terminalFailure.fromText
|
|
6780
|
+
? trimmedText
|
|
6781
|
+
: buildTerminalFailureTextForChat(terminalFailure, trimmedText);
|
|
6782
|
+
// What the turn had written before it failed stays readable: the note
|
|
6783
|
+
// goes below it instead of replacing it.
|
|
6784
|
+
const partialText = String(req.body.partialText || '').trim();
|
|
6619
6785
|
const failed = failQueueMessage({
|
|
6620
6786
|
queueRow: q,
|
|
6621
6787
|
messageId,
|
|
6622
6788
|
conversationId: targetConversationId,
|
|
6623
6789
|
relayMode,
|
|
6624
6790
|
model: model || q?.model || null,
|
|
6625
|
-
responseText: failureText,
|
|
6791
|
+
responseText: partialText ? `${partialText}\n\n${failureText}` : failureText,
|
|
6626
6792
|
executedProvider: terminalExecutedProvider,
|
|
6627
6793
|
failureRecord: {
|
|
6628
6794
|
kind: 'terminal',
|
|
@@ -7015,6 +7181,13 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
7015
7181
|
},
|
|
7016
7182
|
});
|
|
7017
7183
|
io.emit('message_status', { messageId, conversationId: targetConversationId, status: 'done' });
|
|
7184
|
+
if (usageLimitPause) {
|
|
7185
|
+
try {
|
|
7186
|
+
usageLimitPauseService.schedulePause(usageLimitPause, { queueRow: q });
|
|
7187
|
+
} catch (error) {
|
|
7188
|
+
console.warn(`[${ts()}] USAGE LIMIT ${messageId?.slice(0, 8)} conv=${targetConversationId?.slice(0, 8)} the follow-up could not be queued: ${error?.message || error}`);
|
|
7189
|
+
}
|
|
7190
|
+
}
|
|
7018
7191
|
// An absorbed row is not a finished turn — the reply continues on the
|
|
7019
7192
|
// steered message's row, whose own completion sends the one notification.
|
|
7020
7193
|
// A stopped steer has no reply at all, and a message answered on a
|
|
@@ -7035,7 +7208,10 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
7035
7208
|
// Providers whose workers turn runtime-initiated activity into continuation
|
|
7036
7209
|
// rows: the Claude worker (background tasks/agents) and the Copilot SDK
|
|
7037
7210
|
// worker (detached background shells) serving both Copilot-CLI providers.
|
|
7038
|
-
|
|
7211
|
+
// A Claude Cloud session can take a turn of its own too: one the agent
|
|
7212
|
+
// starts when its background work reports back, or one started on
|
|
7213
|
+
// claude.ai; the cloud worker follows it and registers it here.
|
|
7214
|
+
const CONTINUATION_PROVIDER_TYPES = new Set(['claude', 'github', 'openai', 'claude-cloud']);
|
|
7039
7215
|
|
|
7040
7216
|
// POST /api/continuation-turn — a session worker's CLI started a turn on its
|
|
7041
7217
|
// own (a background task's notification). The synthetic queue row gives that
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
import { persistConversationPreferences } from '../services/conversation-preferences-service.mjs';
|
|
4
|
+
|
|
3
5
|
function normalizeId(value) {
|
|
4
6
|
const text = String(value || '').trim();
|
|
5
7
|
return text || null;
|
|
@@ -86,6 +88,7 @@ export function registerRelayBoardRoutes(app, deps) {
|
|
|
86
88
|
const {
|
|
87
89
|
auth,
|
|
88
90
|
io,
|
|
91
|
+
db,
|
|
89
92
|
stmts,
|
|
90
93
|
uuidv4,
|
|
91
94
|
normalizeRelayMode,
|
|
@@ -96,9 +99,52 @@ export function registerRelayBoardRoutes(app, deps) {
|
|
|
96
99
|
pushDispatchService,
|
|
97
100
|
} = deps;
|
|
98
101
|
|
|
102
|
+
/**
|
|
103
|
+
* The session goes on in the mode that was chosen on the board: after
|
|
104
|
+
* "Implement in autopilot" the composer says Autopilot, and the next message
|
|
105
|
+
* carries the work on instead of planning again. The model and the effort
|
|
106
|
+
* stay as they are. Advisory: the follow-up is queued whatever happens here.
|
|
107
|
+
*/
|
|
108
|
+
function followChosenMode(conversationId, relayMode, updatedAt) {
|
|
109
|
+
try {
|
|
110
|
+
const existing = stmts.getConvAnyStatus?.get?.(conversationId) || null;
|
|
111
|
+
if (!existing || String(existing.status || '').trim() === 'deleted') return;
|
|
112
|
+
if ((normalizeRelayMode(existing.preferred_relay_mode) || '') === relayMode) return;
|
|
113
|
+
const persisted = persistConversationPreferences({
|
|
114
|
+
db,
|
|
115
|
+
stmts,
|
|
116
|
+
conversationId,
|
|
117
|
+
preferredRelayMode: relayMode,
|
|
118
|
+
preferredModel: existing.preferred_model,
|
|
119
|
+
preferredReasoningEffort: existing.preferred_reasoning_effort,
|
|
120
|
+
updatedAt,
|
|
121
|
+
});
|
|
122
|
+
if (!persisted.ok) return;
|
|
123
|
+
// No sender: the page that pressed the button has to follow as well.
|
|
124
|
+
io.emit('conversation_preferences_updated', {
|
|
125
|
+
conversationId,
|
|
126
|
+
preferredRelayMode: persisted.preferredRelayMode,
|
|
127
|
+
preferredModel: persisted.preferredModel,
|
|
128
|
+
preferredReasoningEffort: persisted.preferredReasoningEffort,
|
|
129
|
+
autoCompactWindow: persisted.autoCompactWindow ?? null,
|
|
130
|
+
thinkingEnabled: persisted.thinkingEnabled ?? null,
|
|
131
|
+
thinkingDisplay: persisted.thinkingDisplay ?? null,
|
|
132
|
+
updatedAt: persisted.updatedAt,
|
|
133
|
+
senderClientId: null,
|
|
134
|
+
});
|
|
135
|
+
} catch {}
|
|
136
|
+
}
|
|
137
|
+
|
|
99
138
|
app.get('/api/relay-boards', auth, (req, res) => {
|
|
100
139
|
const conversationId = req.query.conversationId ? String(req.query.conversationId) : null;
|
|
101
140
|
const status = String(req.query.status || 'pending').trim() || 'pending';
|
|
141
|
+
// Every board of one conversation, settled ones included: a reply goes on
|
|
142
|
+
// showing the plan it offered and what was chosen. Per conversation only.
|
|
143
|
+
if (status === 'all') {
|
|
144
|
+
if (!conversationId) return res.status(400).json({ error: 'Missing conversationId' });
|
|
145
|
+
const all = stmts.listBoardsForConversation.all(conversationId);
|
|
146
|
+
return res.json({ boards: all.map(formatRelayBoardRow).filter(Boolean) });
|
|
147
|
+
}
|
|
102
148
|
const rows = stmts.listBoards.all(status, conversationId, conversationId);
|
|
103
149
|
res.json({ boards: rows.map(formatRelayBoardRow).filter(Boolean) });
|
|
104
150
|
});
|
|
@@ -222,7 +268,11 @@ export function registerRelayBoardRoutes(app, deps) {
|
|
|
222
268
|
String(resolvedVariantModel || '').trim().toLowerCase() === 'auto' ? 'auto' : 'manual',
|
|
223
269
|
);
|
|
224
270
|
stmts.updateConvTime.run(now, convId);
|
|
225
|
-
|
|
271
|
+
// One value per placeholder of the queue insert: the context tier
|
|
272
|
+
// (none: the follow-up runs on the conversation's own) and, where the
|
|
273
|
+
// queue has the column, the image operation. Two short, the insert
|
|
274
|
+
// threw and the action answered 500 with the board already marked.
|
|
275
|
+
const queueArgs = [
|
|
226
276
|
queuedMessageId,
|
|
227
277
|
convId,
|
|
228
278
|
runtimeSession?.id || null,
|
|
@@ -230,6 +280,7 @@ export function registerRelayBoardRoutes(app, deps) {
|
|
|
230
280
|
resolvedBaseModel,
|
|
231
281
|
resolvedVariantModel,
|
|
232
282
|
resolvedReasoningEffort,
|
|
283
|
+
null,
|
|
233
284
|
relayMode,
|
|
234
285
|
followupPrompt,
|
|
235
286
|
null,
|
|
@@ -238,7 +289,9 @@ export function registerRelayBoardRoutes(app, deps) {
|
|
|
238
289
|
null,
|
|
239
290
|
null,
|
|
240
291
|
null,
|
|
241
|
-
|
|
292
|
+
];
|
|
293
|
+
if (stmts.queueHasImageOperationId) queueArgs.push(null);
|
|
294
|
+
stmts.insertQ.run(...queueArgs);
|
|
242
295
|
io.emit('user_message', {
|
|
243
296
|
conversationId: convId,
|
|
244
297
|
messageId: queuedMessageId,
|
|
@@ -253,6 +306,7 @@ export function registerRelayBoardRoutes(app, deps) {
|
|
|
253
306
|
timestamp: now,
|
|
254
307
|
},
|
|
255
308
|
});
|
|
309
|
+
followChosenMode(convId, relayMode, now);
|
|
256
310
|
}
|
|
257
311
|
}
|
|
258
312
|
|
|
@@ -42,12 +42,15 @@ export function registerRemoteRelayRoutes(app, deps) {
|
|
|
42
42
|
});
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
-
// Workers ask at start whether to register the remote_relay tool at all
|
|
45
|
+
// Workers ask at start whether to register the remote_relay tool at all:
|
|
46
|
+
// when a relay is paired (`count`), or when this relay's own sessions are
|
|
47
|
+
// open to its agents (`localEnabled`, the local target).
|
|
46
48
|
app.get('/api/remote-relays/summary', auth, (_req, res) => {
|
|
47
49
|
const relays = registry.listPublic();
|
|
48
50
|
res.json({
|
|
49
51
|
count: relays.length,
|
|
50
52
|
online: relays.filter((relay) => relay.lastStatus === 'online').length,
|
|
53
|
+
localEnabled: registry.getAgentSessionsSettings?.().enabled === true,
|
|
51
54
|
});
|
|
52
55
|
});
|
|
53
56
|
|
|
@@ -111,6 +114,17 @@ export function registerRemoteRelayRoutes(app, deps) {
|
|
|
111
114
|
sendResult(res, registry.setSelfSettings(req.body || {}));
|
|
112
115
|
});
|
|
113
116
|
|
|
117
|
+
// Agent sessions: whether this relay's agents may start and use sessions on
|
|
118
|
+
// it, and the longest wait of one tool call. POST takes
|
|
119
|
+
// `{ enabled?, maxWaitSeconds? }` and answers the GET shape plus `ok`.
|
|
120
|
+
app.get('/api/settings/agent-sessions', auth, (_req, res) => {
|
|
121
|
+
res.json(registry.getAgentSessionsSettings());
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
app.post('/api/settings/agent-sessions', auth, (req, res) => {
|
|
125
|
+
sendResult(res, registry.setAgentSessionsSettings(req.body || {}));
|
|
126
|
+
});
|
|
127
|
+
|
|
114
128
|
// PATCH /api/remote-relays/:id — { permission, url, token, tokenMode }.
|
|
115
129
|
app.patch('/api/remote-relays/:id', auth, (req, res) => {
|
|
116
130
|
sendResult(res, registry.update(req.params?.id, req.body || {}));
|