@oar-sh/oar 0.9.5 → 0.9.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/extensions/web-relay/polling/polling-loop.mjs +12 -32
- package/.github/extensions/web-relay/skills/question-bridge.mjs +14 -39
- package/CHANGELOG.md +340 -0
- package/README.md +196 -26
- package/package.json +2 -1
- package/server/README.md +174 -7
- package/server/claude-cloud-worker/claude-cloud-event-normalizer.mjs +340 -0
- package/server/claude-cloud-worker/claude-cloud-session-process.mjs +1325 -0
- package/server/claude-cloud-worker/claude-cloud-session-worker.mjs +155 -0
- package/server/claude-worker/claude-remote-relay-tool.mjs +2 -0
- package/server/claude-worker/claude-sdk-adapter.mjs +18 -2
- package/server/claude-worker/claude-session-process.mjs +151 -1
- package/server/claude-worker/claude-session-worker.mjs +6 -0
- package/server/claude-worker/claude-turn-failure.mjs +92 -0
- package/server/claude-worker/claude-turn-publisher.mjs +52 -3
- package/server/claude-worker/sdk-message-normalizer.mjs +13 -1
- package/server/copilot-worker/copilot-model-switch.mjs +29 -3
- package/server/copilot-worker/copilot-question-bridge.mjs +2 -4
- package/server/copilot-worker/copilot-runtime-tree.mjs +91 -0
- package/server/copilot-worker/copilot-sdk-adapter.mjs +48 -8
- package/server/copilot-worker/copilot-sdk-session-process.mjs +897 -166
- package/server/cursor-worker/cursor-sdk-adapter.mjs +80 -13
- package/server/cursor-worker/cursor-turn-runner.mjs +5 -2
- package/server/db-schema.mjs +24 -0
- package/server/grok-worker/acp-client.mjs +68 -12
- package/server/grok-worker/grok-sdk-adapter.mjs +4 -0
- package/server/grok-worker/grok-turn-runner.mjs +6 -2
- package/server/migrations/0006-workspace-root-settings.mjs +67 -0
- package/server/migrations/0007-claude-cloud.mjs +82 -0
- package/server/public/app/agent-sessions-settings-ui.js +197 -0
- package/server/public/app/agent-sessions-settings.mjs +134 -0
- package/server/public/app/api-client.js +160 -6
- package/server/public/app/attachments-view.js +20 -1
- package/server/public/app/attribution-control.mjs +46 -0
- package/server/public/app/bootstrap.js +347 -31
- package/server/public/app/claude-cloud-conversation-ui.js +110 -0
- package/server/public/app/claude-cloud-settings-ui.js +374 -0
- package/server/public/app/claude-cloud-ui.mjs +611 -0
- package/server/public/app/composer-control-state.mjs +15 -3
- package/server/public/app/context-menu.mjs +140 -0
- package/server/public/app/context-tier-options.mjs +4 -0
- package/server/public/app/conversation-origin-ui.js +27 -0
- package/server/public/app/conversation-provider-indicator.mjs +22 -4
- package/server/public/app/conversation-view.js +178 -8
- package/server/public/app/cwd-picker.js +12 -0
- package/server/public/app/journal-view.js +704 -28
- package/server/public/app/markdown-strikethrough.mjs +31 -0
- package/server/public/app/math-dollars.mjs +89 -0
- package/server/public/app/model-selector-options.mjs +1 -1
- package/server/public/app/plan-usage-view.mjs +36 -8
- package/server/public/app/relay-board-inline.mjs +70 -0
- package/server/public/app/relay-board-view.js +157 -61
- package/server/public/app/relay-error-ctas.mjs +28 -5
- package/server/public/app/remote-relay-origin-view.mjs +120 -4
- package/server/public/app/router.js +49 -12
- package/server/public/app/settings-modal.js +33 -2
- package/server/public/app/settings-tabs.js +1 -1
- package/server/public/app/socket-handlers.js +60 -0
- package/server/public/app/store.js +4 -1
- package/server/public/app/subagent-fold.mjs +61 -0
- package/server/public/app/usage-limit-ui.js +225 -0
- package/server/public/index.html +471 -8
- package/server/relay-tools.md +13 -1
- package/server/repositories/message-repository.mjs +10 -1
- package/server/repositories/question-repository.mjs +1 -0
- package/server/repositories/remote-relay-repository.mjs +72 -1
- package/server/repositories/session-repository.mjs +47 -1
- package/server/routes/claude-cloud-routes.mjs +140 -0
- package/server/routes/git-routes.mjs +37 -0
- package/server/routes/messages-routes.mjs +202 -26
- package/server/routes/relay-board-routes.mjs +56 -2
- package/server/routes/remote-relay-routes.mjs +15 -1
- package/server/routes/sessions-routes.mjs +538 -72
- package/server/server-runtime.mjs +291 -27
- package/server/services/claude-account-usage-service.mjs +176 -0
- package/server/services/claude-cloud-repo-service.mjs +408 -0
- package/server/services/claude-cloud-session-service.mjs +326 -0
- package/server/services/claude-cloud-settings-service.mjs +269 -0
- package/server/services/git-remote-service.mjs +115 -0
- package/server/services/plan-usage-claude-cloud.mjs +211 -0
- package/server/services/plan-usage-claude.mjs +202 -40
- package/server/services/plan-usage-contract.mjs +39 -3
- package/server/services/plan-usage-service.mjs +82 -2
- package/server/services/preview-proxy-server.mjs +64 -36
- package/server/services/process-list-service.mjs +122 -0
- package/server/services/provider-reasoning-effort.mjs +3 -0
- package/server/services/relay-console-log.mjs +199 -0
- package/server/services/remote-relay-caller-context.mjs +78 -19
- package/server/services/remote-relay-client.mjs +9 -0
- package/server/services/remote-relay-dispatcher.mjs +451 -39
- package/server/services/remote-relay-inbound.mjs +83 -1
- package/server/services/remote-relay-loopback.mjs +245 -0
- package/server/services/remote-relay-registry-service.mjs +52 -0
- package/server/services/session-worker-launch-service.mjs +100 -15
- package/server/services/session-worker-process-service.mjs +410 -138
- package/server/services/session-worker-registry-service.mjs +1 -1
- package/server/services/session-worker-stop-service.mjs +24 -13
- package/server/services/session-worker-supervisor-service.mjs +89 -2
- package/server/services/usage-limit-pause-service.mjs +443 -0
- package/server/services/worker-launch-note-service.mjs +151 -0
- package/shared/ask-user-bridge.mjs +14 -39
- package/shared/claude-attribution.mjs +117 -0
- package/shared/claude-cloud/api-client.mjs +809 -0
- package/shared/claude-cloud/base-url.mjs +47 -0
- package/shared/claude-cloud/credentials.mjs +221 -0
- package/shared/claude-cloud/repo-url.mjs +111 -0
- package/shared/claude-usage-limit.mjs +257 -0
- package/shared/control-poller.mjs +69 -20
- package/shared/failure-note-text.mjs +38 -0
- package/shared/provider-routing.mjs +4 -2
- package/shared/question-wait.mjs +108 -0
- package/shared/remote-relay-contract.mjs +167 -22
- package/shared/remote-relay-tool-core.mjs +11 -8
- package/shared/steer-settle-failure.mjs +17 -0
- package/shared/steer-settle-markers.mjs +8 -0
- package/shared/worker-runtime/process-tree.mjs +483 -0
- package/shared/worker-runtime/relay-errors.mjs +31 -0
- package/shared/worker-runtime/send-and-wait-errors.mjs +4 -4
- package/shared/worker-runtime/turn-liveness.mjs +249 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
5939
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
6689
|
-
|
|
6690
|
-
|
|
6691
|
-
|
|
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);
|