@oar-sh/oar 0.9.12 → 0.9.13

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 (62) hide show
  1. package/CHANGELOG.md +206 -0
  2. package/README.md +9 -9
  3. package/package.json +1 -1
  4. package/server/README.md +186 -11
  5. package/server/claude-worker/claude-session-process.mjs +53 -2
  6. package/server/claude-worker/task-output-discovery.mjs +28 -2
  7. package/server/copilot-worker/copilot-continuation-signals.mjs +11 -5
  8. package/server/copilot-worker/copilot-sdk-session-process.mjs +10 -0
  9. package/server/db-schema.mjs +10 -0
  10. package/server/grok-worker/acp-host-services.mjs +6 -3
  11. package/server/grok-worker/grok-background-tasks.mjs +7 -0
  12. package/server/migrations/0018-background-task-rows.mjs +101 -0
  13. package/server/public/app/action-confirmations.js +12 -15
  14. package/server/public/app/api-client.js +57 -3
  15. package/server/public/app/background-tasks-view.mjs +56 -7
  16. package/server/public/app/bootstrap.js +7 -0
  17. package/server/public/app/conversation-preferences.mjs +10 -4
  18. package/server/public/app/conversation-view.js +33 -9
  19. package/server/public/app/host-suspend-ui.js +45 -44
  20. package/server/public/app/journal-view.js +9 -3
  21. package/server/public/app/push-settings.js +1 -0
  22. package/server/public/app/relay-prompt-sanitizer.mjs +9 -2
  23. package/server/public/app/relay-restart-note-view.mjs +36 -0
  24. package/server/public/app/relay-restart-ui.js +568 -0
  25. package/server/public/app/settings-modal.js +52 -0
  26. package/server/public/app/socket-handlers.js +1 -1
  27. package/server/public/app/task-console-render.mjs +171 -7
  28. package/server/public/app/task-console.mjs +111 -11
  29. package/server/public/app/task-output-text.mjs +15 -6
  30. package/server/public/app/transcript-jump-bottom-view.mjs +5 -1
  31. package/server/public/app/transcript-jump-bottom.mjs +20 -0
  32. package/server/public/index.html +67 -0
  33. package/server/relay-tools.md +5 -0
  34. package/server/repositories/message-repository.mjs +3 -0
  35. package/server/repositories/remote-relay-repository.mjs +4 -2
  36. package/server/routes/messages-routes.mjs +155 -27
  37. package/server/routes/relay-board-routes.mjs +19 -2
  38. package/server/routes/relay-restart-routes.mjs +73 -0
  39. package/server/routes/sessions-routes.mjs +75 -3
  40. package/server/server-runtime.mjs +192 -137
  41. package/server/services/about-service.mjs +13 -12
  42. package/server/services/background-task-persistence.mjs +133 -0
  43. package/server/services/background-task-store.mjs +240 -13
  44. package/server/services/conversation-model-fallback.mjs +57 -0
  45. package/server/services/host-suspend-command.mjs +2 -1
  46. package/server/services/preview-proxy-server.mjs +14 -1
  47. package/server/services/process-list-service.mjs +2 -1
  48. package/server/services/provider-model-selection.mjs +195 -0
  49. package/server/services/push-dispatch-service.mjs +9 -2
  50. package/server/services/relay-event-service.mjs +27 -9
  51. package/server/services/relay-prompt-sanitizer.mjs +7 -0
  52. package/server/services/relay-restart-picture.mjs +209 -0
  53. package/server/services/relay-restart-queue.mjs +380 -0
  54. package/server/services/relay-shutdown-gate.mjs +4 -1
  55. package/server/services/remote-relay-dispatcher.mjs +187 -19
  56. package/server/services/restart-resume-service.mjs +303 -0
  57. package/server/services/session-worker-process-service.mjs +6 -4
  58. package/server/services/task-output-service.mjs +217 -18
  59. package/shared/relay-events.mjs +55 -2
  60. package/shared/remote-relay-contract.mjs +15 -8
  61. package/shared/task-output-text.mjs +196 -83
  62. package/shared/worker-runtime/worker-websocket-link.mjs +96 -20
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  import { persistConversationPreferences } from '../services/conversation-preferences-service.mjs';
4
+ import { fallbackReasoningEffort } from '../services/conversation-model-fallback.mjs';
4
5
 
5
6
  function normalizeId(value) {
6
7
  const text = String(value || '').trim();
@@ -93,12 +94,28 @@ export function registerRelayBoardRoutes(app, deps) {
93
94
  uuidv4,
94
95
  normalizeRelayMode,
95
96
  resolveRequestedModel,
97
+ resolveRequestedReasoningEffort,
96
98
  DEFAULT_RELAY_MODE,
97
99
  DEFAULT_MODEL,
98
100
  formatRelayBoardRow,
99
101
  pushDispatchService,
100
102
  } = deps;
101
103
 
104
+ /**
105
+ * The effort the follow-up runs with when the model id carries none: the
106
+ * conversation's own, clamped to the levels the model has. A model the
107
+ * catalog cannot check (another provider's) takes it as it is.
108
+ */
109
+ function conversationEffortFor(conversationId, model) {
110
+ const preferred = String(stmts.getConvAnyStatus?.get?.(conversationId)?.preferred_reasoning_effort || '').trim().toLowerCase();
111
+ if (!preferred) return null;
112
+ if (typeof resolveRequestedReasoningEffort !== 'function') return preferred;
113
+ const check = resolveRequestedReasoningEffort(model, preferred);
114
+ if (check?.ok) return String(check.effort || '').trim() || null;
115
+ const supported = Array.isArray(check?.supported) ? check.supported : [];
116
+ return supported.length ? (fallbackReasoningEffort([preferred], supported) || null) : preferred;
117
+ }
118
+
102
119
  /**
103
120
  * The session goes on in the mode that was chosen on the board: after
104
121
  * "Implement in autopilot" the composer says Autopilot, and the next message
@@ -250,9 +267,9 @@ export function registerRelayBoardRoutes(app, deps) {
250
267
  const resolvedVariantModel = modelResolution?.ok
251
268
  ? String(modelResolution.modelVariantId || latestModel || resolvedBaseModel).trim()
252
269
  : String(latestModel || resolvedBaseModel).trim();
253
- const resolvedReasoningEffort = modelResolution?.ok
270
+ const resolvedReasoningEffort = (modelResolution?.ok
254
271
  ? String(modelResolution.reasoningEffort || '').trim() || null
255
- : null;
272
+ : null) || conversationEffortFor(convId, resolvedBaseModel);
256
273
  queuedMessageId = uuidv4();
257
274
  stmts.insertMsg.run(
258
275
  queuedMessageId,
@@ -0,0 +1,73 @@
1
+ 'use strict';
2
+
3
+ // The queued relay restart beyond queueing and cancelling it (those two are
4
+ // POST /api/relay/shutdown and /api/relay/shutdown/cancel in
5
+ // messages-routes.mjs): what a restart would wait for and leave running, the
6
+ // "keep waiting" answer to the max-wait question, and the max-wait setting
7
+ // (services/relay-restart-queue.mjs). Token only, like the restart itself.
8
+
9
+ import {
10
+ DEFAULT_RELAY_RESTART_MAX_WAIT_MINUTES,
11
+ RELAY_RESTART_MAX_WAIT_CHOICES,
12
+ parseRelayRestartMaxWaitUpdate,
13
+ } from '../services/relay-restart-queue.mjs';
14
+
15
+ function requestedByOf(req) {
16
+ return String(req.body?.requestedBy || 'localhost-api').trim().slice(0, 80) || 'localhost-api';
17
+ }
18
+
19
+ export function registerRelayRestartRoutes(app, {
20
+ auth,
21
+ getState = null,
22
+ collectRestartPicture = null,
23
+ keepWaiting = null,
24
+ getMaxWaitMinutes = () => DEFAULT_RELAY_RESTART_MAX_WAIT_MINUTES,
25
+ setMaxWaitMinutes = null,
26
+ maxWaitOverrideMs = null,
27
+ } = {}) {
28
+ // The queued request's state, and with `?action=` the picture for a
29
+ // restart or shutdown asked now (the confirmation reads it first).
30
+ app.get('/api/relay/shutdown', auth, (req, res) => {
31
+ if (typeof getState !== 'function') {
32
+ return res.status(501).json({ error: 'Relay shutdown orchestration is unavailable' });
33
+ }
34
+ const state = getState();
35
+ const action = String(req.query?.action || 'restart').trim().toLowerCase() === 'shutdown' ? 'shutdown' : 'restart';
36
+ let preview = null;
37
+ if (typeof collectRestartPicture === 'function') {
38
+ try { preview = { action, ...collectRestartPicture({ action }) }; } catch {}
39
+ }
40
+ return res.json({ ok: true, ...state, preview });
41
+ });
42
+
43
+ app.post('/api/relay/shutdown/keep-waiting', auth, (req, res) => {
44
+ if (typeof keepWaiting !== 'function') {
45
+ return res.status(501).json({ error: 'Relay shutdown orchestration is unavailable' });
46
+ }
47
+ return res.json({ ok: true, ...keepWaiting({ requestedBy: requestedByOf(req) }) });
48
+ });
49
+
50
+ const settingPayload = (maxWaitMinutes) => ({
51
+ ok: true,
52
+ maxWaitMinutes,
53
+ choices: [...RELAY_RESTART_MAX_WAIT_CHOICES],
54
+ defaultMinutes: DEFAULT_RELAY_RESTART_MAX_WAIT_MINUTES,
55
+ // A test relay's environment override wins over the stored value.
56
+ overrideMs: maxWaitOverrideMs,
57
+ });
58
+
59
+ app.get('/api/settings/relay-restart-max-wait', auth, (_req, res) => {
60
+ res.json(settingPayload(getMaxWaitMinutes()));
61
+ });
62
+
63
+ app.post('/api/settings/relay-restart-max-wait', auth, (req, res) => {
64
+ const parsed = parseRelayRestartMaxWaitUpdate(req.body?.maxWaitMinutes);
65
+ if (!parsed.ok) return res.status(400).json({ ok: false, error: parsed.error });
66
+ if (typeof setMaxWaitMinutes !== 'function') {
67
+ return res.status(503).json({ ok: false, error: 'Relay restart settings are unavailable' });
68
+ }
69
+ const result = setMaxWaitMinutes(parsed.minutes);
70
+ if (!result?.ok) return res.status(500).json({ ok: false, error: result?.error || 'Saving failed' });
71
+ return res.json(settingPayload(result.maxWaitMinutes));
72
+ });
73
+ }
@@ -24,7 +24,13 @@ import {
24
24
  } from '../../shared/claude-background-command-limit.mjs';
25
25
  import { CLAUDE_ATTRIBUTION_MODES, DEFAULT_CLAUDE_ATTRIBUTION_MODE, claudeAttributionModelLabel, claudeAttributionTrailer, normalizeClaudeAttributionMode, resolveClaudeAttributionMode } from '../../shared/claude-attribution.mjs';
26
26
  import { parseThinkingDisplay, parseThinkingEnabled } from '../../shared/claude-thinking.mjs';
27
- import { isProviderModelAvailable, resolveProviderModelSelection } from '../services/provider-model-selection.mjs';
27
+ import {
28
+ checkRequestedProviderModel,
29
+ isAutoModelId,
30
+ isProviderModelAvailable,
31
+ resolveProviderModelSelection,
32
+ unknownProviderModelText,
33
+ } from '../services/provider-model-selection.mjs';
28
34
  import {
29
35
  DEFAULT_CLAUDE_REASONING_EFFORTS,
30
36
  resolveProviderReasoningEffort,
@@ -117,6 +123,7 @@ import {
117
123
  PWA_APP_NAME_MAX_LENGTH,
118
124
  derivePwaShortName,
119
125
  } from '../../shared/pwa-app-name.mjs';
126
+ import { parseRestartResumeUpdate } from '../services/restart-resume-service.mjs';
120
127
 
121
128
  export { mapUsageSnapshotRow };
122
129
 
@@ -2139,6 +2146,8 @@ export function registerSessionsRoutes(app, deps) {
2139
2146
  setBackgroundTaskTimeoutMinutes = () => ({ ok: false, error: 'Background task timeout settings are unavailable' }),
2140
2147
  getTaskStallMinutes = () => DEFAULT_TASK_STALL_MINUTES,
2141
2148
  setTaskStallMinutes = () => ({ ok: false, error: 'Task stall settings are unavailable' }),
2149
+ getRestartResumeEnabled = () => false,
2150
+ setRestartResumeEnabled = () => ({ ok: false, error: 'Restart settings are unavailable' }),
2142
2151
  markSharedViewerPresence,
2143
2152
  getSharedWatcherCount,
2144
2153
  statusEventService,
@@ -5792,6 +5801,55 @@ export function registerSessionsRoutes(app, deps) {
5792
5801
  supportedModels: providerModelSelection.availableModels || [],
5793
5802
  });
5794
5803
  }
5804
+ // Copilot falls back to the catalogue's current model for an id it does
5805
+ // not list, which New Chat (it only offers listed ids) never meets. An
5806
+ // agent naming a mistyped id ("gpt-5-4") would get a session on another
5807
+ // model than it asked for, or one its next send cannot reach: refused.
5808
+ // A listed id is bound in the list's casing, and one with its effort
5809
+ // ("gpt-5.4-high") as the listed id plus that effort: the catalogue
5810
+ // lookup below knows exact base ids only and would fall back too.
5811
+ let copilotBoundModel = '';
5812
+ let copilotVariantEffort = '';
5813
+ if (
5814
+ remoteRequest.remote
5815
+ && !useClaudeCloudProvider && !useOpenAIProvider && !useClaudeProvider && !useCursorProvider && !useGrokProvider
5816
+ ) {
5817
+ const mergedModels = Array.isArray(modelState?.models) ? modelState.models : [];
5818
+ const providersByModel = modelState?.providersByModel && typeof modelState.providersByModel === 'object'
5819
+ ? modelState.providersByModel
5820
+ : {};
5821
+ // Copilot's own: listed for it, or for no provider in particular.
5822
+ const copilotModels = mergedModels.filter((modelId) => {
5823
+ const providers = providersByModel[String(modelId || '').trim().toLowerCase()];
5824
+ return !Array.isArray(providers) || providers.length === 0
5825
+ || providers.some((provider) => String(provider || '').trim().toLowerCase() === 'github-copilot');
5826
+ });
5827
+ const reasoningByModel = modelState?.reasoningByModel && typeof modelState.reasoningByModel === 'object'
5828
+ ? modelState.reasoningByModel
5829
+ : {};
5830
+ const copilotCheck = checkRequestedProviderModel({
5831
+ requestedModel: requestedBootstrapModel,
5832
+ provider: 'github',
5833
+ models: copilotModels,
5834
+ effortsFor: (base) => reasoningByModel[base] || reasoningByModel[String(base).toLowerCase()] || null,
5835
+ });
5836
+ // Another provider's model is answered by the checks below ("requires
5837
+ // the Claude provider"), not as an unknown one.
5838
+ const otherProviders = mergedModels.some((modelId) => String(modelId || '').trim().toLowerCase() === requestedBootstrapModel.toLowerCase());
5839
+ if (copilotCheck.status === 'known' && copilotCheck.model && !isAutoModelId(copilotCheck.model)) {
5840
+ copilotBoundModel = copilotCheck.base || copilotCheck.model;
5841
+ copilotVariantEffort = copilotCheck.effort || '';
5842
+ }
5843
+ if (copilotCheck.status === 'unknown' && !otherProviders) {
5844
+ return res.status(400).json({
5845
+ ok: false,
5846
+ error: unknownProviderModelText({ model: copilotCheck.model, provider: 'github', suggestions: copilotCheck.suggestions, models: copilotCheck.models }),
5847
+ code: 'COPILOT_MODEL_UNAVAILABLE',
5848
+ supportedModels: copilotCheck.models,
5849
+ ...(copilotCheck.suggestions.length ? { suggestedModels: copilotCheck.suggestions } : {}),
5850
+ });
5851
+ }
5852
+ }
5795
5853
  if (useOpenAIProvider && openAISettings?.configured !== true) {
5796
5854
  return res.status(400).json({
5797
5855
  ok: false,
@@ -5842,7 +5900,7 @@ export function registerSessionsRoutes(app, deps) {
5842
5900
  bootstrapWorkspaceRootPath = validatedRoot.realPath;
5843
5901
  }
5844
5902
 
5845
- const selectedModel = providerModelSelection ? providerModelSelection.model : catalogSelectedModel;
5903
+ const selectedModel = providerModelSelection ? providerModelSelection.model : (copilotBoundModel || catalogSelectedModel);
5846
5904
  if (requestedProviderType === 'openai-image' && !isOpenAIImageModelId(selectedModel)) {
5847
5905
  return res.status(400).json({
5848
5906
  ok: false,
@@ -5914,7 +5972,7 @@ export function registerSessionsRoutes(app, deps) {
5914
5972
  // first send would apply, so the composer can restore it from the server
5915
5973
  // instead of guessing from localStorage.
5916
5974
  const bootstrapReasoning = resolveProviderReasoningEffort({
5917
- requestedEffort: req.body?.reasoningEffort,
5975
+ requestedEffort: req.body?.reasoningEffort || copilotVariantEffort,
5918
5976
  strict: bootstrapProviderType === 'openai',
5919
5977
  supportedEfforts: supportedReasoningEffortsForProviderModel({
5920
5978
  providerType: bootstrapProviderType,
@@ -7145,6 +7203,20 @@ export function registerSessionsRoutes(app, deps) {
7145
7203
  return res.json(taskStallMinutesPayload(result.minutes));
7146
7204
  });
7147
7205
 
7206
+ // "After a relay restart, tell interrupted sessions to carry on"
7207
+ // (restart_resume_notes, off by default; services/restart-resume-service.mjs).
7208
+ app.get('/api/settings/restart-resume', auth, (_req, res) => {
7209
+ res.json({ enabled: getRestartResumeEnabled() === true, default: false });
7210
+ });
7211
+
7212
+ app.post('/api/settings/restart-resume', auth, (req, res) => {
7213
+ const parsed = parseRestartResumeUpdate(req.body?.enabled);
7214
+ if (!parsed.ok) return res.status(400).json({ error: parsed.error });
7215
+ const result = setRestartResumeEnabled(parsed.enabled);
7216
+ if (!result.ok) return res.status(500).json({ error: result.error });
7217
+ return res.json({ enabled: result.enabled, default: false });
7218
+ });
7219
+
7148
7220
  const pwaAppNamePayload = (appName) => ({
7149
7221
  appName,
7150
7222
  defaultName: PWA_APP_NAME_DEFAULT,