@oar-sh/oar 0.9.6 → 0.9.8

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.
Files changed (105) hide show
  1. package/CHANGELOG.md +285 -0
  2. package/README.md +222 -40
  3. package/bin/oar.js +320 -18
  4. package/package.json +1 -1
  5. package/server/README.md +174 -7
  6. package/server/claude-cloud-worker/claude-cloud-event-normalizer.mjs +340 -0
  7. package/server/claude-cloud-worker/claude-cloud-session-process.mjs +1325 -0
  8. package/server/claude-cloud-worker/claude-cloud-session-worker.mjs +155 -0
  9. package/server/claude-worker/claude-remote-relay-tool.mjs +2 -0
  10. package/server/claude-worker/claude-sdk-adapter.mjs +18 -13
  11. package/server/claude-worker/claude-session-process.mjs +225 -24
  12. package/server/claude-worker/claude-session-worker.mjs +6 -0
  13. package/server/claude-worker/sdk-message-normalizer.mjs +28 -0
  14. package/server/db-schema.mjs +17 -0
  15. package/server/migrations/0006-workspace-root-settings.mjs +67 -0
  16. package/server/migrations/0007-claude-cloud.mjs +82 -0
  17. package/server/public/app/activity-replay-state.mjs +68 -5
  18. package/server/public/app/agent-sessions-settings-ui.js +197 -0
  19. package/server/public/app/agent-sessions-settings.mjs +134 -0
  20. package/server/public/app/api-client.js +149 -6
  21. package/server/public/app/attachments-view.js +20 -1
  22. package/server/public/app/attribution-control.mjs +46 -0
  23. package/server/public/app/bootstrap.js +468 -88
  24. package/server/public/app/claude-cloud-conversation-ui.js +110 -0
  25. package/server/public/app/claude-cloud-settings-ui.js +374 -0
  26. package/server/public/app/claude-cloud-ui.mjs +611 -0
  27. package/server/public/app/composer-control-state.mjs +11 -0
  28. package/server/public/app/context-menu.mjs +140 -0
  29. package/server/public/app/context-tier-options.mjs +4 -0
  30. package/server/public/app/context-usage-view.mjs +127 -8
  31. package/server/public/app/conversation-origin-ui.js +27 -0
  32. package/server/public/app/conversation-provider-indicator.mjs +22 -4
  33. package/server/public/app/conversation-view.js +145 -11
  34. package/server/public/app/cwd-picker.js +12 -0
  35. package/server/public/app/journal-view.js +698 -28
  36. package/server/public/app/math-dollars.mjs +89 -0
  37. package/server/public/app/model-selector-options.mjs +1 -1
  38. package/server/public/app/plan-usage-view.mjs +36 -8
  39. package/server/public/app/pwa-install.js +5 -0
  40. package/server/public/app/relay-error-ctas.mjs +16 -2
  41. package/server/public/app/remote-relay-origin-view.mjs +120 -4
  42. package/server/public/app/router.js +49 -12
  43. package/server/public/app/scrollbar-hover.mjs +78 -0
  44. package/server/public/app/settings-modal.js +37 -3
  45. package/server/public/app/settings-tabs.js +1 -1
  46. package/server/public/app/socket-handlers.js +53 -1
  47. package/server/public/app/store.js +26 -0
  48. package/server/public/app/transcript-separators.mjs +48 -11
  49. package/server/public/app-icon-192.png +0 -0
  50. package/server/public/app-icon-512.png +0 -0
  51. package/server/public/app-icon-light-192.png +0 -0
  52. package/server/public/app-icon-light-512.png +0 -0
  53. package/server/public/app-icon-light-maskable-512.png +0 -0
  54. package/server/public/app-icon-light.svg +4 -0
  55. package/server/public/app-icon-maskable-512.png +0 -0
  56. package/server/public/app-icon.svg +3 -3
  57. package/server/public/favicon.ico +0 -0
  58. package/server/public/index.html +564 -20
  59. package/server/public/manifest.webmanifest +9 -3
  60. package/server/public/sw.js +6 -1
  61. package/server/relay-tools.md +13 -1
  62. package/server/repositories/remote-relay-repository.mjs +42 -1
  63. package/server/repositories/session-repository.mjs +47 -1
  64. package/server/routes/claude-cloud-routes.mjs +140 -0
  65. package/server/routes/git-routes.mjs +37 -0
  66. package/server/routes/messages-routes.mjs +123 -16
  67. package/server/routes/remote-relay-routes.mjs +15 -1
  68. package/server/routes/sessions-routes.mjs +517 -74
  69. package/server/server-runtime.mjs +268 -33
  70. package/server/services/claude-account-usage-service.mjs +176 -0
  71. package/server/services/claude-cloud-repo-service.mjs +408 -0
  72. package/server/services/claude-cloud-session-service.mjs +326 -0
  73. package/server/services/claude-cloud-settings-service.mjs +269 -0
  74. package/server/services/claude-context-usage.mjs +51 -3
  75. package/server/services/git-remote-service.mjs +115 -0
  76. package/server/services/oar-cli-helpers.mjs +67 -1
  77. package/server/services/plan-usage-claude-cloud.mjs +211 -0
  78. package/server/services/plan-usage-claude.mjs +202 -40
  79. package/server/services/plan-usage-contract.mjs +39 -3
  80. package/server/services/plan-usage-service.mjs +82 -2
  81. package/server/services/provider-reasoning-effort.mjs +3 -0
  82. package/server/services/pwa-manifest-theme.mjs +30 -0
  83. package/server/services/remote-relay-caller-context.mjs +49 -10
  84. package/server/services/remote-relay-client.mjs +9 -0
  85. package/server/services/remote-relay-dispatcher.mjs +383 -35
  86. package/server/services/remote-relay-inbound.mjs +49 -2
  87. package/server/services/remote-relay-loopback.mjs +245 -0
  88. package/server/services/remote-relay-registry-service.mjs +52 -0
  89. package/server/services/session-worker-launch-service.mjs +100 -15
  90. package/server/services/session-worker-process-service.mjs +348 -129
  91. package/server/services/session-worker-registry-service.mjs +18 -1
  92. package/server/services/session-worker-stop-service.mjs +24 -13
  93. package/server/services/session-worker-supervisor-service.mjs +56 -1
  94. package/server/services/update-install-service.mjs +9 -3
  95. package/server/services/usage-limit-pause-service.mjs +3 -1
  96. package/server/services/worker-launch-note-service.mjs +151 -0
  97. package/shared/claude-attribution.mjs +117 -0
  98. package/shared/claude-cloud/api-client.mjs +809 -0
  99. package/shared/claude-cloud/base-url.mjs +47 -0
  100. package/shared/claude-cloud/credentials.mjs +221 -0
  101. package/shared/claude-cloud/repo-url.mjs +111 -0
  102. package/shared/provider-routing.mjs +4 -2
  103. package/shared/remote-relay-contract.mjs +153 -22
  104. package/shared/remote-relay-tool-core.mjs +11 -8
  105. package/shared/steer-settle-failure.mjs +1 -0
@@ -43,6 +43,8 @@ import { ANSWERED_ELSEWHERE_KIND, STEER_FOLDED_TEXT } from '../../shared/steer-s
43
43
  import { isLiveResend } from '../services/steer-resend.mjs';
44
44
  import { resolveProviderModelSelection } from '../services/provider-model-selection.mjs';
45
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';
46
48
  import {
47
49
  admitRemoteRelayRequest,
48
50
  findMentionedRemoteRelays,
@@ -114,6 +116,8 @@ function normalizeRequestedProviderType(value = '') {
114
116
  // "no provider stated" and the guards below could never fire.
115
117
  if (normalized === 'grok' || normalized === 'xai' || normalized === 'xai-grok') return 'grok';
116
118
  if (normalized === 'claude' || normalized === 'anthropic') return 'claude';
119
+ // Its own provider, never read as `claude`.
120
+ if (normalized === 'claude-cloud') return 'claude-cloud';
117
121
  return '';
118
122
  }
119
123
 
@@ -121,6 +125,7 @@ const BOOTSTRAP_ONLY_PROVIDER_LABELS = Object.freeze({
121
125
  cursor: 'Cursor',
122
126
  grok: 'Grok',
123
127
  claude: 'Claude',
128
+ 'claude-cloud': 'Claude Cloud',
124
129
  });
125
130
 
126
131
  function deriveModelOrigin(model) {
@@ -589,9 +594,17 @@ async function resolveRelayResponseText({
589
594
  }
590
595
  }
591
596
 
592
- export function resolveTerminalFailurePayload(payload = {}, { fallbackText = null } = {}) {
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 } = {}) {
593
606
  const body = payload && typeof payload === 'object' ? payload : {};
594
- const parsedTextFailure = parseTerminalFailureText(body.text);
607
+ const parsedTextFailure = inferFromText ? parseTerminalFailureText(body.text) : null;
595
608
  const direct = body.terminalError && typeof body.terminalError === 'object'
596
609
  ? body.terminalError
597
610
  : null;
@@ -1121,6 +1134,7 @@ export function buildDequeuedRelayMessage({
1121
1134
  defaultRelayMode,
1122
1135
  defaultModel,
1123
1136
  getCursorProviderSettings = () => null,
1137
+ getClaudeCloudProviderSettings = () => null,
1124
1138
  } = {}) {
1125
1139
  if (!msg) return null;
1126
1140
  const attachments = parseAttachments(msg.attachments).map(hydrateAttachment).filter(Boolean);
@@ -1150,6 +1164,17 @@ export function buildDequeuedRelayMessage({
1150
1164
  .map((value) => String(value || '').trim())
1151
1165
  .filter(Boolean);
1152
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;
1153
1178
  return {
1154
1179
  id: msg.id,
1155
1180
  conversationId: msg.conversation_id,
@@ -1163,6 +1188,7 @@ export function buildDequeuedRelayMessage({
1163
1188
  cursorAgentId: String(runtimeSession?.cursor_agent_id || '').trim() || null,
1164
1189
  cursorSubagentModels,
1165
1190
  grokNativeSessionId: String(runtimeSession?.grok_native_session_id || '').trim() || null,
1191
+ ...(isClaudeCloudProviderType(providerType) ? { claudeCloud } : {}),
1166
1192
  reasoningEffort: String(msg.reasoning_effort || '').trim() || null,
1167
1193
  contextTier: String(msg.context_tier || '').trim() || 'default',
1168
1194
  quality: normalizeOpenAIImageQuality(msg.reasoning_effort),
@@ -1783,6 +1809,7 @@ export function registerMessagesRoutes(app, deps) {
1783
1809
  getClaudeProviderSettings = () => ({ enabled: false, model: '', models: [] }),
1784
1810
  getCursorProviderSettings = () => ({ enabled: false, model: '', models: [] }),
1785
1811
  getGrokProviderSettings = () => ({ enabled: false, model: '', models: [] }),
1812
+ getClaudeCloudProviderSettings = () => ({ enabled: false, defaultModel: '', environmentId: '', models: [] }),
1786
1813
  rebindUnstartedOpenAIConversationModel = null,
1787
1814
  normalizeRelayMode,
1788
1815
  DEFAULT_RELAY_MODE,
@@ -4166,7 +4193,8 @@ export function registerMessagesRoutes(app, deps) {
4166
4193
  const runtimeBoundToExplicitProvider = runtimeProviderType === 'openai'
4167
4194
  || runtimeProviderType === 'claude'
4168
4195
  || runtimeProviderType === 'cursor'
4169
- || runtimeProviderType === 'grok';
4196
+ || runtimeProviderType === 'grok'
4197
+ || runtimeProviderType === 'claude-cloud';
4170
4198
  const configuredOpenAIModel = String(configuredOpenAI?.model || '').trim();
4171
4199
  const availableOpenAIModels = new Set(
4172
4200
  (Array.isArray(configuredOpenAI?.models) ? configuredOpenAI.models : [])
@@ -4290,6 +4318,17 @@ export function registerMessagesRoutes(app, deps) {
4290
4318
  });
4291
4319
  }
4292
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
+ }
4293
4332
  if (
4294
4333
  useOpenAIProvider
4295
4334
  && requestedOpenAIModel
@@ -4422,6 +4461,32 @@ export function registerMessagesRoutes(app, deps) {
4422
4461
  const grokModel = runtimeUsesGrok
4423
4462
  ? (pinnedGrokModel || String(configuredGrok?.model || '').trim())
4424
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 : '';
4425
4490
  const modelResolution = useOpenAIProvider
4426
4491
  ? {
4427
4492
  ok: !!openAIModel,
@@ -4454,7 +4519,15 @@ export function registerMessagesRoutes(app, deps) {
4454
4519
  reasoningEffort: null,
4455
4520
  error: grokModel ? null : 'Grok model is not configured',
4456
4521
  }
4457
- : resolveRequestedModel(model))));
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)))));
4458
4531
  if (!modelResolution.ok) return res.status(400).json({ error: modelResolution.error, supportedModels: modelResolution.available || [] });
4459
4532
  const requestedModel = String(modelResolution.model || '').trim();
4460
4533
  const requestedAutoModel = requestedModel.toLowerCase() === AUTO_MODEL_SENTINEL;
@@ -4523,10 +4596,13 @@ export function registerMessagesRoutes(app, deps) {
4523
4596
  ? resolveCursorReasoningEffort()
4524
4597
  : (runtimeUsesGrok
4525
4598
  ? resolveGrokReasoningEffort()
4526
- : resolveRequestedReasoningEffort(
4527
- requestedModel,
4528
- explicitReasoningEffort || modelResolution.reasoningEffort || null,
4529
- ))));
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
+ )))));
4530
4606
  if (!reasoningResolution?.ok && !explicitReasoningEffort) {
4531
4607
  const supportedEfforts = Array.isArray(reasoningResolution?.supported)
4532
4608
  ? reasoningResolution.supported.map((value) => String(value || '').trim().toLowerCase()).filter(Boolean)
@@ -4553,6 +4629,9 @@ export function registerMessagesRoutes(app, deps) {
4553
4629
  }
4554
4630
  } else if (!['default', 'long_context'].includes(requestedContextTier)) {
4555
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';
4556
4635
  }
4557
4636
  if (!requestedRelayMode) return res.status(400).json({ error: 'Unsupported relay mode' });
4558
4637
  const conversationWorkspaceState = (!shouldCreateConversation && typeof resolveConversationWorkspaceState === 'function')
@@ -4784,6 +4863,17 @@ export function registerMessagesRoutes(app, deps) {
4784
4863
  ) {
4785
4864
  stmts.updateRuntimeSessionProvider.run('claude', claudeModel, claudeModel, now, runtimeSession.id);
4786
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
+ }
4787
4877
  // Another relay's agent picks the model and mode of its own turn; the
4788
4878
  // composer keeps restoring the user's choice for this conversation.
4789
4879
  conversationPreferences = remoteRequest.remote && !shouldCreateConversation
@@ -5057,7 +5147,9 @@ export function registerMessagesRoutes(app, deps) {
5057
5147
  // in either must reach the status payload (composer gate, bubble
5058
5148
  // Cancel), so it counts as a change like the hold fields do.
5059
5149
  || (stored.supported === true) !== (steering.supported === true)
5060
- || storedCancellableIds.join('\n') !== steering.cancellableIds.join('\n');
5150
+ || storedCancellableIds.join('\n') !== steering.cancellableIds.join('\n')
5151
+ // The running CLI's compaction window (context-usage modal).
5152
+ || JSON.stringify(stored.autoCompactWindow ?? null) !== JSON.stringify(steering.autoCompactWindow ?? null);
5061
5153
  if (workerForSteering && changed) {
5062
5154
  sessionWorkerRegistry?.upsertWorker?.({ ...workerForSteering, steering });
5063
5155
  // Pushed rather than left for the clients' status poll: the
@@ -5285,7 +5377,10 @@ export function registerMessagesRoutes(app, deps) {
5285
5377
  if (!runtimeSession) {
5286
5378
  return res.status(404).json({ error: 'Runtime session not found for conversation' });
5287
5379
  }
5288
- if (String(runtimeSession.provider_type || 'github').trim().toLowerCase() !== 'claude') {
5380
+ // The Claude Cloud worker reports the same shape (the cloud session's
5381
+ // context and token usage), so both providers are taken here.
5382
+ const contextUsageProvider = String(runtimeSession.provider_type || 'github').trim().toLowerCase();
5383
+ if (contextUsageProvider !== 'claude' && contextUsageProvider !== 'claude-cloud') {
5289
5384
  return res.status(409).json({ error: 'Conversation is not bound to the Claude provider' });
5290
5385
  }
5291
5386
  if (typeof stmts.updateRuntimeSessionContextUsage?.run !== 'function') {
@@ -5345,13 +5440,17 @@ export function registerMessagesRoutes(app, deps) {
5345
5440
  // Claude-bound conversation's worker may write the Claude card. The
5346
5441
  // snapshot itself is global, so a misrouted poster would corrupt it for
5347
5442
  // everyone.
5443
+ // A Claude Cloud worker posts here too; its report is kept under its own
5444
+ // snapshot key, so the Claude card stays what the Claude workers wrote.
5348
5445
  const conversationId = String(req.body?.conversationId || '').trim();
5446
+ let snapshotProvider = 'claude';
5349
5447
  if (conversationId) {
5350
5448
  const runtimeSession = stmts.getRuntimeSessionByConversation?.get?.(conversationId) || null;
5351
5449
  const boundProvider = String(runtimeSession?.provider_type || 'github').trim().toLowerCase();
5352
- if (runtimeSession && boundProvider !== 'claude') {
5450
+ if (runtimeSession && boundProvider !== 'claude' && boundProvider !== 'claude-cloud') {
5353
5451
  return res.status(409).json({ error: 'Conversation is not bound to the Claude provider' });
5354
5452
  }
5453
+ if (runtimeSession && boundProvider === 'claude-cloud') snapshotProvider = 'claude-cloud';
5355
5454
  }
5356
5455
  const usage = normalizeClaudePlanUsage(req.body?.usage)
5357
5456
  || claudePlanUsageFromResult({
@@ -5359,7 +5458,7 @@ export function registerMessagesRoutes(app, deps) {
5359
5458
  totalCostUsd: req.body?.totalCostUsd,
5360
5459
  });
5361
5460
  if (!usage) return res.status(400).json({ error: 'Missing usable usage payload' });
5362
- planUsageService.saveSnapshot('claude', usage, {
5461
+ planUsageService.saveSnapshot(snapshotProvider, usage, {
5363
5462
  source: 'worker',
5364
5463
  error: String(req.body?.error || '').trim() || null,
5365
5464
  });
@@ -5373,12 +5472,13 @@ export function registerMessagesRoutes(app, deps) {
5373
5472
  touchCli();
5374
5473
  if (!usageLimitPauseService) return res.status(501).json({ error: 'Usage limit handling is unavailable' });
5375
5474
  // The report is the account's, so only a Claude-bound conversation's
5376
- // worker may write it (same validation as the plan usage card).
5475
+ // worker may write it (same validation as the plan usage card); a Claude
5476
+ // Cloud chat runs on the same account and reports the same limit.
5377
5477
  const conversationId = String(req.body?.conversationId || '').trim();
5378
5478
  if (conversationId) {
5379
5479
  const runtimeSession = stmts.getRuntimeSessionByConversation?.get?.(conversationId) || null;
5380
5480
  const boundProvider = String(runtimeSession?.provider_type || 'github').trim().toLowerCase();
5381
- if (runtimeSession && boundProvider !== 'claude') {
5481
+ if (runtimeSession && boundProvider !== 'claude' && boundProvider !== 'claude-cloud') {
5382
5482
  return res.status(409).json({ error: 'Conversation is not bound to the Claude provider' });
5383
5483
  }
5384
5484
  }
@@ -5982,6 +6082,7 @@ export function registerMessagesRoutes(app, deps) {
5982
6082
  defaultRelayMode: DEFAULT_RELAY_MODE,
5983
6083
  defaultModel: DEFAULT_MODEL,
5984
6084
  getCursorProviderSettings,
6085
+ getClaudeCloudProviderSettings,
5985
6086
  });
5986
6087
 
5987
6088
  if (out.attachments.length) {
@@ -6576,7 +6677,10 @@ export function registerMessagesRoutes(app, deps) {
6576
6677
  }
6577
6678
  };
6578
6679
  let trimmedText = String(text || '').trim();
6579
- let terminalFailure = resolveTerminalFailurePayload(req.body, { fallbackText: trimmedText });
6680
+ let terminalFailure = resolveTerminalFailurePayload(req.body, {
6681
+ fallbackText: trimmedText,
6682
+ inferFromText: !String(req.body?.attemptId || '').trim(),
6683
+ });
6580
6684
  // Final digests of background workflows that settled during this turn —
6581
6685
  // the transcript's "Finished background task" cards. Same structural
6582
6686
  // sanitizer as the live panel rows; junk entries drop silently (this
@@ -7106,7 +7210,10 @@ export function registerMessagesRoutes(app, deps) {
7106
7210
  // Providers whose workers turn runtime-initiated activity into continuation
7107
7211
  // rows: the Claude worker (background tasks/agents) and the Copilot SDK
7108
7212
  // worker (detached background shells) serving both Copilot-CLI providers.
7109
- const CONTINUATION_PROVIDER_TYPES = new Set(['claude', 'github', 'openai']);
7213
+ // A Claude Cloud session can take a turn of its own too: one the agent
7214
+ // starts when its background work reports back, or one started on
7215
+ // claude.ai; the cloud worker follows it and registers it here.
7216
+ const CONTINUATION_PROVIDER_TYPES = new Set(['claude', 'github', 'openai', 'claude-cloud']);
7110
7217
 
7111
7218
  // POST /api/continuation-turn — a session worker's CLI started a turn on its
7112
7219
  // own (a background task's notification). The synthetic queue row gives that
@@ -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 || {}));