@oar-sh/oar 0.9.13 → 0.9.15
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/extension.mjs +9 -0
- package/.github/extensions/web-relay/server-lifecycle/managed-server.mjs +15 -1
- package/.github/extensions/web-relay/skills/question-bridge.mjs +4 -1
- package/CHANGELOG.md +396 -0
- package/README.md +12 -12
- package/bin/oar.js +677 -103
- package/package.json +7 -5
- package/server/claude-cloud-worker/claude-cloud-session-process.mjs +10 -4
- package/server/claude-worker/claude-session-process.mjs +39 -13
- package/server/claude-worker/claude-turn-publisher.mjs +20 -1
- package/server/claude-worker/subagent-timeline-recorder.mjs +75 -8
- package/server/copilot-worker/copilot-ask-user-tool.mjs +1 -1
- package/server/copilot-worker/copilot-prompt-context.mjs +3 -1
- package/server/copilot-worker/copilot-question-bridge.mjs +11 -6
- package/server/copilot-worker/copilot-sdk-adapter.mjs +11 -0
- package/server/copilot-worker/copilot-sdk-session-process.mjs +14 -1
- package/server/cursor-worker/cursor-ask-user-tool.mjs +1 -1
- package/server/cursor-worker/cursor-sdk-adapter.mjs +22 -7
- package/server/cursor-worker/cursor-session-worker.mjs +9 -2
- package/server/cursor-worker/cursor-turn-runner.mjs +18 -3
- package/server/grok-worker/grok-sdk-adapter.mjs +14 -0
- package/server/grok-worker/grok-turn-runner.mjs +13 -3
- package/server/public/app/api-client.js +3 -1
- package/server/public/app/ask-user-view.js +29 -4
- package/server/public/app/conversation-provider-indicator.mjs +18 -8
- package/server/public/app/question-answer-rule.mjs +15 -0
- package/server/public/app/question-multi-select.mjs +9 -2
- package/server/public/app/relay-event-view.mjs +5 -1
- package/server/public/app/settings-modal.js +17 -3
- package/server/public/index.html +9 -5
- package/server/relay-exit-codes.mjs +6 -0
- package/server/relay-self-restart.mjs +6 -1
- package/server/relay-tools.md +14 -11
- package/server/repositories/message-repository.mjs +29 -28
- package/server/repositories/subagent-timeline-statements.mjs +2 -0
- package/server/routes/ask-user-routes.mjs +41 -7
- package/server/routes/messages-routes.mjs +65 -10
- package/server/routes/preview-routes.mjs +73 -7
- package/server/routes/sessions-routes.mjs +49 -16
- package/server/server-runtime.mjs +156 -94
- package/server/services/cursor-session-root-service.mjs +70 -6
- package/server/services/data-dir-resolution.mjs +208 -19
- package/server/services/earlier-database-service.mjs +631 -0
- package/server/services/oar-cli-helpers.mjs +18 -5
- package/server/services/oar-state-layout.mjs +107 -0
- package/server/services/oar-state-migration-service.mjs +387 -96
- package/server/services/package-data-rescue-preinstall.mjs +22 -0
- package/server/services/package-data-rescue.mjs +137 -0
- package/server/services/preview-registry-service.mjs +3 -2
- package/server/services/push-dispatch-service.mjs +33 -6
- package/server/services/relay-event-service.mjs +183 -5
- package/server/services/relay-restart-queue.mjs +10 -3
- package/server/services/relay-singleton-guard.mjs +300 -36
- package/server/services/remote-relay-caller-context.mjs +7 -2
- package/server/services/remote-relay-dispatcher.mjs +30 -9
- package/server/services/session-activity-tracker.mjs +52 -15
- package/server/services/session-worker-launch-service.mjs +31 -1
- package/server/services/stale-turn-recovery.mjs +99 -0
- package/server/services/subagent-timeline-service.mjs +4 -1
- package/server/services/windows-autostart-service.mjs +415 -45
- package/server/services/windows-boot-autostart-service.mjs +962 -185
- package/shared/preview-instructions.mjs +17 -12
- package/shared/preview-tool-core.mjs +32 -14
- package/shared/question-answer-rule.mjs +63 -0
- package/shared/relay-events.mjs +54 -4
- package/shared/remote-relay-contract.mjs +20 -10
- package/shared/subagent-timeline.mjs +4 -0
- package/shared/windows-system-tools.mjs +25 -0
- package/shared/worker-runtime/config-loader.mjs +54 -8
- package/shared/worker-runtime/prompt-context.mjs +3 -1
- package/shared/worker-runtime/working-directory.mjs +65 -0
- package/.github/extensions/web-relay/README.md +0 -190
- package/server/README.md +0 -2187
- package/server/uploads/image_input.md +0 -540
|
@@ -9,6 +9,7 @@ import { randomUUID } from 'crypto';
|
|
|
9
9
|
import { createSdkSessionSyncService } from '../services/sdk-session-sync-service.mjs';
|
|
10
10
|
import { serveFileWithRangeSupport } from '../services/file-serving.mjs';
|
|
11
11
|
import { stripRelayPromptContext } from '../services/relay-prompt-sanitizer.mjs';
|
|
12
|
+
import { describeWindowsAutostart, handMadeLines, switchBlockers } from '../services/windows-boot-autostart-service.mjs';
|
|
12
13
|
import {
|
|
13
14
|
buildConversationHistoryPageInfo,
|
|
14
15
|
normalizeConversationHistoryLimit,
|
|
@@ -4032,8 +4033,10 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
4032
4033
|
// then nothing is shared.
|
|
4033
4034
|
function collectSharePreviewTokens(conversationId) {
|
|
4034
4035
|
const tokens = new Set();
|
|
4035
|
-
// Every preview of the relay, not only this conversation's:
|
|
4036
|
-
//
|
|
4036
|
+
// Every preview of the relay, not only this conversation's: a reply may
|
|
4037
|
+
// quote any token its agent got hold of — one the user pasted from
|
|
4038
|
+
// Settings → Previews, or a relay-wide list fetched by hand with the relay
|
|
4039
|
+
// token (the preview tool's own list is scoped to its conversation).
|
|
4037
4040
|
try {
|
|
4038
4041
|
for (const knownToken of listKnownPreviewTokens?.() || []) tokens.add(String(knownToken));
|
|
4039
4042
|
} catch {
|
|
@@ -7297,6 +7300,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
7297
7300
|
legacyTaskPresent: boot.legacyTaskPresent,
|
|
7298
7301
|
customised: boot.customised === true,
|
|
7299
7302
|
customisations: Array.isArray(boot.customisations) ? boot.customisations : [],
|
|
7303
|
+
handMade: Array.isArray(boot.handMade) ? boot.handMade : [],
|
|
7300
7304
|
launcherBackupPath: boot.launcherBackupPath || null,
|
|
7301
7305
|
readError: boot.readError || null,
|
|
7302
7306
|
pendingElevation: boot.pendingElevation || null,
|
|
@@ -7304,6 +7308,9 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
7304
7308
|
manualCommand: boot.manualCommand || null,
|
|
7305
7309
|
} : null,
|
|
7306
7310
|
signinEnabled: signin.enabled,
|
|
7311
|
+
// The Startup entries OAR did not make, which it leaves as they are.
|
|
7312
|
+
signinHandMade: (windowsAutostartService.listEntries?.() || []).filter((entry) => !entry.generated)
|
|
7313
|
+
.map(({ path: entryPath, otherInstall }) => ({ path: entryPath, otherInstall: otherInstall || null })),
|
|
7307
7314
|
};
|
|
7308
7315
|
}
|
|
7309
7316
|
|
|
@@ -7340,9 +7347,31 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
7340
7347
|
if (!windowsAutostartService.getState().supported) {
|
|
7341
7348
|
return res.status(400).json({ error: 'Windows autostart is only available on Windows' });
|
|
7342
7349
|
}
|
|
7350
|
+
if (mode === 'boot' && !windowsBootAutostartService) {
|
|
7351
|
+
return res.status(500).json({ error: 'Windows boot autostart is unavailable' });
|
|
7352
|
+
}
|
|
7353
|
+
// The rules of `oar autostart`: only entries OAR generated are deleted
|
|
7354
|
+
// or written over. A hand-made one (a Startup entry, our task or its
|
|
7355
|
+
// launcher changed by hand, a task someone else made) that would stay
|
|
7356
|
+
// beside the new mode, or be replaced by it, stops the switch with the
|
|
7357
|
+
// same message; replacing it is `oar autostart <mode> --replace`.
|
|
7358
|
+
const boot = windowsBootAutostartService ? await windowsBootAutostartService.getState() : null;
|
|
7359
|
+
const now = describeWindowsAutostart({
|
|
7360
|
+
signinEntries: windowsAutostartService.listEntries?.() || [],
|
|
7361
|
+
boot: boot?.supported ? boot : {},
|
|
7362
|
+
});
|
|
7363
|
+
if (now.mode === 'unknown') {
|
|
7364
|
+
return res.status(409).json({ error: 'The boot task could not be read, so nothing was changed; the relay log has the details.' });
|
|
7365
|
+
}
|
|
7366
|
+
const blocking = switchBlockers(now, mode);
|
|
7367
|
+
if (blocking.length) return res.status(409).json({ error: handMadeLines(blocking, mode).join(' ') });
|
|
7343
7368
|
if (mode === 'boot') {
|
|
7344
|
-
|
|
7345
|
-
|
|
7369
|
+
// A generated launcher that is missing or of an earlier version is
|
|
7370
|
+
// regenerated by enabling again; a hand-made one stays.
|
|
7371
|
+
const regenerate = boot.customised && !boot.handMade?.length;
|
|
7372
|
+
if (now.mode === 'boot' && !regenerate) {
|
|
7373
|
+
windowsAutostartService.setEnabled(false);
|
|
7374
|
+
return res.json({ ...(await windowsAutostartMergedState()), accepted: true });
|
|
7346
7375
|
}
|
|
7347
7376
|
// The sweep's completion also removes the Startup .cmd entries; the
|
|
7348
7377
|
// sign-in service is not touched here so a declined UAC leaves the
|
|
@@ -7350,19 +7379,23 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
7350
7379
|
const result = await windowsBootAutostartService.requestEnable();
|
|
7351
7380
|
return res.json({ ...(await windowsAutostartMergedState()), accepted: result.accepted });
|
|
7352
7381
|
}
|
|
7353
|
-
// Leaving boot mode needs its own elevated sweep
|
|
7354
|
-
//
|
|
7355
|
-
|
|
7356
|
-
|
|
7357
|
-
|
|
7358
|
-
|
|
7359
|
-
|
|
7360
|
-
|
|
7361
|
-
|
|
7362
|
-
|
|
7363
|
-
|
|
7364
|
-
|
|
7382
|
+
// Leaving boot mode needs its own elevated sweep; the sign-in half
|
|
7383
|
+
// applies once the prompt was confirmed and the task is gone, so a
|
|
7384
|
+
// declined or failed one leaves the earlier mode as it was.
|
|
7385
|
+
if (now.mode === 'boot') {
|
|
7386
|
+
const result = await windowsBootAutostartService.requestDisable({
|
|
7387
|
+
onSettled: (ok) => {
|
|
7388
|
+
if (!ok) return;
|
|
7389
|
+
try {
|
|
7390
|
+
windowsAutostartService.setEnabled(mode === 'signin');
|
|
7391
|
+
} catch (error) {
|
|
7392
|
+
console.warn(`[autostart] the boot task is gone, but the sign-in entry could not be changed: ${error?.message || error}`);
|
|
7393
|
+
}
|
|
7394
|
+
},
|
|
7395
|
+
});
|
|
7396
|
+
return res.json({ ...(await windowsAutostartMergedState()), accepted: result.accepted });
|
|
7365
7397
|
}
|
|
7398
|
+
const accepted = true;
|
|
7366
7399
|
windowsAutostartService.setEnabled(mode === 'signin');
|
|
7367
7400
|
return res.json({ ...(await windowsAutostartMergedState()), accepted });
|
|
7368
7401
|
} catch (error) {
|
|
@@ -42,7 +42,7 @@ import { registerRelayBoardRoutes } from './routes/relay-board-routes.mjs';
|
|
|
42
42
|
import { registerCacheRoutes } from './routes/cache-routes.mjs';
|
|
43
43
|
import { registerGitRoutes } from './routes/git-routes.mjs';
|
|
44
44
|
import { registerClaudeCloudRoutes } from './routes/claude-cloud-routes.mjs';
|
|
45
|
-
import { registerPreviewRoutes } from './routes/preview-routes.mjs';
|
|
45
|
+
import { createPreviewScopeResolver, registerPreviewRoutes } from './routes/preview-routes.mjs';
|
|
46
46
|
import { registerRemoteRelayRoutes } from './routes/remote-relay-routes.mjs';
|
|
47
47
|
import { registerRelayEventRoutes } from './routes/relay-event-routes.mjs';
|
|
48
48
|
import { createRemoteRelayRepository } from './repositories/remote-relay-repository.mjs';
|
|
@@ -124,7 +124,9 @@ import { createClaudeSessionRootResolver } from './services/claude-session-root-
|
|
|
124
124
|
import { createCursorSessionRootResolver } from './services/cursor-session-root-service.mjs';
|
|
125
125
|
import { createGitChangesService } from './services/git-changes-service.mjs';
|
|
126
126
|
import { createRelaySingletonGuard } from './services/relay-singleton-guard.mjs';
|
|
127
|
-
import {
|
|
127
|
+
import { adoptEarlierDatabase, lostPackageDatabaseWarning, packageDatabaseEvidence } from './services/earlier-database-service.mjs';
|
|
128
|
+
import { ensureDataDirs, resolveDataDir, resolveUploadDir, resolveUploadReadDirs, uploadPathInDirs } from './services/data-dir-resolution.mjs';
|
|
129
|
+
import { mergeStrayMigratedUploads } from './services/oar-state-migration-service.mjs';
|
|
128
130
|
import { createRelayRestartOrchestrator } from './services/relay-restart-orchestrator-service.mjs';
|
|
129
131
|
import { createRelayBridgeOwnerService } from './services/relay-bridge-owner-service.mjs';
|
|
130
132
|
import { createRelayCliLauncherService } from './services/relay-cli-launcher-service.mjs';
|
|
@@ -173,7 +175,7 @@ import {
|
|
|
173
175
|
migrateLegacyConfigFeatures,
|
|
174
176
|
resolveBootFeatureFlags,
|
|
175
177
|
} from './features.mjs';
|
|
176
|
-
import { RELAY_RESTART_EXIT_CODE } from './relay-exit-codes.mjs';
|
|
178
|
+
import { RELAY_RESTART_EXIT_CODE, RELAY_STOP_EXIT_CODE } from './relay-exit-codes.mjs';
|
|
177
179
|
import { DEFAULT_QUESTION_TIMEOUT_MS, questionExpiresAt } from '../shared/question-timeout.mjs';
|
|
178
180
|
import {
|
|
179
181
|
parseTurnCeilingUpdate,
|
|
@@ -201,6 +203,7 @@ import {
|
|
|
201
203
|
} from './services/relay-restart-queue.mjs';
|
|
202
204
|
import { createRestartPictureCollector } from './services/relay-restart-picture.mjs';
|
|
203
205
|
import { createStaleRecoveryClockGuard } from './services/stale-recovery-clock-guard.mjs';
|
|
206
|
+
import { createStaleTurnRecovery } from './services/stale-turn-recovery.mjs';
|
|
204
207
|
import {
|
|
205
208
|
STALE_SUBAGENT_RUN_BOOT_DELAY_MS,
|
|
206
209
|
STALE_SUBAGENT_RUN_SWEEP_INTERVAL_MS,
|
|
@@ -248,7 +251,7 @@ import {
|
|
|
248
251
|
import { createModelVariantCatalogService } from './services/model-variant-catalog-service.mjs';
|
|
249
252
|
import { applyPwaManifestTheme, normalizePwaTheme } from './services/pwa-manifest-theme.mjs';
|
|
250
253
|
import { mapSubagentRunHeader } from './services/subagent-timeline-service.mjs';
|
|
251
|
-
import { describeExecError, describeSpawnEnvironment, windowsSystemToolPath } from '../shared/windows-system-tools.mjs';
|
|
254
|
+
import { describeExecError, describeSpawnEnvironment, normalizeTempEnv, windowsSystemToolPath } from '../shared/windows-system-tools.mjs';
|
|
252
255
|
|
|
253
256
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
254
257
|
const REPO_ROOT = path.resolve(__dirname, '..');
|
|
@@ -279,9 +282,8 @@ const APP_CONFIG_PLACEHOLDER = /window\.__COPILOT_APP_CONFIG = \{ basePath: '[^'
|
|
|
279
282
|
const PWA_VERSION_PLACEHOLDER = /const __PWA_VERSION = '[^']*';/;
|
|
280
283
|
// The console goes to a terminal, a tmux pane or nowhere: keep a copy to read
|
|
281
284
|
// after a restart or an incident.
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
});
|
|
285
|
+
const relayConsoleLogPath = resolveRelayConsoleLogPath({ env: process.env, serverDir: __dirname });
|
|
286
|
+
const relayConsoleLog = installRelayConsoleLog({ logPath: relayConsoleLogPath });
|
|
285
287
|
const ttyConsoleRuntime = await maybeStartTtyConsole({
|
|
286
288
|
serverDir: __dirname,
|
|
287
289
|
logsDir: path.join(__dirname, 'logs'),
|
|
@@ -289,6 +291,8 @@ const ttyConsoleRuntime = await maybeStartTtyConsole({
|
|
|
289
291
|
});
|
|
290
292
|
// The terminal console takes the console's methods over when it starts.
|
|
291
293
|
relayConsoleLog?.rewrap();
|
|
294
|
+
// TEMP and TMP for this relay and everything it starts (normalizeTempEnv).
|
|
295
|
+
for (const change of normalizeTempEnv()) console.log(`[startup] ${change} (quotes taken off, or the folder is not there)`);
|
|
292
296
|
|
|
293
297
|
// ─── Config ────────────────────────────────────────────────────────────────────
|
|
294
298
|
const CONFIG_PATH = process.env.COPILOT_WEB_RELAY_CONFIG
|
|
@@ -296,26 +300,32 @@ const CONFIG_PATH = process.env.COPILOT_WEB_RELAY_CONFIG
|
|
|
296
300
|
: path.join(__dirname, 'config.json');
|
|
297
301
|
// env var > config.json `dataDir` > <server>/data — see data-dir-resolution.mjs
|
|
298
302
|
// for why the config can carry it (a dev checkout pointing at a release
|
|
299
|
-
// install's data
|
|
303
|
+
// install's data).
|
|
300
304
|
const DATA_DIR = resolveDataDir({ env: process.env, configPath: CONFIG_PATH, serverDir: __dirname });
|
|
301
305
|
const DB_PATH = path.join(DATA_DIR, 'copilot.db');
|
|
302
306
|
const RELAY_LOCK_PATH = path.join(DATA_DIR, 'relay-server.lock');
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
307
|
+
// Uploads follow the data dir whenever the env var or config.json chose it.
|
|
308
|
+
const UPLOAD_DIR = resolveUploadDir({ dataDir: DATA_DIR, env: process.env, serverDir: __dirname });
|
|
309
|
+
// Uploads earlier versions left in <server>/uploads or <state root>/uploads
|
|
310
|
+
// are read in place; nothing there is written, moved or deleted.
|
|
311
|
+
const UPLOAD_READ_DIRS = resolveUploadReadDirs({ uploadDir: UPLOAD_DIR, dataDir: DATA_DIR, serverDir: __dirname });
|
|
306
312
|
const INITIAL_WORKSPACE_ROOT = resolveStartupWorkspaceRoot(__dirname);
|
|
307
313
|
const WORKSPACE_ROOT_LOCKED = true;
|
|
308
314
|
const SESSION_COOKIE = 'copilot_session';
|
|
309
315
|
const AUTH_COOKIE = 'copilot_auth';
|
|
310
316
|
|
|
311
|
-
|
|
312
|
-
fs.mkdirSync(DATA_DIR, { recursive: true, mode: 0o700 });
|
|
313
|
-
}
|
|
317
|
+
ensureDataDirs({ dataDir: DATA_DIR, uploadDir: UPLOAD_DIR });
|
|
314
318
|
if (process.platform !== 'win32') {
|
|
315
319
|
fs.chmodSync(DATA_DIR, 0o700);
|
|
316
320
|
}
|
|
317
|
-
|
|
318
|
-
|
|
321
|
+
// Earlier migrations put a checkout's uploads in <state root>/uploads, which
|
|
322
|
+
// nothing read: merge them in (never overwriting, never deleting the source).
|
|
323
|
+
mergeStrayMigratedUploads({ dataDir: DATA_DIR, uploadDir: UPLOAD_DIR, logger: console });
|
|
324
|
+
for (const dir of UPLOAD_READ_DIRS.slice(1)) {
|
|
325
|
+
try {
|
|
326
|
+
const count = fs.readdirSync(dir).filter((name) => isSha256(name)).length;
|
|
327
|
+
if (count) console.log(`[uploads] New uploads go to ${UPLOAD_DIR}; ${count} earlier upload${count === 1 ? '' : 's'} in ${dir} are read from there.`);
|
|
328
|
+
} catch {}
|
|
319
329
|
}
|
|
320
330
|
|
|
321
331
|
async function refreshOpenAIProviderModels() {
|
|
@@ -1097,18 +1107,20 @@ function setCursorProviderSettings(update = {}) {
|
|
|
1097
1107
|
const CURSOR_MODEL_DISCOVERY_TIMEOUT_MS = 20_000;
|
|
1098
1108
|
|
|
1099
1109
|
// Reasoning tiers for one discovered Cursor model, in composer vocabulary.
|
|
1100
|
-
// Cursor exposes effort as a per-model parameter named 'effort'
|
|
1101
|
-
// whose value list varies by model; 'extra-high' is
|
|
1102
|
-
// composer's 'xhigh'. Values with no composer
|
|
1103
|
-
// dropped rather than remapped onto a tier the
|
|
1110
|
+
// Cursor exposes effort as a per-model parameter named 'effort', 'reasoning'
|
|
1111
|
+
// or 'reasoning_effort' whose value list varies by model; 'extra-high' is
|
|
1112
|
+
// Cursor's spelling of the composer's 'xhigh'. Values with no composer
|
|
1113
|
+
// equivalent (e.g. 'minimal') are dropped rather than remapped onto a tier the
|
|
1114
|
+
// model does not actually have.
|
|
1104
1115
|
// Mirrors resolveCursorReasoningParams exactly, including its parameter
|
|
1105
|
-
// precedence: it consults 'effort' first
|
|
1106
|
-
// so scanning
|
|
1116
|
+
// precedence: it consults 'effort' first, then 'reasoning', then
|
|
1117
|
+
// 'reasoning_effort', so scanning several would advertise an off switch the
|
|
1118
|
+
// worker never sends.
|
|
1107
1119
|
function cursorEntrySupportsReasoningOff(entry) {
|
|
1108
1120
|
const parameters = Array.isArray(entry?.parameters) ? entry.parameters : [];
|
|
1109
1121
|
const byId = new Map(parameters.map((param) => [String(param?.id || '').trim().toLowerCase(), param]));
|
|
1110
1122
|
if (byId.has('thinking')) return true;
|
|
1111
|
-
const effortParam = byId.get('effort') || byId.get('reasoning');
|
|
1123
|
+
const effortParam = byId.get('effort') || byId.get('reasoning') || byId.get('reasoning_effort');
|
|
1112
1124
|
const values = Array.isArray(effortParam?.values) ? effortParam.values : [];
|
|
1113
1125
|
return values.some((value) => (
|
|
1114
1126
|
String((value && typeof value === 'object' ? value.value : value) || '').trim().toLowerCase() === 'none'
|
|
@@ -1121,7 +1133,7 @@ function cursorEntryEffortLevels(entry) {
|
|
|
1121
1133
|
// listing both parameters must not have its tiers read from one and its off
|
|
1122
1134
|
// switch from the other.
|
|
1123
1135
|
const byId = new Map(parameters.map((param) => [String(param?.id || '').trim().toLowerCase(), param]));
|
|
1124
|
-
const effortParam = byId.get('effort') || byId.get('reasoning');
|
|
1136
|
+
const effortParam = byId.get('effort') || byId.get('reasoning') || byId.get('reasoning_effort');
|
|
1125
1137
|
if (!effortParam) return [];
|
|
1126
1138
|
const levels = [];
|
|
1127
1139
|
for (const value of (Array.isArray(effortParam.values) ? effortParam.values : [])) {
|
|
@@ -1824,18 +1836,43 @@ function isProcessAlive(pid) {
|
|
|
1824
1836
|
}
|
|
1825
1837
|
}
|
|
1826
1838
|
|
|
1839
|
+
// The guard's own stale-lock rules (lockHolderMayRun; an EPERM is alive).
|
|
1827
1840
|
const relaySingletonGuard = createRelaySingletonGuard({
|
|
1828
1841
|
lockPath: RELAY_LOCK_PATH,
|
|
1829
1842
|
pid: process.pid,
|
|
1830
1843
|
token: config.authToken,
|
|
1831
|
-
isProcessAlive,
|
|
1832
1844
|
logger: console,
|
|
1833
1845
|
});
|
|
1834
1846
|
try {
|
|
1835
1847
|
relaySingletonGuard.acquire();
|
|
1836
1848
|
} catch (error) {
|
|
1849
|
+
// A held lock is not a crash: the supervisors do not retry 78.
|
|
1837
1850
|
console.error(`[server] ${error?.message || String(error)}`);
|
|
1838
|
-
process.exit(1);
|
|
1851
|
+
process.exit(error?.code === 'ERELAYLOCKED' ? RELAY_STOP_EXIT_CODE : 1);
|
|
1852
|
+
}
|
|
1853
|
+
// Before the database opens, under the lock (adoptEarlierDatabase); the
|
|
1854
|
+
// result also feeds lostPackageDatabaseWarning.
|
|
1855
|
+
let earlierDatabase;
|
|
1856
|
+
{
|
|
1857
|
+
try {
|
|
1858
|
+
earlierDatabase = adoptEarlierDatabase({
|
|
1859
|
+
dataDir: DATA_DIR,
|
|
1860
|
+
uploadDir: UPLOAD_DIR,
|
|
1861
|
+
serverDir: __dirname,
|
|
1862
|
+
configPath: CONFIG_PATH,
|
|
1863
|
+
packageRoot: REPO_ROOT,
|
|
1864
|
+
Database,
|
|
1865
|
+
authToken: config.authToken,
|
|
1866
|
+
logger: console,
|
|
1867
|
+
});
|
|
1868
|
+
} catch (error) {
|
|
1869
|
+
earlierDatabase = { status: 'refused', error: `[data] Not starting: the database this relay ran on until now could not be looked at (${error?.message || error}).` };
|
|
1870
|
+
}
|
|
1871
|
+
if (earlierDatabase.status === 'refused') {
|
|
1872
|
+
console.error(earlierDatabase.error);
|
|
1873
|
+
try { relaySingletonGuard.release(); } catch {}
|
|
1874
|
+
process.exit(RELAY_STOP_EXIT_CODE);
|
|
1875
|
+
}
|
|
1839
1876
|
}
|
|
1840
1877
|
|
|
1841
1878
|
const MAX_REQUEUE_RETRIES = Number.isFinite(Number(config.maxRequeueRetries))
|
|
@@ -2128,7 +2165,7 @@ function setSharePathPrivacyMode(value) {
|
|
|
2128
2165
|
// checkout, data and upload dirs, the workspace roots it knows, and the OS
|
|
2129
2166
|
// user name. The share routes add the conversation's own roots.
|
|
2130
2167
|
function sharePathKnownRoots() {
|
|
2131
|
-
const roots = [REPO_ROOT, DATA_DIR,
|
|
2168
|
+
const roots = [REPO_ROOT, DATA_DIR, ...UPLOAD_READ_DIRS, currentWorkspaceRootPath(), INITIAL_WORKSPACE_ROOT];
|
|
2132
2169
|
const userNames = [];
|
|
2133
2170
|
try { roots.push(os.homedir()); } catch {}
|
|
2134
2171
|
try { userNames.push(os.userInfo().username); } catch {}
|
|
@@ -2679,6 +2716,7 @@ const configuredConversationSessionMode =
|
|
|
2679
2716
|
updateModelCatalog({ models: [DEFAULT_MODEL], currentModel: DEFAULT_MODEL, defaultModel: DEFAULT_MODEL, source: 'bootstrap' });
|
|
2680
2717
|
|
|
2681
2718
|
// ─── SQLite Setup ─────────────────────────────────────────────────────────────
|
|
2719
|
+
const databaseCreatedAtStart = !fs.existsSync(DB_PATH);
|
|
2682
2720
|
const db = new Database(DB_PATH);
|
|
2683
2721
|
if (process.platform !== 'win32') {
|
|
2684
2722
|
fs.chmodSync(DB_PATH, 0o600);
|
|
@@ -3042,6 +3080,7 @@ function buildSessionWorkerLaunchEnvForSession(targetSessionId) {
|
|
|
3042
3080
|
enabled: true,
|
|
3043
3081
|
model: cursorModel,
|
|
3044
3082
|
apiKey: cursorSettings.apiKey,
|
|
3083
|
+
dataDir: DATA_DIR,
|
|
3045
3084
|
});
|
|
3046
3085
|
}
|
|
3047
3086
|
if (providerType === 'grok') {
|
|
@@ -3526,8 +3565,11 @@ function collectRestartPicture({ action = 'restart' } = {}) {
|
|
|
3526
3565
|
const relayRestartQueue = createRelayRestartQueue({
|
|
3527
3566
|
getCounts: () => queueCounts(),
|
|
3528
3567
|
collectPicture: collectRestartPicture,
|
|
3529
|
-
startShutdown: ({ action, reason }) => {
|
|
3568
|
+
startShutdown: ({ action, reason, requestedBy, requestedAt }) => {
|
|
3530
3569
|
const exitCode = action === 'restart' ? RELAY_RESTART_EXIT_CODE : 0;
|
|
3570
|
+
// The next start tells the conversations that asked for relay_restarted
|
|
3571
|
+
// how this one ended (services/relay-event-service.mjs).
|
|
3572
|
+
try { relayEventService?.noteRelayExit({ action, reason, requestedBy, requestedAt }); } catch {}
|
|
3531
3573
|
sessionWorkerWebSocketService.emitDraining(`api-${action}:${reason}`);
|
|
3532
3574
|
void shutdownRuntime(`api-${action}:${reason}`, { exitCode });
|
|
3533
3575
|
},
|
|
@@ -3554,7 +3596,9 @@ function relayShutdownStatePayload() {
|
|
|
3554
3596
|
}
|
|
3555
3597
|
|
|
3556
3598
|
function cancelRelayShutdown({ requestedBy = 'localhost-api' } = {}) {
|
|
3557
|
-
|
|
3599
|
+
const result = relayRestartQueue.cancel({ requestedBy });
|
|
3600
|
+
if (result?.cancelled) relayEventService?.noteRestartCancelled();
|
|
3601
|
+
return result;
|
|
3558
3602
|
}
|
|
3559
3603
|
|
|
3560
3604
|
function keepWaitingForRelayShutdown({ requestedBy = 'localhost-api' } = {}) {
|
|
@@ -3562,7 +3606,13 @@ function keepWaitingForRelayShutdown({ requestedBy = 'localhost-api' } = {}) {
|
|
|
3562
3606
|
}
|
|
3563
3607
|
|
|
3564
3608
|
function requestRelayShutdown(options = {}) {
|
|
3565
|
-
|
|
3609
|
+
// The loop guard of relay_restarted: a conversation whose turn answers the
|
|
3610
|
+
// last restart is not woken by this one. Noted first, since an immediate
|
|
3611
|
+
// request exits from inside the call.
|
|
3612
|
+
if (!runtimeShutdownStarted) relayEventService?.noteRestartRequested();
|
|
3613
|
+
const result = relayRestartQueue.request(options);
|
|
3614
|
+
if (!relayRestartQueue.isQueued() && !runtimeShutdownStarted) relayEventService?.noteRestartCancelled();
|
|
3615
|
+
return result;
|
|
3566
3616
|
}
|
|
3567
3617
|
|
|
3568
3618
|
function toNullableInt(value) {
|
|
@@ -4205,7 +4255,9 @@ function isSha256(value) {
|
|
|
4205
4255
|
}
|
|
4206
4256
|
|
|
4207
4257
|
function uploadPathForSha(sha256) {
|
|
4208
|
-
|
|
4258
|
+
let expectedSize = null;
|
|
4259
|
+
try { expectedSize = stmts.getUploadFile.get(String(sha256 || '').trim().toLowerCase())?.size_bytes ?? null; } catch {}
|
|
4260
|
+
return uploadPathInDirs(UPLOAD_READ_DIRS, sha256, { expectedSize });
|
|
4209
4261
|
}
|
|
4210
4262
|
|
|
4211
4263
|
function uploadContentUrlForSha(sha256) {
|
|
@@ -4886,7 +4938,10 @@ function persistUploadBuffer(buffer, options = {}) {
|
|
|
4886
4938
|
if (!Buffer.isBuffer(buffer) || !buffer.length) throw new Error('Empty upload');
|
|
4887
4939
|
if (buffer.length > MAX_UPLOAD_BYTES) throw new Error('Uploaded file too large');
|
|
4888
4940
|
const sha256 = crypto.createHash('sha256').update(buffer).digest('hex');
|
|
4889
|
-
|
|
4941
|
+
// Always kept in the upload dir: the folders of earlier versions can go
|
|
4942
|
+
// (a package update, a clean checkout), so a new reference never relies on
|
|
4943
|
+
// them. An existing file is never overwritten.
|
|
4944
|
+
const filePath = path.join(UPLOAD_DIR, sha256);
|
|
4890
4945
|
if (!fs.existsSync(filePath)) {
|
|
4891
4946
|
const tmpPath = path.join(UPLOAD_DIR, `${sha256}.${process.pid}.${Date.now()}.tmp`);
|
|
4892
4947
|
fs.writeFileSync(tmpPath, buffer);
|
|
@@ -4924,13 +4979,14 @@ function buildStoredAttachment({ sha256, name, type, size }) {
|
|
|
4924
4979
|
if (!isSha256(normalizedSha)) return null;
|
|
4925
4980
|
const normalizedName = String(name || '').trim().slice(0, 255) || `upload-${normalizedSha.slice(0, 12)}`;
|
|
4926
4981
|
const normalizedType = String(type || '').trim().toLowerCase().slice(0, 127) || 'application/octet-stream';
|
|
4982
|
+
const filePath = uploadPathForSha(normalizedSha);
|
|
4927
4983
|
return {
|
|
4928
4984
|
name: normalizedName,
|
|
4929
4985
|
type: normalizedType,
|
|
4930
4986
|
size: Number(size || 0),
|
|
4931
4987
|
sha256: normalizedSha,
|
|
4932
|
-
path:
|
|
4933
|
-
reference: `@${
|
|
4988
|
+
path: filePath,
|
|
4989
|
+
reference: `@${filePath}`,
|
|
4934
4990
|
contentUrl: uploadContentUrlForSha(normalizedSha),
|
|
4935
4991
|
};
|
|
4936
4992
|
}
|
|
@@ -5003,7 +5059,9 @@ function deleteOrphanedUploads(hashes) {
|
|
|
5003
5059
|
const refs = Number(stmts.countUploadRefsBySha.get(sha256)?.cnt || 0);
|
|
5004
5060
|
if (refs > 0) continue;
|
|
5005
5061
|
stmts.deleteUploadFile.run(sha256);
|
|
5006
|
-
|
|
5062
|
+
// Only the upload dir is swept; the folders of earlier versions are
|
|
5063
|
+
// read-only to the relay.
|
|
5064
|
+
const filePath = path.join(UPLOAD_DIR, sha256);
|
|
5007
5065
|
if (fs.existsSync(filePath)) {
|
|
5008
5066
|
try { fs.unlinkSync(filePath); } catch {}
|
|
5009
5067
|
}
|
|
@@ -5536,64 +5594,21 @@ function failConsumedRowTerminally(row, reason) {
|
|
|
5536
5594
|
});
|
|
5537
5595
|
}
|
|
5538
5596
|
|
|
5539
|
-
|
|
5540
|
-
|
|
5541
|
-
|
|
5542
|
-
|
|
5543
|
-
|
|
5544
|
-
|
|
5545
|
-
|
|
5546
|
-
|
|
5547
|
-
|
|
5548
|
-
|
|
5549
|
-
|
|
5550
|
-
|
|
5551
|
-
|
|
5552
|
-
|
|
5553
|
-
|
|
5554
|
-
}
|
|
5555
|
-
const listedRows = stmts.listRecoverableProcessing.all(params);
|
|
5556
|
-
for (const row of listedRows.filter((entry) => entry?.consumed_at)) {
|
|
5557
|
-
failConsumedRowTerminally(row, 'stale-recovery');
|
|
5558
|
-
noteRestartTurnOutcome(row, 'cut');
|
|
5559
|
-
}
|
|
5560
|
-
const allRows = listedRows.filter((entry) => !entry?.consumed_at);
|
|
5561
|
-
if (!allRows.length) return [];
|
|
5562
|
-
// Enforce the retry budget before requeueing: failing these rows first
|
|
5563
|
-
// flips them out of 'processing' so the recovery UPDATE below skips them.
|
|
5564
|
-
const overBudget = allRows.filter((row) => Number(row?.retry_count || 0) + 1 >= MAX_REQUEUE_RETRIES);
|
|
5565
|
-
for (const row of overBudget) {
|
|
5566
|
-
failRecoveredRowTerminally(row, 'stale-recovery');
|
|
5567
|
-
noteRestartTurnOutcome(row, 'failed');
|
|
5568
|
-
}
|
|
5569
|
-
const rows = allRows.filter((row) => Number(row?.retry_count || 0) + 1 < MAX_REQUEUE_RETRIES);
|
|
5570
|
-
if (!rows.length) return [];
|
|
5571
|
-
|
|
5572
|
-
const tx = db.transaction(() => {
|
|
5573
|
-
stmts.recoverProcessingBefore.run({ ...params, requeueAt: requeueAtIso });
|
|
5574
|
-
const settleRecoveredAbortControls = db.prepare(`
|
|
5575
|
-
UPDATE relay_control_requests
|
|
5576
|
-
SET status = 'failed',
|
|
5577
|
-
error = ?,
|
|
5578
|
-
updated_at = ?,
|
|
5579
|
-
completed_at = ?
|
|
5580
|
-
WHERE queue_message_id = ?
|
|
5581
|
-
AND type IN ('abort_turn', 'abort_subagent')
|
|
5582
|
-
AND status IN ('pending', 'processing')
|
|
5583
|
-
`);
|
|
5584
|
-
const now = new Date().toISOString();
|
|
5585
|
-
for (const row of rows) {
|
|
5586
|
-
settleRecoveredAbortControls.run('queue-recovered', now, now, row.id);
|
|
5587
|
-
}
|
|
5588
|
-
});
|
|
5589
|
-
tx();
|
|
5590
|
-
|
|
5591
|
-
for (const row of rows) {
|
|
5592
|
-
io.emit('message_status', { messageId: row.id, conversationId: row.conversation_id, status: 'pending' });
|
|
5593
|
-
noteRestartTurnOutcome(row, 'requeued');
|
|
5594
|
-
}
|
|
5595
|
-
io.emit('queue_updated', { recovered: rows.length });
|
|
5596
|
-
return rows;
|
|
5597
|
+
// The stale-turn sweep itself lives in services/stale-turn-recovery.mjs so it
|
|
5598
|
+
// can be tested against a real database; io is created further down, hence
|
|
5599
|
+
// the late-bound emit.
|
|
5600
|
+
const staleTurnRecovery = createStaleTurnRecovery({
|
|
5601
|
+
db,
|
|
5602
|
+
stmts,
|
|
5603
|
+
emit: (event, payload) => io.emit(event, payload),
|
|
5604
|
+
maxRequeueRetries: MAX_REQUEUE_RETRIES,
|
|
5605
|
+
failRecoveredRowTerminally,
|
|
5606
|
+
failConsumedRowTerminally,
|
|
5607
|
+
noteRestartTurnOutcome,
|
|
5608
|
+
warn: (line) => console.warn(`${runtimeLogPrefix()}${line}`),
|
|
5609
|
+
});
|
|
5610
|
+
function recoverProcessingOlderThan(cutoffIso, requeueAtIso, options) {
|
|
5611
|
+
return staleTurnRecovery.recoverProcessingOlderThan(cutoffIso, requeueAtIso, options);
|
|
5597
5612
|
}
|
|
5598
5613
|
|
|
5599
5614
|
// One `/api/usage` needs the same token for the quota read and the billing
|
|
@@ -5928,7 +5943,7 @@ const readContextFromSessionEvents = contextSnapshotService.readContextFromSessi
|
|
|
5928
5943
|
// Claude-provider sessions live under the Agent SDK's project layout rather than
|
|
5929
5944
|
// ~/.copilot/session-state, so their browsable session folder resolves separately.
|
|
5930
5945
|
const claudeSessionRootResolver = createClaudeSessionRootResolver({ fs, path });
|
|
5931
|
-
const cursorSessionRootResolver = createCursorSessionRootResolver({ fs, path, serverDir: __dirname });
|
|
5946
|
+
const cursorSessionRootResolver = createCursorSessionRootResolver({ fs, path, serverDir: __dirname, dataDir: DATA_DIR });
|
|
5932
5947
|
|
|
5933
5948
|
// ─── Express + Socket.io Setup ────────────────────────────────────────────────
|
|
5934
5949
|
const app = express();
|
|
@@ -6805,6 +6820,34 @@ const windowsBootAutostartService = createWindowsBootAutostartService({
|
|
|
6805
6820
|
packageRoot: REPO_ROOT,
|
|
6806
6821
|
configPath: CONFIG_PATH,
|
|
6807
6822
|
});
|
|
6823
|
+
// Autostart entries written before they pinned the data and log folders start
|
|
6824
|
+
// the relay on a database inside the package directory, or before TEMP and
|
|
6825
|
+
// the data drive are there. Brought up to date once this relay holds the
|
|
6826
|
+
// lock and its port; edited ones stay.
|
|
6827
|
+
function refreshWindowsAutostartEntries() {
|
|
6828
|
+
// Entries that ran the relay on the database inside the package.
|
|
6829
|
+
const unpinnedLaunchers = [];
|
|
6830
|
+
const onEarlierForm = ({ path: launcher, pinnedData }) => { if (!pinnedData) unpinnedLaunchers.push(launcher); };
|
|
6831
|
+
for (const [name, refresh] of [
|
|
6832
|
+
['sign-in entry', () => windowsAutostartService.refreshInstalledEntry({ onEarlierForm })],
|
|
6833
|
+
['boot launcher', () => windowsBootAutostartService.refreshInstalledLauncher({ onEarlierForm })],
|
|
6834
|
+
]) {
|
|
6835
|
+
try {
|
|
6836
|
+
if (refresh()) console.log(`[autostart] brought the ${name} up to date: it pins the data and log folders and waits for TEMP and the data drive`);
|
|
6837
|
+
} catch (error) {
|
|
6838
|
+
console.warn(`[autostart] could not bring the ${name} up to date: ${error?.message || error}`);
|
|
6839
|
+
}
|
|
6840
|
+
}
|
|
6841
|
+
const warning = lostPackageDatabaseWarning({
|
|
6842
|
+
unpinnedLaunchers,
|
|
6843
|
+
earlierDatabase,
|
|
6844
|
+
databaseCreated: databaseCreatedAtStart,
|
|
6845
|
+
evidence: unpinnedLaunchers.length > 0 && packageDatabaseEvidence({ serverDir: __dirname, ownDirs: [DATA_DIR, path.dirname(relayConsoleLogPath)] }),
|
|
6846
|
+
serverDir: __dirname,
|
|
6847
|
+
dbPath: DB_PATH,
|
|
6848
|
+
});
|
|
6849
|
+
if (warning) console.error(warning);
|
|
6850
|
+
}
|
|
6808
6851
|
|
|
6809
6852
|
const gitChangesService = createGitChangesService();
|
|
6810
6853
|
// GET /api/git/remote: where a folder lives on GitHub (New Chat, Claude Cloud).
|
|
@@ -7069,6 +7112,7 @@ const sharedRouteDeps = {
|
|
|
7069
7112
|
fs,
|
|
7070
7113
|
path,
|
|
7071
7114
|
uploadsDir: UPLOAD_DIR,
|
|
7115
|
+
uploadReadDirs: UPLOAD_READ_DIRS,
|
|
7072
7116
|
fetchUsageSummary,
|
|
7073
7117
|
fetchCopilotBillingUsage,
|
|
7074
7118
|
fetchGrokBillingUsage: createGrokBillingUsageFetcher(),
|
|
@@ -7776,6 +7820,14 @@ registerPreviewRoutes(app, {
|
|
|
7776
7820
|
new Date().toISOString(),
|
|
7777
7821
|
);
|
|
7778
7822
|
},
|
|
7823
|
+
// A scope named by a conversation's id or its session id covers both.
|
|
7824
|
+
resolveConversationScope: createPreviewScopeResolver({
|
|
7825
|
+
getConversation: (id) => {
|
|
7826
|
+
const conv = stmts.getConvAnyStatus.get(id) || null;
|
|
7827
|
+
return conv && String(conv.status || '').trim().toLowerCase() !== 'deleted' ? conv : null;
|
|
7828
|
+
},
|
|
7829
|
+
getConversationBySdkSessionId: (id) => stmts.getConvBySdkSessionId?.get?.(id) || null,
|
|
7830
|
+
}),
|
|
7779
7831
|
});
|
|
7780
7832
|
|
|
7781
7833
|
function clearRuntimeTimers() {
|
|
@@ -7835,6 +7887,9 @@ function shutdownRuntime(reason = 'unknown', { exitCode = 0 } = {}) {
|
|
|
7835
7887
|
console.warn(`${runtimeLogPrefix()}Grok auth service shutdown failed: ${error?.message || error}`);
|
|
7836
7888
|
}
|
|
7837
7889
|
sessionWorkerWebSocketService.stop();
|
|
7890
|
+
// A queued request wrote its own end already; this covers the rest
|
|
7891
|
+
// (signals, the owner gone).
|
|
7892
|
+
try { relayEventService.noteRelayExit({ action: runtimeShutdownExitCode === RELAY_RESTART_EXIT_CODE ? 'restart' : 'shutdown', reason }); } catch {}
|
|
7838
7893
|
try { relayEventService.stop(); } catch {}
|
|
7839
7894
|
try { relayEventLinkService.stop(); } catch {}
|
|
7840
7895
|
tmuxInspectorSocketService.stop();
|
|
@@ -7965,12 +8020,19 @@ httpServer.on('error', (error) => {
|
|
|
7965
8020
|
process.exit(1);
|
|
7966
8021
|
});
|
|
7967
8022
|
httpServer.listen(config.port, listenHost, () => {
|
|
8023
|
+
// Conversations that asked for relay_restarted hear that the relay is back:
|
|
8024
|
+
// once it listens, so a start that fails to bind does not take the record
|
|
8025
|
+
// of how the run before ended.
|
|
8026
|
+
try { relayEventService.noteRelayStarted(); } catch (error) {
|
|
8027
|
+
console.warn(`${runtimeLogPrefix()}relay_restarted not recorded: ${error?.message || error}`);
|
|
8028
|
+
}
|
|
7968
8029
|
const readyBanner = buildRelayReadyBannerData();
|
|
7969
8030
|
console.log('');
|
|
7970
8031
|
for (const line of (readyBanner.lines || [])) {
|
|
7971
8032
|
console.log(line);
|
|
7972
8033
|
}
|
|
7973
8034
|
console.log('\nCLI status: waiting for first heartbeat...\n');
|
|
8035
|
+
refreshWindowsAutostartEntries();
|
|
7974
8036
|
|
|
7975
8037
|
// Start SSH tunnel after server is listening
|
|
7976
8038
|
sshTunnelManager.start();
|