@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
|
@@ -6,8 +6,19 @@
|
|
|
6
6
|
// host action" setting on purpose: a device with the action hidden must still
|
|
7
7
|
// be able to see and cancel a queued suspend.
|
|
8
8
|
|
|
9
|
-
import {
|
|
10
|
-
import { cancelHostSuspend
|
|
9
|
+
import { showTransientRelayNotice } from './store.js';
|
|
10
|
+
import { cancelHostSuspend } from './api-client.js';
|
|
11
|
+
import {
|
|
12
|
+
cancelQueuedRelayRestart,
|
|
13
|
+
isRelayRestartRequestInFlight,
|
|
14
|
+
keepWaitingForQueuedRelayRestart,
|
|
15
|
+
noteRelayRestartStateForDialogs,
|
|
16
|
+
openRestartNowConfirmation,
|
|
17
|
+
refreshRelayRestartWaitedLabels,
|
|
18
|
+
relayRestartHeadline,
|
|
19
|
+
renderRelayRestartRow,
|
|
20
|
+
setRelayRestartStateSink,
|
|
21
|
+
} from './relay-restart-ui.js';
|
|
11
22
|
|
|
12
23
|
const BANNER_ID = 'pending-action-banner';
|
|
13
24
|
const SUSPEND_MENU_ID = 'chat-menu-suspend-host';
|
|
@@ -20,6 +31,7 @@ const RESTART_MENU_PENDING_LABEL = '🌄 Restart pending…';
|
|
|
20
31
|
let hostSuspendState = null;
|
|
21
32
|
let relayShutdownState = null;
|
|
22
33
|
let countdownTimer = null;
|
|
34
|
+
let waitedTimer = null;
|
|
23
35
|
let cancelInFlight = false;
|
|
24
36
|
|
|
25
37
|
export function getHostSuspendState() {
|
|
@@ -96,20 +108,10 @@ export function hostSuspendBannerText(state, nowMs = Date.now()) {
|
|
|
96
108
|
: '💤 Suspend queued — waiting for running agents to finish';
|
|
97
109
|
}
|
|
98
110
|
|
|
111
|
+
/** The restart row's headline (relay-restart-ui.js renders the whole row). */
|
|
99
112
|
export function relayShutdownBannerText(state) {
|
|
100
113
|
if (!isRelayShutdownPending(state)) return '';
|
|
101
|
-
|
|
102
|
-
const icon = action === 'restart' ? '🌄' : '⏻';
|
|
103
|
-
if (String(state.status || '') === 'shutting_down') {
|
|
104
|
-
return `${icon} Relay ${action === 'restart' ? 'restarting' : 'shutting down'} now…`;
|
|
105
|
-
}
|
|
106
|
-
// Only the processing turns hold it; queued messages wait for the restart.
|
|
107
|
-
const queue = state.queue || {};
|
|
108
|
-
const turns = Number(queue.processingCount || 0);
|
|
109
|
-
const held = Number(queue.pendingCount || 0) + Number(queue.parkedCount || 0);
|
|
110
|
-
const waiting = turns > 0 ? `waiting for ${turns} running turn${turns === 1 ? '' : 's'} to finish` : 'nothing is running, going down now';
|
|
111
|
-
const queued = held > 0 ? `; ${held} queued message${held === 1 ? '' : 's'} wait${held === 1 ? 's' : ''} for it` : '';
|
|
112
|
-
return `${icon} Relay ${action} queued — ${waiting}${queued}`;
|
|
114
|
+
return relayRestartHeadline(state);
|
|
113
115
|
}
|
|
114
116
|
|
|
115
117
|
function ensureBanner() {
|
|
@@ -165,30 +167,6 @@ async function runCancelHostSuspend() {
|
|
|
165
167
|
}
|
|
166
168
|
}
|
|
167
169
|
|
|
168
|
-
async function runCancelRelayShutdown() {
|
|
169
|
-
if (cancelInFlight) return;
|
|
170
|
-
cancelInFlight = true;
|
|
171
|
-
renderPendingActionBanner();
|
|
172
|
-
try {
|
|
173
|
-
let result = null;
|
|
174
|
-
try {
|
|
175
|
-
result = await cancelRelayRestart();
|
|
176
|
-
} catch (error) {
|
|
177
|
-
showTransientRelayNotice(`Failed to cancel the queued relay restart: ${error?.message || 'unknown error'}`, 6000);
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
if (!result?.ok) {
|
|
181
|
-
showTransientRelayNotice('Failed to cancel the queued relay restart.', 5000);
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
applyRelayShutdownState(result);
|
|
185
|
-
showTransientRelayNotice(result.cancelled ? 'Queued relay restart cancelled.' : 'No relay restart was pending.', 4000);
|
|
186
|
-
} finally {
|
|
187
|
-
cancelInFlight = false;
|
|
188
|
-
renderPendingActionBanner();
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
170
|
function syncCountdownTimer() {
|
|
193
171
|
const needsTicker = isHostSuspendPending() && String(hostSuspendState?.status || '') === 'countdown' && hostSuspendState?.fireAt;
|
|
194
172
|
if (needsTicker && !countdownTimer) {
|
|
@@ -197,8 +175,27 @@ function syncCountdownTimer() {
|
|
|
197
175
|
clearInterval(countdownTimer);
|
|
198
176
|
countdownTimer = null;
|
|
199
177
|
}
|
|
178
|
+
// The restart's waited time ticks in place: rebuilding the row every
|
|
179
|
+
// second would swallow a click on its buttons.
|
|
180
|
+
const needsWaitedTicker = String(relayShutdownState?.status || '') === 'queued';
|
|
181
|
+
if (needsWaitedTicker && !waitedTimer) {
|
|
182
|
+
waitedTimer = setInterval(() => refreshRelayRestartWaitedLabels(relayShutdownState), 15_000);
|
|
183
|
+
waitedTimer?.unref?.();
|
|
184
|
+
} else if (!needsWaitedTicker && waitedTimer) {
|
|
185
|
+
clearInterval(waitedTimer);
|
|
186
|
+
waitedTimer = null;
|
|
187
|
+
}
|
|
200
188
|
}
|
|
201
189
|
|
|
190
|
+
const relayRestartHandlers = {
|
|
191
|
+
onOpen: (conversationId) => {
|
|
192
|
+
if (conversationId && typeof window.openConversation === 'function') void window.openConversation(conversationId);
|
|
193
|
+
},
|
|
194
|
+
onRestartNow: () => openRestartNowConfirmation(relayShutdownState),
|
|
195
|
+
onKeepWaiting: () => keepWaitingForQueuedRelayRestart(),
|
|
196
|
+
onCancel: () => cancelQueuedRelayRestart(),
|
|
197
|
+
};
|
|
198
|
+
|
|
202
199
|
export function renderPendingActionBanner() {
|
|
203
200
|
if (typeof document === 'undefined') return;
|
|
204
201
|
const rows = [];
|
|
@@ -211,12 +208,9 @@ export function renderPendingActionBanner() {
|
|
|
211
208
|
}));
|
|
212
209
|
}
|
|
213
210
|
if (isRelayShutdownPending()) {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
text: relayShutdownBannerText(relayShutdownState),
|
|
218
|
-
cancelLabel: restarting ? '' : 'Cancel',
|
|
219
|
-
onCancel: runCancelRelayShutdown,
|
|
211
|
+
rows.push(renderRelayRestartRow(relayShutdownState, {
|
|
212
|
+
handlers: relayRestartHandlers,
|
|
213
|
+
busy: isRelayRestartRequestInFlight(),
|
|
220
214
|
}));
|
|
221
215
|
}
|
|
222
216
|
const banner = document.getElementById(BANNER_ID) || (rows.length ? ensureBanner() : null);
|
|
@@ -246,6 +240,7 @@ export function applyHostSuspendState(state) {
|
|
|
246
240
|
|
|
247
241
|
export function applyRelayShutdownState(state) {
|
|
248
242
|
relayShutdownState = state && typeof state === 'object' ? state : null;
|
|
243
|
+
noteRelayRestartStateForDialogs(relayShutdownState);
|
|
249
244
|
renderPendingActionBanner();
|
|
250
245
|
}
|
|
251
246
|
|
|
@@ -268,6 +263,11 @@ export function applyPendingActionsFromStatus(status) {
|
|
|
268
263
|
}
|
|
269
264
|
|
|
270
265
|
export function initHostSuspendUi() {
|
|
266
|
+
// A cancel, keep-waiting or restart-now answer carries the new state.
|
|
267
|
+
setRelayRestartStateSink((state, { rerenderOnly = false } = {}) => {
|
|
268
|
+
if (rerenderOnly) renderPendingActionBanner();
|
|
269
|
+
else applyRelayShutdownState(state);
|
|
270
|
+
});
|
|
271
271
|
renderPendingActionBanner();
|
|
272
272
|
}
|
|
273
273
|
|
|
@@ -276,6 +276,7 @@ export function __resetHostSuspendUiForTests() {
|
|
|
276
276
|
relayShutdownState = null;
|
|
277
277
|
cancelInFlight = false;
|
|
278
278
|
if (countdownTimer) { clearInterval(countdownTimer); countdownTimer = null; }
|
|
279
|
+
if (waitedTimer) { clearInterval(waitedTimer); waitedTimer = null; }
|
|
279
280
|
document.getElementById(BANNER_ID)?.remove();
|
|
280
281
|
syncMenuLabels();
|
|
281
282
|
}
|
|
@@ -82,6 +82,7 @@ import { mergeTailPage } from './transcript-reading-anchor.mjs';
|
|
|
82
82
|
import { readWindowRetention } from './conversation-window-retention.mjs';
|
|
83
83
|
import { MENU_SEPARATOR, bindLongPress, closeContextMenu, openContextMenu } from './context-menu.mjs';
|
|
84
84
|
import {
|
|
85
|
+
isBackgroundTasksSnapshotCurrent,
|
|
85
86
|
setBackgroundTasksConversation,
|
|
86
87
|
setConversationBackgroundTasks,
|
|
87
88
|
setConversationEndedBackgroundTasks,
|
|
@@ -777,6 +778,8 @@ export function toggleConversationGroupFold(event, groupId) {
|
|
|
777
778
|
// rather than drawn anew (mergeMessagesIntoTranscript).
|
|
778
779
|
// windowPayload: with keepTranscript, the window the page keeps, kept for
|
|
779
780
|
// the next visit in place of nothing.
|
|
781
|
+
// backgroundTasksAsOf: backgroundTasksSnapshotMark(id) as the request left;
|
|
782
|
+
// the payload's task sets are left out when an event brought newer ones since.
|
|
780
783
|
export function applyLoadedConversationState(id, response, {
|
|
781
784
|
restoreScroll = false,
|
|
782
785
|
savedScrollTop = null,
|
|
@@ -786,6 +789,7 @@ export function applyLoadedConversationState(id, response, {
|
|
|
786
789
|
fromCache = false,
|
|
787
790
|
mergeFrom = null,
|
|
788
791
|
windowPayload = null,
|
|
792
|
+
backgroundTasksAsOf = null,
|
|
789
793
|
} = {}) {
|
|
790
794
|
if (!response) {
|
|
791
795
|
if (!IS_SHARED_VIEW) conversationPayloadCache.forget(id);
|
|
@@ -892,9 +896,11 @@ export function applyLoadedConversationState(id, response, {
|
|
|
892
896
|
setBackgroundTasksConversation(id);
|
|
893
897
|
// Absent from an older relay's payload: the threshold keeps its default.
|
|
894
898
|
if ('taskStallMinutes' in response) setConversationTaskStallMinutes(id, response.taskStallMinutes);
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
899
|
+
if (isBackgroundTasksSnapshotCurrent(id, backgroundTasksAsOf)) {
|
|
900
|
+
setConversationBackgroundTasks(id, response.backgroundTasks || []);
|
|
901
|
+
// Absent from an older relay's payload: keep what the socket said.
|
|
902
|
+
if (Array.isArray(response.endedBackgroundTasks)) setConversationEndedBackgroundTasks(id, response.endedBackgroundTasks);
|
|
903
|
+
}
|
|
898
904
|
setUsageLimitConversation(id, {
|
|
899
905
|
// Absent from an older relay's payload: keep what the socket said.
|
|
900
906
|
pause: 'usageLimitPause' in response ? response.usageLimitPause : undefined,
|
|
@@ -11,6 +11,7 @@ const PUSH_EVENT_CONTROL_IDS = {
|
|
|
11
11
|
board: 'push-event-board-toggle',
|
|
12
12
|
cliOffline: 'push-event-cli-offline-toggle',
|
|
13
13
|
hostSuspend: 'push-event-host-suspend-toggle',
|
|
14
|
+
relayRestart: 'push-event-relay-restart-toggle',
|
|
14
15
|
};
|
|
15
16
|
|
|
16
17
|
let pushUpdateInFlight = false;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Browser mirror of questionAcceptsFreeText in shared/question-answer-rule.mjs
|
|
2
|
+
// (the relay's answer route applies the same rule). The browser cannot import
|
|
3
|
+
// shared/ (only server/public is served), so this is a hand-kept copy;
|
|
4
|
+
// question-answer-rule.test.mjs asserts it behaves exactly like the original.
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Whether a card takes typed text. A card with choices takes it only when it
|
|
8
|
+
* says so (`allowFreeform: true`); a missing flag counts as false there, as
|
|
9
|
+
* the create route and the relay's question rows read it. Without choices a
|
|
10
|
+
* card always does: it could not be answered otherwise.
|
|
11
|
+
*/
|
|
12
|
+
export function questionAcceptsFreeText(question) {
|
|
13
|
+
const choices = Array.isArray(question?.choices) ? question.choices : [];
|
|
14
|
+
return question?.allowFreeform === true || choices.length === 0;
|
|
15
|
+
}
|
|
@@ -14,14 +14,21 @@
|
|
|
14
14
|
// answer is the selected labels joined with ", " — plus whatever the user
|
|
15
15
|
// typed as an extra answer — which is Claude's own multi-select answer format.
|
|
16
16
|
|
|
17
|
+
import { questionAcceptsFreeText } from './question-answer-rule.mjs';
|
|
18
|
+
|
|
17
19
|
function choiceCount(question) {
|
|
18
20
|
return Array.isArray(question?.choices) ? question.choices.length : 0;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
|
-
/**
|
|
23
|
+
/**
|
|
24
|
+
* Whether the card offers the "Select several" switch. Never on a card that
|
|
25
|
+
* takes no typed text: the relay takes several choices there only when the
|
|
26
|
+
* provider flagged the card multi-select.
|
|
27
|
+
*/
|
|
22
28
|
export function offersMultiSelectToggle(question) {
|
|
23
29
|
if (!question || typeof question !== 'object') return false;
|
|
24
|
-
return choiceCount(question) >= 2 && question?.context?.allowMultiSelect === true
|
|
30
|
+
return choiceCount(question) >= 2 && question?.context?.allowMultiSelect === true
|
|
31
|
+
&& questionAcceptsFreeText(question);
|
|
25
32
|
}
|
|
26
33
|
|
|
27
34
|
/**
|
|
@@ -24,6 +24,7 @@ const EVENT_TYPES = new Set([
|
|
|
24
24
|
'heartbeat',
|
|
25
25
|
'link_lost',
|
|
26
26
|
'link_restored',
|
|
27
|
+
'relay_restarted',
|
|
27
28
|
]);
|
|
28
29
|
const URGENT_TYPES = new Set(['question_asked', 'error']);
|
|
29
30
|
const DIGEST_PATTERN = /^Digest of \d+ events?/;
|
|
@@ -43,6 +44,7 @@ const TYPE_LABELS = Object.freeze({
|
|
|
43
44
|
heartbeat: 'heartbeat',
|
|
44
45
|
link_lost: 'link lost',
|
|
45
46
|
link_restored: 'link back',
|
|
47
|
+
relay_restarted: 'relay restarted',
|
|
46
48
|
});
|
|
47
49
|
|
|
48
50
|
function defaultEscape(value) {
|
|
@@ -181,6 +183,8 @@ export function watchersOf(subscriptions, conversationId, { includeAll = true }
|
|
|
181
183
|
export function watchingChipText(summary) {
|
|
182
184
|
if (!summary?.subscriptions?.length) return '';
|
|
183
185
|
if (summary.all) return summary.count ? `watching all sessions (+${summary.count} listed)` : 'watching all sessions';
|
|
186
|
+
// Only relay_restarted: no session is watched.
|
|
187
|
+
if (!summary.count) return 'waiting for a relay restart';
|
|
184
188
|
return `watching ${summary.count} session${summary.count === 1 ? '' : 's'}`;
|
|
185
189
|
}
|
|
186
190
|
|
|
@@ -198,7 +202,7 @@ function describeSubscription(sub, conversations) {
|
|
|
198
202
|
const listed = (Array.isArray(sub.sessions) ? sub.sessions : []).filter((session) => session?.id).map(titleOf).join(', ');
|
|
199
203
|
const what = sub.scope === 'all'
|
|
200
204
|
? (paired ? `All sessions of ${relay}` : 'All sessions')
|
|
201
|
-
: (paired ? `${relay} · ${listed}` : listed);
|
|
205
|
+
: (paired ? `${relay} · ${listed}` : (listed || 'This relay'));
|
|
202
206
|
const extras = [
|
|
203
207
|
sub.once ? 'once' : '',
|
|
204
208
|
sub.heartbeatMinutes ? `heartbeat ${sub.heartbeatMinutes} min` : '',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { stripRemotePromptHeader } from './remote-relay-shared.mjs';
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const RELAY_TOOL_GUIDANCE_LINES = [
|
|
4
4
|
'# Relay Tool Guidance',
|
|
5
5
|
'For any user-facing question or clarification, use the ask_user tool so the web relay can render question cards and buttons. Never ask questions in plain assistant text.',
|
|
6
6
|
'When using ask_user, ALWAYS include a `choices` array with 2-6 answer options so the web relay can render clickable buttons. Only omit choices when the question genuinely requires freeform text input.',
|
|
@@ -8,7 +8,12 @@ const RELAY_TOOL_GUIDANCE = [
|
|
|
8
8
|
'For relay restarts in extension-managed mode, require explicit user permission first, then use the authenticated localhost API `POST /api/relay/shutdown`. Do not restart by killing processes or using respawn scripts.',
|
|
9
9
|
'Note: shutdown is queued and only completes when the current turn finishes, so it is pointless to wait for it to interrupt an active turn.',
|
|
10
10
|
'Use `restart: true` in the request body when the user wants a real relay restart rather than a plain shutdown. Example request body: `{ "reason": "manual-restart", "requestedBy": "localhost-api", "restart": true }`.',
|
|
11
|
-
|
|
11
|
+
'Never pass `immediate` in that request body: it restarts without waiting for the running turns, which only the user may decide, with Restart now in the web UI.',
|
|
12
|
+
];
|
|
13
|
+
const RELAY_TOOL_GUIDANCE = RELAY_TOOL_GUIDANCE_LINES.join(' ');
|
|
14
|
+
// Prompts stored before the `immediate` line was added carry the guidance
|
|
15
|
+
// without it.
|
|
16
|
+
const PREVIOUS_RELAY_TOOL_GUIDANCE = RELAY_TOOL_GUIDANCE_LINES.slice(0, -1).join(' ');
|
|
12
17
|
|
|
13
18
|
const MODE_MARKERS = {
|
|
14
19
|
plan: '[Relay mode: plan]',
|
|
@@ -94,6 +99,8 @@ function buildPromptPrefixPatterns(mode) {
|
|
|
94
99
|
return [
|
|
95
100
|
new RegExp(`^${whitespaceFlexiblePattern(currentFullPrefix)}\\s*`, 'i'),
|
|
96
101
|
new RegExp(`^${whitespaceFlexiblePattern(legacyFullPrefix)}\\s*`, 'i'),
|
|
102
|
+
new RegExp(`^${whitespaceFlexiblePattern(`${marker} ${MODE_INSTRUCTIONS[key]} ${PREVIOUS_RELAY_TOOL_GUIDANCE}`)}\\s*`, 'i'),
|
|
103
|
+
new RegExp(`^${whitespaceFlexiblePattern(`${marker} ${LEGACY_MODE_INSTRUCTIONS[key]} ${PREVIOUS_RELAY_TOOL_GUIDANCE}`)}\\s*`, 'i'),
|
|
97
104
|
new RegExp(`^${whitespaceFlexiblePattern(marker)}\\s*`, 'i'),
|
|
98
105
|
];
|
|
99
106
|
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// The note the relay writes into a conversation whose turn a relay restart
|
|
2
|
+
// cut off ("After a relay restart, tell interrupted sessions to carry on",
|
|
3
|
+
// server/services/restart-resume-service.mjs): a compact row in the look of
|
|
4
|
+
// the relay event rows, never a bubble of the user's.
|
|
5
|
+
//
|
|
6
|
+
// DOM-free, like relay-event-view.mjs: the caller owns the element.
|
|
7
|
+
|
|
8
|
+
export const RESTART_RESUME_KIND = 'relay-restart-resume';
|
|
9
|
+
export const RESTART_RESUME_LEAD = 'Automatic message from OAR, not from the user:';
|
|
10
|
+
|
|
11
|
+
function defaultEscape(value) {
|
|
12
|
+
return String(value ?? '')
|
|
13
|
+
.replace(/&/g, '&')
|
|
14
|
+
.replace(/</g, '<')
|
|
15
|
+
.replace(/>/g, '>')
|
|
16
|
+
.replace(/"/g, '"')
|
|
17
|
+
.replace(/'/g, ''');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function isRestartResumeNote(message) {
|
|
21
|
+
return message?.role === 'user' && String(message?.kind || '').trim() === RESTART_RESUME_KIND;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The row's markup: folded to one line, opened to the note as the agent got it. */
|
|
25
|
+
export function renderRestartResumeNoteHtml(message, { escHtml = defaultEscape, timeText = '' } = {}) {
|
|
26
|
+
const text = String(message?.text ?? '').trim();
|
|
27
|
+
const body = text.startsWith(RESTART_RESUME_LEAD) ? text.slice(RESTART_RESUME_LEAD.length).trim() : text;
|
|
28
|
+
return '<details class="relay-event-row relay-restart-note">'
|
|
29
|
+
+ '<summary class="relay-event-summary">'
|
|
30
|
+
+ '<span class="relay-event-tag" title="An automated message of the relay, not from you.">relay</span>'
|
|
31
|
+
+ '<span class="relay-event-text">Relay restarted · the interrupted turn was asked to carry on</span>'
|
|
32
|
+
+ (timeText ? `<span class="relay-event-time">${escHtml(timeText)}</span>` : '')
|
|
33
|
+
+ '</summary>'
|
|
34
|
+
+ `<div class="relay-event-lines relay-restart-note-text">${escHtml(body)}</div>`
|
|
35
|
+
+ '</details>';
|
|
36
|
+
}
|