@oar-sh/oar 0.9.12 → 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 +508 -0
- package/README.md +12 -12
- package/bin/oar.js +155 -94
- package/package.json +5 -5
- package/server/README.md +479 -157
- package/server/claude-cloud-worker/claude-cloud-session-process.mjs +10 -4
- package/server/claude-worker/claude-session-process.mjs +92 -15
- package/server/claude-worker/claude-turn-publisher.mjs +20 -1
- package/server/claude-worker/subagent-timeline-recorder.mjs +75 -8
- package/server/claude-worker/task-output-discovery.mjs +28 -2
- package/server/copilot-worker/copilot-ask-user-tool.mjs +1 -1
- package/server/copilot-worker/copilot-continuation-signals.mjs +11 -5
- 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 +24 -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/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/grok-worker/grok-sdk-adapter.mjs +14 -0
- package/server/grok-worker/grok-turn-runner.mjs +13 -3
- 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 +60 -4
- package/server/public/app/ask-user-view.js +29 -4
- 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-provider-indicator.mjs +18 -8
- 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/question-answer-rule.mjs +15 -0
- package/server/public/app/question-multi-select.mjs +9 -2
- package/server/public/app/relay-event-view.mjs +5 -1
- package/server/public/app/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 +76 -5
- package/server/relay-exit-codes.mjs +6 -0
- package/server/relay-self-restart.mjs +6 -1
- package/server/relay-tools.md +17 -9
- package/server/repositories/message-repository.mjs +32 -28
- package/server/repositories/remote-relay-repository.mjs +4 -2
- package/server/repositories/subagent-timeline-statements.mjs +2 -0
- package/server/routes/ask-user-routes.mjs +41 -7
- package/server/routes/messages-routes.mjs +220 -37
- package/server/routes/preview-routes.mjs +73 -7
- package/server/routes/relay-board-routes.mjs +19 -2
- package/server/routes/relay-restart-routes.mjs +73 -0
- package/server/routes/sessions-routes.mjs +79 -5
- package/server/server-runtime.mjs +335 -218
- 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/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/host-suspend-command.mjs +2 -1
- 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-proxy-server.mjs +14 -1
- package/server/services/preview-registry-service.mjs +3 -2
- 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 +42 -8
- package/server/services/relay-event-service.mjs +210 -14
- 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 +387 -0
- package/server/services/relay-shutdown-gate.mjs +4 -1
- 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 +217 -28
- package/server/services/restart-resume-service.mjs +303 -0
- package/server/services/session-activity-tracker.mjs +52 -15
- package/server/services/session-worker-launch-service.mjs +31 -1
- package/server/services/session-worker-process-service.mjs +6 -4
- package/server/services/stale-turn-recovery.mjs +99 -0
- package/server/services/subagent-timeline-service.mjs +4 -1
- package/server/services/task-output-service.mjs +217 -18
- 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 +106 -3
- package/shared/remote-relay-contract.mjs +32 -15
- package/shared/subagent-timeline.mjs +4 -0
- package/shared/task-output-text.mjs +196 -83
- 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/worker-websocket-link.mjs +96 -20
- package/shared/worker-runtime/working-directory.mjs +65 -0
- package/server/uploads/image_input.md +0 -540
|
@@ -4,9 +4,46 @@
|
|
|
4
4
|
// listener itself has no routes and no way to register anything. Creating or
|
|
5
5
|
// closing a preview therefore always requires the relay token, even though the
|
|
6
6
|
// resulting URL is public.
|
|
7
|
+
//
|
|
8
|
+
// Conversation scope. A preview's token is its only credential, so an agent's
|
|
9
|
+
// preview tool must not see or close another conversation's. A request that
|
|
10
|
+
// names a conversation — the `conversationId` query, else the
|
|
11
|
+
// `X-Relay-Conversation-Id` header every worker and the OAR MCP server send —
|
|
12
|
+
// is scoped to it: GET lists only its previews, DELETE refuses another
|
|
13
|
+
// conversation's with a 403. Only a request with neither (the web UI's
|
|
14
|
+
// Settings → Previews) gets the relay-wide view. Agent-side callers present the
|
|
15
|
+
// same relay token as the owner, so this is a boundary for the tools, not
|
|
16
|
+
// against a caller that deliberately leaves both out.
|
|
7
17
|
|
|
8
18
|
import { validateStaticRoot } from '../services/preview-static-handler.mjs';
|
|
9
19
|
|
|
20
|
+
/**
|
|
21
|
+
* The conversation ids a named scope covers: the id itself, and — when it
|
|
22
|
+
* names a conversation by its id or by its session id — that conversation's id
|
|
23
|
+
* and session id. They differ after a placeholder rekey, and a worker's or
|
|
24
|
+
* tool server's own identity is the session id. A lookup that fails narrows
|
|
25
|
+
* the scope to the named id rather than failing the request.
|
|
26
|
+
*/
|
|
27
|
+
export function createPreviewScopeResolver({
|
|
28
|
+
getConversation = () => null,
|
|
29
|
+
getConversationBySdkSessionId = () => null,
|
|
30
|
+
} = {}) {
|
|
31
|
+
return function resolveConversationScope(id) {
|
|
32
|
+
const named = String(id || '').trim();
|
|
33
|
+
if (!named) return [];
|
|
34
|
+
const ids = new Set([named]);
|
|
35
|
+
let conversation = null;
|
|
36
|
+
try {
|
|
37
|
+
conversation = getConversation(named) || getConversationBySdkSessionId(named) || null;
|
|
38
|
+
} catch {}
|
|
39
|
+
for (const value of [conversation?.id, conversation?.sdk_session_id]) {
|
|
40
|
+
const text = String(value || '').trim();
|
|
41
|
+
if (text) ids.add(text);
|
|
42
|
+
}
|
|
43
|
+
return [...ids];
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
10
47
|
export function registerPreviewRoutes(app, deps) {
|
|
11
48
|
const {
|
|
12
49
|
auth,
|
|
@@ -20,6 +57,8 @@ export function registerPreviewRoutes(app, deps) {
|
|
|
20
57
|
// publishes stay panel-only — the transcript records the conversation, not
|
|
21
58
|
// panel actions).
|
|
22
59
|
recordPreviewCard = () => {},
|
|
60
|
+
// (scope id) => the conversation ids it covers; createPreviewScopeResolver.
|
|
61
|
+
resolveConversationScope = (id) => (id ? [id] : []),
|
|
23
62
|
} = deps;
|
|
24
63
|
|
|
25
64
|
function laneUnavailable(res) {
|
|
@@ -36,13 +75,27 @@ export function registerPreviewRoutes(app, deps) {
|
|
|
36
75
|
});
|
|
37
76
|
}
|
|
38
77
|
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
78
|
+
// The conversation ids a request is scoped to, or null for the owner's
|
|
79
|
+
// view. The query wins over the header: the header is the worker's own
|
|
80
|
+
// session, while a worker can be handed a row of another conversation (a
|
|
81
|
+
// placeholder rekey, or the shared fallback of the worker's pending lookup)
|
|
82
|
+
// and its tool names the conversation of the turn it is running.
|
|
83
|
+
function requestScope(req) {
|
|
84
|
+
const named = String(req.query?.conversationId || '').trim()
|
|
85
|
+
|| String(req.headers?.['x-relay-conversation-id'] || '').trim();
|
|
86
|
+
if (!named) return null;
|
|
87
|
+
const ids = resolveConversationScope(named);
|
|
88
|
+
return Array.isArray(ids) && ids.length ? ids : [named];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// GET /api/previews — one conversation's live previews when scoped (see the
|
|
92
|
+
// top of the file), else every one. Unscoped is what the settings "Live
|
|
93
|
+
// previews" list uses: nothing expires on its own, so there has to be one
|
|
94
|
+
// place that shows everything.
|
|
42
95
|
app.get('/api/previews', auth, (req, res) => {
|
|
43
|
-
const
|
|
44
|
-
const previews =
|
|
45
|
-
? previewRegistry.
|
|
96
|
+
const scope = requestScope(req);
|
|
97
|
+
const previews = scope
|
|
98
|
+
? previewRegistry.list().filter((entry) => scope.includes(entry.conversationId))
|
|
46
99
|
: previewRegistry.list();
|
|
47
100
|
res.json({
|
|
48
101
|
previews,
|
|
@@ -107,8 +160,21 @@ export function registerPreviewRoutes(app, deps) {
|
|
|
107
160
|
});
|
|
108
161
|
|
|
109
162
|
// DELETE /api/previews/:token — the link stops resolving immediately. The dev
|
|
110
|
-
// server behind it is untouched; it is a separate background task.
|
|
163
|
+
// server behind it is untouched; it is a separate background task. A scoped
|
|
164
|
+
// request may close only its own conversation's previews.
|
|
111
165
|
app.delete('/api/previews/:token', auth, (req, res) => {
|
|
166
|
+
const scope = requestScope(req);
|
|
167
|
+
if (scope) {
|
|
168
|
+
const token = String(req.params?.token || '').trim();
|
|
169
|
+
// list() also holds the stored previews a restart could not check yet.
|
|
170
|
+
const target = previewRegistry.list().find((entry) => entry.token === token);
|
|
171
|
+
if (target && !scope.includes(target.conversationId)) {
|
|
172
|
+
return res.status(403).json({
|
|
173
|
+
error: 'This preview belongs to another conversation; a conversation can close only its own previews. '
|
|
174
|
+
+ 'The owner can close it in Settings → Previews.',
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
112
178
|
const closed = previewRegistry.close(req.params?.token);
|
|
113
179
|
if (!closed.ok) return res.status(closed.status || 404).json({ error: closed.error });
|
|
114
180
|
res.json({ ok: true, preview: closed.preview });
|
|
@@ -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,
|
|
@@ -4023,8 +4032,10 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
4023
4032
|
// then nothing is shared.
|
|
4024
4033
|
function collectSharePreviewTokens(conversationId) {
|
|
4025
4034
|
const tokens = new Set();
|
|
4026
|
-
// Every preview of the relay, not only this conversation's:
|
|
4027
|
-
//
|
|
4035
|
+
// Every preview of the relay, not only this conversation's: a reply may
|
|
4036
|
+
// quote any token its agent got hold of — one the user pasted from
|
|
4037
|
+
// Settings → Previews, or a relay-wide list fetched by hand with the relay
|
|
4038
|
+
// token (the preview tool's own list is scoped to its conversation).
|
|
4028
4039
|
try {
|
|
4029
4040
|
for (const knownToken of listKnownPreviewTokens?.() || []) tokens.add(String(knownToken));
|
|
4030
4041
|
} catch {
|
|
@@ -5792,6 +5803,55 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
5792
5803
|
supportedModels: providerModelSelection.availableModels || [],
|
|
5793
5804
|
});
|
|
5794
5805
|
}
|
|
5806
|
+
// Copilot falls back to the catalogue's current model for an id it does
|
|
5807
|
+
// not list, which New Chat (it only offers listed ids) never meets. An
|
|
5808
|
+
// agent naming a mistyped id ("gpt-5-4") would get a session on another
|
|
5809
|
+
// model than it asked for, or one its next send cannot reach: refused.
|
|
5810
|
+
// A listed id is bound in the list's casing, and one with its effort
|
|
5811
|
+
// ("gpt-5.4-high") as the listed id plus that effort: the catalogue
|
|
5812
|
+
// lookup below knows exact base ids only and would fall back too.
|
|
5813
|
+
let copilotBoundModel = '';
|
|
5814
|
+
let copilotVariantEffort = '';
|
|
5815
|
+
if (
|
|
5816
|
+
remoteRequest.remote
|
|
5817
|
+
&& !useClaudeCloudProvider && !useOpenAIProvider && !useClaudeProvider && !useCursorProvider && !useGrokProvider
|
|
5818
|
+
) {
|
|
5819
|
+
const mergedModels = Array.isArray(modelState?.models) ? modelState.models : [];
|
|
5820
|
+
const providersByModel = modelState?.providersByModel && typeof modelState.providersByModel === 'object'
|
|
5821
|
+
? modelState.providersByModel
|
|
5822
|
+
: {};
|
|
5823
|
+
// Copilot's own: listed for it, or for no provider in particular.
|
|
5824
|
+
const copilotModels = mergedModels.filter((modelId) => {
|
|
5825
|
+
const providers = providersByModel[String(modelId || '').trim().toLowerCase()];
|
|
5826
|
+
return !Array.isArray(providers) || providers.length === 0
|
|
5827
|
+
|| providers.some((provider) => String(provider || '').trim().toLowerCase() === 'github-copilot');
|
|
5828
|
+
});
|
|
5829
|
+
const reasoningByModel = modelState?.reasoningByModel && typeof modelState.reasoningByModel === 'object'
|
|
5830
|
+
? modelState.reasoningByModel
|
|
5831
|
+
: {};
|
|
5832
|
+
const copilotCheck = checkRequestedProviderModel({
|
|
5833
|
+
requestedModel: requestedBootstrapModel,
|
|
5834
|
+
provider: 'github',
|
|
5835
|
+
models: copilotModels,
|
|
5836
|
+
effortsFor: (base) => reasoningByModel[base] || reasoningByModel[String(base).toLowerCase()] || null,
|
|
5837
|
+
});
|
|
5838
|
+
// Another provider's model is answered by the checks below ("requires
|
|
5839
|
+
// the Claude provider"), not as an unknown one.
|
|
5840
|
+
const otherProviders = mergedModels.some((modelId) => String(modelId || '').trim().toLowerCase() === requestedBootstrapModel.toLowerCase());
|
|
5841
|
+
if (copilotCheck.status === 'known' && copilotCheck.model && !isAutoModelId(copilotCheck.model)) {
|
|
5842
|
+
copilotBoundModel = copilotCheck.base || copilotCheck.model;
|
|
5843
|
+
copilotVariantEffort = copilotCheck.effort || '';
|
|
5844
|
+
}
|
|
5845
|
+
if (copilotCheck.status === 'unknown' && !otherProviders) {
|
|
5846
|
+
return res.status(400).json({
|
|
5847
|
+
ok: false,
|
|
5848
|
+
error: unknownProviderModelText({ model: copilotCheck.model, provider: 'github', suggestions: copilotCheck.suggestions, models: copilotCheck.models }),
|
|
5849
|
+
code: 'COPILOT_MODEL_UNAVAILABLE',
|
|
5850
|
+
supportedModels: copilotCheck.models,
|
|
5851
|
+
...(copilotCheck.suggestions.length ? { suggestedModels: copilotCheck.suggestions } : {}),
|
|
5852
|
+
});
|
|
5853
|
+
}
|
|
5854
|
+
}
|
|
5795
5855
|
if (useOpenAIProvider && openAISettings?.configured !== true) {
|
|
5796
5856
|
return res.status(400).json({
|
|
5797
5857
|
ok: false,
|
|
@@ -5842,7 +5902,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
5842
5902
|
bootstrapWorkspaceRootPath = validatedRoot.realPath;
|
|
5843
5903
|
}
|
|
5844
5904
|
|
|
5845
|
-
const selectedModel = providerModelSelection ? providerModelSelection.model : catalogSelectedModel;
|
|
5905
|
+
const selectedModel = providerModelSelection ? providerModelSelection.model : (copilotBoundModel || catalogSelectedModel);
|
|
5846
5906
|
if (requestedProviderType === 'openai-image' && !isOpenAIImageModelId(selectedModel)) {
|
|
5847
5907
|
return res.status(400).json({
|
|
5848
5908
|
ok: false,
|
|
@@ -5914,7 +5974,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
5914
5974
|
// first send would apply, so the composer can restore it from the server
|
|
5915
5975
|
// instead of guessing from localStorage.
|
|
5916
5976
|
const bootstrapReasoning = resolveProviderReasoningEffort({
|
|
5917
|
-
requestedEffort: req.body?.reasoningEffort,
|
|
5977
|
+
requestedEffort: req.body?.reasoningEffort || copilotVariantEffort,
|
|
5918
5978
|
strict: bootstrapProviderType === 'openai',
|
|
5919
5979
|
supportedEfforts: supportedReasoningEffortsForProviderModel({
|
|
5920
5980
|
providerType: bootstrapProviderType,
|
|
@@ -7145,6 +7205,20 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
7145
7205
|
return res.json(taskStallMinutesPayload(result.minutes));
|
|
7146
7206
|
});
|
|
7147
7207
|
|
|
7208
|
+
// "After a relay restart, tell interrupted sessions to carry on"
|
|
7209
|
+
// (restart_resume_notes, off by default; services/restart-resume-service.mjs).
|
|
7210
|
+
app.get('/api/settings/restart-resume', auth, (_req, res) => {
|
|
7211
|
+
res.json({ enabled: getRestartResumeEnabled() === true, default: false });
|
|
7212
|
+
});
|
|
7213
|
+
|
|
7214
|
+
app.post('/api/settings/restart-resume', auth, (req, res) => {
|
|
7215
|
+
const parsed = parseRestartResumeUpdate(req.body?.enabled);
|
|
7216
|
+
if (!parsed.ok) return res.status(400).json({ error: parsed.error });
|
|
7217
|
+
const result = setRestartResumeEnabled(parsed.enabled);
|
|
7218
|
+
if (!result.ok) return res.status(500).json({ error: result.error });
|
|
7219
|
+
return res.json({ enabled: result.enabled, default: false });
|
|
7220
|
+
});
|
|
7221
|
+
|
|
7148
7222
|
const pwaAppNamePayload = (appName) => ({
|
|
7149
7223
|
appName,
|
|
7150
7224
|
defaultName: PWA_APP_NAME_DEFAULT,
|