@oar-sh/oar 0.9.1 → 0.9.3

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.
Files changed (146) hide show
  1. package/.github/extensions/web-relay/extension.mjs +0 -3
  2. package/.github/extensions/web-relay/polling/heartbeat.mjs +2 -55
  3. package/.github/extensions/web-relay/polling/polling-loop.mjs +0 -1
  4. package/.github/extensions/web-relay/runtime/api-client.mjs +2 -34
  5. package/.github/extensions/web-relay/runtime/config-loader.mjs +2 -104
  6. package/.github/extensions/web-relay/runtime/send-and-wait-errors.mjs +2 -170
  7. package/.github/extensions/web-relay/runtime/session-io.mjs +1 -71
  8. package/.github/extensions/web-relay/runtime/worker-websocket-link.mjs +2 -325
  9. package/.github/extensions/web-relay/skills/prompt-context.mjs +2 -160
  10. package/CHANGELOG.md +269 -0
  11. package/README.md +407 -424
  12. package/bin/oar.js +142 -11
  13. package/package.json +8 -4
  14. package/server/README.md +59 -22
  15. package/server/claude-worker/claude-sdk-adapter.mjs +7 -2
  16. package/server/claude-worker/claude-session-process.mjs +1013 -45
  17. package/server/claude-worker/claude-session-worker.mjs +54 -9
  18. package/server/claude-worker/claude-turn-publisher.mjs +110 -19
  19. package/server/claude-worker/claude-worker-link-wiring.mjs +9 -0
  20. package/server/copilot-sdk-runtime.mjs +12 -3
  21. package/server/copilot-worker/copilot-ask-user-tool.mjs +89 -0
  22. package/server/copilot-worker/copilot-attachments.mjs +35 -2
  23. package/server/copilot-worker/copilot-continuation-signals.mjs +55 -0
  24. package/server/copilot-worker/copilot-model-switch.mjs +425 -0
  25. package/server/copilot-worker/copilot-prompt-context.mjs +20 -8
  26. package/server/copilot-worker/copilot-question-bridge.mjs +129 -16
  27. package/server/copilot-worker/copilot-sdk-adapter.mjs +59 -11
  28. package/server/copilot-worker/copilot-sdk-event-normalizer.mjs +136 -12
  29. package/server/copilot-worker/copilot-sdk-session-process.mjs +2753 -527
  30. package/server/copilot-worker/copilot-sdk-session-worker.mjs +94 -25
  31. package/server/cursor-worker/cursor-session-worker.mjs +5 -5
  32. package/server/cursor-worker/cursor-turn-runner.mjs +50 -4
  33. package/server/db-schema.mjs +61 -1
  34. package/server/features.mjs +155 -41
  35. package/server/grok-worker/grok-session-worker.mjs +5 -5
  36. package/server/grok-worker/grok-turn-runner.mjs +45 -4
  37. package/server/migrations/0004-steer-settle-markers.mjs +119 -0
  38. package/server/public/app/annotate-core.mjs +181 -0
  39. package/server/public/app/annotate-editor.js +617 -0
  40. package/server/public/app/api-client.js +86 -5
  41. package/server/public/app/ask-user-view.js +102 -13
  42. package/server/public/app/attachments-view.js +172 -3
  43. package/server/public/app/background-task-timeout-options.mjs +9 -0
  44. package/server/public/app/background-tasks-view.mjs +34 -19
  45. package/server/public/app/bootstrap.js +153 -69
  46. package/server/public/app/composer-control-state.mjs +52 -34
  47. package/server/public/app/context-tier-options.mjs +24 -4
  48. package/server/public/app/conversation-draft-sync.mjs +187 -0
  49. package/server/public/app/conversation-list-filter.mjs +86 -0
  50. package/server/public/app/conversation-preferences.mjs +47 -10
  51. package/server/public/app/conversation-provider-indicator.mjs +8 -0
  52. package/server/public/app/conversation-view.js +1032 -218
  53. package/server/public/app/emoji-view.js +2 -0
  54. package/server/public/app/font-scaling.js +4 -37
  55. package/server/public/app/journal-view.js +190 -24
  56. package/server/public/app/model-selector-options.mjs +185 -1
  57. package/server/public/app/new-conversation-model-choice.mjs +29 -9
  58. package/server/public/app/pwa-install.js +64 -136
  59. package/server/public/app/question-multi-select.mjs +52 -0
  60. package/server/public/app/reasoning-effort-labels.mjs +13 -3
  61. package/server/public/app/relay-prompt-sanitizer.mjs +26 -3
  62. package/server/public/app/router.js +120 -11
  63. package/server/public/app/settings-modal.js +225 -20
  64. package/server/public/app/settings-tabs.js +1 -1
  65. package/server/public/app/socket-handlers.js +47 -4
  66. package/server/public/app/steered-turn-merge.mjs +100 -0
  67. package/server/public/app/store.js +159 -16
  68. package/server/public/app/sync-outbox.mjs +16 -8
  69. package/server/public/app/tool-activity-emoji.mjs +36 -0
  70. package/server/public/app/transcript-viewport-anchor.mjs +210 -0
  71. package/server/public/app/update-ui.js +194 -0
  72. package/server/public/index.html +483 -24
  73. package/server/public/sw.js +7 -0
  74. package/server/relay-tools.md +2 -0
  75. package/server/relay.mjs +23 -21
  76. package/server/repositories/message-repository.mjs +78 -21
  77. package/server/repositories/question-repository.mjs +19 -1
  78. package/server/repositories/session-repository.mjs +7 -31
  79. package/server/routes/ask-user-routes.mjs +10 -0
  80. package/server/routes/messages-routes.mjs +689 -159
  81. package/server/routes/sessions-routes.mjs +574 -80
  82. package/server/server-runtime.mjs +393 -925
  83. package/server/services/claude-auth-service.mjs +7 -1
  84. package/server/services/cli-install-service.mjs +8 -27
  85. package/server/services/cli-process-runner.mjs +30 -0
  86. package/server/services/copilot-model-discovery-service.mjs +202 -0
  87. package/server/services/data-dir-resolution.mjs +47 -0
  88. package/server/services/grok-auth-service.mjs +9 -1
  89. package/server/services/live-turn-picker.mjs +47 -0
  90. package/server/services/model-variant-catalog-service.mjs +1183 -0
  91. package/server/services/oar-cli-helpers.mjs +6 -2
  92. package/server/services/relay-prompt-sanitizer.mjs +30 -3
  93. package/server/services/relay-question-context.mjs +46 -0
  94. package/server/services/sdk-session-import-service.mjs +144 -16
  95. package/server/services/sdk-session-sync-service.mjs +84 -27
  96. package/server/services/session-worker-registry-service.mjs +116 -3
  97. package/server/services/session-worker-supervisor-service.mjs +7 -1
  98. package/server/services/session-worker-websocket-service.mjs +119 -24
  99. package/server/services/status-event-service.mjs +8 -2
  100. package/server/services/steer-resend.mjs +45 -0
  101. package/server/services/update-check-service.mjs +220 -0
  102. package/server/services/update-install-service.mjs +279 -0
  103. package/server/services/windows-autostart-service.mjs +42 -6
  104. package/server/services/windows-boot-autostart-service.mjs +487 -0
  105. package/shared/ask-user-bridge.mjs +37 -5
  106. package/shared/background-task-timeout.mjs +12 -4
  107. package/shared/claude-model-catalog.mjs +49 -0
  108. package/shared/context-window-fallbacks.mjs +1 -0
  109. package/shared/control-poller.mjs +4 -1
  110. package/shared/copilot-model-order.mjs +182 -0
  111. package/shared/media-embed-instructions.mjs +35 -0
  112. package/shared/model-descriptors.mjs +181 -30
  113. package/shared/model-id.mjs +6 -0
  114. package/shared/pwa-app-name.mjs +44 -0
  115. package/shared/question-multi-select.mjs +16 -0
  116. package/shared/relay-stop-failure.mjs +15 -0
  117. package/shared/slash-command-guard.mjs +76 -0
  118. package/shared/steer-note.mjs +66 -0
  119. package/shared/steer-settle-failure.mjs +89 -0
  120. package/shared/steer-settle-markers.mjs +18 -0
  121. package/shared/update-semver.mjs +50 -0
  122. package/shared/worker-crash-guard.mjs +40 -14
  123. package/shared/worker-runtime/api-client.mjs +34 -0
  124. package/shared/worker-runtime/config-loader.mjs +104 -0
  125. package/shared/worker-runtime/heartbeat.mjs +109 -0
  126. package/shared/worker-runtime/prompt-context.mjs +160 -0
  127. package/shared/worker-runtime/runner-link-wiring.mjs +44 -0
  128. package/shared/worker-runtime/send-and-wait-errors.mjs +170 -0
  129. package/shared/worker-runtime/worker-websocket-link.mjs +436 -0
  130. package/server/claude-worker/claude-session-test-harness.mjs +0 -236
  131. package/server/claude-worker/fixtures/workflow-run-record.json +0 -1
  132. package/server/copilot-worker/copilot-sdk-test-harness.mjs +0 -329
  133. package/server/copilot-worker/fixtures/abort-turn.json +0 -588
  134. package/server/copilot-worker/fixtures/ask-user-turn.json +0 -1154
  135. package/server/copilot-worker/fixtures/background-timer-continuation.json +0 -445
  136. package/server/copilot-worker/fixtures/background-timer-turn.json +0 -497
  137. package/server/copilot-worker/fixtures/happy-turn.json +0 -566
  138. package/server/copilot-worker/fixtures/quota-turn.json +0 -647
  139. package/server/copilot-worker/fixtures/reasoning-turn.json +0 -625
  140. package/server/copilot-worker/fixtures/resume-turn.json +0 -700
  141. package/server/copilot-worker/fixtures/subagent-turn.json +0 -1378
  142. package/server/copilot-worker/fixtures/tool-permission-turn.json +0 -1293
  143. package/server/routes/messages-routes-test-harness.mjs +0 -111
  144. package/server/services/fixtures/claude-auth-stub.sh +0 -62
  145. package/server/services/fixtures/cli-install-stub.sh +0 -75
  146. package/server/services/fixtures/grok-stub.sh +0 -86
package/bin/oar.js CHANGED
@@ -17,7 +17,7 @@ function resolvePackageRoot(metaUrl = import.meta.url) {
17
17
  return path.resolve(path.dirname(fileURLToPath(metaUrl)), '..');
18
18
  }
19
19
 
20
- function parsePort(argv = []) {
20
+ function parsePort(argv = [], fallback = 3333) {
21
21
  const args = Array.isArray(argv) ? argv : [];
22
22
  for (let index = 0; index < args.length; index += 1) {
23
23
  const arg = String(args[index] || '');
@@ -30,7 +30,12 @@ function parsePort(argv = []) {
30
30
  if (Number.isInteger(parsed) && parsed > 0 && parsed <= 65535) return parsed;
31
31
  }
32
32
  }
33
- return 3333;
33
+ return fallback;
34
+ }
35
+
36
+ function normalizePort(value) {
37
+ const parsed = Number.parseInt(String(value ?? ''), 10);
38
+ return Number.isInteger(parsed) && parsed > 0 && parsed <= 65535 ? parsed : null;
34
39
  }
35
40
 
36
41
  function readRelayLock(lockPath) {
@@ -184,7 +189,7 @@ function createDefaultConfig({ port = 3333, token = '' } = {}) {
184
189
  };
185
190
  }
186
191
 
187
- function resolveLauncherConfig({ packageRoot, env = process.env, relayToken = '', relayPort = 3333, layout = null } = {}) {
192
+ function resolveConfigPaths({ packageRoot, env = process.env, layout = null } = {}) {
188
193
  const envConfigPath = String(env.COPILOT_WEB_RELAY_CONFIG || '').trim();
189
194
  const repoConfigPath = path.join(packageRoot, 'server', 'config.json');
190
195
  const oarConfigPath = layout && !layout.checkout ? path.join(layout.configDir, 'config.json') : null;
@@ -195,7 +200,11 @@ function resolveLauncherConfig({ packageRoot, env = process.env, relayToken = ''
195
200
  || null
196
201
  );
197
202
  const managedPath = oarConfigPath || path.join(getLegacyManagedConfigDir(env), 'config.json');
198
- const configPath = seedPath || managedPath;
203
+ return { seedPath, configPath: seedPath || managedPath };
204
+ }
205
+
206
+ function resolveLauncherConfig({ packageRoot, env = process.env, relayToken = '', relayPort = 3333, portOverride = null, layout = null } = {}) {
207
+ const { seedPath, configPath } = resolveConfigPaths({ packageRoot, env, layout });
199
208
  const seedConfig = readJsonFile(seedPath) || {};
200
209
  const runtimeConfig = {
201
210
  ...createDefaultConfig({ port: relayPort }),
@@ -203,10 +212,9 @@ function resolveLauncherConfig({ packageRoot, env = process.env, relayToken = ''
203
212
  };
204
213
  if (relayToken) runtimeConfig.authToken = relayToken;
205
214
  if (!String(runtimeConfig.authToken || '').trim()) runtimeConfig.authToken = randomUUID();
206
- const parsedPort = Number.parseInt(String(runtimeConfig.port || relayPort || 3333), 10);
207
- runtimeConfig.port = Number.isInteger(parsedPort) && parsedPort > 0 && parsedPort <= 65535
208
- ? parsedPort
209
- : 3333;
215
+ // An explicit --port wins over the config for this run: the server gets it
216
+ // on argv, and only a config this command creates saves it.
217
+ runtimeConfig.port = normalizePort(portOverride) ?? normalizePort(runtimeConfig.port) ?? normalizePort(relayPort) ?? 3333;
210
218
  return {
211
219
  configPath,
212
220
  managed: !seedPath,
@@ -262,7 +270,7 @@ export async function launchRelay({
262
270
  const forwardedArgs = separatorIdx === -1 ? [] : args.slice(separatorIdx + 1);
263
271
  if (args.includes('--help') || args.includes('-h')) {
264
272
  logger.log([
265
- 'Usage: oar [--port <port>] [--token <token>] [--migrate-from <old-checkout>] [--install-extension] [--no-install-extension] [-- [gh copilot args...]]',
273
+ 'Usage: oar [--port <port>] [--migrate-from <old-checkout>] [--install-extension] [--no-install-extension] [-- [gh copilot args...]]',
266
274
  '',
267
275
  'Starts the web relay server if needed, then launches gh copilot in the current shell.',
268
276
  '--install-extension installs/updates a user-global web-relay wrapper extension and exits.',
@@ -284,7 +292,7 @@ export async function launchRelay({
284
292
  return { code: 0, reason: 'extension-installed' };
285
293
  }
286
294
 
287
- const port = parsePort(launcherArgs);
295
+ const cliPort = parsePort(launcherArgs, null);
288
296
  const serverDir = path.join(packageRoot, 'server');
289
297
  const layout = resolveStateLayout({ packageRoot, env });
290
298
 
@@ -311,6 +319,12 @@ export async function launchRelay({
311
319
  const lockPath = layout.checkout
312
320
  ? path.join(serverDir, 'data', 'relay-server.lock')
313
321
  : path.join(layout.dataDir, 'relay-server.lock');
322
+ // One port finds, launches and probes the relay: an explicit --port for this
323
+ // run, else the config's, else 3333. The probe used to take --port (or 3333)
324
+ // while the server started on the config's port, so any mismatch waited out
325
+ // the readiness deadline and killed the relay it had just started.
326
+ const { seedPath } = resolveConfigPaths({ packageRoot, env, layout });
327
+ const port = cliPort ?? normalizePort(readJsonFile(seedPath)?.port) ?? 3333;
314
328
  const statusUrl = `http://localhost:${port}/api/status`;
315
329
  const running = await detectRunningRelay({
316
330
  lockPath,
@@ -323,6 +337,7 @@ export async function launchRelay({
323
337
  env,
324
338
  relayToken: running?.lock?.token || '',
325
339
  relayPort: port,
340
+ portOverride: cliPort,
326
341
  layout,
327
342
  });
328
343
  if (runtimeConfig.managed || !fs.existsSync(runtimeConfig.configPath)) {
@@ -363,7 +378,10 @@ export async function launchRelay({
363
378
  const serverErrFd = openAppendFileDescriptor(serverErrPath);
364
379
  try {
365
380
  serverProc = spawnImpl(nodeBin, serverArgs, {
366
- cwd: packageRoot,
381
+ // Global installs run from the OAR state root: on Windows a process
382
+ // pins its cwd, and `npm i -g` (self-update) must be able to replace
383
+ // the package directory. Checkouts keep the repo as cwd.
384
+ cwd: layout.checkout ? packageRoot : layout.root,
367
385
  env: baseEnv,
368
386
  stdio: ['ignore', serverOutFd, serverErrFd],
369
387
  windowsHide: false,
@@ -640,6 +658,118 @@ async function runDoctor({ env = process.env, logger = console } = {}) {
640
658
  return { code: 0, reason: 'doctor' };
641
659
  }
642
660
 
661
+ /**
662
+ * `oar update [--beta] [--to X.Y.Z]` — the terminal twin of the web UI's
663
+ * Update button. Fetching the manifest here is an explicit user action, so it
664
+ * happens regardless of the relay's opt-in auto-check setting.
665
+ */
666
+ async function runUpdate({ argv = [], env = process.env, logger = console, fetchImpl = globalThis.fetch, spawnImpl = spawn } = {}) {
667
+ const { channelForVersion, compareSemverIsh, parseSemverIsh } = await import('../shared/update-semver.mjs');
668
+ const packageRoot = resolvePackageRoot();
669
+ const runningVersion = readPackageVersion(packageRoot);
670
+
671
+ if (isGitCheckout(packageRoot)) {
672
+ logger.error('[oar] This install runs from a git checkout — update with git pull, then restart the relay.');
673
+ return { code: 1, reason: 'git-checkout' };
674
+ }
675
+
676
+ const toIdx = argv.indexOf('--to');
677
+ let target = toIdx !== -1 ? String(argv[toIdx + 1] || '').trim() : '';
678
+ if (target && !parseSemverIsh(target)) {
679
+ logger.error(`[oar] --to expects a release version like 0.9.2, got "${target}".`);
680
+ return { code: 1, reason: 'bad-target' };
681
+ }
682
+ if (!target) {
683
+ // The kill switch means "never phone home from this host", so it blocks
684
+ // the manifest fetch too — an explicit `--to X.Y.Z` still works because
685
+ // that path talks only to the npm registry the install already uses.
686
+ if (String(env.OAR_NO_UPDATE_CHECK || '').trim() === '1') {
687
+ logger.error('[oar] Update checks are disabled on this host (OAR_NO_UPDATE_CHECK). Use `oar update --to X.Y.Z` to update to a known version.');
688
+ return { code: 1, reason: 'check-killed' };
689
+ }
690
+ const manifestUrl = String(env.OAR_UPDATE_MANIFEST_URL || '').trim() || 'https://oar.sh/latest.json';
691
+ let manifest = null;
692
+ try {
693
+ const response = await fetchImpl(manifestUrl, { signal: AbortSignal.timeout?.(5000) });
694
+ manifest = response?.ok ? await response.json() : null;
695
+ } catch {}
696
+ if (!manifest) {
697
+ logger.error(`[oar] Could not fetch ${manifestUrl}.`);
698
+ return { code: 1, reason: 'manifest-unreachable' };
699
+ }
700
+ if (manifest.schemaVersion !== 1) {
701
+ logger.error(`[oar] ${manifestUrl} has schemaVersion ${manifest.schemaVersion}, which this oar does not understand — update manually.`);
702
+ return { code: 1, reason: 'manifest-unsupported' };
703
+ }
704
+ const channel = argv.includes('--beta') ? 'beta' : channelForVersion(runningVersion);
705
+ target = String(manifest.channels?.[channel]?.version || manifest.channels?.stable?.version || '').trim();
706
+ if (!parseSemverIsh(target)) {
707
+ logger.error('[oar] The update manifest carries no usable version.');
708
+ return { code: 1, reason: 'manifest-empty' };
709
+ }
710
+ if (compareSemverIsh(target, runningVersion) <= 0) {
711
+ logger.log(`[oar] Already up to date (${runningVersion}).`);
712
+ return { code: 0, reason: 'up-to-date' };
713
+ }
714
+ }
715
+
716
+ logger.log(`[oar] Updating @oar-sh/oar ${runningVersion} -> ${target} ...`);
717
+ const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
718
+ const exitCode = await new Promise((resolve) => {
719
+ const child = spawnImpl(npmCommand, ['install', '-g', `@oar-sh/oar@${target}`], {
720
+ stdio: 'inherit',
721
+ ...(process.platform === 'win32' ? { shell: true, windowsHide: true } : {}),
722
+ });
723
+ child.on('error', (error) => {
724
+ logger.error(`[oar] npm spawn failed: ${error?.message || error}`);
725
+ resolve(1);
726
+ });
727
+ child.on('close', (code) => resolve(code ?? 1));
728
+ });
729
+ if (exitCode !== 0) {
730
+ logger.error('[oar] npm install failed; the running install is unchanged.');
731
+ return { code: 1, reason: 'npm-failed' };
732
+ }
733
+ logger.log(`[oar] Installed @oar-sh/oar@${target}.`);
734
+
735
+ // A running relay picks the new code up on restart; ask for the queue-idle
736
+ // deferred one so no in-flight turn is cut off.
737
+ const layout = resolveStateLayout({ packageRoot, env });
738
+ const configPath = layout.checkout
739
+ ? path.join(packageRoot, 'server', 'config.json')
740
+ : path.join(layout.configDir, 'config.json');
741
+ const config = readJsonFile(configPath);
742
+ const port = Number(config?.port) || 3333;
743
+ const lockPath = layout.checkout
744
+ ? path.join(packageRoot, 'server', 'data', 'relay-server.lock')
745
+ : path.join(layout.dataDir, 'relay-server.lock');
746
+ const running = await detectRunningRelay({ lockPath, statusUrl: `http://localhost:${port}/api/status`, fetchImpl });
747
+ if (!running.running) {
748
+ logger.log('[oar] No running relay detected — the update loads on the next start.');
749
+ return { code: 0, reason: 'updated' };
750
+ }
751
+ const token = String(running.lock?.token || config?.authToken || '').trim();
752
+ try {
753
+ const response = await fetchImpl(`http://localhost:${port}/api/relay/shutdown`, {
754
+ method: 'POST',
755
+ headers: {
756
+ 'Content-Type': 'application/json',
757
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
758
+ },
759
+ body: JSON.stringify({ reason: 'self-update-cli', requestedBy: 'oar-update', restart: true }),
760
+ signal: AbortSignal.timeout?.(5000),
761
+ });
762
+ if (response?.ok) {
763
+ logger.log('[oar] Relay restart requested — it restarts once the queue is idle.');
764
+ } else {
765
+ logger.log('[oar] Could not request a relay restart; restart it manually to load the update.');
766
+ }
767
+ } catch {
768
+ logger.log('[oar] Could not request a relay restart; restart it manually to load the update.');
769
+ }
770
+ return { code: 0, reason: 'updated' };
771
+ }
772
+
643
773
  function main() {
644
774
  const argv = process.argv.slice(2);
645
775
  const command = String(argv[0] || '').trim();
@@ -649,6 +779,7 @@ function main() {
649
779
  }
650
780
  if (command === 'setup') return runSetup({ argv: argv.slice(1) });
651
781
  if (command === 'doctor') return runDoctor({});
782
+ if (command === 'update') return runUpdate({ argv: argv.slice(1) });
652
783
  return launchRelay();
653
784
  }
654
785
 
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@oar-sh/oar",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
4
4
  "type": "module",
5
- "description": "OAR \u2014 Open Agent Relay: drive your local coding agents from any browser",
5
+ "description": "OAR — Open Agent Relay: drive your local coding agents from any browser",
6
6
  "license": "MIT",
7
7
  "homepage": "https://oar.sh",
8
8
  "repository": {
@@ -20,6 +20,8 @@
20
20
  "bin/",
21
21
  "server/",
22
22
  "!server/**/*.test.mjs",
23
+ "!server/**/*-test-harness.mjs",
24
+ "!server/**/fixtures/",
23
25
  "!server/data/",
24
26
  "!server/logs/",
25
27
  "!server/config.json",
@@ -28,6 +30,7 @@
28
30
  "server/uploads/image_input.md",
29
31
  "shared/",
30
32
  "!shared/**/*.test.mjs",
33
+ "!shared/**/fixtures/",
31
34
  ".github/extensions/web-relay/",
32
35
  "!.github/extensions/web-relay/**/*.test.mjs",
33
36
  "README.md",
@@ -43,8 +46,9 @@
43
46
  "test:e2e:raw": "node ./node_modules/@playwright/test/cli.js test --config tests/playwright.config.mjs"
44
47
  },
45
48
  "dependencies": {
46
- "@anthropic-ai/claude-agent-sdk": "^0.3.226",
47
- "@cursor/sdk": "^1.0.27",
49
+ "@anthropic-ai/claude-agent-sdk": "^0.3.283",
50
+ "@cursor/sdk": "^1.0.32",
51
+ "@github/copilot-sdk": "^1.0.14",
48
52
  "better-sqlite3": "^12.10.0",
49
53
  "express": "^4.18.2",
50
54
  "qrcode-terminal": "^0.12.0",
package/server/README.md CHANGED
@@ -194,8 +194,12 @@ When opened in a regular browser tab, the in-app **Install** button remains avai
194
194
  in the chat header (shown as `⬇` on small screens).
195
195
 
196
196
  You can rename the installed app label from **⚙️ Settings → Install app name**.
197
- The new label is stored per browser and used for future installs; some platforms may
198
- require reinstalling the app before the launcher label updates.
197
+ The name is stored on the relay (app settings) and served in the web app manifest
198
+ to every browser and origin (LAN and tunnel agree), so future installs pick it up
199
+ immediately and already-installed Android apps receive it through Chrome's normal
200
+ manifest-update flow — one confirmation prompt showing the new name, then the
201
+ launcher label updates. A legacy per-browser name is adopted by the relay the
202
+ first time that browser loads the app.
199
203
 
200
204
  You can also hide the **💤 Suspend host** action from **⚙️ Settings** with the
201
205
  **Show Suspend host action** checkbox. This only controls UI visibility; it does not
@@ -523,25 +527,58 @@ cards with a reply box.
523
527
  The active conversation header includes a `✍️` button for renaming the conversation inline.
524
528
  Title edits are saved to the database and broadcast to other open clients immediately.
525
529
 
526
- ## Session worker rollout flags
527
-
528
- Session-worker refactor gates are OFF by default and can be enabled per flag:
529
-
530
- - `SESSION_WORKER_ROUTING_ENABLED`
531
- - `SESSION_WORKER_CONTINUATION_ROUTING_ENABLED`
532
- - `SESSION_WORKER_FALLBACK_RESTART_ENABLED`
533
-
534
- Other flags:
535
-
536
- - `IMAGE_CONVERSATION_CONTINUITY_ENABLED` — ON by default; keeps generated-image context across turns
537
- so *Edit this image* can continue from the right source image.
538
-
539
- Configuration precedence is:
540
-
541
- 1. `server/config.json` → `features.{FLAG_NAME}` (`true/false`, `1/0`, `yes/no`, `on/off`)
542
- 2. Environment override: `COPILOT_REMOTE_{FLAG_NAME}`
543
-
544
- Unknown flag names and invalid values are ignored safely.
530
+ ## Updates
531
+
532
+ OAR never phones home by default. Automatic update checking is **opt-in**:
533
+ enable **Check for updates automatically** in ⚙️ Settings (stored in app
534
+ settings, off unless you turn it on) and the relay asks `oar.sh/latest.json`
535
+ for the newest version about twice a day (12 h ± 1 h jitter, ETag-aware, 5 s
536
+ timeout, failures silent). A manual **Check for updates** button works without
537
+ opting in — that single request is the only traffic it creates. Set
538
+ `OAR_NO_UPDATE_CHECK=1` in the relay's environment to disable even manual
539
+ checks; `OAR_UPDATE_MANIFEST_URL` points the check at a different manifest
540
+ (used by tests and staging).
541
+
542
+ When a newer version is available, Settings shows a dismissible-per-version
543
+ card (not dismissible if the release is marked critical — it still never
544
+ auto-applies). On npm-global installs, **Update** runs
545
+ `npm i -g @oar-sh/oar@<version>` with the log streamed into the card, then the
546
+ relay restarts once the message queue is idle. After the restart the relay
547
+ verifies the running version actually changed; if not, the npm log is surfaced
548
+ as a failure. Git checkouts are detect-only — the card shows a "pull to
549
+ update" hint instead of the button. `oar update [--beta] [--to X.Y.Z]` does
550
+ the same from a terminal, and rollback is `oar update --to <previous>`.
551
+
552
+ ## Feature flags
553
+
554
+ Feature flags are declared in one registry (`server/features.mjs`) and managed
555
+ from the web UI's Settings → Features tab, which renders each flag with its
556
+ user-facing description. Current flags and defaults:
557
+
558
+ - `SESSION_WORKER_ROUTING_ENABLED` — ON by default; per-conversation worker
559
+ processes with ownership and crash recovery (off = legacy shared polling).
560
+ - `SESSION_WORKER_CONTINUATION_ROUTING_ENABLED` — ON by default; strict
561
+ ownership validation when routing clarification answers back to the asking
562
+ worker.
563
+ - `SESSION_WORKER_FALLBACK_RESTART_ENABLED` — OFF by default; reserved rollout
564
+ gate, no effect yet.
565
+ - `IMAGE_CONVERSATION_CONTINUITY_ENABLED` — ON by default; keeps
566
+ generated-image context across turns so *Edit this image* can continue from
567
+ the right source image.
568
+
569
+ Resolution happens once at relay startup, with this precedence:
570
+
571
+ 1. Registry defaults
572
+ 2. Stored settings (`app_settings` rows, written by the Features tab via
573
+ `GET/POST /api/settings/features`)
574
+ 3. Environment override: `COPILOT_REMOTE_{FLAG_NAME}` (`true/false`, `1/0`,
575
+ `yes/no`, `on/off`)
576
+
577
+ Changes saved in the Features tab take effect after a relay restart; the tab
578
+ shows a restart notice and offers the queue-idle-deferred restart. A legacy
579
+ `features` key in `config.json` is migrated into `app_settings` once at boot
580
+ and stripped from the file. Unknown flag names and invalid values are ignored
581
+ safely.
545
582
 
546
583
  Question bridge rule:
547
584
 
@@ -736,7 +773,7 @@ Queue metrics include `parkedCount` for turns deferred behind restart/rebind gat
736
773
  | GET | `/api/search/messages` | Search message text across all conversations (`q`, `limit`, `offset`) |
737
774
  | PATCH | `/api/conversation/:id` | Update a conversation title |
738
775
  | POST | `/api/conversation/:id/compact` | Compact a conversation into a new one with carry-over summary seed |
739
- | DELETE | `/api/conversation/:id` | Delete a conversation |
776
+ | DELETE | `/api/conversation/:id` | Delete a conversation: stops its session worker, deletes its CLI session, removes its rows. A Copilot session the relay only imported and never ran is hidden and tombstoned instead: nothing is stopped and its CLI session is kept. `409 conversation-active` (`reason: turn-running` / `background-tasks`) while it is still working; `409 worker-stop-failed` when its worker survives the stop (nothing deleted) |
740
777
  | GET | `/api/sdk-session-delete/pending` | (CLI relay) Fetch next pending SDK session delete request |
741
778
  | POST | `/api/sdk-session-delete/result` | (CLI relay) Report SDK session delete result |
742
779
  | POST | `/api/session-sync` | (CLI relay) Sync conversation↔SDK binding and optionally confirm orchestrator rebind completion |
@@ -1,6 +1,7 @@
1
1
  import { createSdkMcpServer, query } from '@anthropic-ai/claude-agent-sdk';
2
2
 
3
3
  import { parseThinkingDisplay } from '../../shared/claude-thinking.mjs';
4
+ import { MEDIA_EMBED_INSTRUCTION_TEXT } from '../../shared/media-embed-instructions.mjs';
4
5
  import { createPreviewToolDefinition } from './claude-preview-tool.mjs';
5
6
 
6
7
  // The name the in-process MCP server is registered under; it prefixes every
@@ -122,11 +123,15 @@ export function permissionModeForRelayMode(relayMode) {
122
123
 
123
124
  export function systemPromptForRelayMode(relayMode) {
124
125
  const mode = String(relayMode || 'agent').trim().toLowerCase();
125
- const append = MODE_SYSTEM_PROMPT_APPEND[mode];
126
+ // The media-embed capability rides every session — unlike the mode nudges it
127
+ // is renderer knowledge the model cannot discover any other way.
128
+ const append = [MEDIA_EMBED_INSTRUCTION_TEXT, MODE_SYSTEM_PROMPT_APPEND[mode]]
129
+ .filter(Boolean)
130
+ .join('\n\n');
126
131
  return {
127
132
  type: 'preset',
128
133
  preset: 'claude_code',
129
- ...(append ? { append } : {}),
134
+ append,
130
135
  };
131
136
  }
132
137