@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.
Files changed (90) hide show
  1. package/CHANGELOG.md +332 -0
  2. package/README.md +10 -10
  3. package/package.json +1 -1
  4. package/server/README.md +201 -12
  5. package/server/claude-worker/claude-remote-relay-tool.mjs +2 -0
  6. package/server/claude-worker/claude-sdk-adapter.mjs +57 -4
  7. package/server/claude-worker/claude-session-process.mjs +179 -0
  8. package/server/claude-worker/claude-session-worker.mjs +17 -0
  9. package/server/claude-worker/task-output-discovery.mjs +152 -0
  10. package/server/copilot-worker/copilot-continuation-signals.mjs +11 -5
  11. package/server/copilot-worker/copilot-sdk-session-process.mjs +169 -3
  12. package/server/copilot-worker/copilot-sdk-session-worker.mjs +2 -0
  13. package/server/copilot-worker/copilot-shell-output-mirror.mjs +94 -0
  14. package/server/copilot-worker/copilot-task-settle.mjs +96 -0
  15. package/server/db-schema.mjs +19 -0
  16. package/server/grok-worker/acp-host-services.mjs +48 -6
  17. package/server/grok-worker/grok-background-tasks.mjs +248 -0
  18. package/server/grok-worker/grok-sdk-adapter.mjs +3 -1
  19. package/server/grok-worker/grok-session-worker.mjs +33 -1
  20. package/server/grok-worker/grok-turn-runner.mjs +11 -0
  21. package/server/migrations/0017-conversation-history-indexes.mjs +89 -0
  22. package/server/migrations/0018-background-task-rows.mjs +101 -0
  23. package/server/public/app/action-confirmations.js +12 -15
  24. package/server/public/app/api-client.js +70 -3
  25. package/server/public/app/background-tasks-view.mjs +334 -16
  26. package/server/public/app/bootstrap.js +102 -5
  27. package/server/public/app/claude-background-command-limit-options.mjs +21 -0
  28. package/server/public/app/conversation-payload-cache.mjs +357 -0
  29. package/server/public/app/conversation-preferences.mjs +10 -4
  30. package/server/public/app/conversation-turn-state.mjs +1 -1
  31. package/server/public/app/conversation-view.js +462 -8
  32. package/server/public/app/conversation-window-retention.mjs +96 -0
  33. package/server/public/app/host-suspend-ui.js +45 -44
  34. package/server/public/app/journal-view.js +259 -67
  35. package/server/public/app/push-settings.js +1 -0
  36. package/server/public/app/relay-prompt-sanitizer.mjs +20 -6
  37. package/server/public/app/relay-restart-note-view.mjs +36 -0
  38. package/server/public/app/relay-restart-ui.js +568 -0
  39. package/server/public/app/settings-modal.js +162 -2
  40. package/server/public/app/socket-handlers.js +59 -7
  41. package/server/public/app/store.js +25 -0
  42. package/server/public/app/subagent-viewer.mjs +7 -1
  43. package/server/public/app/task-console-render.mjs +401 -0
  44. package/server/public/app/task-console-watch.mjs +42 -0
  45. package/server/public/app/task-console.mjs +730 -0
  46. package/server/public/app/task-output-text.mjs +243 -0
  47. package/server/public/app/transcript-island.mjs +73 -0
  48. package/server/public/app/transcript-jump-bottom-view.mjs +42 -8
  49. package/server/public/app/transcript-jump-bottom.mjs +68 -11
  50. package/server/public/app/transcript-reading-anchor.mjs +256 -0
  51. package/server/public/app/transcript-reload-freshness.mjs +17 -1
  52. package/server/public/app/transcript-viewport-anchor.mjs +20 -1
  53. package/server/public/index.html +306 -6
  54. package/server/relay-tools.md +6 -0
  55. package/server/repositories/message-repository.mjs +11 -0
  56. package/server/repositories/question-repository.mjs +4 -0
  57. package/server/repositories/remote-relay-repository.mjs +4 -2
  58. package/server/routes/messages-routes.mjs +397 -23
  59. package/server/routes/relay-board-routes.mjs +19 -2
  60. package/server/routes/relay-restart-routes.mjs +73 -0
  61. package/server/routes/sessions-routes.mjs +208 -66
  62. package/server/server-runtime.mjs +269 -137
  63. package/server/services/about-service.mjs +13 -12
  64. package/server/services/background-task-persistence.mjs +133 -0
  65. package/server/services/background-task-store.mjs +470 -6
  66. package/server/services/cli-process-runner.mjs +4 -21
  67. package/server/services/conversation-history-page.mjs +199 -0
  68. package/server/services/conversation-model-fallback.mjs +57 -0
  69. package/server/services/host-suspend-command.mjs +2 -1
  70. package/server/services/preview-proxy-server.mjs +14 -1
  71. package/server/services/process-list-service.mjs +2 -1
  72. package/server/services/provider-model-selection.mjs +195 -0
  73. package/server/services/push-dispatch-service.mjs +9 -2
  74. package/server/services/relay-event-service.mjs +27 -9
  75. package/server/services/relay-prompt-sanitizer.mjs +18 -4
  76. package/server/services/relay-restart-picture.mjs +209 -0
  77. package/server/services/relay-restart-queue.mjs +380 -0
  78. package/server/services/relay-shutdown-gate.mjs +4 -1
  79. package/server/services/remote-relay-dispatcher.mjs +316 -20
  80. package/server/services/restart-resume-service.mjs +303 -0
  81. package/server/services/session-activity-tracker.mjs +6 -2
  82. package/server/services/session-worker-process-service.mjs +6 -4
  83. package/server/services/task-output-rooms.mjs +259 -0
  84. package/server/services/task-output-service.mjs +761 -0
  85. package/shared/claude-background-command-limit.mjs +83 -0
  86. package/shared/relay-events.mjs +55 -2
  87. package/shared/remote-relay-contract.mjs +47 -10
  88. package/shared/task-output-text.mjs +850 -0
  89. package/shared/worker-runtime/task-output-spool.mjs +181 -0
  90. 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 { escHtml, showTransientRelayNotice } from './store.js';
10
- import { cancelHostSuspend, cancelRelayRestart } from './api-client.js';
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
- const action = state.action === 'restart' || state.restart ? 'restart' : 'shutdown';
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
- const restarting = String(relayShutdownState?.status || '') === 'shutting_down';
215
- rows.push(bannerRow({
216
- key: 'relay-shutdown',
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
  }