@oar-sh/oar 0.9.13 → 0.9.14
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 +302 -0
- package/README.md +12 -12
- package/bin/oar.js +155 -94
- package/package.json +5 -5
- package/server/README.md +296 -149
- 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/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 +4 -2
- 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 +6 -1
- 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 +329 -31
- package/server/services/windows-boot-autostart-service.mjs +188 -60
- 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/server/uploads/image_input.md +0 -540
package/bin/oar.js
CHANGED
|
@@ -11,8 +11,17 @@ import net from 'net';
|
|
|
11
11
|
|
|
12
12
|
import {
|
|
13
13
|
migrateStateToOarRoot,
|
|
14
|
-
resolveOarRoot,
|
|
15
14
|
} from '../server/services/oar-state-migration-service.mjs';
|
|
15
|
+
import { resolveDataDir, resolveUploadDir } from '../server/services/data-dir-resolution.mjs';
|
|
16
|
+
import { TAKEOVER_PROGRESS_FILE } from '../server/services/earlier-database-service.mjs';
|
|
17
|
+
import { defaultIsProcessAlive, lockHolderMayRun, processIdentity } from '../server/services/relay-singleton-guard.mjs';
|
|
18
|
+
import {
|
|
19
|
+
getLauncherLogDir,
|
|
20
|
+
getLegacyManagedConfigDir,
|
|
21
|
+
isGitCheckout,
|
|
22
|
+
resolveRelayStateDirs,
|
|
23
|
+
resolveStateLayout,
|
|
24
|
+
} from '../server/services/oar-state-layout.mjs';
|
|
16
25
|
import {
|
|
17
26
|
buildDefaultConfig, buildServicePath, buildSystemdUnit, describeRelayAddress, DOCTOR_PROBES, generateAuthToken,
|
|
18
27
|
NPM_ALLOW_SCRIPTS_ARG, parseCliArgs, primaryLanAddress, renderDoctorReport, resolveNpmInvocation, usageText,
|
|
@@ -56,20 +65,42 @@ function readRelayLock(lockPath) {
|
|
|
56
65
|
pid: Number.parseInt(String(parsed?.pid ?? ''), 10),
|
|
57
66
|
startedAt: typeof parsed?.startedAt === 'string' ? parsed.startedAt : null,
|
|
58
67
|
token: typeof parsed?.token === 'string' ? parsed.token : '',
|
|
68
|
+
format: Number(parsed?.format) || 0,
|
|
59
69
|
};
|
|
60
70
|
} catch {
|
|
61
71
|
return null;
|
|
62
72
|
}
|
|
63
73
|
}
|
|
64
74
|
|
|
75
|
+
// What a relay that holds its lock and does not answer yet is bringing over
|
|
76
|
+
// (earlier-database-service), or null: the progress file names the lock's
|
|
77
|
+
// pid, and that pid is the lock's holder still (lockHolderMayRun: a pid
|
|
78
|
+
// reused after a crash is not).
|
|
79
|
+
function readTakeover(lockPath, isProcessAliveImpl = isProcessAlive) {
|
|
80
|
+
const progress = readJsonFile(path.join(path.dirname(lockPath), TAKEOVER_PROGRESS_FILE));
|
|
81
|
+
const lock = readJsonFile(lockPath);
|
|
82
|
+
const pid = Number.parseInt(String(progress?.pid ?? ''), 10);
|
|
83
|
+
return Number.isInteger(pid) && Number(lock?.pid) === pid && lockHolderMayRun({ lock, isProcessAlive: isProcessAliveImpl }) ? progress : null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// The lock a relay that did not come up found held, and whether its holder
|
|
87
|
+
// is known to be a relay (its recorded identity matches): only when it is
|
|
88
|
+
// not may the user delete the lock. Null when no live process holds it.
|
|
89
|
+
function heldLock(lockPath, isProcessAliveImpl = isProcessAlive) {
|
|
90
|
+
const lock = readJsonFile(lockPath);
|
|
91
|
+
const pid = Number.parseInt(String(lock?.pid ?? ''), 10);
|
|
92
|
+
if (!Number.isInteger(pid) || !lockHolderMayRun({ lock, isProcessAlive: isProcessAliveImpl })) return null;
|
|
93
|
+
return { pid, path: lockPath, startedAt: lock.startedAt || 'unknown', known: !!lock.holder && !!processIdentity(pid) };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function takeoverLine(takeover, logDir) {
|
|
97
|
+
const what = takeover ? ` (${takeover.from}, ${Math.max(1, Math.round(Number(takeover.bytes || 0) / 1048576))} MB)` : '';
|
|
98
|
+
return `[oar] The relay${takeover ? ` (process ${takeover.pid})` : ''} is bringing over the database it ran on until now${what} and is still running; it answers once that is done. Its log: ${path.join(String(logDir || ''), 'server.log')}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// As the server tells it: a process this user may not signal (EPERM) lives.
|
|
65
102
|
function isProcessAlive(pid) {
|
|
66
|
-
|
|
67
|
-
try {
|
|
68
|
-
process.kill(pid, 0);
|
|
69
|
-
return true;
|
|
70
|
-
} catch {
|
|
71
|
-
return false;
|
|
72
|
-
}
|
|
103
|
+
return defaultIsProcessAlive(pid);
|
|
73
104
|
}
|
|
74
105
|
|
|
75
106
|
function readJsonFile(filePath) {
|
|
@@ -91,45 +122,66 @@ function openAppendFileDescriptor(filePath) {
|
|
|
91
122
|
return fs.openSync(filePath, 'a');
|
|
92
123
|
}
|
|
93
124
|
|
|
94
|
-
// Pre-OAR managed config location — kept only as a migration source and as the
|
|
95
|
-
// config home for git checkouts, where nothing may change underfoot.
|
|
96
|
-
function getLegacyManagedConfigDir(env = process.env) {
|
|
97
|
-
if (process.platform === 'win32') {
|
|
98
|
-
return path.join(
|
|
99
|
-
env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'),
|
|
100
|
-
'copilot-remote',
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
return path.join(
|
|
104
|
-
env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'),
|
|
105
|
-
'copilot-remote',
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function isGitCheckout(packageRoot) {
|
|
110
|
-
try { return fs.existsSync(path.join(packageRoot, '.git')); } catch { return false; }
|
|
111
|
-
}
|
|
112
|
-
|
|
113
125
|
/**
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
126
|
+
* Brings pre-OAR state into a global install's state root. Without
|
|
127
|
+
* --migrate-from it runs quietly, once; with it, it runs unless that source
|
|
128
|
+
* was migrated already, never overwrites, and says what it did or why it did
|
|
129
|
+
* nothing. Database and uploads go where the relay `oar` starts will read
|
|
130
|
+
* them, resolved as the relay resolves them. Returns null to go on, or the
|
|
131
|
+
* result to stop with. A refusal stops only `oar setup` (`strict`): `oar
|
|
132
|
+
* copilot` warns and starts the session on the state root as it is.
|
|
119
133
|
*/
|
|
120
|
-
function
|
|
121
|
-
|
|
122
|
-
|
|
134
|
+
async function migrateIntoStateRoot({
|
|
135
|
+
layout, env, packageRoot, migrateFrom = '', logger, isProcessAliveImpl = isProcessAlive, strict = true,
|
|
136
|
+
}) {
|
|
137
|
+
const serverDir = path.join(packageRoot, 'server');
|
|
138
|
+
const relayEnv = { COPILOT_WEB_RELAY_DATA_DIR: resolveRelayStateDirs({ env, layout }).dataDir };
|
|
139
|
+
const dataDir = resolveDataDir({ env: relayEnv, serverDir });
|
|
140
|
+
const source = migrateFrom ? path.resolve(migrateFrom) : '';
|
|
141
|
+
let migration;
|
|
142
|
+
try {
|
|
143
|
+
migration = await migrateStateToOarRoot({
|
|
144
|
+
targetRoot: layout.root,
|
|
145
|
+
targetDataDir: dataDir,
|
|
146
|
+
targetUploadDir: resolveUploadDir({ dataDir, env: relayEnv, serverDir }),
|
|
147
|
+
repoServerDir: source ? path.join(source, 'server') : null,
|
|
148
|
+
managedConfigDir: getLegacyManagedConfigDir(env),
|
|
149
|
+
isProcessAliveImpl,
|
|
150
|
+
});
|
|
151
|
+
} catch (error) {
|
|
152
|
+
logger.error?.(`[oar] State migration failed, nothing was changed at the source: ${error?.message || error}`);
|
|
153
|
+
return { code: 1, reason: 'migration-failed', error };
|
|
154
|
+
}
|
|
155
|
+
const refused = (message, reason) => {
|
|
156
|
+
logger.error?.(`[oar] ${message}`);
|
|
157
|
+
if (strict) return { code: 1, reason };
|
|
158
|
+
logger.error?.('[oar] Nothing was migrated; starting on the state root as it is.');
|
|
159
|
+
return null;
|
|
160
|
+
};
|
|
161
|
+
switch (migration.status) {
|
|
162
|
+
case 'blocked-live-relay': return refused(migration.error, 'migration-blocked');
|
|
163
|
+
case 'refused-relay-running': return refused(migration.error, 'migration-relay-running');
|
|
164
|
+
case 'refused-existing-database': return refused(migration.error, 'migration-refused');
|
|
165
|
+
case 'nothing-to-migrate':
|
|
166
|
+
return source ? refused(`Nothing to migrate from ${source}: ${migration.reason}.`, 'migration-nothing') : null;
|
|
167
|
+
case 'already-migrated':
|
|
168
|
+
if (source) logger.log?.(`[oar] ${layout.root} is already migrated from ${source}; nothing to do.`);
|
|
169
|
+
return null;
|
|
170
|
+
case 'migrated':
|
|
171
|
+
if (migration.movedAsideTo) {
|
|
172
|
+
logger.log?.(`[oar] The state root's database had no conversations yet (a fresh one); moved it aside to ${migration.movedAsideTo}.`);
|
|
173
|
+
}
|
|
174
|
+
logger.log?.(`[oar] Migrated ${source || 'earlier relay state'} into ${layout.root} (${migration.copied} items, sources untouched; receipt in backups/MIGRATION.md).`);
|
|
175
|
+
for (const kept of migration.kept || []) {
|
|
176
|
+
logger.log?.(`[oar] Kept the existing ${kept}; the one from the source was not copied.`);
|
|
177
|
+
}
|
|
178
|
+
for (const link of migration.skipped || []) {
|
|
179
|
+
logger.log?.(`[oar] Skipped the link ${link}: what it points to was not copied.`);
|
|
180
|
+
}
|
|
181
|
+
return null;
|
|
182
|
+
default:
|
|
183
|
+
return null;
|
|
123
184
|
}
|
|
124
|
-
const root = resolveOarRoot(env);
|
|
125
|
-
return { checkout: false, root, configDir: root, dataDir: path.join(root, 'data') };
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function getLauncherLogDir(env = process.env, layout = null) {
|
|
129
|
-
const envLogDir = String(env.COPILOT_WEB_RELAY_LOG_DIR || '').trim();
|
|
130
|
-
if (envLogDir) return envLogDir;
|
|
131
|
-
if (layout && !layout.checkout) return path.join(layout.root, 'logs');
|
|
132
|
-
return path.join(getLegacyManagedConfigDir(env), 'logs');
|
|
133
185
|
}
|
|
134
186
|
|
|
135
187
|
function getCopilotHomeDir(env = process.env) {
|
|
@@ -278,6 +330,7 @@ export async function launchRelay({
|
|
|
278
330
|
fetchImpl = globalThis.fetch,
|
|
279
331
|
isProcessAliveImpl = isProcessAlive,
|
|
280
332
|
lockGraceMs = 5000,
|
|
333
|
+
readyWaitMs = 20_000,
|
|
281
334
|
env = process.env,
|
|
282
335
|
logger = console,
|
|
283
336
|
} = {}) {
|
|
@@ -328,22 +381,11 @@ export async function launchRelay({
|
|
|
328
381
|
|
|
329
382
|
if (!layout.checkout) {
|
|
330
383
|
// Global install: state lives in the OAR root. Migrate pre-OAR state in
|
|
331
|
-
//
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
managedConfigDir: getLegacyManagedConfigDir(env),
|
|
337
|
-
logger,
|
|
338
|
-
});
|
|
339
|
-
if (migration.status === 'blocked-live-relay') {
|
|
340
|
-
logger.error?.(`[oar] ${migration.error}`);
|
|
341
|
-
return { code: 1, reason: 'migration-blocked' };
|
|
342
|
-
}
|
|
343
|
-
} catch (error) {
|
|
344
|
-
logger.error?.(`[oar] State migration failed, nothing was changed at the source: ${error?.message || error}`);
|
|
345
|
-
return { code: 1, reason: 'migration-failed', error };
|
|
346
|
-
}
|
|
384
|
+
// (once, or as --migrate-from asks); a live source relay blocks it.
|
|
385
|
+
const stop = await migrateIntoStateRoot({
|
|
386
|
+
layout, env, packageRoot, migrateFrom: migrateFromRoot, logger, isProcessAliveImpl, strict: false,
|
|
387
|
+
});
|
|
388
|
+
if (stop) return stop;
|
|
347
389
|
}
|
|
348
390
|
|
|
349
391
|
const lockPath = layout.checkout
|
|
@@ -372,14 +414,21 @@ export async function launchRelay({
|
|
|
372
414
|
? await probeRelayPort(probe)
|
|
373
415
|
: await probeRelayBehindLock({ ...probe, lockPath, port, isProcessAliveImpl, lockGraceMs, logger });
|
|
374
416
|
if (answer === 'taken') {
|
|
375
|
-
logger.error?.(`[oar] Port ${port} is in use by another program
|
|
417
|
+
logger.error?.(`[oar] Port ${port} is in use by another program, or by a relay with another token.`);
|
|
376
418
|
logger.error?.('[oar] Stop that program, or pick another port: oar copilot --port <port>');
|
|
377
419
|
return { code: 1, reason: 'port-taken' };
|
|
378
420
|
}
|
|
421
|
+
if (answer === 'bringing-over') {
|
|
422
|
+
logger.log(takeoverLine(readTakeover(lockPath, isProcessAliveImpl), getLauncherLogDir(env, layout)));
|
|
423
|
+
return { code: 1, reason: 'bringing-over' };
|
|
424
|
+
}
|
|
379
425
|
if (runtimeConfig.managed || !fs.existsSync(runtimeConfig.configPath)) {
|
|
380
426
|
writeJsonFile(runtimeConfig.configPath, runtimeConfig.config);
|
|
381
427
|
}
|
|
382
428
|
|
|
429
|
+
// Checkouts keep the repo-local server/data default; global installs point
|
|
430
|
+
// the server at the OAR root so updates cannot wipe the database.
|
|
431
|
+
const stateDirs = resolveRelayStateDirs({ env, layout });
|
|
383
432
|
const baseEnv = {
|
|
384
433
|
...env,
|
|
385
434
|
COPILOT_WORKSPACE_ROOT: cwd,
|
|
@@ -387,11 +436,7 @@ export async function launchRelay({
|
|
|
387
436
|
COPILOT_WEB_RELAY_SERVER_DIR: serverDir,
|
|
388
437
|
COPILOT_WEB_RELAY_CONFIG: runtimeConfig.configPath,
|
|
389
438
|
COPILOT_WEB_RELAY_LOG_DIR: getLauncherLogDir(env, layout),
|
|
390
|
-
|
|
391
|
-
// the server at the OAR root so updates cannot wipe the database.
|
|
392
|
-
...(layout.checkout ? {} : {
|
|
393
|
-
COPILOT_WEB_RELAY_DATA_DIR: String(env.COPILOT_WEB_RELAY_DATA_DIR || '').trim() || layout.dataDir,
|
|
394
|
-
}),
|
|
439
|
+
...(stateDirs.dataDir ? { COPILOT_WEB_RELAY_DATA_DIR: stateDirs.dataDir } : {}),
|
|
395
440
|
GITHUB_COPILOT_PROMPT_MODE_EXTENSIONS: String(env.GITHUB_COPILOT_PROMPT_MODE_EXTENSIONS || 'true'),
|
|
396
441
|
};
|
|
397
442
|
|
|
@@ -429,7 +474,7 @@ export async function launchRelay({
|
|
|
429
474
|
return { code: 1, reason: 'spawn-invalid-child' };
|
|
430
475
|
}
|
|
431
476
|
|
|
432
|
-
const readyDeadlineMs =
|
|
477
|
+
const readyDeadlineMs = readyWaitMs;
|
|
433
478
|
const ready = await new Promise((resolve) => {
|
|
434
479
|
const deadline = Date.now() + readyDeadlineMs;
|
|
435
480
|
const probe = async () => {
|
|
@@ -457,6 +502,14 @@ export async function launchRelay({
|
|
|
457
502
|
probe();
|
|
458
503
|
});
|
|
459
504
|
|
|
505
|
+
const takeover = ready ? null : readTakeover(lockPath, isProcessAliveImpl);
|
|
506
|
+
if (takeover) {
|
|
507
|
+
// Left running: a kill would cut the copy short for nothing.
|
|
508
|
+
logger.log(takeoverLine(takeover, logDir));
|
|
509
|
+
try { fs.closeSync(serverOutFd); } catch {}
|
|
510
|
+
try { fs.closeSync(serverErrFd); } catch {}
|
|
511
|
+
return { code: 1, reason: 'bringing-over' };
|
|
512
|
+
}
|
|
460
513
|
if (!ready) {
|
|
461
514
|
logger.error?.(`[oar] Relay did not become ready at ${statusUrl}`);
|
|
462
515
|
try {
|
|
@@ -667,7 +720,11 @@ async function probeRelayBehindLock({
|
|
|
667
720
|
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
668
721
|
answer = await probeRelayPort({ statusUrl, token, fetchImpl });
|
|
669
722
|
}
|
|
670
|
-
if (answer === 'none' &&
|
|
723
|
+
if (answer === 'none' && readTakeover(lockPath, isProcessAliveImpl)) return 'bringing-over';
|
|
724
|
+
// A lock this version wrote is left to the server, which tells a reused
|
|
725
|
+
// process id itself (lockHolderMayRun): removed here, a second relay
|
|
726
|
+
// started beside one still bringing over its database.
|
|
727
|
+
if (answer === 'none' && lockedByLiveProcess() && !readRelayLock(lockPath)?.format) {
|
|
671
728
|
// Nothing listens: the lock is left over from a relay that did not
|
|
672
729
|
// stop cleanly. The server refuses to start beside a lock whose process
|
|
673
730
|
// id is alive, so it goes first.
|
|
@@ -697,6 +754,7 @@ export async function ensureRelayRunning({
|
|
|
697
754
|
fetchImpl = globalThis.fetch,
|
|
698
755
|
isProcessAliveImpl = isProcessAlive,
|
|
699
756
|
lockGraceMs = 5000,
|
|
757
|
+
readyWaitMs = 20_000,
|
|
700
758
|
homeDir = os.homedir(),
|
|
701
759
|
platform = process.platform,
|
|
702
760
|
logger = console,
|
|
@@ -710,8 +768,9 @@ export async function ensureRelayRunning({
|
|
|
710
768
|
statusUrl, token, lockPath, port, fetchImpl, isProcessAliveImpl, lockGraceMs, logger,
|
|
711
769
|
});
|
|
712
770
|
if (answer === 'oar') return { ok: true, how: 'already', logDir };
|
|
771
|
+
if (answer === 'bringing-over') return { ok: false, how: 'bringing-over', logDir, takeover: readTakeover(lockPath, isProcessAliveImpl) };
|
|
713
772
|
if (answer === 'taken') {
|
|
714
|
-
logger.error?.(`[oar] Port ${port} is in use by another program
|
|
773
|
+
logger.error?.(`[oar] Port ${port} is in use by another program, or by a relay with another token.`);
|
|
715
774
|
logger.error?.('[oar] Stop that program, or pick another port: oar setup --port <port>');
|
|
716
775
|
return { ok: false, how: 'port-taken', logDir };
|
|
717
776
|
}
|
|
@@ -739,7 +798,7 @@ export async function ensureRelayRunning({
|
|
|
739
798
|
COPILOT_WEB_RELAY_SERVER_DIR: serverDir,
|
|
740
799
|
COPILOT_WEB_RELAY_CONFIG: configPath,
|
|
741
800
|
COPILOT_WEB_RELAY_LOG_DIR: logDir,
|
|
742
|
-
COPILOT_WEB_RELAY_DATA_DIR:
|
|
801
|
+
COPILOT_WEB_RELAY_DATA_DIR: resolveRelayStateDirs({ env, layout }).dataDir,
|
|
743
802
|
},
|
|
744
803
|
detached: true,
|
|
745
804
|
stdio: ['ignore', outFd, errFd],
|
|
@@ -753,8 +812,10 @@ export async function ensureRelayRunning({
|
|
|
753
812
|
try { fs.closeSync(outFd); } catch {}
|
|
754
813
|
try { fs.closeSync(errFd); } catch {}
|
|
755
814
|
}
|
|
756
|
-
const ok = await waitForRelay({ statusUrl, token, fetchImpl, stillStarting: () => child.exitCode === null });
|
|
757
|
-
|
|
815
|
+
const ok = await waitForRelay({ statusUrl, token, fetchImpl, deadlineMs: readyWaitMs, stillStarting: () => child.exitCode === null });
|
|
816
|
+
const takeover = ok ? null : readTakeover(lockPath, isProcessAliveImpl);
|
|
817
|
+
if (takeover) return { ok: false, how: 'bringing-over', logDir, takeover };
|
|
818
|
+
return { ok, how: 'detached', logDir, ...(ok ? {} : { lock: heldLock(lockPath, isProcessAliveImpl) }) };
|
|
758
819
|
}
|
|
759
820
|
|
|
760
821
|
/**
|
|
@@ -916,7 +977,7 @@ async function restartRelay(ctx, paths, { old, viaService }) {
|
|
|
916
977
|
logger.error('[oar] The relay came back on its earlier port and token: it was started with fixed ones (oar copilot). End that session, then run: oar start');
|
|
917
978
|
return { ok: false, reason: 'fixed-settings' };
|
|
918
979
|
}
|
|
919
|
-
logger.error(`[oar] The relay did not come back — see ${path.join(paths.logDir, 'server-err.log')}`);
|
|
980
|
+
logger.error(`[oar] The relay did not come back — see ${viaService ? 'journalctl --user -u oar' : path.join(paths.logDir, 'server-err.log')}`);
|
|
920
981
|
return { ok: false, reason: 'server-not-ready' };
|
|
921
982
|
}
|
|
922
983
|
|
|
@@ -987,6 +1048,7 @@ function startRelay(ctx, paths) {
|
|
|
987
1048
|
fetchImpl: ctx.fetchImpl,
|
|
988
1049
|
isProcessAliveImpl: ctx.isProcessAliveImpl,
|
|
989
1050
|
lockGraceMs: ctx.lockGraceMs,
|
|
1051
|
+
readyWaitMs: ctx.readyWaitMs,
|
|
990
1052
|
homeDir: ctx.homeDir,
|
|
991
1053
|
platform: ctx.platform,
|
|
992
1054
|
logger: ctx.logger,
|
|
@@ -1028,12 +1090,18 @@ function warnWindowsPort(ctx, port, windowsHolder = createWindowsPortCheck(ctx))
|
|
|
1028
1090
|
function reportRelayStart(result, logger) {
|
|
1029
1091
|
// A port held by something else was already explained.
|
|
1030
1092
|
if (result.how === 'port-taken') return;
|
|
1093
|
+
if (result.how === 'bringing-over') {
|
|
1094
|
+
logger.log(takeoverLine(result.takeover, result.logDir));
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1031
1097
|
if (!result.ok) {
|
|
1032
|
-
logger.error?.(`[oar] The relay did not come up — see ${path.join(result.logDir, 'server-err.log')}`);
|
|
1098
|
+
logger.error?.(`[oar] The relay did not come up — see ${result.how === 'service' ? 'journalctl --user -u oar' : path.join(result.logDir, 'server-err.log')}`);
|
|
1099
|
+
if (result.lock?.known) logger.error?.(`[oar] The lock ${result.lock.path} is held by relay process ${result.lock.pid} (started ${result.lock.startedAt}): oar stop, or end process ${result.lock.pid}.`);
|
|
1100
|
+
else if (result.lock) logger.error?.(`[oar] The lock ${result.lock.path} names process ${result.lock.pid}, which cannot be told to be a relay: if no relay is running, end process ${result.lock.pid} or delete the lock.`);
|
|
1033
1101
|
return;
|
|
1034
1102
|
}
|
|
1035
1103
|
if (result.how === 'already') {
|
|
1036
|
-
logger.log('[oar] The relay is already running.
|
|
1104
|
+
logger.log('[oar] The relay is already running. It keeps the version and settings it started with until you restart it: oar restart');
|
|
1037
1105
|
} else if (result.how === 'service') {
|
|
1038
1106
|
logger.log('[oar] The relay is running as a systemd user service (systemctl --user status oar).');
|
|
1039
1107
|
} else {
|
|
@@ -1048,7 +1116,7 @@ function requireConfig(ctx, paths) {
|
|
|
1048
1116
|
}
|
|
1049
1117
|
|
|
1050
1118
|
function reportPortTaken(ctx, paths, port) {
|
|
1051
|
-
ctx.logger.error(`[oar] Port ${port} is
|
|
1119
|
+
ctx.logger.error(`[oar] Port ${port} is in use by another program, or by a relay with another token.`);
|
|
1052
1120
|
// A relay that was started before its token was changed in the config
|
|
1053
1121
|
// cannot be asked to restart: only its lock tells that it is there.
|
|
1054
1122
|
const pid = readRelayLock(paths.lockPath)?.pid;
|
|
@@ -1077,6 +1145,7 @@ export async function runStart(options = {}) {
|
|
|
1077
1145
|
reportRelayStart(result, logger);
|
|
1078
1146
|
if (result.ok) await showAddress(ctx, paths);
|
|
1079
1147
|
warnWindowsPort(ctx, endpointOf(paths.config).port);
|
|
1148
|
+
if (result.how === 'bringing-over') return { code: 0, reason: 'bringing-over' };
|
|
1080
1149
|
return { code: result.ok ? 0 : 1, reason: result.ok ? 'started' : 'server-not-ready' };
|
|
1081
1150
|
}
|
|
1082
1151
|
|
|
@@ -1092,7 +1161,7 @@ export async function runStop(options = {}) {
|
|
|
1092
1161
|
logger.error('[oar] systemctl --user stop oar failed — see: systemctl --user status oar');
|
|
1093
1162
|
return { code: 1, reason: 'service' };
|
|
1094
1163
|
}
|
|
1095
|
-
logger.log('[oar] The relay service is stopped. It starts again at the next login; to take it out: oar service remove');
|
|
1164
|
+
logger.log('[oar] The relay service is stopped. It starts again at the next boot (without lingering: at the next login); to take it out: oar service remove');
|
|
1096
1165
|
return { code: 0, reason: 'stopped' };
|
|
1097
1166
|
}
|
|
1098
1167
|
const relay = await findRelay(ctx, paths);
|
|
@@ -1172,7 +1241,7 @@ export async function runStatus(options = {}) {
|
|
|
1172
1241
|
const access = status.access === 'lan' ? 'LAN access' : 'this machine only';
|
|
1173
1242
|
if (status.relay === 'running') logger.log(`[oar] The relay is running on port ${status.port} (${access}): ${status.url}`);
|
|
1174
1243
|
else if (status.relay === 'stopped') logger.log(`[oar] The relay is stopped (port ${status.port}, ${access}). Start it with: oar start`);
|
|
1175
|
-
else logger.log(`[oar] Port ${status.port} is
|
|
1244
|
+
else logger.log(`[oar] Port ${status.port} is in use by another program, or by a relay with another token.`);
|
|
1176
1245
|
if (status.service !== 'unavailable') {
|
|
1177
1246
|
logger.log(`[oar] Service: ${{ active: 'installed, running', installed: 'installed', none: 'not installed' }[status.service]}`);
|
|
1178
1247
|
}
|
|
@@ -1205,7 +1274,7 @@ async function runWindowsService(ctx, paths, action) {
|
|
|
1205
1274
|
return { code: 0, reason: 'service-status' };
|
|
1206
1275
|
}
|
|
1207
1276
|
if (boot) {
|
|
1208
|
-
logger.error('[oar] The relay starts at boot through a scheduled task. Change that in the web UI: Settings → Autostart.');
|
|
1277
|
+
logger.error('[oar] The relay starts at boot through a scheduled task. Change that in the web UI: Settings → General → Autostart (Windows).');
|
|
1209
1278
|
return { code: 1, reason: 'boot-task' };
|
|
1210
1279
|
}
|
|
1211
1280
|
if (action === 'install' && requireConfig(ctx, paths)) return { code: 1, reason: 'no-config' };
|
|
@@ -1261,7 +1330,7 @@ export async function runService(options = {}) {
|
|
|
1261
1330
|
// A relay started by hand holds the port and the lock: it makes way first.
|
|
1262
1331
|
if ((await runStop({ ...options, argv: [] })).reason !== 'stopped') {
|
|
1263
1332
|
runQuiet('systemctl', ['--user', 'enable', 'oar'], ctx);
|
|
1264
|
-
logger.log('[oar] The service is installed and takes over at the next login, or once the relay has stopped: oar start');
|
|
1333
|
+
logger.log('[oar] The service is installed and takes over at the next boot (without lingering: at the next login), or once the relay has stopped: oar start');
|
|
1265
1334
|
return { code: 0, reason: 'service-install' };
|
|
1266
1335
|
}
|
|
1267
1336
|
}
|
|
@@ -1310,21 +1379,13 @@ export async function runSetup(options = {}) {
|
|
|
1310
1379
|
let paths = installPaths(ctx);
|
|
1311
1380
|
const { layout } = paths;
|
|
1312
1381
|
if (!layout.checkout) {
|
|
1313
|
-
const
|
|
1314
|
-
const
|
|
1315
|
-
|
|
1316
|
-
repoServerDir: migrateFromRoot ? path.join(path.resolve(migrateFromRoot), 'server') : null,
|
|
1317
|
-
managedConfigDir: getLegacyManagedConfigDir(env),
|
|
1318
|
-
logger,
|
|
1382
|
+
const migrateFrom = String(optionValue(argv, '--migrate-from') || '').trim();
|
|
1383
|
+
const stop = await migrateIntoStateRoot({
|
|
1384
|
+
layout, env, packageRoot: ctx.packageRoot, migrateFrom, logger, isProcessAliveImpl: ctx.isProcessAliveImpl,
|
|
1319
1385
|
});
|
|
1320
|
-
if (
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
}
|
|
1324
|
-
if (migration.status === 'migrated') {
|
|
1325
|
-
logger.log(`[oar] Migrated existing relay state into ${layout.root} (sources untouched).`);
|
|
1326
|
-
paths = installPaths(ctx);
|
|
1327
|
-
}
|
|
1386
|
+
if (stop) return stop;
|
|
1387
|
+
// A migration may have brought a config over.
|
|
1388
|
+
paths = installPaths(ctx);
|
|
1328
1389
|
}
|
|
1329
1390
|
|
|
1330
1391
|
const existing = paths.config;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oar-sh/oar",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.14",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "OAR — Open Agent Relay: drive your local coding agents from any browser",
|
|
6
6
|
"license": "MIT",
|
|
@@ -27,7 +27,6 @@
|
|
|
27
27
|
"!server/config.json",
|
|
28
28
|
"!server/uploads/",
|
|
29
29
|
"server/uploads/README.md",
|
|
30
|
-
"server/uploads/image_input.md",
|
|
31
30
|
"shared/",
|
|
32
31
|
"!shared/**/*.test.mjs",
|
|
33
32
|
"!shared/**/*-test-harness.mjs",
|
|
@@ -39,6 +38,7 @@
|
|
|
39
38
|
"LICENSE"
|
|
40
39
|
],
|
|
41
40
|
"scripts": {
|
|
41
|
+
"preinstall": "node server/services/package-data-rescue-preinstall.mjs",
|
|
42
42
|
"start": "node server/server.js",
|
|
43
43
|
"copilot:relay": "gh copilot -- --allow-all",
|
|
44
44
|
"test": "node --import ./scripts/node-test-keep-alive.mjs --test \"server/**/*.test.mjs\" \"shared/**/*.test.mjs\" \".github/extensions/web-relay/**/*.test.mjs\" \"scripts/**/*.test.mjs\"",
|
|
@@ -50,9 +50,9 @@
|
|
|
50
50
|
"test:e2e:raw": "node ./node_modules/@playwright/test/cli.js test --config tests/playwright.config.mjs"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@anthropic-ai/claude-agent-sdk": "0.3.
|
|
54
|
-
"@cursor/sdk": "1.0.
|
|
55
|
-
"@github/copilot-sdk": "1.0.
|
|
53
|
+
"@anthropic-ai/claude-agent-sdk": "0.3.304",
|
|
54
|
+
"@cursor/sdk": "1.0.38",
|
|
55
|
+
"@github/copilot-sdk": "1.0.23",
|
|
56
56
|
"better-sqlite3": "12.11.1",
|
|
57
57
|
"express": "4.22.3",
|
|
58
58
|
"qrcode-terminal": "0.12.0",
|