@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.
Files changed (119) hide show
  1. package/.github/extensions/web-relay/polling/polling-loop.mjs +12 -32
  2. package/.github/extensions/web-relay/skills/question-bridge.mjs +14 -39
  3. package/CHANGELOG.md +340 -0
  4. package/README.md +196 -26
  5. package/package.json +2 -1
  6. package/server/README.md +174 -7
  7. package/server/claude-cloud-worker/claude-cloud-event-normalizer.mjs +340 -0
  8. package/server/claude-cloud-worker/claude-cloud-session-process.mjs +1325 -0
  9. package/server/claude-cloud-worker/claude-cloud-session-worker.mjs +155 -0
  10. package/server/claude-worker/claude-remote-relay-tool.mjs +2 -0
  11. package/server/claude-worker/claude-sdk-adapter.mjs +18 -2
  12. package/server/claude-worker/claude-session-process.mjs +151 -1
  13. package/server/claude-worker/claude-session-worker.mjs +6 -0
  14. package/server/claude-worker/claude-turn-failure.mjs +92 -0
  15. package/server/claude-worker/claude-turn-publisher.mjs +52 -3
  16. package/server/claude-worker/sdk-message-normalizer.mjs +13 -1
  17. package/server/copilot-worker/copilot-model-switch.mjs +29 -3
  18. package/server/copilot-worker/copilot-question-bridge.mjs +2 -4
  19. package/server/copilot-worker/copilot-runtime-tree.mjs +91 -0
  20. package/server/copilot-worker/copilot-sdk-adapter.mjs +48 -8
  21. package/server/copilot-worker/copilot-sdk-session-process.mjs +897 -166
  22. package/server/cursor-worker/cursor-sdk-adapter.mjs +80 -13
  23. package/server/cursor-worker/cursor-turn-runner.mjs +5 -2
  24. package/server/db-schema.mjs +24 -0
  25. package/server/grok-worker/acp-client.mjs +68 -12
  26. package/server/grok-worker/grok-sdk-adapter.mjs +4 -0
  27. package/server/grok-worker/grok-turn-runner.mjs +6 -2
  28. package/server/migrations/0006-workspace-root-settings.mjs +67 -0
  29. package/server/migrations/0007-claude-cloud.mjs +82 -0
  30. package/server/public/app/agent-sessions-settings-ui.js +197 -0
  31. package/server/public/app/agent-sessions-settings.mjs +134 -0
  32. package/server/public/app/api-client.js +160 -6
  33. package/server/public/app/attachments-view.js +20 -1
  34. package/server/public/app/attribution-control.mjs +46 -0
  35. package/server/public/app/bootstrap.js +347 -31
  36. package/server/public/app/claude-cloud-conversation-ui.js +110 -0
  37. package/server/public/app/claude-cloud-settings-ui.js +374 -0
  38. package/server/public/app/claude-cloud-ui.mjs +611 -0
  39. package/server/public/app/composer-control-state.mjs +15 -3
  40. package/server/public/app/context-menu.mjs +140 -0
  41. package/server/public/app/context-tier-options.mjs +4 -0
  42. package/server/public/app/conversation-origin-ui.js +27 -0
  43. package/server/public/app/conversation-provider-indicator.mjs +22 -4
  44. package/server/public/app/conversation-view.js +178 -8
  45. package/server/public/app/cwd-picker.js +12 -0
  46. package/server/public/app/journal-view.js +704 -28
  47. package/server/public/app/markdown-strikethrough.mjs +31 -0
  48. package/server/public/app/math-dollars.mjs +89 -0
  49. package/server/public/app/model-selector-options.mjs +1 -1
  50. package/server/public/app/plan-usage-view.mjs +36 -8
  51. package/server/public/app/relay-board-inline.mjs +70 -0
  52. package/server/public/app/relay-board-view.js +157 -61
  53. package/server/public/app/relay-error-ctas.mjs +28 -5
  54. package/server/public/app/remote-relay-origin-view.mjs +120 -4
  55. package/server/public/app/router.js +49 -12
  56. package/server/public/app/settings-modal.js +33 -2
  57. package/server/public/app/settings-tabs.js +1 -1
  58. package/server/public/app/socket-handlers.js +60 -0
  59. package/server/public/app/store.js +4 -1
  60. package/server/public/app/subagent-fold.mjs +61 -0
  61. package/server/public/app/usage-limit-ui.js +225 -0
  62. package/server/public/index.html +471 -8
  63. package/server/relay-tools.md +13 -1
  64. package/server/repositories/message-repository.mjs +10 -1
  65. package/server/repositories/question-repository.mjs +1 -0
  66. package/server/repositories/remote-relay-repository.mjs +72 -1
  67. package/server/repositories/session-repository.mjs +47 -1
  68. package/server/routes/claude-cloud-routes.mjs +140 -0
  69. package/server/routes/git-routes.mjs +37 -0
  70. package/server/routes/messages-routes.mjs +202 -26
  71. package/server/routes/relay-board-routes.mjs +56 -2
  72. package/server/routes/remote-relay-routes.mjs +15 -1
  73. package/server/routes/sessions-routes.mjs +538 -72
  74. package/server/server-runtime.mjs +291 -27
  75. package/server/services/claude-account-usage-service.mjs +176 -0
  76. package/server/services/claude-cloud-repo-service.mjs +408 -0
  77. package/server/services/claude-cloud-session-service.mjs +326 -0
  78. package/server/services/claude-cloud-settings-service.mjs +269 -0
  79. package/server/services/git-remote-service.mjs +115 -0
  80. package/server/services/plan-usage-claude-cloud.mjs +211 -0
  81. package/server/services/plan-usage-claude.mjs +202 -40
  82. package/server/services/plan-usage-contract.mjs +39 -3
  83. package/server/services/plan-usage-service.mjs +82 -2
  84. package/server/services/preview-proxy-server.mjs +64 -36
  85. package/server/services/process-list-service.mjs +122 -0
  86. package/server/services/provider-reasoning-effort.mjs +3 -0
  87. package/server/services/relay-console-log.mjs +199 -0
  88. package/server/services/remote-relay-caller-context.mjs +78 -19
  89. package/server/services/remote-relay-client.mjs +9 -0
  90. package/server/services/remote-relay-dispatcher.mjs +451 -39
  91. package/server/services/remote-relay-inbound.mjs +83 -1
  92. package/server/services/remote-relay-loopback.mjs +245 -0
  93. package/server/services/remote-relay-registry-service.mjs +52 -0
  94. package/server/services/session-worker-launch-service.mjs +100 -15
  95. package/server/services/session-worker-process-service.mjs +410 -138
  96. package/server/services/session-worker-registry-service.mjs +1 -1
  97. package/server/services/session-worker-stop-service.mjs +24 -13
  98. package/server/services/session-worker-supervisor-service.mjs +89 -2
  99. package/server/services/usage-limit-pause-service.mjs +443 -0
  100. package/server/services/worker-launch-note-service.mjs +151 -0
  101. package/shared/ask-user-bridge.mjs +14 -39
  102. package/shared/claude-attribution.mjs +117 -0
  103. package/shared/claude-cloud/api-client.mjs +809 -0
  104. package/shared/claude-cloud/base-url.mjs +47 -0
  105. package/shared/claude-cloud/credentials.mjs +221 -0
  106. package/shared/claude-cloud/repo-url.mjs +111 -0
  107. package/shared/claude-usage-limit.mjs +257 -0
  108. package/shared/control-poller.mjs +69 -20
  109. package/shared/failure-note-text.mjs +38 -0
  110. package/shared/provider-routing.mjs +4 -2
  111. package/shared/question-wait.mjs +108 -0
  112. package/shared/remote-relay-contract.mjs +167 -22
  113. package/shared/remote-relay-tool-core.mjs +11 -8
  114. package/shared/steer-settle-failure.mjs +17 -0
  115. package/shared/steer-settle-markers.mjs +8 -0
  116. package/shared/worker-runtime/process-tree.mjs +483 -0
  117. package/shared/worker-runtime/relay-errors.mjs +31 -0
  118. package/shared/worker-runtime/send-and-wait-errors.mjs +4 -4
  119. package/shared/worker-runtime/turn-liveness.mjs +249 -0
@@ -39,10 +39,12 @@ import { registerAskUserRoutes } from './routes/ask-user-routes.mjs';
39
39
  import { registerRelayBoardRoutes } from './routes/relay-board-routes.mjs';
40
40
  import { registerCacheRoutes } from './routes/cache-routes.mjs';
41
41
  import { registerGitRoutes } from './routes/git-routes.mjs';
42
+ import { registerClaudeCloudRoutes } from './routes/claude-cloud-routes.mjs';
42
43
  import { registerPreviewRoutes } from './routes/preview-routes.mjs';
43
44
  import { registerRemoteRelayRoutes } from './routes/remote-relay-routes.mjs';
44
45
  import { createRemoteRelayRepository } from './repositories/remote-relay-repository.mjs';
45
46
  import { createRemoteRelayClient } from './services/remote-relay-client.mjs';
47
+ import { createRemoteRelayLoopbackClient } from './services/remote-relay-loopback.mjs';
46
48
  import { createRemoteRelayRegistry } from './services/remote-relay-registry-service.mjs';
47
49
  import { createRemoteRelayPairing } from './services/remote-relay-pairing.mjs';
48
50
  import { createRemoteRelayCallerContext } from './services/remote-relay-caller-context.mjs';
@@ -50,12 +52,23 @@ import { createRemoteRelayDispatcher } from './services/remote-relay-dispatcher.
50
52
  import { createDeleteArchiveService } from './services/delete-archive-service.mjs';
51
53
  import { createStatusEventService } from './services/status-event-service.mjs';
52
54
  import { createClaudeAuthService } from './services/claude-auth-service.mjs';
55
+ import { createClaudeCloudCredentials } from '../shared/claude-cloud/credentials.mjs';
56
+ import { createClaudeCloudClient } from '../shared/claude-cloud/api-client.mjs';
57
+ import { resolveClaudeCloudBaseUrl } from '../shared/claude-cloud/base-url.mjs';
58
+ import { createClaudeCloudSettingsService } from './services/claude-cloud-settings-service.mjs';
59
+ import { createClaudeCloudSessionService } from './services/claude-cloud-session-service.mjs';
60
+ import { createClaudeCloudRepoService, listRecentCloudSourcesFromStatements } from './services/claude-cloud-repo-service.mjs';
61
+ import { createClaudeAccountUsageService } from './services/claude-account-usage-service.mjs';
62
+ import { createGitRemoteService } from './services/git-remote-service.mjs';
53
63
  import { createCliInstallService, writeCliBinariesToConfigFile } from './services/cli-install-service.mjs';
54
64
  import { createGrokAuthService } from './services/grok-auth-service.mjs';
55
65
  import { sweepUnreferencedUploads, UNREFERENCED_UPLOADS_QUERY } from './services/upload-sweep.mjs';
56
66
  import webpush from 'web-push';
57
67
  import { createPushDispatchService, ensurePushVapidKeys } from './services/push-dispatch-service.mjs';
58
68
  import { createHostSuspendService } from './services/host-suspend-service.mjs';
69
+ import { USAGE_LIMIT_ACCOUNT_SOCKET_EVENT, createUsageLimitPauseService } from './services/usage-limit-pause-service.mjs';
70
+ import { buildClaudeAttributionSettings, normalizeClaudeAttributionMode, resolveClaudeAttributionMode, DEFAULT_CLAUDE_ATTRIBUTION_MODE } from '../shared/claude-attribution.mjs';
71
+ import { createWorkerLaunchNoteService } from './services/worker-launch-note-service.mjs';
59
72
  import { createGitHubCiActivityService } from './services/github-ci-activity-service.mjs';
60
73
  import { runHostSuspendToRam } from './services/host-suspend-command.mjs';
61
74
  import { createActiveDeviceTracker } from './services/push-active-device-service.mjs';
@@ -92,7 +105,7 @@ import { createRelayCliLauncherService } from './services/relay-cli-launcher-ser
92
105
  import { createSessionWorkerRegistry } from './services/session-worker-registry-service.mjs';
93
106
  import { createSessionWorkerSupervisor } from './services/session-worker-supervisor-service.mjs';
94
107
  import { createSessionWorkerProcessInspector } from './services/session-worker-process-service.mjs';
95
- import { applyClaudeProviderEnvironment, applyCopilotSdkProviderEnvironment, applyCursorProviderEnvironment, applyGrokProviderEnvironment, applyOpenAIProviderEnvironment, copilotSdkEngineUnavailableReason, killTmuxSession, launchSessionCli } from './services/session-worker-launch-service.mjs';
108
+ import { applyClaudeCloudProviderEnvironment, applyClaudeProviderEnvironment, applyCopilotSdkProviderEnvironment, applyCursorProviderEnvironment, applyGrokProviderEnvironment, applyOpenAIProviderEnvironment, copilotSdkEngineUnavailableReason, killTmuxSession, launchSessionCli } from './services/session-worker-launch-service.mjs';
96
109
  import { createSshTunnelManager } from './services/ssh-tunnel-manager-service.mjs';
97
110
  import { createCloudflaredTunnelManager } from './services/cloudflared-tunnel-service.mjs';
98
111
  import {
@@ -113,6 +126,7 @@ import {
113
126
  resolveLaunchWorkspaceRootPath,
114
127
  } from './services/workspace-root-defaults-service.mjs';
115
128
  import { maybeStartTtyConsole } from './tty-console-bootstrap.mjs';
129
+ import { installRelayConsoleLog, resolveRelayConsoleLogPath } from './services/relay-console-log.mjs';
116
130
  import {
117
131
  computeFeatureFlagState,
118
132
  FEATURE_REGISTRY,
@@ -186,11 +200,18 @@ const PWA_SW_PATH = path.join(PUBLIC_DIR, 'sw.js');
186
200
  const SOCKET_IO_CLIENT_JS_PATH = path.join(__dirname, '..', 'node_modules', 'socket.io', 'client-dist', 'socket.io.js');
187
201
  const APP_CONFIG_PLACEHOLDER = /window\.__COPILOT_APP_CONFIG = \{ basePath: '[^']*' \};/;
188
202
  const PWA_VERSION_PLACEHOLDER = /const __PWA_VERSION = '[^']*';/;
203
+ // The console goes to a terminal, a tmux pane or nowhere: keep a copy to read
204
+ // after a restart or an incident.
205
+ const relayConsoleLog = installRelayConsoleLog({
206
+ logPath: resolveRelayConsoleLogPath({ env: process.env, serverDir: __dirname }),
207
+ });
189
208
  const ttyConsoleRuntime = await maybeStartTtyConsole({
190
209
  serverDir: __dirname,
191
210
  logsDir: path.join(__dirname, 'logs'),
192
211
  logger: console,
193
212
  });
213
+ // The terminal console takes the console's methods over when it starts.
214
+ relayConsoleLog?.rewrap();
194
215
 
195
216
  // ─── Config ────────────────────────────────────────────────────────────────────
196
217
  const CONFIG_PATH = process.env.COPILOT_WEB_RELAY_CONFIG
@@ -433,6 +454,7 @@ function getClaudeProviderSettings() {
433
454
  enabledModels: models,
434
455
  effortsByModel,
435
456
  modelsDiscovered,
457
+ attributionMode: normalizeClaudeAttributionMode(readAppSettingValue(CLAUDE_ATTRIBUTION_SETTING_KEY)) || DEFAULT_CLAUDE_ATTRIBUTION_MODE,
436
458
  providerType: 'claude',
437
459
  };
438
460
  }
@@ -451,10 +473,19 @@ function setClaudeProviderSettings(update = {}) {
451
473
  if (!isSafeClaudeModelId(normalizedModel)) {
452
474
  return { ok: false, error: 'Invalid Claude model ID' };
453
475
  }
476
+ const attributionMode = payload.attributionMode === undefined
477
+ ? undefined
478
+ : normalizeClaudeAttributionMode(payload.attributionMode);
479
+ if (attributionMode === null) {
480
+ return { ok: false, error: 'Invalid Claude attribution mode' };
481
+ }
454
482
  const nowIso = new Date().toISOString();
455
483
  const nextEnabled = typeof enabled === 'boolean' ? enabled : existing.enabled;
456
484
  stmts.upsertAppSetting.run(CLAUDE_ENABLED_SETTING_KEY, nextEnabled ? 'true' : 'false', nowIso);
457
485
  stmts.upsertAppSetting.run(CLAUDE_MODEL_SETTING_KEY, normalizedModel, nowIso);
486
+ if (attributionMode) {
487
+ stmts.upsertAppSetting.run(CLAUDE_ATTRIBUTION_SETTING_KEY, attributionMode, nowIso);
488
+ }
458
489
  if (Array.isArray(models)) {
459
490
  const normalizedModels = models
460
491
  .map((value) => String(value || '').trim())
@@ -483,9 +514,38 @@ function setClaudeProviderSettings(update = {}) {
483
514
  model: current.model,
484
515
  models: current.models,
485
516
  availableModels: current.availableModels,
517
+ attributionMode: current.attributionMode,
486
518
  };
487
519
  }
488
520
 
521
+ /** The folder override of a workspace root, or null (inherit); tolerant of a missing table. */
522
+ function workspaceRootAttributionMode(rootPath) {
523
+ const root = String(rootPath || '').trim();
524
+ if (!root || typeof stmts?.getWorkspaceRootSettings?.get !== 'function') return null;
525
+ try {
526
+ return normalizeClaudeAttributionMode(stmts.getWorkspaceRootSettings.get(normalizeWorkspaceRootKey(root))?.attribution_mode);
527
+ } catch {
528
+ return null;
529
+ }
530
+ }
531
+
532
+ /**
533
+ * What a Claude session's commits and PRs say: the provider setting, the
534
+ * folder override of the conversation's workspace root on top, and the
535
+ * model's name in the trailer. null = the CLI's own attribution.
536
+ */
537
+ function claudeAttributionForConversation(conv, modelId, { relayFolderFallback = true } = {}) {
538
+ // A Claude Cloud chat works in a sandbox, not in the relay's folder: only
539
+ // the folder it was started from (if any) can speak for it.
540
+ const root = String(conv?.runtime_workspace_root_path || conv?.configured_workspace_root_path || '').trim()
541
+ || (relayFolderFallback ? currentWorkspaceRootPath() : '');
542
+ const mode = resolveClaudeAttributionMode({
543
+ providerMode: getClaudeProviderSettings().attributionMode,
544
+ folderMode: workspaceRootAttributionMode(root),
545
+ });
546
+ return buildClaudeAttributionSettings({ mode, modelId });
547
+ }
548
+
489
549
  const CLAUDE_MODEL_DISCOVERY_TIMEOUT_MS = 20_000;
490
550
 
491
551
  // Discover the models the Claude Agent SDK can serve by asking a short-lived
@@ -1292,6 +1352,9 @@ const CLAUDE_MODEL_SETTING_KEY = 'claude_model';
1292
1352
  const CLAUDE_MODELS_SETTING_KEY = 'claude_models';
1293
1353
  const CLAUDE_ENABLED_MODELS_SETTING_KEY = 'claude_enabled_models';
1294
1354
  const CLAUDE_MODEL_EFFORTS_SETTING_KEY = 'claude_model_efforts';
1355
+ // oar | vanilla | off — what commits and PRs made in Claude sessions say
1356
+ // (shared/claude-attribution.mjs); absent means oar.
1357
+ const CLAUDE_ATTRIBUTION_SETTING_KEY = 'claude_attribution_mode';
1295
1358
  // The set of levels the SDK itself can report/accept as EffortLevel values.
1296
1359
  // Discovery results are filtered through this, so it must NOT contain the
1297
1360
  // relay's derived 'ultracode' sentinel — supportedModels() never reports it
@@ -1599,6 +1662,7 @@ const CURATED_MODEL_IDS = [
1599
1662
  ];
1600
1663
 
1601
1664
  let config = { ...DEFAULT_CONFIG };
1665
+ relayConsoleLog?.setSecrets(() => [config.authToken]);
1602
1666
  if (fs.existsSync(CONFIG_PATH)) {
1603
1667
  try { config = { ...config, ...JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8')) }; }
1604
1668
  catch (e) { console.error('Failed to read config.json, using defaults.'); }
@@ -2472,6 +2536,59 @@ const claudeAuthService = createClaudeAuthService();
2472
2536
  // group.
2473
2537
  const grokAuthService = createGrokAuthService();
2474
2538
 
2539
+ // ─── Claude Cloud (provider `claude-cloud`) ───────────────────────────────────
2540
+ // One credentials reader and one cloud client for the whole relay: the
2541
+ // settings tab (environments), and the archive of a deleted or archived
2542
+ // conversation's cloud session. The reader hands the Claude CLI's login to
2543
+ // the client only; nothing here stores, logs or forwards it, and the workers
2544
+ // read the credentials file themselves (the launch service hands a cloud
2545
+ // worker the login only where the relay itself runs on an environment token,
2546
+ // and then through the secret file, never a plain launch environment).
2547
+ // An expired login is nudged by letting the CLI look at its own status.
2548
+ const claudeCloudCredentials = createClaudeCloudCredentials({
2549
+ nudge: async () => { await claudeAuthService.getStatus({ force: true }); },
2550
+ });
2551
+ // The Anthropic API, unless OAR_CLAUDE_CLOUD_API_BASE_URL names a fake one on
2552
+ // this machine (tests): base-url.mjs ignores every host that is not loopback.
2553
+ const claudeCloudClient = createClaudeCloudClient({
2554
+ credentials: claudeCloudCredentials,
2555
+ baseUrl: resolveClaudeCloudBaseUrl(process.env),
2556
+ });
2557
+ const claudeCloudSettingsService = createClaudeCloudSettingsService({
2558
+ readSetting: readAppSettingValue,
2559
+ writeSetting: (key, value) => stmts.upsertAppSetting.run(key, String(value), new Date().toISOString()),
2560
+ deleteSetting: (key) => stmts.deleteAppSetting.run(key),
2561
+ getClaudeProviderSettings,
2562
+ claudeAuthService,
2563
+ credentials: claudeCloudCredentials,
2564
+ cloud: claudeCloudClient,
2565
+ // A closure: the socket server is created further down the file.
2566
+ emit: (event, payload) => io.emit(event, payload),
2567
+ });
2568
+ function getClaudeCloudProviderSettings() {
2569
+ return claudeCloudSettingsService.getSettings();
2570
+ }
2571
+ const claudeCloudSessionService = createClaudeCloudSessionService({
2572
+ stmts,
2573
+ getCloudClient: () => claudeCloudClient,
2574
+ emit: (event, payload) => io.emit(event, payload),
2575
+ });
2576
+ // The suggestions of the New Chat repository and branch fields: Anthropic's
2577
+ // list of what the Claude GitHub app can reach (while the provider is on),
2578
+ // the repositories of earlier cloud chats, and `git ls-remote` on the host.
2579
+ const claudeCloudRepoService = createClaudeCloudRepoService({
2580
+ cloud: claudeCloudClient,
2581
+ isEnabled: () => claudeCloudSettingsService.getSettings().enabled === true,
2582
+ listRecentCloudSources: () => listRecentCloudSourcesFromStatements(stmts),
2583
+ });
2584
+ // The account's live usage for the Check Usage modal. The Claude Cloud switch
2585
+ // is the consent to use the Claude CLI's login, so nothing is read while it
2586
+ // is off.
2587
+ const claudeAccountUsageService = createClaudeAccountUsageService({
2588
+ cloudClient: claudeCloudClient,
2589
+ isEnabled: () => claudeCloudSettingsService.getSettings().enabled === true,
2590
+ });
2591
+
2475
2592
  // ─── Provider CLI install / binary binding ────────────────────────────────────
2476
2593
  // A resolved CLI path is host state, not conversation state, so it lives beside
2477
2594
  // the port and the token in config.json rather than in the app_settings table
@@ -2574,6 +2691,8 @@ const sessionWorkerRegistry = createSessionWorkerRegistry();
2574
2691
  const sessionWorkerProcessInspector = createSessionWorkerProcessInspector({
2575
2692
  platform: process.platform,
2576
2693
  execFileSyncImpl: execFileSync,
2694
+ execFileImpl: execFile,
2695
+ logger: console,
2577
2696
  });
2578
2697
  function buildSessionWorkerLaunchEnv() {
2579
2698
  const normalizePathValue = (value) => {
@@ -2690,7 +2809,18 @@ function buildSessionWorkerLaunchEnvForSession(targetSessionId) {
2690
2809
  const providerType = String(runtimeSession?.provider_type || 'github').trim().toLowerCase();
2691
2810
  // Clear every provider's variables first (the appliers' kind-guarded deletes
2692
2811
  // keep the chained clears order-independent), then apply only the bound one.
2693
- const cleared = applyCopilotSdkProviderEnvironment(applyGrokProviderEnvironment(applyCursorProviderEnvironment(applyClaudeProviderEnvironment(applyOpenAIProviderEnvironment(sessionWorkerLaunchEnv)))));
2812
+ const cleared = applyClaudeCloudProviderEnvironment(applyCopilotSdkProviderEnvironment(applyGrokProviderEnvironment(applyCursorProviderEnvironment(applyClaudeProviderEnvironment(applyOpenAIProviderEnvironment(sessionWorkerLaunchEnv))))));
2813
+ // A Claude Cloud conversation gets its own worker and nothing else: it must
2814
+ // never reach the Copilot CLI fall-through at the end of this function. The
2815
+ // environment names the worker and the model, no credential.
2816
+ if (providerType === 'claude-cloud') {
2817
+ const cloudSettings = getClaudeCloudProviderSettings();
2818
+ const cloudModel = String(runtimeSession?.provider_model || runtimeSession?.model || cloudSettings.defaultModel).trim();
2819
+ return applyClaudeCloudProviderEnvironment(cleared, {
2820
+ enabled: true,
2821
+ model: cloudModel,
2822
+ });
2823
+ }
2694
2824
  if (providerType === 'claude') {
2695
2825
  const claudeSettings = getClaudeProviderSettings();
2696
2826
  const claudeModel = String(runtimeSession?.provider_model || runtimeSession?.model || claudeSettings.model).trim();
@@ -2763,8 +2893,18 @@ async function spawnSessionWorkerCli(targetSessionId, { allowProcessReuse = true
2763
2893
  if (!normalizedTargetSessionId) {
2764
2894
  throw new Error('missing-target-session-id');
2765
2895
  }
2896
+ // An unreadable process list must not stop every launch on the relay
2897
+ // (2026-10-01: a control byte in one command line did, for an hour).
2898
+ const warnProcessListUnreadable = (error) => {
2899
+ console.warn(`${runtimeLogPrefix()}worker launcher: process list unreadable, launching session=${normalizedTargetSessionId.slice(0, 8)} without the reuse check: ${error?.message || error}`);
2900
+ };
2766
2901
  if (allowProcessReuse) {
2767
- const liveWorker = sessionWorkerProcessInspector.findProcessForSession(normalizedTargetSessionId);
2902
+ let liveWorker = null;
2903
+ try {
2904
+ liveWorker = await sessionWorkerProcessInspector.findProcessForSessionAsync(normalizedTargetSessionId);
2905
+ } catch (error) {
2906
+ warnProcessListUnreadable(error);
2907
+ }
2768
2908
  if (liveWorker?.processId) {
2769
2909
  const workerId = `worker-${normalizedTargetSessionId.slice(0, 8)}`;
2770
2910
  console.log(`${runtimeLogPrefix()}worker launcher: reused ${workerId} session=${normalizedTargetSessionId.slice(0, 8)} pid=${liveWorker.processId}`);
@@ -2782,6 +2922,7 @@ async function spawnSessionWorkerCli(targetSessionId, { allowProcessReuse = true
2782
2922
  execFileSyncImpl: execFileSync,
2783
2923
  processInspector: sessionWorkerProcessInspector,
2784
2924
  allowProcessReuse,
2925
+ onProcessListError: warnProcessListUnreadable,
2785
2926
  });
2786
2927
  const workerPid = Number.isInteger(Number(launched?.pid)) ? Number(launched.pid) : null;
2787
2928
  const workerId = `worker-${normalizedTargetSessionId.slice(0, 8)}`;
@@ -2794,11 +2935,26 @@ async function spawnSessionWorkerCli(targetSessionId, { allowProcessReuse = true
2794
2935
  console.log(`${runtimeLogPrefix()}worker launcher: ${launchVerb} ${workerId} session=${normalizedTargetSessionId.slice(0, 8)} pid=${workerPid || 'none'} mode=${launchMode}${tmuxSessionName ? ` tmux=${tmuxSessionName}` : ''}`);
2795
2936
  return { workerId, pid: workerPid, reused: launched?.reused === true };
2796
2937
  }
2938
+ // The note in the conversation while a session's worker cannot be started
2939
+ // (worker-launch-note-service.mjs). `io` exists only later in boot; the
2940
+ // supervisor reports launch episodes at run time, never during boot.
2941
+ const workerLaunchNoteService = createWorkerLaunchNoteService({
2942
+ db,
2943
+ stmts,
2944
+ emit: (event, payload) => io.emit(event, payload),
2945
+ resolveConversationId: (sdkSessionId) => (
2946
+ stmts.getConvBySdkSessionId?.get?.(sdkSessionId)?.id
2947
+ || sessionWorkerRegistry.getWorker(sdkSessionId)?.conversationId
2948
+ || null
2949
+ ),
2950
+ logger: console,
2951
+ });
2797
2952
  const sessionWorkerSupervisor = createSessionWorkerSupervisor({
2798
2953
  registry: sessionWorkerRegistry,
2799
2954
  spawnWorker: async (sdkSessionId, options = {}) => spawnSessionWorkerCli(sdkSessionId, options),
2800
2955
  diagnosticPlanReference: () => path.join(currentWorkspaceRootPath(), '.cursor', 'plans', 'worker-startup-monitoring-plan.md'),
2801
2956
  log: (message) => console.warn(`${runtimeLogPrefix()}${message}`),
2957
+ onLaunchState: (event) => workerLaunchNoteService.handle(event),
2802
2958
  });
2803
2959
  // Feature flags moved from config.json into app_settings; adopt any legacy
2804
2960
  // `features` key once, then resolve the boot snapshot (defaults < stored rows
@@ -2866,14 +3022,11 @@ async function stopSessionWorkerForProviderRebind(sdkSessionId) {
2866
3022
  const currentWorker = sessionWorkerRegistry?.getWorker?.(sessionId) || null;
2867
3023
  sessionWorkerSupervisor?.markKilled?.(sessionId);
2868
3024
  const cancelledStart = await sessionWorkerSupervisor?.cancelPendingStart?.(sessionId, { wait: true });
3025
+ // Read off the thread; an unreadable list throws, and this caller treats
3026
+ // that as fatal (a rebind next to a process it could not see is worse).
2869
3027
  const discoveredProcesses = process.platform === 'win32'
2870
- ? (
2871
- sessionWorkerProcessInspector?.findWindowsProcessTreeForSession?.(sessionId)
2872
- || sessionWorkerProcessInspector?.findWindowsProcessesForSession?.(sessionId)
2873
- || sessionWorkerProcessInspector?.findProcessesForSession?.(sessionId)
2874
- || []
2875
- )
2876
- : (sessionWorkerProcessInspector?.findProcessesForSession?.(sessionId) || []);
3028
+ ? await sessionWorkerProcessInspector.findWindowsProcessTreeForSessionAsync(sessionId, { fresh: true })
3029
+ : await sessionWorkerProcessInspector.findProcessesForSessionAsync(sessionId, { fresh: true });
2877
3030
  const currentWorkerPid = Number(currentWorker?.pid);
2878
3031
  const hadActiveWorker = discoveredProcesses.some((entry) => isProcessAlive(Number(entry?.processId)))
2879
3032
  || (Number.isInteger(currentWorkerPid) && currentWorkerPid > 0 && isProcessAlive(currentWorkerPid))
@@ -3176,6 +3329,26 @@ function cancelRelayShutdown({ requestedBy = 'localhost-api' } = {}) {
3176
3329
  return { cancelled: true, dropped, ...relayShutdownStatePayload() };
3177
3330
  }
3178
3331
 
3332
+ const countPendingRowsByOwnerSession = db.prepare(`
3333
+ SELECT owner_sdk_session_id AS sdk_session_id, COUNT(*) AS n
3334
+ FROM queue
3335
+ WHERE status = 'pending' AND owner_sdk_session_id IS NOT NULL AND owner_sdk_session_id != ''
3336
+ GROUP BY owner_sdk_session_id
3337
+ `);
3338
+
3339
+ function countPendingRowsOfStoppedSessions() {
3340
+ try {
3341
+ let stalled = 0;
3342
+ for (const row of countPendingRowsByOwnerSession.all()) {
3343
+ const lifecycle = sessionWorkerSupervisor?.getLifecycleState?.(String(row.sdk_session_id || '').trim());
3344
+ if (lifecycle?.retriesStopped === true) stalled += Number(row.n || 0);
3345
+ }
3346
+ return stalled;
3347
+ } catch {
3348
+ return 0;
3349
+ }
3350
+ }
3351
+
3179
3352
  function requestRelayShutdown({ reason = 'manual-request', requestedBy = 'localhost-api', restart = false } = {}) {
3180
3353
  const safeReason = String(reason || 'manual-request').trim().slice(0, 140) || 'manual-request';
3181
3354
  const safeRequestedBy = String(requestedBy || 'localhost-api').trim().slice(0, 80) || 'localhost-api';
@@ -3212,7 +3385,11 @@ function requestRelayShutdown({ reason = 'manual-request', requestedBy = 'localh
3212
3385
  const tryShutdownWhenIdle = () => {
3213
3386
  if (runtimeShutdownStarted) return;
3214
3387
  const counts = queueCounts();
3215
- const queueFree = (counts.pendingCount + counts.processingCount + counts.parkedCount) === 0;
3388
+ // A row queued for a session whose launches the relay has stopped
3389
+ // retrying makes no progress until somebody presses Retry; it must not
3390
+ // hold the restart (which, fresh, is itself the next launch attempt).
3391
+ const stalledPending = countPendingRowsOfStoppedSessions();
3392
+ const queueFree = (counts.pendingCount - stalledPending + counts.processingCount + counts.parkedCount) <= 0;
3216
3393
  if (!queueFree) return;
3217
3394
  const requestInfo = pendingRelayShutdownRequest;
3218
3395
  pendingRelayShutdownRequest = null;
@@ -4964,12 +5141,19 @@ function collectHostActivity({ request = null } = {}) {
4964
5141
  if (!conversationId) continue;
4965
5142
  hostSuspendWatchedConversationIds.add(conversationId);
4966
5143
  const count = Number(row.cnt || 0);
5144
+ // A turn paused at the Claude usage limit is work still owed: the host
5145
+ // stays up for one that resumes by itself, not for one that waits for
5146
+ // the user.
5147
+ const pause = count === 1 ? usageLimitPauseService.getPause(conversationId) : null;
5148
+ if (pause && !pause.auto) continue;
4967
5149
  blockers.push({
4968
5150
  kind: 'turn',
4969
5151
  conversationId,
4970
5152
  title: hostActivityTitle(conversationId, row.title),
4971
5153
  count,
4972
- detail: count > 1 ? `${count} turns running` : 'turn running',
5154
+ detail: pause
5155
+ ? `turn paused at the Claude ${pause.label} until ${pause.resumeAt}`
5156
+ : (count > 1 ? `${count} turns running` : 'turn running'),
4973
5157
  });
4974
5158
  }
4975
5159
  for (const [key, entry] of backgroundTaskStore.sets) {
@@ -5017,6 +5201,15 @@ const hostSuspendService = createHostSuspendService({
5017
5201
  },
5018
5202
  });
5019
5203
 
5204
+ const usageLimitPauseService = createUsageLimitPauseService({
5205
+ db,
5206
+ stmts,
5207
+ emit: (event, payload) => io.emit(event, payload),
5208
+ // A follow-up resumed by hand is due at once: wake its worker.
5209
+ onQueueChanged: (reason) => { void primePendingSessionWorkers(reason); },
5210
+ logger: console,
5211
+ });
5212
+
5020
5213
  // The live turn among a conversation's processing rows. Mid-turn steering can
5021
5214
  // leave several processing at once — the running turn, steered messages the
5022
5215
  // CLI folded into it (no output of their own), a handed-off row still settling
@@ -5832,6 +6025,7 @@ async function requestSessionWorkerSocketDelivery({ sessionId, pid, reason = 'wo
5832
6025
  defaultRelayMode: DEFAULT_RELAY_MODE,
5833
6026
  defaultModel: DEFAULT_MODEL,
5834
6027
  getCursorProviderSettings,
6028
+ getClaudeCloudProviderSettings,
5835
6029
  });
5836
6030
  if (out?.ownerSessionId) {
5837
6031
  const worker = sessionWorkerRegistry?.getWorker?.(out.ownerSessionId) || null;
@@ -5871,6 +6065,19 @@ async function requestSessionWorkerSocketDelivery({ sessionId, pid, reason = 'wo
5871
6065
  // keys are always present here.
5872
6066
  thinkingEnabled: parseThinkingEnabled(conv?.thinking_enabled),
5873
6067
  thinkingDisplay: parseThinkingDisplay(conv?.thinking_display),
6068
+ // What this session's commits and PRs say (Claude and Claude
6069
+ // Cloud): the finished settings object, or null for the CLI's own
6070
+ // attribution. Always present on such a delivery, so the worker can
6071
+ // tell "left as it was" (absent) from "vanilla" (null).
6072
+ ...((() => {
6073
+ const deliveredProvider = String(out.providerType || '').trim().toLowerCase();
6074
+ if (deliveredProvider !== 'claude' && deliveredProvider !== 'claude-cloud') return {};
6075
+ return {
6076
+ attribution: claudeAttributionForConversation(conv, out.providerModel || out.model, {
6077
+ relayFolderFallback: deliveredProvider === 'claude',
6078
+ }),
6079
+ };
6080
+ })()),
5874
6081
  };
5875
6082
  })()),
5876
6083
  },
@@ -5929,19 +6136,21 @@ async function recoverUndeliveredSessionWorkerMessage({ pending = null, sessionI
5929
6136
 
5930
6137
  const WORKER_DEATH_GRACE_MS = 15_000;
5931
6138
 
5932
- function isWorkerSessionProcessAlive(sessionId, hintPid = null) {
6139
+ async function isWorkerSessionProcessAlive(sessionId, hintPid = null) {
5933
6140
  // A merely-retained socket is not proof of life: a dead worker's socket
5934
6141
  // lingers until its close event lands, and counting it stalled recovery.
5935
6142
  // The socket only counts with a recent heartbeat or a PID that probes
5936
6143
  // alive; otherwise fall through to the process probes below.
5937
6144
  if (sessionWorkerWebSocketService?.hasLiveWorkerSocket?.(sessionId)) return true;
5938
- try {
5939
- if (sessionWorkerProcessInspector?.findProcessForSession?.(sessionId)) return true;
5940
- } catch {}
6145
+ // The pids first: they cost nothing. The process list last: on win32 it is
6146
+ // a PowerShell run (off the thread, shared and cached by the inspector).
5941
6147
  const registryPid = Number(sessionWorkerRegistry?.getWorker?.(sessionId)?.pid || 0);
5942
6148
  if (registryPid > 0 && isProcessAlive(registryPid)) return true;
5943
6149
  const pid = Number(hintPid || 0);
5944
6150
  if (pid > 0 && isProcessAlive(pid)) return true;
6151
+ try {
6152
+ if (await sessionWorkerProcessInspector?.findProcessForSessionAsync?.(sessionId)) return true;
6153
+ } catch {}
5945
6154
  return false;
5946
6155
  }
5947
6156
 
@@ -6009,10 +6218,10 @@ function handleWorkerSocketClosed({ sessionId, pid = null } = {}) {
6009
6218
  // when it owned no rows (the hold can outlive them, e.g. a continuation's).
6010
6219
  const heldSteering = Boolean(sessionWorkerRegistry?.getWorker?.(sid)?.steering);
6011
6220
  if (!owned.length && !heldSteering) return;
6012
- const timer = setTimeout(() => {
6221
+ const timer = setTimeout(async () => {
6013
6222
  try {
6014
6223
  if (runtimeShutdownStarted) return;
6015
- if (isWorkerSessionProcessAlive(sid, pid)) return; // reconnect or socket flap
6224
+ if (await isWorkerSessionProcessAlive(sid, pid)) return; // reconnect or socket flap
6016
6225
  sessionWorkerRegistry?.clearSteering?.(sid);
6017
6226
  const stillOwned = stmts.listProcessingRowsForOwner?.all?.(sid) || [];
6018
6227
  if (!stillOwned.length) return;
@@ -6025,15 +6234,28 @@ function handleWorkerSocketClosed({ sessionId, pid = null } = {}) {
6025
6234
  timer.unref?.();
6026
6235
  }
6027
6236
 
6028
- function sweepDeadWorkerProcessingRows() {
6237
+ // One sweep at a time: a slow process list must not pile sweeps up.
6238
+ let deadWorkerSweepInFlight = false;
6239
+ async function sweepDeadWorkerProcessingRows() {
6029
6240
  if (featureFlags?.SESSION_WORKER_ROUTING_ENABLED !== true) return 0;
6030
6241
  if (runtimeShutdownStarted) return 0;
6242
+ if (deadWorkerSweepInFlight) return 0;
6243
+ deadWorkerSweepInFlight = true;
6244
+ try {
6245
+ return await sweepDeadWorkerProcessingRowsOnce();
6246
+ } finally {
6247
+ deadWorkerSweepInFlight = false;
6248
+ }
6249
+ }
6250
+
6251
+ async function sweepDeadWorkerProcessingRowsOnce() {
6031
6252
  const owners = stmts.listProcessingOwnerSessionIds?.all?.(25) || [];
6032
6253
  let recovered = 0;
6033
6254
  for (const owner of owners) {
6034
6255
  const sid = String(owner?.sdk_session_id || '').trim();
6035
6256
  if (!sid) continue;
6036
- if (isWorkerSessionProcessAlive(sid)) continue;
6257
+ // The owners share one list: the inspector caches it for a moment.
6258
+ if (await isWorkerSessionProcessAlive(sid)) continue;
6037
6259
  console.warn(`${runtimeLogPrefix()}WORKER DEAD session=${sid.slice(0, 8)} trigger=sweep — recovering owned turns`);
6038
6260
  recovered += recoverDeadWorkerProcessingRows(sid, 'sweep');
6039
6261
  }
@@ -6057,7 +6279,11 @@ const sessionWorkerWebSocketService = createSessionWorkerWebSocketService({
6057
6279
  pollIntervalMs: 500,
6058
6280
  logger: console,
6059
6281
  });
6060
- runtimeTimers.deadWorkerSweep = setInterval(sweepDeadWorkerProcessingRows, 30_000);
6282
+ runtimeTimers.deadWorkerSweep = setInterval(() => {
6283
+ sweepDeadWorkerProcessingRows().catch((error) => {
6284
+ console.warn(`${runtimeLogPrefix()}WORKER SWEEP failed: ${error?.message || error}`);
6285
+ });
6286
+ }, 30_000);
6061
6287
  if (typeof runtimeTimers.deadWorkerSweep.unref === 'function') runtimeTimers.deadWorkerSweep.unref();
6062
6288
  const tmuxInspectorAccessPolicy = createTmuxInspectorAccessPolicy({
6063
6289
  sessionWorkerRegistry,
@@ -6182,6 +6408,9 @@ const runtimeState = {
6182
6408
  get ttyConsoleActive() { return Boolean(ttyConsoleRuntime?.tty); },
6183
6409
  get relayShutdown() { return getRelayShutdownState(); },
6184
6410
  get hostSuspend() { return hostSuspendService.getState(); },
6411
+ get usageLimit() {
6412
+ return { account: usageLimitPauseService.getAccountState(), pauses: usageLimitPauseService.listPauses() };
6413
+ },
6185
6414
  get activeBridgeOwner() { return relayBridgeOwnerService.getOwner(); },
6186
6415
  get workerWebSocketStatus() { return sessionWorkerWebSocketService.status(); },
6187
6416
  get tmuxInspectorStatus() { return tmuxInspectorSocketService.status(); },
@@ -6198,6 +6427,8 @@ const windowsBootAutostartService = createWindowsBootAutostartService({
6198
6427
  });
6199
6428
 
6200
6429
  const gitChangesService = createGitChangesService();
6430
+ // GET /api/git/remote: where a folder lives on GitHub (New Chat, Claude Cloud).
6431
+ const gitRemoteService = createGitRemoteService({ gitChangesService });
6201
6432
 
6202
6433
  // ─── Remote Relays ────────────────────────────────────────────────────────────
6203
6434
  // Other OAR relays this one is paired with, so its agents can work there. Built
@@ -6238,7 +6469,7 @@ const remoteRelayPairing = createRemoteRelayPairing({
6238
6469
  });
6239
6470
  // What the inbound routes need: no tokens, only ids, names and state.
6240
6471
  const remoteRelayInbound = {
6241
- listRelays: () => remoteRelayRegistry.list().map(({ id, name, url, lastStatus, version }) => ({ id, name, url, lastStatus, version })),
6472
+ listRelays: () => remoteRelayRegistry.list().map(({ id, relayId, name, url, lastStatus, version }) => ({ id, relayId, name, url, lastStatus, version })),
6242
6473
  selfNames: () => [remoteRelayRegistry.selfName()],
6243
6474
  inboundEnabled: () => remoteRelayRegistry.getSelfSettings().inboundEnabled,
6244
6475
  recordUnlock: (conversationId, relayId, messageId) => remoteRelayRepository?.recordUnlock(conversationId, relayId, messageId) ?? false,
@@ -6263,9 +6494,18 @@ const remoteRelayCallerContext = createRemoteRelayCallerContext({
6263
6494
  uuid: uuidv4,
6264
6495
  logger: console,
6265
6496
  });
6497
+ // Agent sessions: this relay as a target of its own agents. Their calls reach
6498
+ // the routes below in-process, the way a paired relay's reach them over HTTP.
6499
+ const remoteRelayLoopbackClient = createRemoteRelayLoopbackClient({
6500
+ handle: (req, res) => app(req, res),
6501
+ getOwnRelayId: () => remoteRelayRegistry.instanceId(),
6502
+ getOwnToken: () => config.authToken,
6503
+ });
6266
6504
  const remoteRelayDispatcher = createRemoteRelayDispatcher({
6267
6505
  registry: remoteRelayRegistry,
6268
6506
  client: remoteRelayClient,
6507
+ localClient: remoteRelayLoopbackClient,
6508
+ getAgentSessionsSettings: () => remoteRelayRegistry.getAgentSessionsSettings(),
6269
6509
  repository: remoteRelayRepository,
6270
6510
  getCallerContext: (conversationId) => remoteRelayCallerContext.getCallerContext(conversationId),
6271
6511
  requestApproval: (request) => remoteRelayCallerContext.requestApproval(request),
@@ -6277,6 +6517,7 @@ const remoteRelayDispatcher = createRemoteRelayDispatcher({
6277
6517
  const sharedRouteDeps = {
6278
6518
  auth,
6279
6519
  gitChangesService,
6520
+ gitRemoteService,
6280
6521
  currentWorkspaceRootPath,
6281
6522
  io,
6282
6523
  db,
@@ -6339,6 +6580,12 @@ const sharedRouteDeps = {
6339
6580
  getGrokProviderSettings,
6340
6581
  setGrokProviderSettings,
6341
6582
  refreshGrokProviderModels,
6583
+ getClaudeCloudProviderSettings,
6584
+ claudeCloudSettingsService,
6585
+ claudeCloudSessionService,
6586
+ claudeCloudRepoService,
6587
+ // The login's state for the workers' nudge route (never the token).
6588
+ claudeCloudLoginStatus: { describe: () => claudeCloudCredentials.describe() },
6342
6589
  reconcileUnstartedConversationProviders,
6343
6590
  rebindUnstartedOpenAIConversationModel,
6344
6591
  getOrCreateConversation,
@@ -6426,6 +6673,7 @@ const sharedRouteDeps = {
6426
6673
  getCursorDashboardTokenSettings,
6427
6674
  setCursorDashboardTokenSettings,
6428
6675
  planUsageService,
6676
+ claudeAccountUsageService,
6429
6677
  getCursorPlanAllowanceSettings,
6430
6678
  setCursorPlanAllowanceSettings,
6431
6679
  getGrokPlanAllowanceSettings,
@@ -6461,6 +6709,7 @@ const sharedRouteDeps = {
6461
6709
  relayShutdownStatePayload,
6462
6710
  hostSuspendService,
6463
6711
  collectHostActivity,
6712
+ usageLimitPauseService,
6464
6713
  markSharedViewerPresence,
6465
6714
  getSharedWatcherCount,
6466
6715
  statusEventService,
@@ -6483,6 +6732,7 @@ registerAskUserRoutes(app, sharedRouteDeps);
6483
6732
  registerRelayBoardRoutes(app, sharedRouteDeps);
6484
6733
  registerCacheRoutes(app, sharedRouteDeps);
6485
6734
  registerGitRoutes(app, sharedRouteDeps);
6735
+ registerClaudeCloudRoutes(app, sharedRouteDeps);
6486
6736
  registerPushRoutes(app, {
6487
6737
  auth,
6488
6738
  pushDispatchService,
@@ -6635,7 +6885,16 @@ function auth(req, res, next) {
6635
6885
  function getOrCreateConversation(id, firstLine) {
6636
6886
  const now = new Date().toISOString();
6637
6887
  stmts.insertConv.run(id, (firstLine || 'Untitled').slice(0, 80), now, now);
6638
- return stmts.getConv.get(id);
6888
+ const conv = stmts.getConv.get(id);
6889
+ // A message into an archived conversation (an agent's, through a paired
6890
+ // relay, say) brings it back to the live list: its worker starts again
6891
+ // for the delivery, and an archived chat must not work unseen.
6892
+ if (conv && Number(conv.archived || 0) === 1 && typeof stmts.unarchiveConv?.run === 'function') {
6893
+ stmts.unarchiveConv.run(now, id);
6894
+ io.emit('conversation_archived', { conversationId: id, archived: false });
6895
+ return stmts.getConv.get(id);
6896
+ }
6897
+ return conv;
6639
6898
  }
6640
6899
 
6641
6900
  function ensureRuntimeSessionBinding(
@@ -6685,10 +6944,14 @@ function ensureRuntimeSessionBinding(
6685
6944
  ? 'cursor'
6686
6945
  : (normalizedRequestedProviderType === 'grok'
6687
6946
  ? 'grok'
6688
- : (normalizedRequestedProviderType === 'github'
6689
- ? 'github'
6690
- : (openAISettings?.enabled ? 'openai' : 'github')))));
6691
- const resolvedProviderModel = (resolvedProviderType === 'openai' || resolvedProviderType === 'claude' || resolvedProviderType === 'cursor' || resolvedProviderType === 'grok')
6947
+ // Without this entry a `claude-cloud` request would fall to the
6948
+ // default below and bind a GitHub (Copilot CLI) session.
6949
+ : (normalizedRequestedProviderType === 'claude-cloud'
6950
+ ? 'claude-cloud'
6951
+ : (normalizedRequestedProviderType === 'github'
6952
+ ? 'github'
6953
+ : (openAISettings?.enabled ? 'openai' : 'github'))))));
6954
+ const resolvedProviderModel = (resolvedProviderType === 'openai' || resolvedProviderType === 'claude' || resolvedProviderType === 'cursor' || resolvedProviderType === 'grok' || resolvedProviderType === 'claude-cloud')
6692
6955
  ? (String(providerModel || normalizedModel || '').trim() || null)
6693
6956
  : null;
6694
6957
  try {
@@ -6851,6 +7114,7 @@ io.on('connection', (socket) => {
6851
7114
  // Pending host suspend / relay restart banners must survive a reconnect.
6852
7115
  try { socket.emit('host_suspend_state', hostSuspendService.getState()); } catch {}
6853
7116
  try { socket.emit('relay_shutdown_state', relayShutdownStatePayload()); } catch {}
7117
+ try { socket.emit(USAGE_LIMIT_ACCOUNT_SOCKET_EVENT, usageLimitPauseService.getAccountState()); } catch {}
6854
7118
  // Visibility heartbeats for push suppression; also clears a stale
6855
7119
  // deviceVisible flag restored by connectionStateRecovery.
6856
7120
  activeDeviceTracker.registerSocket(socket);