@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
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// "Messages kept per conversation for switching back" (Settings › General):
|
|
2
|
+
// how many loaded messages the page keeps of a conversation it leaves, so a
|
|
3
|
+
// switch back shows the same rows. A setting of this browser, kept in
|
|
4
|
+
// localStorage like the scroll-on-send one. The slider runs from 100 to 1,000
|
|
5
|
+
// by 100 and has one more step at the far end, no limit. The document and
|
|
6
|
+
// the storage are passed in by the tests.
|
|
7
|
+
|
|
8
|
+
export const WINDOW_RETENTION_STORAGE_KEY = 'copilot_conversation_window_retention';
|
|
9
|
+
export const WINDOW_RETENTION_SLIDER_ID = 'conversation-window-retention-slider';
|
|
10
|
+
export const WINDOW_RETENTION_VALUE_ID = 'conversation-window-retention-value';
|
|
11
|
+
export const WINDOW_RETENTION_DEFAULT = 200;
|
|
12
|
+
export const WINDOW_RETENTION_MIN = 100;
|
|
13
|
+
export const WINDOW_RETENTION_MAX = 1000;
|
|
14
|
+
export const WINDOW_RETENTION_STEP = 100;
|
|
15
|
+
/** The slider's last step: no limit. */
|
|
16
|
+
export const WINDOW_RETENTION_UNLIMITED_POSITION = WINDOW_RETENTION_MAX + WINDOW_RETENTION_STEP;
|
|
17
|
+
const UNLIMITED_TOKEN = 'none';
|
|
18
|
+
|
|
19
|
+
function clampToStep(value) {
|
|
20
|
+
const stepped = Math.round(value / WINDOW_RETENTION_STEP) * WINDOW_RETENTION_STEP;
|
|
21
|
+
return Math.min(WINDOW_RETENTION_MAX, Math.max(WINDOW_RETENTION_MIN, stepped));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The cap a stored value stands for: a count, or Infinity for no limit. */
|
|
25
|
+
export function parseWindowRetention(stored) {
|
|
26
|
+
const text = String(stored ?? '').trim().toLowerCase();
|
|
27
|
+
if (!text) return WINDOW_RETENTION_DEFAULT;
|
|
28
|
+
if (text === UNLIMITED_TOKEN) return Infinity;
|
|
29
|
+
const value = Number(text);
|
|
30
|
+
if (!Number.isFinite(value) || value <= 0) return WINDOW_RETENTION_DEFAULT;
|
|
31
|
+
return clampToStep(value);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function serializeWindowRetention(cap) {
|
|
35
|
+
return Number.isFinite(Number(cap)) ? String(clampToStep(Number(cap))) : UNLIMITED_TOKEN;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The cap at a slider position; the far end is no limit. */
|
|
39
|
+
export function windowRetentionFromSlider(position) {
|
|
40
|
+
const value = Number(position);
|
|
41
|
+
if (!Number.isFinite(value)) return WINDOW_RETENTION_DEFAULT;
|
|
42
|
+
if (value >= WINDOW_RETENTION_UNLIMITED_POSITION) return Infinity;
|
|
43
|
+
return clampToStep(value);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function sliderPositionForWindowRetention(cap) {
|
|
47
|
+
return Number.isFinite(Number(cap)) ? clampToStep(Number(cap)) : WINDOW_RETENTION_UNLIMITED_POSITION;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function formatWindowRetention(cap) {
|
|
51
|
+
if (!Number.isFinite(Number(cap))) return 'No limit';
|
|
52
|
+
return `${clampToStep(Number(cap)).toLocaleString('en-US')} messages`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function storage() {
|
|
56
|
+
try { return globalThis.localStorage || null; } catch { return null; }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function readWindowRetention(store = storage()) {
|
|
60
|
+
try {
|
|
61
|
+
return parseWindowRetention(store?.getItem(WINDOW_RETENTION_STORAGE_KEY));
|
|
62
|
+
} catch {
|
|
63
|
+
return WINDOW_RETENTION_DEFAULT;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function showValue(doc, cap) {
|
|
68
|
+
const text = formatWindowRetention(cap);
|
|
69
|
+
const label = doc?.getElementById?.(WINDOW_RETENTION_VALUE_ID);
|
|
70
|
+
if (label) label.textContent = text;
|
|
71
|
+
doc?.getElementById?.(WINDOW_RETENTION_SLIDER_ID)?.setAttribute?.('aria-valuetext', text);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function syncWindowRetentionSlider(doc = globalThis.document, store = storage()) {
|
|
75
|
+
const cap = readWindowRetention(store);
|
|
76
|
+
const slider = doc?.getElementById?.(WINDOW_RETENTION_SLIDER_ID);
|
|
77
|
+
if (slider) slider.value = String(sliderPositionForWindowRetention(cap));
|
|
78
|
+
showValue(doc, cap);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** While the thumb moves: the label follows, nothing is saved. */
|
|
82
|
+
export function previewWindowRetention(position, { doc = globalThis.document } = {}) {
|
|
83
|
+
showValue(doc, windowRetentionFromSlider(position));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Saves the slider's value and hands the new cap to `onChange` (the kept
|
|
88
|
+
* conversations are cut down to it). Returns the cap.
|
|
89
|
+
*/
|
|
90
|
+
export function updateWindowRetention(position, { doc = globalThis.document, store = storage(), onChange = null } = {}) {
|
|
91
|
+
const cap = windowRetentionFromSlider(position);
|
|
92
|
+
try { store?.setItem(WINDOW_RETENTION_STORAGE_KEY, serializeWindowRetention(cap)); } catch {}
|
|
93
|
+
syncWindowRetentionSlider(doc, store);
|
|
94
|
+
if (typeof onChange === 'function') onChange(cap);
|
|
95
|
+
return cap;
|
|
96
|
+
}
|
|
@@ -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
|
}
|