@oar-sh/oar 0.9.6 → 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/CHANGELOG.md +172 -0
- package/README.md +176 -22
- 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 -2
- package/server/claude-worker/claude-session-process.mjs +33 -0
- package/server/claude-worker/claude-session-worker.mjs +6 -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/agent-sessions-settings-ui.js +197 -0
- package/server/public/app/agent-sessions-settings.mjs +134 -0
- package/server/public/app/api-client.js +143 -4
- package/server/public/app/attachments-view.js +20 -1
- package/server/public/app/attribution-control.mjs +46 -0
- package/server/public/app/bootstrap.js +344 -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 +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/conversation-origin-ui.js +27 -0
- package/server/public/app/conversation-provider-indicator.mjs +22 -4
- package/server/public/app/conversation-view.js +83 -4
- 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/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/settings-modal.js +33 -2
- package/server/public/app/settings-tabs.js +1 -1
- package/server/public/app/socket-handlers.js +51 -0
- package/server/public/index.html +381 -7
- 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 +120 -15
- package/server/routes/remote-relay-routes.mjs +15 -1
- package/server/routes/sessions-routes.mjs +502 -72
- package/server/server-runtime.mjs +258 -25
- 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/provider-reasoning-effort.mjs +3 -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-stop-service.mjs +24 -13
- package/server/services/session-worker-supervisor-service.mjs +56 -1
- 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 {
|
|
@@ -442,6 +454,7 @@ function getClaudeProviderSettings() {
|
|
|
442
454
|
enabledModels: models,
|
|
443
455
|
effortsByModel,
|
|
444
456
|
modelsDiscovered,
|
|
457
|
+
attributionMode: normalizeClaudeAttributionMode(readAppSettingValue(CLAUDE_ATTRIBUTION_SETTING_KEY)) || DEFAULT_CLAUDE_ATTRIBUTION_MODE,
|
|
445
458
|
providerType: 'claude',
|
|
446
459
|
};
|
|
447
460
|
}
|
|
@@ -460,10 +473,19 @@ function setClaudeProviderSettings(update = {}) {
|
|
|
460
473
|
if (!isSafeClaudeModelId(normalizedModel)) {
|
|
461
474
|
return { ok: false, error: 'Invalid Claude model ID' };
|
|
462
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
|
+
}
|
|
463
482
|
const nowIso = new Date().toISOString();
|
|
464
483
|
const nextEnabled = typeof enabled === 'boolean' ? enabled : existing.enabled;
|
|
465
484
|
stmts.upsertAppSetting.run(CLAUDE_ENABLED_SETTING_KEY, nextEnabled ? 'true' : 'false', nowIso);
|
|
466
485
|
stmts.upsertAppSetting.run(CLAUDE_MODEL_SETTING_KEY, normalizedModel, nowIso);
|
|
486
|
+
if (attributionMode) {
|
|
487
|
+
stmts.upsertAppSetting.run(CLAUDE_ATTRIBUTION_SETTING_KEY, attributionMode, nowIso);
|
|
488
|
+
}
|
|
467
489
|
if (Array.isArray(models)) {
|
|
468
490
|
const normalizedModels = models
|
|
469
491
|
.map((value) => String(value || '').trim())
|
|
@@ -492,9 +514,38 @@ function setClaudeProviderSettings(update = {}) {
|
|
|
492
514
|
model: current.model,
|
|
493
515
|
models: current.models,
|
|
494
516
|
availableModels: current.availableModels,
|
|
517
|
+
attributionMode: current.attributionMode,
|
|
495
518
|
};
|
|
496
519
|
}
|
|
497
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
|
+
|
|
498
549
|
const CLAUDE_MODEL_DISCOVERY_TIMEOUT_MS = 20_000;
|
|
499
550
|
|
|
500
551
|
// Discover the models the Claude Agent SDK can serve by asking a short-lived
|
|
@@ -1301,6 +1352,9 @@ const CLAUDE_MODEL_SETTING_KEY = 'claude_model';
|
|
|
1301
1352
|
const CLAUDE_MODELS_SETTING_KEY = 'claude_models';
|
|
1302
1353
|
const CLAUDE_ENABLED_MODELS_SETTING_KEY = 'claude_enabled_models';
|
|
1303
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';
|
|
1304
1358
|
// The set of levels the SDK itself can report/accept as EffortLevel values.
|
|
1305
1359
|
// Discovery results are filtered through this, so it must NOT contain the
|
|
1306
1360
|
// relay's derived 'ultracode' sentinel — supportedModels() never reports it
|
|
@@ -2482,6 +2536,59 @@ const claudeAuthService = createClaudeAuthService();
|
|
|
2482
2536
|
// group.
|
|
2483
2537
|
const grokAuthService = createGrokAuthService();
|
|
2484
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
|
+
|
|
2485
2592
|
// ─── Provider CLI install / binary binding ────────────────────────────────────
|
|
2486
2593
|
// A resolved CLI path is host state, not conversation state, so it lives beside
|
|
2487
2594
|
// the port and the token in config.json rather than in the app_settings table
|
|
@@ -2584,6 +2691,8 @@ const sessionWorkerRegistry = createSessionWorkerRegistry();
|
|
|
2584
2691
|
const sessionWorkerProcessInspector = createSessionWorkerProcessInspector({
|
|
2585
2692
|
platform: process.platform,
|
|
2586
2693
|
execFileSyncImpl: execFileSync,
|
|
2694
|
+
execFileImpl: execFile,
|
|
2695
|
+
logger: console,
|
|
2587
2696
|
});
|
|
2588
2697
|
function buildSessionWorkerLaunchEnv() {
|
|
2589
2698
|
const normalizePathValue = (value) => {
|
|
@@ -2700,7 +2809,18 @@ function buildSessionWorkerLaunchEnvForSession(targetSessionId) {
|
|
|
2700
2809
|
const providerType = String(runtimeSession?.provider_type || 'github').trim().toLowerCase();
|
|
2701
2810
|
// Clear every provider's variables first (the appliers' kind-guarded deletes
|
|
2702
2811
|
// keep the chained clears order-independent), then apply only the bound one.
|
|
2703
|
-
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
|
+
}
|
|
2704
2824
|
if (providerType === 'claude') {
|
|
2705
2825
|
const claudeSettings = getClaudeProviderSettings();
|
|
2706
2826
|
const claudeModel = String(runtimeSession?.provider_model || runtimeSession?.model || claudeSettings.model).trim();
|
|
@@ -2773,8 +2893,18 @@ async function spawnSessionWorkerCli(targetSessionId, { allowProcessReuse = true
|
|
|
2773
2893
|
if (!normalizedTargetSessionId) {
|
|
2774
2894
|
throw new Error('missing-target-session-id');
|
|
2775
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
|
+
};
|
|
2776
2901
|
if (allowProcessReuse) {
|
|
2777
|
-
|
|
2902
|
+
let liveWorker = null;
|
|
2903
|
+
try {
|
|
2904
|
+
liveWorker = await sessionWorkerProcessInspector.findProcessForSessionAsync(normalizedTargetSessionId);
|
|
2905
|
+
} catch (error) {
|
|
2906
|
+
warnProcessListUnreadable(error);
|
|
2907
|
+
}
|
|
2778
2908
|
if (liveWorker?.processId) {
|
|
2779
2909
|
const workerId = `worker-${normalizedTargetSessionId.slice(0, 8)}`;
|
|
2780
2910
|
console.log(`${runtimeLogPrefix()}worker launcher: reused ${workerId} session=${normalizedTargetSessionId.slice(0, 8)} pid=${liveWorker.processId}`);
|
|
@@ -2792,6 +2922,7 @@ async function spawnSessionWorkerCli(targetSessionId, { allowProcessReuse = true
|
|
|
2792
2922
|
execFileSyncImpl: execFileSync,
|
|
2793
2923
|
processInspector: sessionWorkerProcessInspector,
|
|
2794
2924
|
allowProcessReuse,
|
|
2925
|
+
onProcessListError: warnProcessListUnreadable,
|
|
2795
2926
|
});
|
|
2796
2927
|
const workerPid = Number.isInteger(Number(launched?.pid)) ? Number(launched.pid) : null;
|
|
2797
2928
|
const workerId = `worker-${normalizedTargetSessionId.slice(0, 8)}`;
|
|
@@ -2804,11 +2935,26 @@ async function spawnSessionWorkerCli(targetSessionId, { allowProcessReuse = true
|
|
|
2804
2935
|
console.log(`${runtimeLogPrefix()}worker launcher: ${launchVerb} ${workerId} session=${normalizedTargetSessionId.slice(0, 8)} pid=${workerPid || 'none'} mode=${launchMode}${tmuxSessionName ? ` tmux=${tmuxSessionName}` : ''}`);
|
|
2805
2936
|
return { workerId, pid: workerPid, reused: launched?.reused === true };
|
|
2806
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
|
+
});
|
|
2807
2952
|
const sessionWorkerSupervisor = createSessionWorkerSupervisor({
|
|
2808
2953
|
registry: sessionWorkerRegistry,
|
|
2809
2954
|
spawnWorker: async (sdkSessionId, options = {}) => spawnSessionWorkerCli(sdkSessionId, options),
|
|
2810
2955
|
diagnosticPlanReference: () => path.join(currentWorkspaceRootPath(), '.cursor', 'plans', 'worker-startup-monitoring-plan.md'),
|
|
2811
2956
|
log: (message) => console.warn(`${runtimeLogPrefix()}${message}`),
|
|
2957
|
+
onLaunchState: (event) => workerLaunchNoteService.handle(event),
|
|
2812
2958
|
});
|
|
2813
2959
|
// Feature flags moved from config.json into app_settings; adopt any legacy
|
|
2814
2960
|
// `features` key once, then resolve the boot snapshot (defaults < stored rows
|
|
@@ -2876,14 +3022,11 @@ async function stopSessionWorkerForProviderRebind(sdkSessionId) {
|
|
|
2876
3022
|
const currentWorker = sessionWorkerRegistry?.getWorker?.(sessionId) || null;
|
|
2877
3023
|
sessionWorkerSupervisor?.markKilled?.(sessionId);
|
|
2878
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).
|
|
2879
3027
|
const discoveredProcesses = process.platform === 'win32'
|
|
2880
|
-
? (
|
|
2881
|
-
|
|
2882
|
-
|| sessionWorkerProcessInspector?.findWindowsProcessesForSession?.(sessionId)
|
|
2883
|
-
|| sessionWorkerProcessInspector?.findProcessesForSession?.(sessionId)
|
|
2884
|
-
|| []
|
|
2885
|
-
)
|
|
2886
|
-
: (sessionWorkerProcessInspector?.findProcessesForSession?.(sessionId) || []);
|
|
3028
|
+
? await sessionWorkerProcessInspector.findWindowsProcessTreeForSessionAsync(sessionId, { fresh: true })
|
|
3029
|
+
: await sessionWorkerProcessInspector.findProcessesForSessionAsync(sessionId, { fresh: true });
|
|
2887
3030
|
const currentWorkerPid = Number(currentWorker?.pid);
|
|
2888
3031
|
const hadActiveWorker = discoveredProcesses.some((entry) => isProcessAlive(Number(entry?.processId)))
|
|
2889
3032
|
|| (Number.isInteger(currentWorkerPid) && currentWorkerPid > 0 && isProcessAlive(currentWorkerPid))
|
|
@@ -3186,6 +3329,26 @@ function cancelRelayShutdown({ requestedBy = 'localhost-api' } = {}) {
|
|
|
3186
3329
|
return { cancelled: true, dropped, ...relayShutdownStatePayload() };
|
|
3187
3330
|
}
|
|
3188
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
|
+
|
|
3189
3352
|
function requestRelayShutdown({ reason = 'manual-request', requestedBy = 'localhost-api', restart = false } = {}) {
|
|
3190
3353
|
const safeReason = String(reason || 'manual-request').trim().slice(0, 140) || 'manual-request';
|
|
3191
3354
|
const safeRequestedBy = String(requestedBy || 'localhost-api').trim().slice(0, 80) || 'localhost-api';
|
|
@@ -3222,7 +3385,11 @@ function requestRelayShutdown({ reason = 'manual-request', requestedBy = 'localh
|
|
|
3222
3385
|
const tryShutdownWhenIdle = () => {
|
|
3223
3386
|
if (runtimeShutdownStarted) return;
|
|
3224
3387
|
const counts = queueCounts();
|
|
3225
|
-
|
|
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;
|
|
3226
3393
|
if (!queueFree) return;
|
|
3227
3394
|
const requestInfo = pendingRelayShutdownRequest;
|
|
3228
3395
|
pendingRelayShutdownRequest = null;
|
|
@@ -5858,6 +6025,7 @@ async function requestSessionWorkerSocketDelivery({ sessionId, pid, reason = 'wo
|
|
|
5858
6025
|
defaultRelayMode: DEFAULT_RELAY_MODE,
|
|
5859
6026
|
defaultModel: DEFAULT_MODEL,
|
|
5860
6027
|
getCursorProviderSettings,
|
|
6028
|
+
getClaudeCloudProviderSettings,
|
|
5861
6029
|
});
|
|
5862
6030
|
if (out?.ownerSessionId) {
|
|
5863
6031
|
const worker = sessionWorkerRegistry?.getWorker?.(out.ownerSessionId) || null;
|
|
@@ -5897,6 +6065,19 @@ async function requestSessionWorkerSocketDelivery({ sessionId, pid, reason = 'wo
|
|
|
5897
6065
|
// keys are always present here.
|
|
5898
6066
|
thinkingEnabled: parseThinkingEnabled(conv?.thinking_enabled),
|
|
5899
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
|
+
})()),
|
|
5900
6081
|
};
|
|
5901
6082
|
})()),
|
|
5902
6083
|
},
|
|
@@ -5955,19 +6136,21 @@ async function recoverUndeliveredSessionWorkerMessage({ pending = null, sessionI
|
|
|
5955
6136
|
|
|
5956
6137
|
const WORKER_DEATH_GRACE_MS = 15_000;
|
|
5957
6138
|
|
|
5958
|
-
function isWorkerSessionProcessAlive(sessionId, hintPid = null) {
|
|
6139
|
+
async function isWorkerSessionProcessAlive(sessionId, hintPid = null) {
|
|
5959
6140
|
// A merely-retained socket is not proof of life: a dead worker's socket
|
|
5960
6141
|
// lingers until its close event lands, and counting it stalled recovery.
|
|
5961
6142
|
// The socket only counts with a recent heartbeat or a PID that probes
|
|
5962
6143
|
// alive; otherwise fall through to the process probes below.
|
|
5963
6144
|
if (sessionWorkerWebSocketService?.hasLiveWorkerSocket?.(sessionId)) return true;
|
|
5964
|
-
|
|
5965
|
-
|
|
5966
|
-
} 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).
|
|
5967
6147
|
const registryPid = Number(sessionWorkerRegistry?.getWorker?.(sessionId)?.pid || 0);
|
|
5968
6148
|
if (registryPid > 0 && isProcessAlive(registryPid)) return true;
|
|
5969
6149
|
const pid = Number(hintPid || 0);
|
|
5970
6150
|
if (pid > 0 && isProcessAlive(pid)) return true;
|
|
6151
|
+
try {
|
|
6152
|
+
if (await sessionWorkerProcessInspector?.findProcessForSessionAsync?.(sessionId)) return true;
|
|
6153
|
+
} catch {}
|
|
5971
6154
|
return false;
|
|
5972
6155
|
}
|
|
5973
6156
|
|
|
@@ -6035,10 +6218,10 @@ function handleWorkerSocketClosed({ sessionId, pid = null } = {}) {
|
|
|
6035
6218
|
// when it owned no rows (the hold can outlive them, e.g. a continuation's).
|
|
6036
6219
|
const heldSteering = Boolean(sessionWorkerRegistry?.getWorker?.(sid)?.steering);
|
|
6037
6220
|
if (!owned.length && !heldSteering) return;
|
|
6038
|
-
const timer = setTimeout(() => {
|
|
6221
|
+
const timer = setTimeout(async () => {
|
|
6039
6222
|
try {
|
|
6040
6223
|
if (runtimeShutdownStarted) return;
|
|
6041
|
-
if (isWorkerSessionProcessAlive(sid, pid)) return; // reconnect or socket flap
|
|
6224
|
+
if (await isWorkerSessionProcessAlive(sid, pid)) return; // reconnect or socket flap
|
|
6042
6225
|
sessionWorkerRegistry?.clearSteering?.(sid);
|
|
6043
6226
|
const stillOwned = stmts.listProcessingRowsForOwner?.all?.(sid) || [];
|
|
6044
6227
|
if (!stillOwned.length) return;
|
|
@@ -6051,15 +6234,28 @@ function handleWorkerSocketClosed({ sessionId, pid = null } = {}) {
|
|
|
6051
6234
|
timer.unref?.();
|
|
6052
6235
|
}
|
|
6053
6236
|
|
|
6054
|
-
|
|
6237
|
+
// One sweep at a time: a slow process list must not pile sweeps up.
|
|
6238
|
+
let deadWorkerSweepInFlight = false;
|
|
6239
|
+
async function sweepDeadWorkerProcessingRows() {
|
|
6055
6240
|
if (featureFlags?.SESSION_WORKER_ROUTING_ENABLED !== true) return 0;
|
|
6056
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() {
|
|
6057
6252
|
const owners = stmts.listProcessingOwnerSessionIds?.all?.(25) || [];
|
|
6058
6253
|
let recovered = 0;
|
|
6059
6254
|
for (const owner of owners) {
|
|
6060
6255
|
const sid = String(owner?.sdk_session_id || '').trim();
|
|
6061
6256
|
if (!sid) continue;
|
|
6062
|
-
|
|
6257
|
+
// The owners share one list: the inspector caches it for a moment.
|
|
6258
|
+
if (await isWorkerSessionProcessAlive(sid)) continue;
|
|
6063
6259
|
console.warn(`${runtimeLogPrefix()}WORKER DEAD session=${sid.slice(0, 8)} trigger=sweep — recovering owned turns`);
|
|
6064
6260
|
recovered += recoverDeadWorkerProcessingRows(sid, 'sweep');
|
|
6065
6261
|
}
|
|
@@ -6083,7 +6279,11 @@ const sessionWorkerWebSocketService = createSessionWorkerWebSocketService({
|
|
|
6083
6279
|
pollIntervalMs: 500,
|
|
6084
6280
|
logger: console,
|
|
6085
6281
|
});
|
|
6086
|
-
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);
|
|
6087
6287
|
if (typeof runtimeTimers.deadWorkerSweep.unref === 'function') runtimeTimers.deadWorkerSweep.unref();
|
|
6088
6288
|
const tmuxInspectorAccessPolicy = createTmuxInspectorAccessPolicy({
|
|
6089
6289
|
sessionWorkerRegistry,
|
|
@@ -6227,6 +6427,8 @@ const windowsBootAutostartService = createWindowsBootAutostartService({
|
|
|
6227
6427
|
});
|
|
6228
6428
|
|
|
6229
6429
|
const gitChangesService = createGitChangesService();
|
|
6430
|
+
// GET /api/git/remote: where a folder lives on GitHub (New Chat, Claude Cloud).
|
|
6431
|
+
const gitRemoteService = createGitRemoteService({ gitChangesService });
|
|
6230
6432
|
|
|
6231
6433
|
// ─── Remote Relays ────────────────────────────────────────────────────────────
|
|
6232
6434
|
// Other OAR relays this one is paired with, so its agents can work there. Built
|
|
@@ -6292,9 +6494,18 @@ const remoteRelayCallerContext = createRemoteRelayCallerContext({
|
|
|
6292
6494
|
uuid: uuidv4,
|
|
6293
6495
|
logger: console,
|
|
6294
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
|
+
});
|
|
6295
6504
|
const remoteRelayDispatcher = createRemoteRelayDispatcher({
|
|
6296
6505
|
registry: remoteRelayRegistry,
|
|
6297
6506
|
client: remoteRelayClient,
|
|
6507
|
+
localClient: remoteRelayLoopbackClient,
|
|
6508
|
+
getAgentSessionsSettings: () => remoteRelayRegistry.getAgentSessionsSettings(),
|
|
6298
6509
|
repository: remoteRelayRepository,
|
|
6299
6510
|
getCallerContext: (conversationId) => remoteRelayCallerContext.getCallerContext(conversationId),
|
|
6300
6511
|
requestApproval: (request) => remoteRelayCallerContext.requestApproval(request),
|
|
@@ -6306,6 +6517,7 @@ const remoteRelayDispatcher = createRemoteRelayDispatcher({
|
|
|
6306
6517
|
const sharedRouteDeps = {
|
|
6307
6518
|
auth,
|
|
6308
6519
|
gitChangesService,
|
|
6520
|
+
gitRemoteService,
|
|
6309
6521
|
currentWorkspaceRootPath,
|
|
6310
6522
|
io,
|
|
6311
6523
|
db,
|
|
@@ -6368,6 +6580,12 @@ const sharedRouteDeps = {
|
|
|
6368
6580
|
getGrokProviderSettings,
|
|
6369
6581
|
setGrokProviderSettings,
|
|
6370
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() },
|
|
6371
6589
|
reconcileUnstartedConversationProviders,
|
|
6372
6590
|
rebindUnstartedOpenAIConversationModel,
|
|
6373
6591
|
getOrCreateConversation,
|
|
@@ -6455,6 +6673,7 @@ const sharedRouteDeps = {
|
|
|
6455
6673
|
getCursorDashboardTokenSettings,
|
|
6456
6674
|
setCursorDashboardTokenSettings,
|
|
6457
6675
|
planUsageService,
|
|
6676
|
+
claudeAccountUsageService,
|
|
6458
6677
|
getCursorPlanAllowanceSettings,
|
|
6459
6678
|
setCursorPlanAllowanceSettings,
|
|
6460
6679
|
getGrokPlanAllowanceSettings,
|
|
@@ -6513,6 +6732,7 @@ registerAskUserRoutes(app, sharedRouteDeps);
|
|
|
6513
6732
|
registerRelayBoardRoutes(app, sharedRouteDeps);
|
|
6514
6733
|
registerCacheRoutes(app, sharedRouteDeps);
|
|
6515
6734
|
registerGitRoutes(app, sharedRouteDeps);
|
|
6735
|
+
registerClaudeCloudRoutes(app, sharedRouteDeps);
|
|
6516
6736
|
registerPushRoutes(app, {
|
|
6517
6737
|
auth,
|
|
6518
6738
|
pushDispatchService,
|
|
@@ -6665,7 +6885,16 @@ function auth(req, res, next) {
|
|
|
6665
6885
|
function getOrCreateConversation(id, firstLine) {
|
|
6666
6886
|
const now = new Date().toISOString();
|
|
6667
6887
|
stmts.insertConv.run(id, (firstLine || 'Untitled').slice(0, 80), now, now);
|
|
6668
|
-
|
|
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;
|
|
6669
6898
|
}
|
|
6670
6899
|
|
|
6671
6900
|
function ensureRuntimeSessionBinding(
|
|
@@ -6715,10 +6944,14 @@ function ensureRuntimeSessionBinding(
|
|
|
6715
6944
|
? 'cursor'
|
|
6716
6945
|
: (normalizedRequestedProviderType === 'grok'
|
|
6717
6946
|
? 'grok'
|
|
6718
|
-
|
|
6719
|
-
|
|
6720
|
-
|
|
6721
|
-
|
|
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')
|
|
6722
6955
|
? (String(providerModel || normalizedModel || '').trim() || null)
|
|
6723
6956
|
: null;
|
|
6724
6957
|
try {
|