@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.
- package/CHANGELOG.md +206 -0
- package/README.md +9 -9
- package/package.json +1 -1
- package/server/README.md +186 -11
- package/server/claude-worker/claude-session-process.mjs +53 -2
- package/server/claude-worker/task-output-discovery.mjs +28 -2
- package/server/copilot-worker/copilot-continuation-signals.mjs +11 -5
- package/server/copilot-worker/copilot-sdk-session-process.mjs +10 -0
- package/server/db-schema.mjs +10 -0
- package/server/grok-worker/acp-host-services.mjs +6 -3
- package/server/grok-worker/grok-background-tasks.mjs +7 -0
- package/server/migrations/0018-background-task-rows.mjs +101 -0
- package/server/public/app/action-confirmations.js +12 -15
- package/server/public/app/api-client.js +57 -3
- package/server/public/app/background-tasks-view.mjs +56 -7
- package/server/public/app/bootstrap.js +7 -0
- package/server/public/app/conversation-preferences.mjs +10 -4
- package/server/public/app/conversation-view.js +33 -9
- package/server/public/app/host-suspend-ui.js +45 -44
- package/server/public/app/journal-view.js +9 -3
- package/server/public/app/push-settings.js +1 -0
- package/server/public/app/relay-prompt-sanitizer.mjs +9 -2
- package/server/public/app/relay-restart-note-view.mjs +36 -0
- package/server/public/app/relay-restart-ui.js +568 -0
- package/server/public/app/settings-modal.js +52 -0
- package/server/public/app/socket-handlers.js +1 -1
- package/server/public/app/task-console-render.mjs +171 -7
- package/server/public/app/task-console.mjs +111 -11
- package/server/public/app/task-output-text.mjs +15 -6
- package/server/public/app/transcript-jump-bottom-view.mjs +5 -1
- package/server/public/app/transcript-jump-bottom.mjs +20 -0
- package/server/public/index.html +67 -0
- package/server/relay-tools.md +5 -0
- package/server/repositories/message-repository.mjs +3 -0
- package/server/repositories/remote-relay-repository.mjs +4 -2
- package/server/routes/messages-routes.mjs +155 -27
- package/server/routes/relay-board-routes.mjs +19 -2
- package/server/routes/relay-restart-routes.mjs +73 -0
- package/server/routes/sessions-routes.mjs +75 -3
- package/server/server-runtime.mjs +192 -137
- package/server/services/about-service.mjs +13 -12
- package/server/services/background-task-persistence.mjs +133 -0
- package/server/services/background-task-store.mjs +240 -13
- package/server/services/conversation-model-fallback.mjs +57 -0
- package/server/services/host-suspend-command.mjs +2 -1
- package/server/services/preview-proxy-server.mjs +14 -1
- package/server/services/process-list-service.mjs +2 -1
- package/server/services/provider-model-selection.mjs +195 -0
- package/server/services/push-dispatch-service.mjs +9 -2
- package/server/services/relay-event-service.mjs +27 -9
- package/server/services/relay-prompt-sanitizer.mjs +7 -0
- package/server/services/relay-restart-picture.mjs +209 -0
- package/server/services/relay-restart-queue.mjs +380 -0
- package/server/services/relay-shutdown-gate.mjs +4 -1
- package/server/services/remote-relay-dispatcher.mjs +187 -19
- package/server/services/restart-resume-service.mjs +303 -0
- package/server/services/session-worker-process-service.mjs +6 -4
- package/server/services/task-output-service.mjs +217 -18
- package/shared/relay-events.mjs +55 -2
- package/shared/remote-relay-contract.mjs +15 -8
- package/shared/task-output-text.mjs +196 -83
- 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 {
|
|
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,
|