@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,101 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Migration: the task console's rows outlive a relay restart.
|
|
5
|
+
*
|
|
6
|
+
* `background_task_rows` holds what the relay keeps of a background task
|
|
7
|
+
* that has a console (a shell or Monitor task naming its output file), per
|
|
8
|
+
* store key (the id the worker posts under, its SDK session id) and task:
|
|
9
|
+
*
|
|
10
|
+
* - `state` 'live': a task of the worker's last set. After a restart it is
|
|
11
|
+
* held back until the worker reports again: the worker's set decides which
|
|
12
|
+
* are still running, and those of a worker that does not report are marked
|
|
13
|
+
* ended, never shown as running again.
|
|
14
|
+
* - `state` 'ended': the dimmed row the panel keeps for an hour after the end,
|
|
15
|
+
* with its status and exit code. `ended_at` (epoch ms) is what the hour
|
|
16
|
+
* counts from.
|
|
17
|
+
* - `row_json` the task row as the store holds it, without the health
|
|
18
|
+
* reading (the poller reads the file again).
|
|
19
|
+
*
|
|
20
|
+
* Rows go when the hour is up, when the conversation is deleted, and live
|
|
21
|
+
* rows when the worker's set no longer names them.
|
|
22
|
+
*
|
|
23
|
+
* Additive and idempotent. Safe to run on every boot. An older build never
|
|
24
|
+
* reads the table; its consoles start empty after a restart as before.
|
|
25
|
+
*
|
|
26
|
+
* Usage: node server/migrations/0018-background-task-rows.mjs [path/to/copilot.db]
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { fileURLToPath } from 'url';
|
|
30
|
+
import path from 'path';
|
|
31
|
+
import process from 'process';
|
|
32
|
+
import Database from 'better-sqlite3';
|
|
33
|
+
|
|
34
|
+
export const BACKGROUND_TASK_ROWS_COLUMNS = Object.freeze(['store_key', 'task_id', 'state', 'row_json', 'ended_at', 'updated_at']);
|
|
35
|
+
|
|
36
|
+
function tableExists(db, name) {
|
|
37
|
+
return !!db
|
|
38
|
+
.prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?`)
|
|
39
|
+
.get(name);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Why the table cannot be used (a table of that name with other columns), or ''. */
|
|
43
|
+
export function backgroundTaskRowsTableProblem(db) {
|
|
44
|
+
if (!tableExists(db, 'background_task_rows')) return 'no background_task_rows table';
|
|
45
|
+
const present = new Set(db.prepare(`PRAGMA table_info(background_task_rows)`).all().map((column) => column.name));
|
|
46
|
+
const missing = BACKGROUND_TASK_ROWS_COLUMNS.filter((name) => !present.has(name));
|
|
47
|
+
return missing.length ? `background_task_rows lacks ${missing.join(', ')}` : '';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function migrateBackgroundTaskRows(db) {
|
|
51
|
+
let tablesAdded = 0;
|
|
52
|
+
if (!tableExists(db, 'background_task_rows')) {
|
|
53
|
+
db.exec(`
|
|
54
|
+
CREATE TABLE background_task_rows (
|
|
55
|
+
store_key TEXT NOT NULL,
|
|
56
|
+
task_id TEXT NOT NULL,
|
|
57
|
+
state TEXT NOT NULL CHECK (state IN ('live', 'ended')),
|
|
58
|
+
row_json TEXT NOT NULL,
|
|
59
|
+
ended_at INTEGER,
|
|
60
|
+
updated_at INTEGER NOT NULL,
|
|
61
|
+
PRIMARY KEY (store_key, task_id)
|
|
62
|
+
)
|
|
63
|
+
`);
|
|
64
|
+
tablesAdded += 1;
|
|
65
|
+
}
|
|
66
|
+
db.exec(`
|
|
67
|
+
CREATE INDEX IF NOT EXISTS idx_background_task_rows_ended
|
|
68
|
+
ON background_task_rows(state, ended_at)
|
|
69
|
+
`);
|
|
70
|
+
return { applied: tablesAdded > 0, tablesAdded };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function defaultDbPath() {
|
|
74
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
75
|
+
return path.resolve(here, '..', 'data', 'copilot.db');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function migrate(dbPath, options = {}) {
|
|
79
|
+
const db = new Database(dbPath, options);
|
|
80
|
+
try {
|
|
81
|
+
return migrateBackgroundTaskRows(db);
|
|
82
|
+
} finally {
|
|
83
|
+
db.close();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
88
|
+
if (isMain) {
|
|
89
|
+
const dbPath = process.argv[2] ? path.resolve(process.argv[2]) : defaultDbPath();
|
|
90
|
+
try {
|
|
91
|
+
const result = migrate(dbPath);
|
|
92
|
+
console.log(`Migration 0018 on ${dbPath}`);
|
|
93
|
+
console.log(result.applied
|
|
94
|
+
? ` background task rows: tables added ${result.tablesAdded}`
|
|
95
|
+
: ' skipped: nothing to change');
|
|
96
|
+
process.exit(0);
|
|
97
|
+
} catch (err) {
|
|
98
|
+
console.error(`Migration 0018 failed: ${err?.message || err}`);
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
} from './api-client.js';
|
|
21
21
|
import { isSuspendHostActionVisible } from './settings-modal.js';
|
|
22
22
|
import { describeRelayRestartError } from './relay-restart-error.mjs';
|
|
23
|
+
import { initRelayRestartUi, openRestartRelayConfirmation } from './relay-restart-ui.js';
|
|
23
24
|
import {
|
|
24
25
|
applyHostSuspendState,
|
|
25
26
|
describeHostSuspendBlocker,
|
|
@@ -106,21 +107,10 @@ export async function confirmKillCurrentSession() {
|
|
|
106
107
|
}
|
|
107
108
|
}
|
|
108
109
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
kind: 'restart-relay',
|
|
114
|
-
bodyHtml: `
|
|
115
|
-
<p>Queue a manual relay restart now?</p>
|
|
116
|
-
<p>The restart waits until the current turn is idle, so it does not interrupt an in-flight turn immediately.</p>
|
|
117
|
-
<div class="summary-modal-actions">
|
|
118
|
-
<button class="chat-title-action-btn" type="button" onclick="confirmRestartWebRelay()">🌄 Restart web relay</button>
|
|
119
|
-
<button class="chat-title-action-btn" type="button" onclick="closeSummaryModal()">Cancel</button>
|
|
120
|
-
</div>
|
|
121
|
-
`,
|
|
122
|
-
});
|
|
123
|
-
}
|
|
110
|
+
// ⋯ → Restart web relay first shows what the restart would wait for and
|
|
111
|
+
// leave running (relay-restart-ui.js); its buttons land in
|
|
112
|
+
// confirmRestartWebRelay below or in confirmRestartRelayNow.
|
|
113
|
+
export { openRestartRelayConfirmation };
|
|
124
114
|
|
|
125
115
|
export function openEmptyQueueConfirmation() {
|
|
126
116
|
menuDeps.lockChatActionsMenuShield(350);
|
|
@@ -375,6 +365,10 @@ export async function confirmRestartWebRelay() {
|
|
|
375
365
|
showTransientRelayNotice('Relay is already shutting down/restarting.', 7000);
|
|
376
366
|
return;
|
|
377
367
|
}
|
|
368
|
+
if (result.status === 'shutting_down') {
|
|
369
|
+
showTransientRelayNotice('Nothing was running: the relay is restarting now.', 7000);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
378
372
|
const queue = result.queue || {};
|
|
379
373
|
showTransientRelayNotice(
|
|
380
374
|
`Relay restart queued (pending=${Number(queue.pendingCount || 0)}, processing=${Number(queue.processingCount || 0)}).`,
|
|
@@ -399,6 +393,9 @@ export function initActionConfirmations({
|
|
|
399
393
|
syncQueueStatusMenuEntry: typeof syncQueueStatusMenuEntry === 'function' ? syncQueueStatusMenuEntry : menuDeps.syncQueueStatusMenuEntry,
|
|
400
394
|
refreshSessionWorkerStatus: typeof refreshSessionWorkerStatus === 'function' ? refreshSessionWorkerStatus : menuDeps.refreshSessionWorkerStatus,
|
|
401
395
|
};
|
|
396
|
+
// The restart confirmations' and banner's inline handlers live on window
|
|
397
|
+
// either way (bootstrap exposes this module's own).
|
|
398
|
+
initRelayRestartUi();
|
|
402
399
|
if (!exposeOnWindow) return;
|
|
403
400
|
window.openKillSessionConfirmation = openKillSessionConfirmation;
|
|
404
401
|
window.openRestartRelayConfirmation = openRestartRelayConfirmation;
|
|
@@ -45,7 +45,7 @@ export function areNetworkRequestsEnabled() {
|
|
|
45
45
|
return networkRequestsEnabled;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
export async function apiFetch(url, opts = {}) {
|
|
48
|
+
export async function apiFetch(url, { onErrorResponse, ...opts } = {}) {
|
|
49
49
|
// A keepalive request is a page's last word; it goes out even when the
|
|
50
50
|
// page's regular network work is already suspended.
|
|
51
51
|
if (!networkRequestsEnabled && !opts.keepalive) return null;
|
|
@@ -61,6 +61,9 @@ export async function apiFetch(url, opts = {}) {
|
|
|
61
61
|
});
|
|
62
62
|
if (!response.ok) {
|
|
63
63
|
console.error('API error', response.status, url);
|
|
64
|
+
// A caller that shows why a request failed reads the answer here; the
|
|
65
|
+
// result is still null.
|
|
66
|
+
if (typeof onErrorResponse === 'function') await onErrorResponse(response);
|
|
64
67
|
return null;
|
|
65
68
|
}
|
|
66
69
|
noteFetchSuccess();
|
|
@@ -328,12 +331,14 @@ export async function updateClaudeSettings({
|
|
|
328
331
|
model = undefined,
|
|
329
332
|
enabledModels = undefined,
|
|
330
333
|
attributionMode = undefined,
|
|
334
|
+
backgroundCommandLimitMinutes = undefined,
|
|
331
335
|
} = {}) {
|
|
332
336
|
const payload = {};
|
|
333
337
|
if (typeof enabled === 'boolean') payload.enabled = enabled;
|
|
334
338
|
if (typeof model === 'string' && model.trim()) payload.model = model.trim();
|
|
335
339
|
if (Array.isArray(enabledModels)) payload.enabledModels = enabledModels;
|
|
336
340
|
if (typeof attributionMode === 'string' && attributionMode.trim()) payload.attributionMode = attributionMode.trim();
|
|
341
|
+
if (Number.isFinite(backgroundCommandLimitMinutes)) payload.backgroundCommandLimitMinutes = backgroundCommandLimitMinutes;
|
|
337
342
|
return settingsRequest('/api/settings/claude', payload, 'Failed to update Claude settings');
|
|
338
343
|
}
|
|
339
344
|
|
|
@@ -572,6 +577,29 @@ export async function updateBackgroundTaskTimeoutSetting(timeoutMinutes) {
|
|
|
572
577
|
});
|
|
573
578
|
}
|
|
574
579
|
|
|
580
|
+
export async function loadTaskStallMinutesSetting() {
|
|
581
|
+
return apiFetch('/api/settings/task-stall-minutes');
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
export async function updateTaskStallMinutesSetting(minutes) {
|
|
585
|
+
return apiFetch('/api/settings/task-stall-minutes', {
|
|
586
|
+
method: 'POST',
|
|
587
|
+
body: JSON.stringify({ minutes: Number(minutes) }),
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// "After a relay restart, tell interrupted sessions to carry on" (off by default).
|
|
592
|
+
export async function loadRestartResumeSetting() {
|
|
593
|
+
return apiFetch('/api/settings/restart-resume');
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
export async function updateRestartResumeSetting(enabled) {
|
|
597
|
+
return apiFetch('/api/settings/restart-resume', {
|
|
598
|
+
method: 'POST',
|
|
599
|
+
body: JSON.stringify({ enabled: enabled === true }),
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
|
|
575
603
|
export async function loadPwaAppNameSetting() {
|
|
576
604
|
return apiFetch('/api/settings/pwa-app-name');
|
|
577
605
|
}
|
|
@@ -1412,6 +1440,31 @@ export async function cancelRelayRestart(body = {}) {
|
|
|
1412
1440
|
});
|
|
1413
1441
|
}
|
|
1414
1442
|
|
|
1443
|
+
// The queued restart's state, and the picture for a restart asked now: the
|
|
1444
|
+
// sessions it would wait for, the messages it would hold, the background
|
|
1445
|
+
// tasks and whether they survive (routes/relay-restart-routes.mjs).
|
|
1446
|
+
export async function loadRelayRestartPreview(action = 'restart') {
|
|
1447
|
+
return apiFetchOrThrow(`/api/relay/shutdown?action=${encodeURIComponent(action)}`);
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
export async function keepWaitingForRelayRestart(body = {}) {
|
|
1451
|
+
return apiFetchOrThrow('/api/relay/shutdown/keep-waiting', {
|
|
1452
|
+
method: 'POST',
|
|
1453
|
+
body: JSON.stringify({ requestedBy: 'localhost-api', ...(body || {}) }),
|
|
1454
|
+
});
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
export async function loadRelayRestartMaxWaitSetting() {
|
|
1458
|
+
return apiFetch('/api/settings/relay-restart-max-wait');
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
export async function updateRelayRestartMaxWaitSetting(maxWaitMinutes) {
|
|
1462
|
+
return apiFetchOrThrow('/api/settings/relay-restart-max-wait', {
|
|
1463
|
+
method: 'POST',
|
|
1464
|
+
body: JSON.stringify({ maxWaitMinutes: Number(maxWaitMinutes) }),
|
|
1465
|
+
});
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1415
1468
|
export async function requestQueueEmpty(body = {}) {
|
|
1416
1469
|
return apiFetchOrThrow('/api/queue/empty', {
|
|
1417
1470
|
method: 'POST',
|
|
@@ -1423,8 +1476,22 @@ export async function requestQueueEmpty(body = {}) {
|
|
|
1423
1476
|
});
|
|
1424
1477
|
}
|
|
1425
1478
|
|
|
1426
|
-
|
|
1427
|
-
|
|
1479
|
+
// Null on any failure, like apiFetch. A send the relay refused for a reason
|
|
1480
|
+
// the user can act on (a 409 such as "this conversation's model is no longer
|
|
1481
|
+
// available") hands that reason to onRefused first, so the composer can say
|
|
1482
|
+
// it instead of "could not be sent". A replayed send that already landed
|
|
1483
|
+
// (DUPLICATE_MESSAGE_ID) is not a refusal.
|
|
1484
|
+
export async function sendMessage(body, { onRefused } = {}) {
|
|
1485
|
+
return apiFetch('/api/message', {
|
|
1486
|
+
method: 'POST',
|
|
1487
|
+
body: JSON.stringify(body),
|
|
1488
|
+
onErrorResponse: async (response) => {
|
|
1489
|
+
if (response.status !== 409 || typeof onRefused !== 'function') return;
|
|
1490
|
+
const payload = await response.json().catch(() => null);
|
|
1491
|
+
const reason = readableServerError(payload?.error);
|
|
1492
|
+
if (reason && payload?.code !== 'DUPLICATE_MESSAGE_ID') onRefused(reason, payload);
|
|
1493
|
+
},
|
|
1494
|
+
});
|
|
1428
1495
|
}
|
|
1429
1496
|
|
|
1430
1497
|
// Raw fetch instead of apiFetch: a rejected bootstrap (bad CWD, unavailable
|