@oar-sh/oar 0.9.11 → 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 +332 -0
- package/README.md +10 -10
- package/package.json +1 -1
- package/server/README.md +201 -12
- package/server/claude-worker/claude-remote-relay-tool.mjs +2 -0
- package/server/claude-worker/claude-sdk-adapter.mjs +57 -4
- package/server/claude-worker/claude-session-process.mjs +179 -0
- package/server/claude-worker/claude-session-worker.mjs +17 -0
- package/server/claude-worker/task-output-discovery.mjs +152 -0
- package/server/copilot-worker/copilot-continuation-signals.mjs +11 -5
- package/server/copilot-worker/copilot-sdk-session-process.mjs +169 -3
- package/server/copilot-worker/copilot-sdk-session-worker.mjs +2 -0
- package/server/copilot-worker/copilot-shell-output-mirror.mjs +94 -0
- package/server/copilot-worker/copilot-task-settle.mjs +96 -0
- package/server/db-schema.mjs +19 -0
- package/server/grok-worker/acp-host-services.mjs +48 -6
- package/server/grok-worker/grok-background-tasks.mjs +248 -0
- package/server/grok-worker/grok-sdk-adapter.mjs +3 -1
- package/server/grok-worker/grok-session-worker.mjs +33 -1
- package/server/grok-worker/grok-turn-runner.mjs +11 -0
- package/server/migrations/0017-conversation-history-indexes.mjs +89 -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 +70 -3
- package/server/public/app/background-tasks-view.mjs +334 -16
- package/server/public/app/bootstrap.js +102 -5
- package/server/public/app/claude-background-command-limit-options.mjs +21 -0
- package/server/public/app/conversation-payload-cache.mjs +357 -0
- package/server/public/app/conversation-preferences.mjs +10 -4
- package/server/public/app/conversation-turn-state.mjs +1 -1
- package/server/public/app/conversation-view.js +462 -8
- package/server/public/app/conversation-window-retention.mjs +96 -0
- package/server/public/app/host-suspend-ui.js +45 -44
- package/server/public/app/journal-view.js +259 -67
- package/server/public/app/push-settings.js +1 -0
- package/server/public/app/relay-prompt-sanitizer.mjs +20 -6
- 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 +162 -2
- package/server/public/app/socket-handlers.js +59 -7
- package/server/public/app/store.js +25 -0
- package/server/public/app/subagent-viewer.mjs +7 -1
- package/server/public/app/task-console-render.mjs +401 -0
- package/server/public/app/task-console-watch.mjs +42 -0
- package/server/public/app/task-console.mjs +730 -0
- package/server/public/app/task-output-text.mjs +243 -0
- package/server/public/app/transcript-island.mjs +73 -0
- package/server/public/app/transcript-jump-bottom-view.mjs +42 -8
- package/server/public/app/transcript-jump-bottom.mjs +68 -11
- package/server/public/app/transcript-reading-anchor.mjs +256 -0
- package/server/public/app/transcript-reload-freshness.mjs +17 -1
- package/server/public/app/transcript-viewport-anchor.mjs +20 -1
- package/server/public/index.html +306 -6
- package/server/relay-tools.md +6 -0
- package/server/repositories/message-repository.mjs +11 -0
- package/server/repositories/question-repository.mjs +4 -0
- package/server/repositories/remote-relay-repository.mjs +4 -2
- package/server/routes/messages-routes.mjs +397 -23
- package/server/routes/relay-board-routes.mjs +19 -2
- package/server/routes/relay-restart-routes.mjs +73 -0
- package/server/routes/sessions-routes.mjs +208 -66
- package/server/server-runtime.mjs +269 -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 +470 -6
- package/server/services/cli-process-runner.mjs +4 -21
- package/server/services/conversation-history-page.mjs +199 -0
- 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 +18 -4
- 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 +316 -20
- package/server/services/restart-resume-service.mjs +303 -0
- package/server/services/session-activity-tracker.mjs +6 -2
- package/server/services/session-worker-process-service.mjs +6 -4
- package/server/services/task-output-rooms.mjs +259 -0
- package/server/services/task-output-service.mjs +761 -0
- package/shared/claude-background-command-limit.mjs +83 -0
- package/shared/relay-events.mjs +55 -2
- package/shared/remote-relay-contract.mjs +47 -10
- package/shared/task-output-text.mjs +850 -0
- package/shared/worker-runtime/task-output-spool.mjs +181 -0
- 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
|
+
}
|
|
@@ -9,13 +9,28 @@ import { randomUUID } from 'crypto';
|
|
|
9
9
|
import { createSdkSessionSyncService } from '../services/sdk-session-sync-service.mjs';
|
|
10
10
|
import { serveFileWithRangeSupport } from '../services/file-serving.mjs';
|
|
11
11
|
import { stripRelayPromptContext } from '../services/relay-prompt-sanitizer.mjs';
|
|
12
|
+
import {
|
|
13
|
+
buildConversationHistoryPageInfo,
|
|
14
|
+
normalizeConversationHistoryLimit,
|
|
15
|
+
normalizeConversationTimestampMs,
|
|
16
|
+
} from '../services/conversation-history-page.mjs';
|
|
12
17
|
import { MESSAGE_PIN_LIMIT, buildPinList } from '../services/message-pin-list.mjs';
|
|
13
18
|
import { buildHiddenMessageList } from '../services/hidden-message-list.mjs';
|
|
14
19
|
import { persistConversationPreferences } from '../services/conversation-preferences-service.mjs';
|
|
15
20
|
import { parseAutoCompactWindow } from '../../shared/auto-compact-window.mjs';
|
|
21
|
+
import {
|
|
22
|
+
claudeBackgroundCommandLimitSettingsFields,
|
|
23
|
+
parseClaudeBackgroundCommandLimitUpdate,
|
|
24
|
+
} from '../../shared/claude-background-command-limit.mjs';
|
|
16
25
|
import { CLAUDE_ATTRIBUTION_MODES, DEFAULT_CLAUDE_ATTRIBUTION_MODE, claudeAttributionModelLabel, claudeAttributionTrailer, normalizeClaudeAttributionMode, resolveClaudeAttributionMode } from '../../shared/claude-attribution.mjs';
|
|
17
26
|
import { parseThinkingDisplay, parseThinkingEnabled } from '../../shared/claude-thinking.mjs';
|
|
18
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
checkRequestedProviderModel,
|
|
29
|
+
isAutoModelId,
|
|
30
|
+
isProviderModelAvailable,
|
|
31
|
+
resolveProviderModelSelection,
|
|
32
|
+
unknownProviderModelText,
|
|
33
|
+
} from '../services/provider-model-selection.mjs';
|
|
19
34
|
import {
|
|
20
35
|
DEFAULT_CLAUDE_REASONING_EFFORTS,
|
|
21
36
|
resolveProviderReasoningEffort,
|
|
@@ -97,11 +112,18 @@ import {
|
|
|
97
112
|
BACKGROUND_TASK_TIMEOUT_STEP_MINUTES,
|
|
98
113
|
parseBackgroundTaskTimeoutUpdate,
|
|
99
114
|
} from '../../shared/background-task-timeout.mjs';
|
|
115
|
+
import {
|
|
116
|
+
DEFAULT_TASK_STALL_MINUTES,
|
|
117
|
+
TASK_STALL_MINUTES_MAX,
|
|
118
|
+
TASK_STALL_MINUTES_MIN,
|
|
119
|
+
parseTaskStallMinutesUpdate,
|
|
120
|
+
} from '../../shared/task-output-text.mjs';
|
|
100
121
|
import {
|
|
101
122
|
PWA_APP_NAME_DEFAULT,
|
|
102
123
|
PWA_APP_NAME_MAX_LENGTH,
|
|
103
124
|
derivePwaShortName,
|
|
104
125
|
} from '../../shared/pwa-app-name.mjs';
|
|
126
|
+
import { parseRestartResumeUpdate } from '../services/restart-resume-service.mjs';
|
|
105
127
|
|
|
106
128
|
export { mapUsageSnapshotRow };
|
|
107
129
|
|
|
@@ -286,9 +308,16 @@ export function parseClaudeSettingsUpdateRequest(body = {}) {
|
|
|
286
308
|
const hasEnabledModels = Array.isArray(payload.enabledModels);
|
|
287
309
|
const hasAttributionMode = Object.prototype.hasOwnProperty.call(payload, 'attributionMode');
|
|
288
310
|
const attributionMode = hasAttributionMode ? normalizeClaudeAttributionMode(payload.attributionMode) : undefined;
|
|
289
|
-
|
|
311
|
+
const hasBackgroundCommandLimit = Object.prototype.hasOwnProperty.call(payload, 'backgroundCommandLimitMinutes');
|
|
312
|
+
const backgroundCommandLimit = hasBackgroundCommandLimit
|
|
313
|
+
? parseClaudeBackgroundCommandLimitUpdate(payload.backgroundCommandLimitMinutes)
|
|
314
|
+
: null;
|
|
315
|
+
if (!hasEnabled && !hasModel && !hasModels && !hasEnabledModels && !hasAttributionMode && !hasBackgroundCommandLimit) {
|
|
290
316
|
return { ok: false, error: 'No Claude settings update provided' };
|
|
291
317
|
}
|
|
318
|
+
if (backgroundCommandLimit && !backgroundCommandLimit.ok) {
|
|
319
|
+
return { ok: false, error: backgroundCommandLimit.error };
|
|
320
|
+
}
|
|
292
321
|
if (hasModel && !isSafeClaudeModelId(model)) {
|
|
293
322
|
return { ok: false, error: 'Invalid Claude model ID' };
|
|
294
323
|
}
|
|
@@ -302,6 +331,8 @@ export function parseClaudeSettingsUpdateRequest(body = {}) {
|
|
|
302
331
|
...(hasModels ? { models: payload.models } : {}),
|
|
303
332
|
...(hasEnabledModels ? { enabledModels: payload.enabledModels } : {}),
|
|
304
333
|
...(hasAttributionMode ? { attributionMode } : {}),
|
|
334
|
+
// Clamped to 30 min … 8 h and snapped to 30-minute steps.
|
|
335
|
+
...(backgroundCommandLimit ? { backgroundCommandLimitMinutes: backgroundCommandLimit.minutes } : {}),
|
|
305
336
|
};
|
|
306
337
|
}
|
|
307
338
|
|
|
@@ -1504,19 +1535,6 @@ function isLikelyCanonicalDuplicateMessage(a, b, timestampWindowMs = 30_000) {
|
|
|
1504
1535
|
return Math.abs(aTs - bTs) <= Math.max(1_000, Number(timestampWindowMs) || 30_000);
|
|
1505
1536
|
}
|
|
1506
1537
|
|
|
1507
|
-
function normalizeConversationTimestampMs(value) {
|
|
1508
|
-
const text = String(value || '').trim();
|
|
1509
|
-
if (!text) return 0;
|
|
1510
|
-
const sqliteUtcLike = /^(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?)$/;
|
|
1511
|
-
const hasExplicitTimezone = /(?:Z|[+-]\d{2}:?\d{2})$/i.test(text);
|
|
1512
|
-
let normalized = text;
|
|
1513
|
-
if (!hasExplicitTimezone && sqliteUtcLike.test(text)) {
|
|
1514
|
-
const [, day, time] = text.match(sqliteUtcLike) || [];
|
|
1515
|
-
normalized = `${day}T${time}Z`;
|
|
1516
|
-
}
|
|
1517
|
-
const parsed = Date.parse(normalized);
|
|
1518
|
-
return Number.isFinite(parsed) ? parsed : 0;
|
|
1519
|
-
}
|
|
1520
1538
|
|
|
1521
1539
|
function normalizeConversationTimestampIso(value, fallback = null) {
|
|
1522
1540
|
const ms = normalizeConversationTimestampMs(value);
|
|
@@ -1563,11 +1581,7 @@ function compareConversationMessageOrder(a, b) {
|
|
|
1563
1581
|
return aId.localeCompare(bId);
|
|
1564
1582
|
}
|
|
1565
1583
|
|
|
1566
|
-
export
|
|
1567
|
-
const numeric = Number(value);
|
|
1568
|
-
if (!Number.isFinite(numeric)) return fallback;
|
|
1569
|
-
return Math.min(100, Math.max(1, Math.trunc(numeric)));
|
|
1570
|
-
}
|
|
1584
|
+
export { normalizeConversationHistoryLimit };
|
|
1571
1585
|
|
|
1572
1586
|
function resolveConversationHistoryCursor(messages = [], {
|
|
1573
1587
|
messageId = '',
|
|
@@ -1672,26 +1686,9 @@ export function selectConversationHistoryPage(messages = [], {
|
|
|
1672
1686
|
}
|
|
1673
1687
|
const hasMoreOlder = firstIdx > 0;
|
|
1674
1688
|
const hasMoreNewer = lastIdx >= 0 && lastIdx < (ordered.length - 1);
|
|
1675
|
-
const oldestMessage = pageMessages[0] || null;
|
|
1676
1689
|
return {
|
|
1677
1690
|
messages: pageMessages,
|
|
1678
|
-
pageInfo: {
|
|
1679
|
-
hasMore: hasMoreOlder,
|
|
1680
|
-
hasMoreOlder,
|
|
1681
|
-
hasMoreNewer,
|
|
1682
|
-
nextCursor: oldestMessage ? {
|
|
1683
|
-
beforeMessageId: String(oldestMessage.id || '').trim(),
|
|
1684
|
-
beforeTimestamp: String(oldestMessage.timestamp || '').trim() || null,
|
|
1685
|
-
} : null,
|
|
1686
|
-
olderCursor: oldestMessage ? {
|
|
1687
|
-
beforeMessageId: String(oldestMessage.id || '').trim(),
|
|
1688
|
-
beforeTimestamp: String(oldestMessage.timestamp || '').trim() || null,
|
|
1689
|
-
} : null,
|
|
1690
|
-
newerCursor: lastMessage ? {
|
|
1691
|
-
afterMessageId: String(lastMessage.id || '').trim(),
|
|
1692
|
-
afterTimestamp: String(lastMessage.timestamp || '').trim() || null,
|
|
1693
|
-
} : null,
|
|
1694
|
-
},
|
|
1691
|
+
pageInfo: buildConversationHistoryPageInfo(pageMessages, { hasMoreOlder, hasMoreNewer }),
|
|
1695
1692
|
};
|
|
1696
1693
|
}
|
|
1697
1694
|
|
|
@@ -1762,6 +1759,10 @@ export function buildConversationMessages({
|
|
|
1762
1759
|
responseMessageToSourceId = new Map(),
|
|
1763
1760
|
queueRows = [],
|
|
1764
1761
|
usageByResponseMessageId = new Map(),
|
|
1762
|
+
// When dbMessages is one page: the conversation's rows liveResendsByOriginal
|
|
1763
|
+
// reads (id, role, source_message_id, resend_of_message_id), since a stopped
|
|
1764
|
+
// marker on the page may have its Resend outside it.
|
|
1765
|
+
resendIndexMessages = null,
|
|
1765
1766
|
} = {}) {
|
|
1766
1767
|
const queueRowsById = new Map(
|
|
1767
1768
|
(Array.isArray(queueRows) ? queueRows : [])
|
|
@@ -1770,7 +1771,7 @@ export function buildConversationMessages({
|
|
|
1770
1771
|
);
|
|
1771
1772
|
// A stopped steer's marker says "Resent" from the data, so it holds across
|
|
1772
1773
|
// reloads and devices (the relay refuses a second Resend on the same basis).
|
|
1773
|
-
const liveResends = liveResendsByOriginal(dbMessages, queueRows);
|
|
1774
|
+
const liveResends = liveResendsByOriginal(Array.isArray(resendIndexMessages) ? resendIndexMessages : dbMessages, queueRows);
|
|
1774
1775
|
const normalizedDbMessages = Array.isArray(dbMessages)
|
|
1775
1776
|
? dbMessages.map((message) => {
|
|
1776
1777
|
const id = String(message?.id || '').trim();
|
|
@@ -2143,6 +2144,10 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
2143
2144
|
setPwaAppName = () => ({ ok: false, status: 500, error: 'App name settings are unavailable' }),
|
|
2144
2145
|
getBackgroundTaskTimeoutMinutes = () => DEFAULT_BACKGROUND_TASK_TIMEOUT_MINUTES,
|
|
2145
2146
|
setBackgroundTaskTimeoutMinutes = () => ({ ok: false, error: 'Background task timeout settings are unavailable' }),
|
|
2147
|
+
getTaskStallMinutes = () => DEFAULT_TASK_STALL_MINUTES,
|
|
2148
|
+
setTaskStallMinutes = () => ({ ok: false, error: 'Task stall settings are unavailable' }),
|
|
2149
|
+
getRestartResumeEnabled = () => false,
|
|
2150
|
+
setRestartResumeEnabled = () => ({ ok: false, error: 'Restart settings are unavailable' }),
|
|
2146
2151
|
markSharedViewerPresence,
|
|
2147
2152
|
getSharedWatcherCount,
|
|
2148
2153
|
statusEventService,
|
|
@@ -2790,6 +2795,36 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
2790
2795
|
};
|
|
2791
2796
|
}
|
|
2792
2797
|
|
|
2798
|
+
// One history page selected in SQL (conversation-history-page.mjs), so
|
|
2799
|
+
// only the page is hydrated; null when the statements are absent or the
|
|
2800
|
+
// conversation cannot be paged in SQL, and the caller loads every row.
|
|
2801
|
+
function selectConversationHistoryPageRows(conversationId, cursor = {}, { sharedOnly = false } = {}) {
|
|
2802
|
+
const pager = stmts.conversationHistoryPage;
|
|
2803
|
+
if (!pager || typeof pager.selectConversationHistoryPage !== 'function') return null;
|
|
2804
|
+
return pager.selectConversationHistoryPage(conversationId, { ...cursor, sharedOnly }) || null;
|
|
2805
|
+
}
|
|
2806
|
+
|
|
2807
|
+
// The usage snapshots of a page's replies, keyed by reply id.
|
|
2808
|
+
function usageSnapshotsForPage(conversationId, pageRows = []) {
|
|
2809
|
+
const usage = new Map();
|
|
2810
|
+
if (!stmts.getMessageUsageSnapshotByResponse) return usage;
|
|
2811
|
+
for (const row of pageRows) {
|
|
2812
|
+
if (row?.role !== 'assistant') continue;
|
|
2813
|
+
const id = String(row?.id || '').trim();
|
|
2814
|
+
const snapshot = id ? mapUsageSnapshotRow(stmts.getMessageUsageSnapshotByResponse.get(id, conversationId)) : null;
|
|
2815
|
+
if (snapshot) usage.set(id, snapshot);
|
|
2816
|
+
}
|
|
2817
|
+
return usage;
|
|
2818
|
+
}
|
|
2819
|
+
|
|
2820
|
+
// What liveResendsByOriginal needs beyond a page: only read when the page
|
|
2821
|
+
// holds a stopped marker, the one place its result is shown.
|
|
2822
|
+
function resendIndexForPage(conversationId, pageRows = [], { sharedOnly = false } = {}) {
|
|
2823
|
+
const hasStoppedMarker = pageRows.some((row) => row?.role === 'assistant' && row?.kind === 'stopped');
|
|
2824
|
+
if (!hasStoppedMarker) return [];
|
|
2825
|
+
return stmts.conversationHistoryPage?.listConversationResendIndex?.(conversationId, { sharedOnly }) || [];
|
|
2826
|
+
}
|
|
2827
|
+
|
|
2793
2828
|
// A shared attachment is served by its share URL: the host path it is
|
|
2794
2829
|
// stored at (and the `@path` reference built from it) stays on the relay.
|
|
2795
2830
|
function shareSafeAttachment(attachment, shareToken) {
|
|
@@ -2820,9 +2855,11 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
2820
2855
|
});
|
|
2821
2856
|
const inFlight = inFlightStateForConversation(resolvedConversationId);
|
|
2822
2857
|
const sdkSessionId = String(conv.sdk_session_id || resolvedConversationId || '').trim();
|
|
2823
|
-
const
|
|
2824
|
-
|
|
2825
|
-
|
|
2858
|
+
const historyCursor = { limit, beforeMessageId, beforeTimestamp, afterMessageId, afterTimestamp, aroundMessageId };
|
|
2859
|
+
const sqlPage = selectConversationHistoryPageRows(resolvedConversationId, historyCursor, { sharedOnly: true });
|
|
2860
|
+
const dbMessages = filterMessagesVisibleToSharedView(sqlPage
|
|
2861
|
+
? sqlPage.rows
|
|
2862
|
+
: (stmts.getSharedMessages || stmts.getMessages).all(resolvedConversationId));
|
|
2826
2863
|
const queueRows = db.prepare(`
|
|
2827
2864
|
SELECT id, response_message_id, text, timestamp, retry_count, reasoning_effort, model, status
|
|
2828
2865
|
FROM queue
|
|
@@ -2853,6 +2890,20 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
2853
2890
|
.filter((m) => m.role === 'assistant')
|
|
2854
2891
|
.map((m) => [m.id, workflowRunsForResponse ? workflowRunsForResponse(m.id) : []]),
|
|
2855
2892
|
);
|
|
2893
|
+
// The page hydrates only its own cards, but the tokens to redact are the
|
|
2894
|
+
// whole conversation's: a reply on this page can quote a card's link from
|
|
2895
|
+
// outside it. The page's cards below add theirs too (a relay without the
|
|
2896
|
+
// statement still redacts what it shows).
|
|
2897
|
+
if (previewTokens) {
|
|
2898
|
+
for (const row of stmts.listPreviewCardJsonByConversation?.all(resolvedConversationId) || []) {
|
|
2899
|
+
try {
|
|
2900
|
+
const token = JSON.parse(String(row?.preview_json || ''))?.token;
|
|
2901
|
+
if (token) previewTokens.add?.(String(token));
|
|
2902
|
+
} catch {
|
|
2903
|
+
// An unreadable snapshot renders no card, so it has no link to quote.
|
|
2904
|
+
}
|
|
2905
|
+
}
|
|
2906
|
+
}
|
|
2856
2907
|
// A preview link is a capability that now outlives the turn and the
|
|
2857
2908
|
// restart: a public transcript keeps only the label, never token or link.
|
|
2858
2909
|
const previewCardsByMessageId = new Map(
|
|
@@ -2870,7 +2921,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
2870
2921
|
redacted: true,
|
|
2871
2922
|
}))]),
|
|
2872
2923
|
);
|
|
2873
|
-
const usageByResponseMessageId = new Map(
|
|
2924
|
+
const usageByResponseMessageId = sqlPage ? usageSnapshotsForPage(conv.id, dbMessages) : new Map(
|
|
2874
2925
|
(stmts.listMessageUsageSnapshotsByConversation?.all(conv.id) || [])
|
|
2875
2926
|
.map((row) => [String(row?.response_message_id || '').trim(), mapUsageSnapshotRow(row)])
|
|
2876
2927
|
.filter(([messageId, usage]) => !!messageId && !!usage),
|
|
@@ -2892,6 +2943,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
2892
2943
|
responseMessageToSourceId,
|
|
2893
2944
|
queueRows,
|
|
2894
2945
|
usageByResponseMessageId,
|
|
2946
|
+
resendIndexMessages: sqlPage ? resendIndexForPage(resolvedConversationId, dbMessages, { sharedOnly: true }) : null,
|
|
2895
2947
|
});
|
|
2896
2948
|
messages = messages.map((message) => {
|
|
2897
2949
|
const sourceMessageId = responseMessageToSourceId.get(String(message.id || '').trim()) || message.sourceMessageId || undefined;
|
|
@@ -2909,14 +2961,9 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
2909
2961
|
? nextMessage
|
|
2910
2962
|
: { ...nextMessage, attachments };
|
|
2911
2963
|
});
|
|
2912
|
-
const history =
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
beforeTimestamp,
|
|
2916
|
-
afterMessageId,
|
|
2917
|
-
afterTimestamp,
|
|
2918
|
-
aroundMessageId,
|
|
2919
|
-
});
|
|
2964
|
+
const history = sqlPage
|
|
2965
|
+
? { messages, pageInfo: sqlPage.pageInfo }
|
|
2966
|
+
: selectConversationHistoryPage(messages, historyCursor);
|
|
2920
2967
|
return {
|
|
2921
2968
|
id: resolvedConversationId,
|
|
2922
2969
|
sdkSessionId: conv.sdk_session_id || null,
|
|
@@ -4377,7 +4424,10 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
4377
4424
|
resolveClaudeSessionRoot,
|
|
4378
4425
|
resolveCursorSessionRoot,
|
|
4379
4426
|
});
|
|
4380
|
-
const
|
|
4427
|
+
const historyCursor = { limit, beforeMessageId, beforeTimestamp, afterMessageId, afterTimestamp, aroundMessageId };
|
|
4428
|
+
// The page's rows only: every later lookup is per page, not per conversation.
|
|
4429
|
+
const sqlPage = selectConversationHistoryPageRows(resolvedConversationId, historyCursor);
|
|
4430
|
+
const dbMessages = sqlPage ? sqlPage.rows : stmts.getMessages.all(resolvedConversationId);
|
|
4381
4431
|
const queueRows = db.prepare(`
|
|
4382
4432
|
SELECT id, response_message_id, text, timestamp, retry_count, reasoning_effort, model, status
|
|
4383
4433
|
FROM queue
|
|
@@ -4413,7 +4463,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
4413
4463
|
.filter((m) => m.role === 'assistant')
|
|
4414
4464
|
.map((m) => [m.id, previewCardsForResponse ? previewCardsForResponse(m.id) : []]),
|
|
4415
4465
|
);
|
|
4416
|
-
const usageByResponseMessageId = new Map(
|
|
4466
|
+
const usageByResponseMessageId = sqlPage ? usageSnapshotsForPage(conv.id, dbMessages) : new Map(
|
|
4417
4467
|
(stmts.listMessageUsageSnapshotsByConversation?.all(conv.id) || [])
|
|
4418
4468
|
.map((row) => [String(row?.response_message_id || '').trim(), mapUsageSnapshotRow(row)])
|
|
4419
4469
|
.filter(([messageId, usage]) => !!messageId && !!usage),
|
|
@@ -4432,20 +4482,16 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
4432
4482
|
responseMessageToSourceId,
|
|
4433
4483
|
queueRows,
|
|
4434
4484
|
usageByResponseMessageId,
|
|
4485
|
+
resendIndexMessages: sqlPage ? resendIndexForPage(resolvedConversationId, dbMessages, { sharedOnly: false }) : null,
|
|
4435
4486
|
});
|
|
4436
4487
|
messages = messages.map((message) => {
|
|
4437
4488
|
if (message.role !== 'assistant') return message;
|
|
4438
4489
|
const sourceMessageId = responseMessageToSourceId.get(String(message.id || '').trim()) || message.sourceMessageId || undefined;
|
|
4439
4490
|
return sourceMessageId ? { ...message, sourceMessageId } : message;
|
|
4440
4491
|
});
|
|
4441
|
-
const history =
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
beforeTimestamp,
|
|
4445
|
-
afterMessageId,
|
|
4446
|
-
afterTimestamp,
|
|
4447
|
-
aroundMessageId,
|
|
4448
|
-
});
|
|
4492
|
+
const history = sqlPage
|
|
4493
|
+
? { messages, pageInfo: sqlPage.pageInfo }
|
|
4494
|
+
: selectConversationHistoryPage(messages, historyCursor);
|
|
4449
4495
|
res.json({
|
|
4450
4496
|
id: conv.id,
|
|
4451
4497
|
sdkSessionId: conv.sdk_session_id || null,
|
|
@@ -4483,9 +4529,20 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
4483
4529
|
...(describeSessionActivity(resolvedConversationId) || {}),
|
|
4484
4530
|
// The published set, else the placeholder for background work a live
|
|
4485
4531
|
// worker reports without this runtime knowing its details.
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4532
|
+
// Each with the health poller's last reading (`health`); the tasks
|
|
4533
|
+
// that ended within the hour with a console beside them.
|
|
4534
|
+
backgroundTasks: (() => {
|
|
4535
|
+
const taskKeys = [resolvedConversationId, runtimeSession?.sdk_session_id];
|
|
4536
|
+
const tasks = backgroundWorkView
|
|
4537
|
+
? backgroundWorkView.panelTasks(...taskKeys)
|
|
4538
|
+
: (backgroundTaskStore?.get?.(resolvedConversationId) || []);
|
|
4539
|
+
return backgroundTaskStore?.withHealth ? backgroundTaskStore.withHealth(tasks, ...taskKeys) : tasks;
|
|
4540
|
+
})(),
|
|
4541
|
+
endedBackgroundTasks: backgroundTaskStore?.ended?.(resolvedConversationId, runtimeSession?.sdk_session_id) || [],
|
|
4542
|
+
// Minutes without output after which the panel calls a task quiet.
|
|
4543
|
+
taskStallMinutes: (() => {
|
|
4544
|
+
try { return getTaskStallMinutes(); } catch { return DEFAULT_TASK_STALL_MINUTES; }
|
|
4545
|
+
})(),
|
|
4489
4546
|
usageLimitPause: usageLimitPauseService?.getPause?.(resolvedConversationId) || null,
|
|
4490
4547
|
previews: listConversationPreviews?.(resolvedConversationId) || [],
|
|
4491
4548
|
// Every pin of the conversation, also those outside this history page.
|
|
@@ -5744,6 +5801,55 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
5744
5801
|
supportedModels: providerModelSelection.availableModels || [],
|
|
5745
5802
|
});
|
|
5746
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
|
+
}
|
|
5747
5853
|
if (useOpenAIProvider && openAISettings?.configured !== true) {
|
|
5748
5854
|
return res.status(400).json({
|
|
5749
5855
|
ok: false,
|
|
@@ -5794,7 +5900,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
5794
5900
|
bootstrapWorkspaceRootPath = validatedRoot.realPath;
|
|
5795
5901
|
}
|
|
5796
5902
|
|
|
5797
|
-
const selectedModel = providerModelSelection ? providerModelSelection.model : catalogSelectedModel;
|
|
5903
|
+
const selectedModel = providerModelSelection ? providerModelSelection.model : (copilotBoundModel || catalogSelectedModel);
|
|
5798
5904
|
if (requestedProviderType === 'openai-image' && !isOpenAIImageModelId(selectedModel)) {
|
|
5799
5905
|
return res.status(400).json({
|
|
5800
5906
|
ok: false,
|
|
@@ -5866,7 +5972,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
5866
5972
|
// first send would apply, so the composer can restore it from the server
|
|
5867
5973
|
// instead of guessing from localStorage.
|
|
5868
5974
|
const bootstrapReasoning = resolveProviderReasoningEffort({
|
|
5869
|
-
requestedEffort: req.body?.reasoningEffort,
|
|
5975
|
+
requestedEffort: req.body?.reasoningEffort || copilotVariantEffort,
|
|
5870
5976
|
strict: bootstrapProviderType === 'openai',
|
|
5871
5977
|
supportedEfforts: supportedReasoningEffortsForProviderModel({
|
|
5872
5978
|
providerType: bootstrapProviderType,
|
|
@@ -6291,6 +6397,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6291
6397
|
models: Array.isArray(settings?.models) ? settings.models : [],
|
|
6292
6398
|
availableModels: Array.isArray(settings?.availableModels) ? settings.availableModels : [],
|
|
6293
6399
|
...claudeAttributionSettingsFields(settings),
|
|
6400
|
+
...claudeBackgroundCommandLimitSettingsFields(settings),
|
|
6294
6401
|
});
|
|
6295
6402
|
});
|
|
6296
6403
|
|
|
@@ -6340,6 +6447,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6340
6447
|
models: Array.isArray(currentSettings?.models) ? currentSettings.models : [],
|
|
6341
6448
|
availableModels: Array.isArray(currentSettings?.availableModels) ? currentSettings.availableModels : [],
|
|
6342
6449
|
...claudeAttributionSettingsFields(currentSettings),
|
|
6450
|
+
...claudeBackgroundCommandLimitSettingsFields(currentSettings),
|
|
6343
6451
|
reconciliation,
|
|
6344
6452
|
};
|
|
6345
6453
|
io.emit('models_updated', buildModelCatalogWithProviders(getModelCatalogState()));
|
|
@@ -7075,6 +7183,40 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
7075
7183
|
});
|
|
7076
7184
|
});
|
|
7077
7185
|
|
|
7186
|
+
// "Mark a background task as quiet after N minutes" (task_stall_minutes).
|
|
7187
|
+
const taskStallMinutesPayload = (minutes) => ({
|
|
7188
|
+
minutes,
|
|
7189
|
+
min: TASK_STALL_MINUTES_MIN,
|
|
7190
|
+
max: TASK_STALL_MINUTES_MAX,
|
|
7191
|
+
default: DEFAULT_TASK_STALL_MINUTES,
|
|
7192
|
+
});
|
|
7193
|
+
|
|
7194
|
+
app.get('/api/settings/task-stall-minutes', auth, (_req, res) => {
|
|
7195
|
+
res.json(taskStallMinutesPayload(getTaskStallMinutes()));
|
|
7196
|
+
});
|
|
7197
|
+
|
|
7198
|
+
app.post('/api/settings/task-stall-minutes', auth, (req, res) => {
|
|
7199
|
+
const parsed = parseTaskStallMinutesUpdate(req.body?.minutes);
|
|
7200
|
+
if (!parsed.ok) return res.status(400).json({ error: parsed.error });
|
|
7201
|
+
const result = setTaskStallMinutes(parsed.minutes);
|
|
7202
|
+
if (!result.ok) return res.status(500).json({ error: result.error });
|
|
7203
|
+
return res.json(taskStallMinutesPayload(result.minutes));
|
|
7204
|
+
});
|
|
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
|
+
|
|
7078
7220
|
const pwaAppNamePayload = (appName) => ({
|
|
7079
7221
|
appName,
|
|
7080
7222
|
defaultName: PWA_APP_NAME_DEFAULT,
|