@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.
- package/CHANGELOG.md +285 -0
- package/README.md +222 -40
- package/bin/oar.js +320 -18
- package/package.json +1 -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 -13
- package/server/claude-worker/claude-session-process.mjs +225 -24
- package/server/claude-worker/claude-session-worker.mjs +6 -0
- package/server/claude-worker/sdk-message-normalizer.mjs +28 -0
- package/server/db-schema.mjs +17 -0
- package/server/migrations/0006-workspace-root-settings.mjs +67 -0
- package/server/migrations/0007-claude-cloud.mjs +82 -0
- package/server/public/app/activity-replay-state.mjs +68 -5
- 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 +149 -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 +468 -88
- 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 +11 -0
- package/server/public/app/context-menu.mjs +140 -0
- package/server/public/app/context-tier-options.mjs +4 -0
- package/server/public/app/context-usage-view.mjs +127 -8
- 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 +145 -11
- package/server/public/app/cwd-picker.js +12 -0
- package/server/public/app/journal-view.js +698 -28
- 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/pwa-install.js +5 -0
- package/server/public/app/relay-error-ctas.mjs +16 -2
- package/server/public/app/remote-relay-origin-view.mjs +120 -4
- package/server/public/app/router.js +49 -12
- package/server/public/app/scrollbar-hover.mjs +78 -0
- package/server/public/app/settings-modal.js +37 -3
- package/server/public/app/settings-tabs.js +1 -1
- package/server/public/app/socket-handlers.js +53 -1
- package/server/public/app/store.js +26 -0
- package/server/public/app/transcript-separators.mjs +48 -11
- package/server/public/app-icon-192.png +0 -0
- package/server/public/app-icon-512.png +0 -0
- package/server/public/app-icon-light-192.png +0 -0
- package/server/public/app-icon-light-512.png +0 -0
- package/server/public/app-icon-light-maskable-512.png +0 -0
- package/server/public/app-icon-light.svg +4 -0
- package/server/public/app-icon-maskable-512.png +0 -0
- package/server/public/app-icon.svg +3 -3
- package/server/public/favicon.ico +0 -0
- package/server/public/index.html +564 -20
- package/server/public/manifest.webmanifest +9 -3
- package/server/public/sw.js +6 -1
- package/server/relay-tools.md +13 -1
- package/server/repositories/remote-relay-repository.mjs +42 -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 +123 -16
- package/server/routes/remote-relay-routes.mjs +15 -1
- package/server/routes/sessions-routes.mjs +517 -74
- package/server/server-runtime.mjs +268 -33
- 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/claude-context-usage.mjs +51 -3
- package/server/services/git-remote-service.mjs +115 -0
- package/server/services/oar-cli-helpers.mjs +67 -1
- 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/provider-reasoning-effort.mjs +3 -0
- package/server/services/pwa-manifest-theme.mjs +30 -0
- package/server/services/remote-relay-caller-context.mjs +49 -10
- package/server/services/remote-relay-client.mjs +9 -0
- package/server/services/remote-relay-dispatcher.mjs +383 -35
- package/server/services/remote-relay-inbound.mjs +49 -2
- 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 +348 -129
- package/server/services/session-worker-registry-service.mjs +18 -1
- package/server/services/session-worker-stop-service.mjs +24 -13
- package/server/services/session-worker-supervisor-service.mjs +56 -1
- package/server/services/update-install-service.mjs +9 -3
- package/server/services/usage-limit-pause-service.mjs +3 -1
- package/server/services/worker-launch-note-service.mjs +151 -0
- 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/provider-routing.mjs +4 -2
- package/shared/remote-relay-contract.mjs +153 -22
- package/shared/remote-relay-tool-core.mjs +11 -8
- package/shared/steer-settle-failure.mjs +1 -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,6 +52,14 @@ 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';
|
|
@@ -57,6 +67,8 @@ 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';
|
|
59
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';
|
|
60
72
|
import { createGitHubCiActivityService } from './services/github-ci-activity-service.mjs';
|
|
61
73
|
import { runHostSuspendToRam } from './services/host-suspend-command.mjs';
|
|
62
74
|
import { createActiveDeviceTracker } from './services/push-active-device-service.mjs';
|
|
@@ -93,7 +105,7 @@ import { createRelayCliLauncherService } from './services/relay-cli-launcher-ser
|
|
|
93
105
|
import { createSessionWorkerRegistry } from './services/session-worker-registry-service.mjs';
|
|
94
106
|
import { createSessionWorkerSupervisor } from './services/session-worker-supervisor-service.mjs';
|
|
95
107
|
import { createSessionWorkerProcessInspector } from './services/session-worker-process-service.mjs';
|
|
96
|
-
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';
|
|
97
109
|
import { createSshTunnelManager } from './services/ssh-tunnel-manager-service.mjs';
|
|
98
110
|
import { createCloudflaredTunnelManager } from './services/cloudflared-tunnel-service.mjs';
|
|
99
111
|
import {
|
|
@@ -164,6 +176,7 @@ import {
|
|
|
164
176
|
isSafeProviderModelId,
|
|
165
177
|
} from '../shared/model-id.mjs';
|
|
166
178
|
import { createModelVariantCatalogService } from './services/model-variant-catalog-service.mjs';
|
|
179
|
+
import { applyPwaManifestTheme, normalizePwaTheme } from './services/pwa-manifest-theme.mjs';
|
|
167
180
|
|
|
168
181
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
169
182
|
const REPO_ROOT = path.resolve(__dirname, '..');
|
|
@@ -442,6 +455,7 @@ function getClaudeProviderSettings() {
|
|
|
442
455
|
enabledModels: models,
|
|
443
456
|
effortsByModel,
|
|
444
457
|
modelsDiscovered,
|
|
458
|
+
attributionMode: normalizeClaudeAttributionMode(readAppSettingValue(CLAUDE_ATTRIBUTION_SETTING_KEY)) || DEFAULT_CLAUDE_ATTRIBUTION_MODE,
|
|
445
459
|
providerType: 'claude',
|
|
446
460
|
};
|
|
447
461
|
}
|
|
@@ -460,10 +474,19 @@ function setClaudeProviderSettings(update = {}) {
|
|
|
460
474
|
if (!isSafeClaudeModelId(normalizedModel)) {
|
|
461
475
|
return { ok: false, error: 'Invalid Claude model ID' };
|
|
462
476
|
}
|
|
477
|
+
const attributionMode = payload.attributionMode === undefined
|
|
478
|
+
? undefined
|
|
479
|
+
: normalizeClaudeAttributionMode(payload.attributionMode);
|
|
480
|
+
if (attributionMode === null) {
|
|
481
|
+
return { ok: false, error: 'Invalid Claude attribution mode' };
|
|
482
|
+
}
|
|
463
483
|
const nowIso = new Date().toISOString();
|
|
464
484
|
const nextEnabled = typeof enabled === 'boolean' ? enabled : existing.enabled;
|
|
465
485
|
stmts.upsertAppSetting.run(CLAUDE_ENABLED_SETTING_KEY, nextEnabled ? 'true' : 'false', nowIso);
|
|
466
486
|
stmts.upsertAppSetting.run(CLAUDE_MODEL_SETTING_KEY, normalizedModel, nowIso);
|
|
487
|
+
if (attributionMode) {
|
|
488
|
+
stmts.upsertAppSetting.run(CLAUDE_ATTRIBUTION_SETTING_KEY, attributionMode, nowIso);
|
|
489
|
+
}
|
|
467
490
|
if (Array.isArray(models)) {
|
|
468
491
|
const normalizedModels = models
|
|
469
492
|
.map((value) => String(value || '').trim())
|
|
@@ -492,9 +515,38 @@ function setClaudeProviderSettings(update = {}) {
|
|
|
492
515
|
model: current.model,
|
|
493
516
|
models: current.models,
|
|
494
517
|
availableModels: current.availableModels,
|
|
518
|
+
attributionMode: current.attributionMode,
|
|
495
519
|
};
|
|
496
520
|
}
|
|
497
521
|
|
|
522
|
+
/** The folder override of a workspace root, or null (inherit); tolerant of a missing table. */
|
|
523
|
+
function workspaceRootAttributionMode(rootPath) {
|
|
524
|
+
const root = String(rootPath || '').trim();
|
|
525
|
+
if (!root || typeof stmts?.getWorkspaceRootSettings?.get !== 'function') return null;
|
|
526
|
+
try {
|
|
527
|
+
return normalizeClaudeAttributionMode(stmts.getWorkspaceRootSettings.get(normalizeWorkspaceRootKey(root))?.attribution_mode);
|
|
528
|
+
} catch {
|
|
529
|
+
return null;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* What a Claude session's commits and PRs say: the provider setting, the
|
|
535
|
+
* folder override of the conversation's workspace root on top, and the
|
|
536
|
+
* model's name in the trailer. null = the CLI's own attribution.
|
|
537
|
+
*/
|
|
538
|
+
function claudeAttributionForConversation(conv, modelId, { relayFolderFallback = true } = {}) {
|
|
539
|
+
// A Claude Cloud chat works in a sandbox, not in the relay's folder: only
|
|
540
|
+
// the folder it was started from (if any) can speak for it.
|
|
541
|
+
const root = String(conv?.runtime_workspace_root_path || conv?.configured_workspace_root_path || '').trim()
|
|
542
|
+
|| (relayFolderFallback ? currentWorkspaceRootPath() : '');
|
|
543
|
+
const mode = resolveClaudeAttributionMode({
|
|
544
|
+
providerMode: getClaudeProviderSettings().attributionMode,
|
|
545
|
+
folderMode: workspaceRootAttributionMode(root),
|
|
546
|
+
});
|
|
547
|
+
return buildClaudeAttributionSettings({ mode, modelId });
|
|
548
|
+
}
|
|
549
|
+
|
|
498
550
|
const CLAUDE_MODEL_DISCOVERY_TIMEOUT_MS = 20_000;
|
|
499
551
|
|
|
500
552
|
// Discover the models the Claude Agent SDK can serve by asking a short-lived
|
|
@@ -1301,6 +1353,9 @@ const CLAUDE_MODEL_SETTING_KEY = 'claude_model';
|
|
|
1301
1353
|
const CLAUDE_MODELS_SETTING_KEY = 'claude_models';
|
|
1302
1354
|
const CLAUDE_ENABLED_MODELS_SETTING_KEY = 'claude_enabled_models';
|
|
1303
1355
|
const CLAUDE_MODEL_EFFORTS_SETTING_KEY = 'claude_model_efforts';
|
|
1356
|
+
// oar | vanilla | off — what commits and PRs made in Claude sessions say
|
|
1357
|
+
// (shared/claude-attribution.mjs); absent means oar.
|
|
1358
|
+
const CLAUDE_ATTRIBUTION_SETTING_KEY = 'claude_attribution_mode';
|
|
1304
1359
|
// The set of levels the SDK itself can report/accept as EffortLevel values.
|
|
1305
1360
|
// Discovery results are filtered through this, so it must NOT contain the
|
|
1306
1361
|
// relay's derived 'ultracode' sentinel — supportedModels() never reports it
|
|
@@ -2482,6 +2537,59 @@ const claudeAuthService = createClaudeAuthService();
|
|
|
2482
2537
|
// group.
|
|
2483
2538
|
const grokAuthService = createGrokAuthService();
|
|
2484
2539
|
|
|
2540
|
+
// ─── Claude Cloud (provider `claude-cloud`) ───────────────────────────────────
|
|
2541
|
+
// One credentials reader and one cloud client for the whole relay: the
|
|
2542
|
+
// settings tab (environments), and the archive of a deleted or archived
|
|
2543
|
+
// conversation's cloud session. The reader hands the Claude CLI's login to
|
|
2544
|
+
// the client only; nothing here stores, logs or forwards it, and the workers
|
|
2545
|
+
// read the credentials file themselves (the launch service hands a cloud
|
|
2546
|
+
// worker the login only where the relay itself runs on an environment token,
|
|
2547
|
+
// and then through the secret file, never a plain launch environment).
|
|
2548
|
+
// An expired login is nudged by letting the CLI look at its own status.
|
|
2549
|
+
const claudeCloudCredentials = createClaudeCloudCredentials({
|
|
2550
|
+
nudge: async () => { await claudeAuthService.getStatus({ force: true }); },
|
|
2551
|
+
});
|
|
2552
|
+
// The Anthropic API, unless OAR_CLAUDE_CLOUD_API_BASE_URL names a fake one on
|
|
2553
|
+
// this machine (tests): base-url.mjs ignores every host that is not loopback.
|
|
2554
|
+
const claudeCloudClient = createClaudeCloudClient({
|
|
2555
|
+
credentials: claudeCloudCredentials,
|
|
2556
|
+
baseUrl: resolveClaudeCloudBaseUrl(process.env),
|
|
2557
|
+
});
|
|
2558
|
+
const claudeCloudSettingsService = createClaudeCloudSettingsService({
|
|
2559
|
+
readSetting: readAppSettingValue,
|
|
2560
|
+
writeSetting: (key, value) => stmts.upsertAppSetting.run(key, String(value), new Date().toISOString()),
|
|
2561
|
+
deleteSetting: (key) => stmts.deleteAppSetting.run(key),
|
|
2562
|
+
getClaudeProviderSettings,
|
|
2563
|
+
claudeAuthService,
|
|
2564
|
+
credentials: claudeCloudCredentials,
|
|
2565
|
+
cloud: claudeCloudClient,
|
|
2566
|
+
// A closure: the socket server is created further down the file.
|
|
2567
|
+
emit: (event, payload) => io.emit(event, payload),
|
|
2568
|
+
});
|
|
2569
|
+
function getClaudeCloudProviderSettings() {
|
|
2570
|
+
return claudeCloudSettingsService.getSettings();
|
|
2571
|
+
}
|
|
2572
|
+
const claudeCloudSessionService = createClaudeCloudSessionService({
|
|
2573
|
+
stmts,
|
|
2574
|
+
getCloudClient: () => claudeCloudClient,
|
|
2575
|
+
emit: (event, payload) => io.emit(event, payload),
|
|
2576
|
+
});
|
|
2577
|
+
// The suggestions of the New Chat repository and branch fields: Anthropic's
|
|
2578
|
+
// list of what the Claude GitHub app can reach (while the provider is on),
|
|
2579
|
+
// the repositories of earlier cloud chats, and `git ls-remote` on the host.
|
|
2580
|
+
const claudeCloudRepoService = createClaudeCloudRepoService({
|
|
2581
|
+
cloud: claudeCloudClient,
|
|
2582
|
+
isEnabled: () => claudeCloudSettingsService.getSettings().enabled === true,
|
|
2583
|
+
listRecentCloudSources: () => listRecentCloudSourcesFromStatements(stmts),
|
|
2584
|
+
});
|
|
2585
|
+
// The account's live usage for the Check Usage modal. The Claude Cloud switch
|
|
2586
|
+
// is the consent to use the Claude CLI's login, so nothing is read while it
|
|
2587
|
+
// is off.
|
|
2588
|
+
const claudeAccountUsageService = createClaudeAccountUsageService({
|
|
2589
|
+
cloudClient: claudeCloudClient,
|
|
2590
|
+
isEnabled: () => claudeCloudSettingsService.getSettings().enabled === true,
|
|
2591
|
+
});
|
|
2592
|
+
|
|
2485
2593
|
// ─── Provider CLI install / binary binding ────────────────────────────────────
|
|
2486
2594
|
// A resolved CLI path is host state, not conversation state, so it lives beside
|
|
2487
2595
|
// the port and the token in config.json rather than in the app_settings table
|
|
@@ -2584,6 +2692,8 @@ const sessionWorkerRegistry = createSessionWorkerRegistry();
|
|
|
2584
2692
|
const sessionWorkerProcessInspector = createSessionWorkerProcessInspector({
|
|
2585
2693
|
platform: process.platform,
|
|
2586
2694
|
execFileSyncImpl: execFileSync,
|
|
2695
|
+
execFileImpl: execFile,
|
|
2696
|
+
logger: console,
|
|
2587
2697
|
});
|
|
2588
2698
|
function buildSessionWorkerLaunchEnv() {
|
|
2589
2699
|
const normalizePathValue = (value) => {
|
|
@@ -2700,7 +2810,18 @@ function buildSessionWorkerLaunchEnvForSession(targetSessionId) {
|
|
|
2700
2810
|
const providerType = String(runtimeSession?.provider_type || 'github').trim().toLowerCase();
|
|
2701
2811
|
// Clear every provider's variables first (the appliers' kind-guarded deletes
|
|
2702
2812
|
// keep the chained clears order-independent), then apply only the bound one.
|
|
2703
|
-
const cleared = applyCopilotSdkProviderEnvironment(applyGrokProviderEnvironment(applyCursorProviderEnvironment(applyClaudeProviderEnvironment(applyOpenAIProviderEnvironment(sessionWorkerLaunchEnv)))));
|
|
2813
|
+
const cleared = applyClaudeCloudProviderEnvironment(applyCopilotSdkProviderEnvironment(applyGrokProviderEnvironment(applyCursorProviderEnvironment(applyClaudeProviderEnvironment(applyOpenAIProviderEnvironment(sessionWorkerLaunchEnv))))));
|
|
2814
|
+
// A Claude Cloud conversation gets its own worker and nothing else: it must
|
|
2815
|
+
// never reach the Copilot CLI fall-through at the end of this function. The
|
|
2816
|
+
// environment names the worker and the model, no credential.
|
|
2817
|
+
if (providerType === 'claude-cloud') {
|
|
2818
|
+
const cloudSettings = getClaudeCloudProviderSettings();
|
|
2819
|
+
const cloudModel = String(runtimeSession?.provider_model || runtimeSession?.model || cloudSettings.defaultModel).trim();
|
|
2820
|
+
return applyClaudeCloudProviderEnvironment(cleared, {
|
|
2821
|
+
enabled: true,
|
|
2822
|
+
model: cloudModel,
|
|
2823
|
+
});
|
|
2824
|
+
}
|
|
2704
2825
|
if (providerType === 'claude') {
|
|
2705
2826
|
const claudeSettings = getClaudeProviderSettings();
|
|
2706
2827
|
const claudeModel = String(runtimeSession?.provider_model || runtimeSession?.model || claudeSettings.model).trim();
|
|
@@ -2773,8 +2894,18 @@ async function spawnSessionWorkerCli(targetSessionId, { allowProcessReuse = true
|
|
|
2773
2894
|
if (!normalizedTargetSessionId) {
|
|
2774
2895
|
throw new Error('missing-target-session-id');
|
|
2775
2896
|
}
|
|
2897
|
+
// An unreadable process list must not stop every launch on the relay
|
|
2898
|
+
// (2026-10-01: a control byte in one command line did, for an hour).
|
|
2899
|
+
const warnProcessListUnreadable = (error) => {
|
|
2900
|
+
console.warn(`${runtimeLogPrefix()}worker launcher: process list unreadable, launching session=${normalizedTargetSessionId.slice(0, 8)} without the reuse check: ${error?.message || error}`);
|
|
2901
|
+
};
|
|
2776
2902
|
if (allowProcessReuse) {
|
|
2777
|
-
|
|
2903
|
+
let liveWorker = null;
|
|
2904
|
+
try {
|
|
2905
|
+
liveWorker = await sessionWorkerProcessInspector.findProcessForSessionAsync(normalizedTargetSessionId);
|
|
2906
|
+
} catch (error) {
|
|
2907
|
+
warnProcessListUnreadable(error);
|
|
2908
|
+
}
|
|
2778
2909
|
if (liveWorker?.processId) {
|
|
2779
2910
|
const workerId = `worker-${normalizedTargetSessionId.slice(0, 8)}`;
|
|
2780
2911
|
console.log(`${runtimeLogPrefix()}worker launcher: reused ${workerId} session=${normalizedTargetSessionId.slice(0, 8)} pid=${liveWorker.processId}`);
|
|
@@ -2792,6 +2923,7 @@ async function spawnSessionWorkerCli(targetSessionId, { allowProcessReuse = true
|
|
|
2792
2923
|
execFileSyncImpl: execFileSync,
|
|
2793
2924
|
processInspector: sessionWorkerProcessInspector,
|
|
2794
2925
|
allowProcessReuse,
|
|
2926
|
+
onProcessListError: warnProcessListUnreadable,
|
|
2795
2927
|
});
|
|
2796
2928
|
const workerPid = Number.isInteger(Number(launched?.pid)) ? Number(launched.pid) : null;
|
|
2797
2929
|
const workerId = `worker-${normalizedTargetSessionId.slice(0, 8)}`;
|
|
@@ -2804,11 +2936,26 @@ async function spawnSessionWorkerCli(targetSessionId, { allowProcessReuse = true
|
|
|
2804
2936
|
console.log(`${runtimeLogPrefix()}worker launcher: ${launchVerb} ${workerId} session=${normalizedTargetSessionId.slice(0, 8)} pid=${workerPid || 'none'} mode=${launchMode}${tmuxSessionName ? ` tmux=${tmuxSessionName}` : ''}`);
|
|
2805
2937
|
return { workerId, pid: workerPid, reused: launched?.reused === true };
|
|
2806
2938
|
}
|
|
2939
|
+
// The note in the conversation while a session's worker cannot be started
|
|
2940
|
+
// (worker-launch-note-service.mjs). `io` exists only later in boot; the
|
|
2941
|
+
// supervisor reports launch episodes at run time, never during boot.
|
|
2942
|
+
const workerLaunchNoteService = createWorkerLaunchNoteService({
|
|
2943
|
+
db,
|
|
2944
|
+
stmts,
|
|
2945
|
+
emit: (event, payload) => io.emit(event, payload),
|
|
2946
|
+
resolveConversationId: (sdkSessionId) => (
|
|
2947
|
+
stmts.getConvBySdkSessionId?.get?.(sdkSessionId)?.id
|
|
2948
|
+
|| sessionWorkerRegistry.getWorker(sdkSessionId)?.conversationId
|
|
2949
|
+
|| null
|
|
2950
|
+
),
|
|
2951
|
+
logger: console,
|
|
2952
|
+
});
|
|
2807
2953
|
const sessionWorkerSupervisor = createSessionWorkerSupervisor({
|
|
2808
2954
|
registry: sessionWorkerRegistry,
|
|
2809
2955
|
spawnWorker: async (sdkSessionId, options = {}) => spawnSessionWorkerCli(sdkSessionId, options),
|
|
2810
2956
|
diagnosticPlanReference: () => path.join(currentWorkspaceRootPath(), '.cursor', 'plans', 'worker-startup-monitoring-plan.md'),
|
|
2811
2957
|
log: (message) => console.warn(`${runtimeLogPrefix()}${message}`),
|
|
2958
|
+
onLaunchState: (event) => workerLaunchNoteService.handle(event),
|
|
2812
2959
|
});
|
|
2813
2960
|
// Feature flags moved from config.json into app_settings; adopt any legacy
|
|
2814
2961
|
// `features` key once, then resolve the boot snapshot (defaults < stored rows
|
|
@@ -2846,6 +2993,7 @@ const updateCheckService = updateCheckKilled ? null : createUpdateCheckService({
|
|
|
2846
2993
|
const updateInstallService = createUpdateInstallService({
|
|
2847
2994
|
runningVersion: RUNNING_VERSION,
|
|
2848
2995
|
installMethod: INSTALL_METHOD,
|
|
2996
|
+
packageRoot: PACKAGE_ROOT,
|
|
2849
2997
|
readSetting: readAppSettingValue,
|
|
2850
2998
|
writeSetting: (key, value) => stmts.upsertAppSetting.run(key, value, new Date().toISOString()),
|
|
2851
2999
|
deleteSetting: (key) => stmts.deleteAppSetting.run(key),
|
|
@@ -2876,14 +3024,11 @@ async function stopSessionWorkerForProviderRebind(sdkSessionId) {
|
|
|
2876
3024
|
const currentWorker = sessionWorkerRegistry?.getWorker?.(sessionId) || null;
|
|
2877
3025
|
sessionWorkerSupervisor?.markKilled?.(sessionId);
|
|
2878
3026
|
const cancelledStart = await sessionWorkerSupervisor?.cancelPendingStart?.(sessionId, { wait: true });
|
|
3027
|
+
// Read off the thread; an unreadable list throws, and this caller treats
|
|
3028
|
+
// that as fatal (a rebind next to a process it could not see is worse).
|
|
2879
3029
|
const discoveredProcesses = process.platform === 'win32'
|
|
2880
|
-
? (
|
|
2881
|
-
|
|
2882
|
-
|| sessionWorkerProcessInspector?.findWindowsProcessesForSession?.(sessionId)
|
|
2883
|
-
|| sessionWorkerProcessInspector?.findProcessesForSession?.(sessionId)
|
|
2884
|
-
|| []
|
|
2885
|
-
)
|
|
2886
|
-
: (sessionWorkerProcessInspector?.findProcessesForSession?.(sessionId) || []);
|
|
3030
|
+
? await sessionWorkerProcessInspector.findWindowsProcessTreeForSessionAsync(sessionId, { fresh: true })
|
|
3031
|
+
: await sessionWorkerProcessInspector.findProcessesForSessionAsync(sessionId, { fresh: true });
|
|
2887
3032
|
const currentWorkerPid = Number(currentWorker?.pid);
|
|
2888
3033
|
const hadActiveWorker = discoveredProcesses.some((entry) => isProcessAlive(Number(entry?.processId)))
|
|
2889
3034
|
|| (Number.isInteger(currentWorkerPid) && currentWorkerPid > 0 && isProcessAlive(currentWorkerPid))
|
|
@@ -3186,6 +3331,26 @@ function cancelRelayShutdown({ requestedBy = 'localhost-api' } = {}) {
|
|
|
3186
3331
|
return { cancelled: true, dropped, ...relayShutdownStatePayload() };
|
|
3187
3332
|
}
|
|
3188
3333
|
|
|
3334
|
+
const countPendingRowsByOwnerSession = db.prepare(`
|
|
3335
|
+
SELECT owner_sdk_session_id AS sdk_session_id, COUNT(*) AS n
|
|
3336
|
+
FROM queue
|
|
3337
|
+
WHERE status = 'pending' AND owner_sdk_session_id IS NOT NULL AND owner_sdk_session_id != ''
|
|
3338
|
+
GROUP BY owner_sdk_session_id
|
|
3339
|
+
`);
|
|
3340
|
+
|
|
3341
|
+
function countPendingRowsOfStoppedSessions() {
|
|
3342
|
+
try {
|
|
3343
|
+
let stalled = 0;
|
|
3344
|
+
for (const row of countPendingRowsByOwnerSession.all()) {
|
|
3345
|
+
const lifecycle = sessionWorkerSupervisor?.getLifecycleState?.(String(row.sdk_session_id || '').trim());
|
|
3346
|
+
if (lifecycle?.retriesStopped === true) stalled += Number(row.n || 0);
|
|
3347
|
+
}
|
|
3348
|
+
return stalled;
|
|
3349
|
+
} catch {
|
|
3350
|
+
return 0;
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
3353
|
+
|
|
3189
3354
|
function requestRelayShutdown({ reason = 'manual-request', requestedBy = 'localhost-api', restart = false } = {}) {
|
|
3190
3355
|
const safeReason = String(reason || 'manual-request').trim().slice(0, 140) || 'manual-request';
|
|
3191
3356
|
const safeRequestedBy = String(requestedBy || 'localhost-api').trim().slice(0, 80) || 'localhost-api';
|
|
@@ -3222,7 +3387,11 @@ function requestRelayShutdown({ reason = 'manual-request', requestedBy = 'localh
|
|
|
3222
3387
|
const tryShutdownWhenIdle = () => {
|
|
3223
3388
|
if (runtimeShutdownStarted) return;
|
|
3224
3389
|
const counts = queueCounts();
|
|
3225
|
-
|
|
3390
|
+
// A row queued for a session whose launches the relay has stopped
|
|
3391
|
+
// retrying makes no progress until somebody presses Retry; it must not
|
|
3392
|
+
// hold the restart (which, fresh, is itself the next launch attempt).
|
|
3393
|
+
const stalledPending = countPendingRowsOfStoppedSessions();
|
|
3394
|
+
const queueFree = (counts.pendingCount - stalledPending + counts.processingCount + counts.parkedCount) <= 0;
|
|
3226
3395
|
if (!queueFree) return;
|
|
3227
3396
|
const requestInfo = pendingRelayShutdownRequest;
|
|
3228
3397
|
pendingRelayShutdownRequest = null;
|
|
@@ -5358,8 +5527,7 @@ function shouldIncludeInPwaVersion(relativePath) {
|
|
|
5358
5527
|
const normalized = String(relativePath).replace(/\\/g, '/').replace(/^\/+/, '');
|
|
5359
5528
|
if (!normalized || normalized.startsWith('.')) return false;
|
|
5360
5529
|
if (normalized === 'index.html' || normalized === 'sw.js' || normalized === 'manifest.webmanifest') return true;
|
|
5361
|
-
if (/^app-icon(?:-\d+)?\.png$/i.test(normalized)) return true;
|
|
5362
|
-
if (/^app-icon\.svg$/i.test(normalized)) return true;
|
|
5530
|
+
if (/^app-icon(?:-light)?(?:-maskable)?(?:-\d+)?\.(?:png|svg)$/i.test(normalized)) return true;
|
|
5363
5531
|
if (/^favicon\.ico$/i.test(normalized)) return true;
|
|
5364
5532
|
if (normalized.startsWith('app/')) {
|
|
5365
5533
|
const ext = path.extname(normalized).toLowerCase();
|
|
@@ -5431,12 +5599,13 @@ function loadPwaManifestTemplate() {
|
|
|
5431
5599
|
description: 'OAR — Open Agent Relay: drive your local coding agents from any browser.',
|
|
5432
5600
|
display_override: ['standalone'],
|
|
5433
5601
|
display: 'standalone',
|
|
5434
|
-
background_color: '#
|
|
5435
|
-
theme_color: '#
|
|
5602
|
+
background_color: '#0d1117',
|
|
5603
|
+
theme_color: '#0d1117',
|
|
5436
5604
|
icons: [
|
|
5437
|
-
{ src: 'app-icon.svg?v=
|
|
5605
|
+
{ src: 'app-icon.svg?v=26', sizes: 'any', type: 'image/svg+xml', purpose: 'any' },
|
|
5438
5606
|
{ src: 'app-icon-192.png', sizes: '192x192', type: 'image/png', purpose: 'any' },
|
|
5439
5607
|
{ src: 'app-icon-512.png', sizes: '512x512', type: 'image/png', purpose: 'any' },
|
|
5608
|
+
{ src: 'app-icon-maskable-512.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' },
|
|
5440
5609
|
],
|
|
5441
5610
|
};
|
|
5442
5611
|
try {
|
|
@@ -5447,8 +5616,8 @@ function loadPwaManifestTemplate() {
|
|
|
5447
5616
|
return fallback;
|
|
5448
5617
|
}
|
|
5449
5618
|
|
|
5450
|
-
function buildScopedPwaManifest({ shared = false } = {}) {
|
|
5451
|
-
const manifest = loadPwaManifestTemplate();
|
|
5619
|
+
function buildScopedPwaManifest({ shared = false, theme = 'dark' } = {}) {
|
|
5620
|
+
const manifest = applyPwaManifestTheme(loadPwaManifestTemplate(), theme);
|
|
5452
5621
|
// The custom name must be baked into every served manifest: Android's WebAPK
|
|
5453
5622
|
// update check re-fetches this route outside any page session, so a name the
|
|
5454
5623
|
// route doesn't know reverts on the phone with an accept-or-uninstall prompt.
|
|
@@ -5858,6 +6027,7 @@ async function requestSessionWorkerSocketDelivery({ sessionId, pid, reason = 'wo
|
|
|
5858
6027
|
defaultRelayMode: DEFAULT_RELAY_MODE,
|
|
5859
6028
|
defaultModel: DEFAULT_MODEL,
|
|
5860
6029
|
getCursorProviderSettings,
|
|
6030
|
+
getClaudeCloudProviderSettings,
|
|
5861
6031
|
});
|
|
5862
6032
|
if (out?.ownerSessionId) {
|
|
5863
6033
|
const worker = sessionWorkerRegistry?.getWorker?.(out.ownerSessionId) || null;
|
|
@@ -5897,6 +6067,19 @@ async function requestSessionWorkerSocketDelivery({ sessionId, pid, reason = 'wo
|
|
|
5897
6067
|
// keys are always present here.
|
|
5898
6068
|
thinkingEnabled: parseThinkingEnabled(conv?.thinking_enabled),
|
|
5899
6069
|
thinkingDisplay: parseThinkingDisplay(conv?.thinking_display),
|
|
6070
|
+
// What this session's commits and PRs say (Claude and Claude
|
|
6071
|
+
// Cloud): the finished settings object, or null for the CLI's own
|
|
6072
|
+
// attribution. Always present on such a delivery, so the worker can
|
|
6073
|
+
// tell "left as it was" (absent) from "vanilla" (null).
|
|
6074
|
+
...((() => {
|
|
6075
|
+
const deliveredProvider = String(out.providerType || '').trim().toLowerCase();
|
|
6076
|
+
if (deliveredProvider !== 'claude' && deliveredProvider !== 'claude-cloud') return {};
|
|
6077
|
+
return {
|
|
6078
|
+
attribution: claudeAttributionForConversation(conv, out.providerModel || out.model, {
|
|
6079
|
+
relayFolderFallback: deliveredProvider === 'claude',
|
|
6080
|
+
}),
|
|
6081
|
+
};
|
|
6082
|
+
})()),
|
|
5900
6083
|
};
|
|
5901
6084
|
})()),
|
|
5902
6085
|
},
|
|
@@ -5955,19 +6138,21 @@ async function recoverUndeliveredSessionWorkerMessage({ pending = null, sessionI
|
|
|
5955
6138
|
|
|
5956
6139
|
const WORKER_DEATH_GRACE_MS = 15_000;
|
|
5957
6140
|
|
|
5958
|
-
function isWorkerSessionProcessAlive(sessionId, hintPid = null) {
|
|
6141
|
+
async function isWorkerSessionProcessAlive(sessionId, hintPid = null) {
|
|
5959
6142
|
// A merely-retained socket is not proof of life: a dead worker's socket
|
|
5960
6143
|
// lingers until its close event lands, and counting it stalled recovery.
|
|
5961
6144
|
// The socket only counts with a recent heartbeat or a PID that probes
|
|
5962
6145
|
// alive; otherwise fall through to the process probes below.
|
|
5963
6146
|
if (sessionWorkerWebSocketService?.hasLiveWorkerSocket?.(sessionId)) return true;
|
|
5964
|
-
|
|
5965
|
-
|
|
5966
|
-
} catch {}
|
|
6147
|
+
// The pids first: they cost nothing. The process list last: on win32 it is
|
|
6148
|
+
// a PowerShell run (off the thread, shared and cached by the inspector).
|
|
5967
6149
|
const registryPid = Number(sessionWorkerRegistry?.getWorker?.(sessionId)?.pid || 0);
|
|
5968
6150
|
if (registryPid > 0 && isProcessAlive(registryPid)) return true;
|
|
5969
6151
|
const pid = Number(hintPid || 0);
|
|
5970
6152
|
if (pid > 0 && isProcessAlive(pid)) return true;
|
|
6153
|
+
try {
|
|
6154
|
+
if (await sessionWorkerProcessInspector?.findProcessForSessionAsync?.(sessionId)) return true;
|
|
6155
|
+
} catch {}
|
|
5971
6156
|
return false;
|
|
5972
6157
|
}
|
|
5973
6158
|
|
|
@@ -6035,10 +6220,10 @@ function handleWorkerSocketClosed({ sessionId, pid = null } = {}) {
|
|
|
6035
6220
|
// when it owned no rows (the hold can outlive them, e.g. a continuation's).
|
|
6036
6221
|
const heldSteering = Boolean(sessionWorkerRegistry?.getWorker?.(sid)?.steering);
|
|
6037
6222
|
if (!owned.length && !heldSteering) return;
|
|
6038
|
-
const timer = setTimeout(() => {
|
|
6223
|
+
const timer = setTimeout(async () => {
|
|
6039
6224
|
try {
|
|
6040
6225
|
if (runtimeShutdownStarted) return;
|
|
6041
|
-
if (isWorkerSessionProcessAlive(sid, pid)) return; // reconnect or socket flap
|
|
6226
|
+
if (await isWorkerSessionProcessAlive(sid, pid)) return; // reconnect or socket flap
|
|
6042
6227
|
sessionWorkerRegistry?.clearSteering?.(sid);
|
|
6043
6228
|
const stillOwned = stmts.listProcessingRowsForOwner?.all?.(sid) || [];
|
|
6044
6229
|
if (!stillOwned.length) return;
|
|
@@ -6051,15 +6236,28 @@ function handleWorkerSocketClosed({ sessionId, pid = null } = {}) {
|
|
|
6051
6236
|
timer.unref?.();
|
|
6052
6237
|
}
|
|
6053
6238
|
|
|
6054
|
-
|
|
6239
|
+
// One sweep at a time: a slow process list must not pile sweeps up.
|
|
6240
|
+
let deadWorkerSweepInFlight = false;
|
|
6241
|
+
async function sweepDeadWorkerProcessingRows() {
|
|
6055
6242
|
if (featureFlags?.SESSION_WORKER_ROUTING_ENABLED !== true) return 0;
|
|
6056
6243
|
if (runtimeShutdownStarted) return 0;
|
|
6244
|
+
if (deadWorkerSweepInFlight) return 0;
|
|
6245
|
+
deadWorkerSweepInFlight = true;
|
|
6246
|
+
try {
|
|
6247
|
+
return await sweepDeadWorkerProcessingRowsOnce();
|
|
6248
|
+
} finally {
|
|
6249
|
+
deadWorkerSweepInFlight = false;
|
|
6250
|
+
}
|
|
6251
|
+
}
|
|
6252
|
+
|
|
6253
|
+
async function sweepDeadWorkerProcessingRowsOnce() {
|
|
6057
6254
|
const owners = stmts.listProcessingOwnerSessionIds?.all?.(25) || [];
|
|
6058
6255
|
let recovered = 0;
|
|
6059
6256
|
for (const owner of owners) {
|
|
6060
6257
|
const sid = String(owner?.sdk_session_id || '').trim();
|
|
6061
6258
|
if (!sid) continue;
|
|
6062
|
-
|
|
6259
|
+
// The owners share one list: the inspector caches it for a moment.
|
|
6260
|
+
if (await isWorkerSessionProcessAlive(sid)) continue;
|
|
6063
6261
|
console.warn(`${runtimeLogPrefix()}WORKER DEAD session=${sid.slice(0, 8)} trigger=sweep — recovering owned turns`);
|
|
6064
6262
|
recovered += recoverDeadWorkerProcessingRows(sid, 'sweep');
|
|
6065
6263
|
}
|
|
@@ -6083,7 +6281,11 @@ const sessionWorkerWebSocketService = createSessionWorkerWebSocketService({
|
|
|
6083
6281
|
pollIntervalMs: 500,
|
|
6084
6282
|
logger: console,
|
|
6085
6283
|
});
|
|
6086
|
-
runtimeTimers.deadWorkerSweep = setInterval(
|
|
6284
|
+
runtimeTimers.deadWorkerSweep = setInterval(() => {
|
|
6285
|
+
sweepDeadWorkerProcessingRows().catch((error) => {
|
|
6286
|
+
console.warn(`${runtimeLogPrefix()}WORKER SWEEP failed: ${error?.message || error}`);
|
|
6287
|
+
});
|
|
6288
|
+
}, 30_000);
|
|
6087
6289
|
if (typeof runtimeTimers.deadWorkerSweep.unref === 'function') runtimeTimers.deadWorkerSweep.unref();
|
|
6088
6290
|
const tmuxInspectorAccessPolicy = createTmuxInspectorAccessPolicy({
|
|
6089
6291
|
sessionWorkerRegistry,
|
|
@@ -6158,7 +6360,7 @@ app.get('/socket.io/socket.io.js', (req, res, next) => {
|
|
|
6158
6360
|
app.get('/manifest.webmanifest', (req, res, next) => {
|
|
6159
6361
|
try {
|
|
6160
6362
|
const shared = String(req.query?.shared || '').trim() === '1';
|
|
6161
|
-
const manifest = buildScopedPwaManifest({ shared });
|
|
6363
|
+
const manifest = buildScopedPwaManifest({ shared, theme: normalizePwaTheme(req.query?.theme) });
|
|
6162
6364
|
res.setHeader('Cache-Control', 'no-store');
|
|
6163
6365
|
res.type('application/manifest+json').send(JSON.stringify(manifest, null, 2));
|
|
6164
6366
|
} catch (error) {
|
|
@@ -6227,6 +6429,8 @@ const windowsBootAutostartService = createWindowsBootAutostartService({
|
|
|
6227
6429
|
});
|
|
6228
6430
|
|
|
6229
6431
|
const gitChangesService = createGitChangesService();
|
|
6432
|
+
// GET /api/git/remote: where a folder lives on GitHub (New Chat, Claude Cloud).
|
|
6433
|
+
const gitRemoteService = createGitRemoteService({ gitChangesService });
|
|
6230
6434
|
|
|
6231
6435
|
// ─── Remote Relays ────────────────────────────────────────────────────────────
|
|
6232
6436
|
// Other OAR relays this one is paired with, so its agents can work there. Built
|
|
@@ -6292,9 +6496,18 @@ const remoteRelayCallerContext = createRemoteRelayCallerContext({
|
|
|
6292
6496
|
uuid: uuidv4,
|
|
6293
6497
|
logger: console,
|
|
6294
6498
|
});
|
|
6499
|
+
// Agent sessions: this relay as a target of its own agents. Their calls reach
|
|
6500
|
+
// the routes below in-process, the way a paired relay's reach them over HTTP.
|
|
6501
|
+
const remoteRelayLoopbackClient = createRemoteRelayLoopbackClient({
|
|
6502
|
+
handle: (req, res) => app(req, res),
|
|
6503
|
+
getOwnRelayId: () => remoteRelayRegistry.instanceId(),
|
|
6504
|
+
getOwnToken: () => config.authToken,
|
|
6505
|
+
});
|
|
6295
6506
|
const remoteRelayDispatcher = createRemoteRelayDispatcher({
|
|
6296
6507
|
registry: remoteRelayRegistry,
|
|
6297
6508
|
client: remoteRelayClient,
|
|
6509
|
+
localClient: remoteRelayLoopbackClient,
|
|
6510
|
+
getAgentSessionsSettings: () => remoteRelayRegistry.getAgentSessionsSettings(),
|
|
6298
6511
|
repository: remoteRelayRepository,
|
|
6299
6512
|
getCallerContext: (conversationId) => remoteRelayCallerContext.getCallerContext(conversationId),
|
|
6300
6513
|
requestApproval: (request) => remoteRelayCallerContext.requestApproval(request),
|
|
@@ -6306,6 +6519,7 @@ const remoteRelayDispatcher = createRemoteRelayDispatcher({
|
|
|
6306
6519
|
const sharedRouteDeps = {
|
|
6307
6520
|
auth,
|
|
6308
6521
|
gitChangesService,
|
|
6522
|
+
gitRemoteService,
|
|
6309
6523
|
currentWorkspaceRootPath,
|
|
6310
6524
|
io,
|
|
6311
6525
|
db,
|
|
@@ -6368,6 +6582,12 @@ const sharedRouteDeps = {
|
|
|
6368
6582
|
getGrokProviderSettings,
|
|
6369
6583
|
setGrokProviderSettings,
|
|
6370
6584
|
refreshGrokProviderModels,
|
|
6585
|
+
getClaudeCloudProviderSettings,
|
|
6586
|
+
claudeCloudSettingsService,
|
|
6587
|
+
claudeCloudSessionService,
|
|
6588
|
+
claudeCloudRepoService,
|
|
6589
|
+
// The login's state for the workers' nudge route (never the token).
|
|
6590
|
+
claudeCloudLoginStatus: { describe: () => claudeCloudCredentials.describe() },
|
|
6371
6591
|
reconcileUnstartedConversationProviders,
|
|
6372
6592
|
rebindUnstartedOpenAIConversationModel,
|
|
6373
6593
|
getOrCreateConversation,
|
|
@@ -6455,6 +6675,7 @@ const sharedRouteDeps = {
|
|
|
6455
6675
|
getCursorDashboardTokenSettings,
|
|
6456
6676
|
setCursorDashboardTokenSettings,
|
|
6457
6677
|
planUsageService,
|
|
6678
|
+
claudeAccountUsageService,
|
|
6458
6679
|
getCursorPlanAllowanceSettings,
|
|
6459
6680
|
setCursorPlanAllowanceSettings,
|
|
6460
6681
|
getGrokPlanAllowanceSettings,
|
|
@@ -6513,6 +6734,7 @@ registerAskUserRoutes(app, sharedRouteDeps);
|
|
|
6513
6734
|
registerRelayBoardRoutes(app, sharedRouteDeps);
|
|
6514
6735
|
registerCacheRoutes(app, sharedRouteDeps);
|
|
6515
6736
|
registerGitRoutes(app, sharedRouteDeps);
|
|
6737
|
+
registerClaudeCloudRoutes(app, sharedRouteDeps);
|
|
6516
6738
|
registerPushRoutes(app, {
|
|
6517
6739
|
auth,
|
|
6518
6740
|
pushDispatchService,
|
|
@@ -6665,7 +6887,16 @@ function auth(req, res, next) {
|
|
|
6665
6887
|
function getOrCreateConversation(id, firstLine) {
|
|
6666
6888
|
const now = new Date().toISOString();
|
|
6667
6889
|
stmts.insertConv.run(id, (firstLine || 'Untitled').slice(0, 80), now, now);
|
|
6668
|
-
|
|
6890
|
+
const conv = stmts.getConv.get(id);
|
|
6891
|
+
// A message into an archived conversation (an agent's, through a paired
|
|
6892
|
+
// relay, say) brings it back to the live list: its worker starts again
|
|
6893
|
+
// for the delivery, and an archived chat must not work unseen.
|
|
6894
|
+
if (conv && Number(conv.archived || 0) === 1 && typeof stmts.unarchiveConv?.run === 'function') {
|
|
6895
|
+
stmts.unarchiveConv.run(now, id);
|
|
6896
|
+
io.emit('conversation_archived', { conversationId: id, archived: false });
|
|
6897
|
+
return stmts.getConv.get(id);
|
|
6898
|
+
}
|
|
6899
|
+
return conv;
|
|
6669
6900
|
}
|
|
6670
6901
|
|
|
6671
6902
|
function ensureRuntimeSessionBinding(
|
|
@@ -6715,10 +6946,14 @@ function ensureRuntimeSessionBinding(
|
|
|
6715
6946
|
? 'cursor'
|
|
6716
6947
|
: (normalizedRequestedProviderType === 'grok'
|
|
6717
6948
|
? 'grok'
|
|
6718
|
-
|
|
6719
|
-
|
|
6720
|
-
|
|
6721
|
-
|
|
6949
|
+
// Without this entry a `claude-cloud` request would fall to the
|
|
6950
|
+
// default below and bind a GitHub (Copilot CLI) session.
|
|
6951
|
+
: (normalizedRequestedProviderType === 'claude-cloud'
|
|
6952
|
+
? 'claude-cloud'
|
|
6953
|
+
: (normalizedRequestedProviderType === 'github'
|
|
6954
|
+
? 'github'
|
|
6955
|
+
: (openAISettings?.enabled ? 'openai' : 'github'))))));
|
|
6956
|
+
const resolvedProviderModel = (resolvedProviderType === 'openai' || resolvedProviderType === 'claude' || resolvedProviderType === 'cursor' || resolvedProviderType === 'grok' || resolvedProviderType === 'claude-cloud')
|
|
6722
6957
|
? (String(providerModel || normalizedModel || '').trim() || null)
|
|
6723
6958
|
: null;
|
|
6724
6959
|
try {
|