@oar-sh/oar 0.9.8 → 0.9.10
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 +478 -0
- package/README.md +129 -39
- package/bin/bat/oar.bat +46 -46
- package/bin/oar.js +814 -228
- package/package.json +5 -6
- package/server/README.md +306 -4
- package/server/claude-worker/claude-remote-relay-tool.mjs +10 -0
- package/server/claude-worker/claude-session-process.mjs +35 -1
- package/server/claude-worker/claude-turn-publisher.mjs +3 -0
- package/server/claude-worker/sdk-message-normalizer.mjs +31 -4
- package/server/claude-worker/subagent-timeline-recorder.mjs +572 -0
- package/server/copilot-worker/copilot-sdk-session-process.mjs +3 -1
- package/server/cursor-worker/cursor-sdk-adapter.mjs +56 -2
- package/server/db-schema.mjs +44 -0
- package/server/migrations/0008-message-pins.mjs +85 -0
- package/server/migrations/0009-subagent-timeline.mjs +135 -0
- package/server/migrations/0010-subagent-timeline-pruning.mjs +89 -0
- package/server/migrations/0011-relay-event-subscriptions.mjs +125 -0
- package/server/migrations/0012-cross-relay-event-subscriptions.mjs +116 -0
- package/server/public/app/agent-exchange-fold-view.mjs +248 -0
- package/server/public/app/api-client.js +91 -0
- package/server/public/app/archive-confirm.mjs +28 -0
- package/server/public/app/ask-user-view.js +3 -0
- package/server/public/app/attachments-view.js +1024 -131
- package/server/public/app/back-guard.mjs +58 -0
- package/server/public/app/background-tasks-view.mjs +18 -26
- package/server/public/app/bootstrap.js +230 -35
- package/server/public/app/chat-actions-menu-state.mjs +116 -0
- package/server/public/app/claude-cloud-conversation-ui.js +10 -22
- package/server/public/app/composer-control-state.mjs +16 -0
- package/server/public/app/composer-keyboard.mjs +54 -0
- package/server/public/app/conversation-view.js +471 -110
- package/server/public/app/explorer-edit-state.mjs +68 -0
- package/server/public/app/external-link-policy.mjs +4 -1
- package/server/public/app/file-version.mjs +58 -0
- package/server/public/app/host-suspend-ui.js +6 -3
- package/server/public/app/journal-view.js +161 -27
- package/server/public/app/no-conversation-state.mjs +58 -0
- package/server/public/app/pinned-messages-view.mjs +253 -0
- package/server/public/app/provider-readiness.mjs +120 -0
- package/server/public/app/pwa-install.js +1 -1
- package/server/public/app/relay-board-view.js +3 -0
- package/server/public/app/relay-dot-state.mjs +8 -1
- package/server/public/app/relay-event-subscriptions-ui.js +184 -0
- package/server/public/app/relay-event-view.mjs +234 -0
- package/server/public/app/router.js +8 -2
- package/server/public/app/settings-modal.js +53 -1
- package/server/public/app/socket-handlers.js +65 -28
- package/server/public/app/store.js +25 -2
- package/server/public/app/subagent-row.mjs +138 -0
- package/server/public/app/subagent-timeline-view.mjs +186 -0
- package/server/public/app/subagent-viewer-watch.mjs +50 -0
- package/server/public/app/subagent-viewer.mjs +534 -0
- package/server/public/app/task-row-format.mjs +29 -0
- package/server/public/app/thinking-follow.js +4 -0
- package/server/public/app/transcript-jump-bottom-view.mjs +290 -0
- package/server/public/app/transcript-jump-bottom.mjs +99 -0
- package/server/public/app/transcript-reload-freshness.mjs +25 -0
- package/server/public/app/update-ui.js +1 -1
- package/server/public/app/viewer-chat-gallery.mjs +59 -0
- package/server/public/app/viewer-controls-state.mjs +59 -0
- package/server/public/app/viewer-gallery.mjs +46 -0
- package/server/public/app/viewer-gestures.mjs +105 -0
- package/server/public/app/viewer-history.mjs +45 -0
- package/server/public/app/viewer-tap.mjs +112 -0
- package/server/public/index.html +700 -59
- package/server/repositories/message-pin-statements.mjs +39 -0
- package/server/repositories/message-repository.mjs +34 -3
- package/server/repositories/question-repository.mjs +9 -3
- package/server/repositories/remote-relay-repository.mjs +6 -2
- package/server/repositories/subagent-timeline-statements.mjs +170 -0
- package/server/routes/ask-user-routes.mjs +3 -0
- package/server/routes/messages-routes.mjs +548 -111
- package/server/routes/relay-event-routes.mjs +157 -0
- package/server/routes/remote-relay-routes.mjs +15 -0
- package/server/routes/sessions-routes.mjs +164 -24
- package/server/server-runtime.mjs +274 -62
- package/server/services/cloudflared-tunnel-service.mjs +139 -41
- package/server/services/explorer-entry-name.mjs +26 -0
- package/server/services/explorer-mutations.mjs +122 -0
- package/server/services/file-serving.mjs +155 -0
- package/server/services/message-pin-list.mjs +52 -0
- package/server/services/oar-cli-helpers.mjs +139 -4
- package/server/services/oar-cli-menu.mjs +240 -0
- package/server/services/oar-cli-wsl.mjs +129 -0
- package/server/services/preview-proxy-server.mjs +41 -23
- package/server/services/push-dispatch-service.mjs +18 -0
- package/server/services/relay-event-link-service.mjs +289 -0
- package/server/services/relay-event-service.mjs +1935 -0
- package/server/services/relay-shutdown-gate.mjs +31 -0
- package/server/services/remote-relay-caller-context.mjs +52 -7
- package/server/services/remote-relay-dispatcher.mjs +494 -2
- package/server/services/remote-relay-registry-service.mjs +11 -1
- package/server/services/safe-served-content.mjs +61 -11
- package/server/services/session-activity-tracker.mjs +487 -0
- package/server/services/session-history-refresh-service.mjs +31 -0
- package/server/services/stale-attempt-log-limiter.mjs +96 -0
- package/server/services/stale-recovery-clock-guard.mjs +57 -0
- package/server/services/stale-subagent-run-sweep.mjs +109 -0
- package/server/services/subagent-record-retention.mjs +164 -0
- package/server/services/subagent-timeline-service.mjs +328 -0
- package/server/services/subagent-viewer-rooms.mjs +119 -0
- package/server/services/upgrade-socket-guard.mjs +16 -0
- package/shared/relay-events.mjs +371 -0
- package/shared/remote-relay-contract.mjs +157 -5
- package/shared/subagent-record-retention.mjs +31 -0
- package/shared/subagent-timeline.mjs +79 -0
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
normalizeWorkspaceRootKey,
|
|
25
25
|
} from './services/workspace-root-path-policy.mjs';
|
|
26
26
|
import { stopSessionWorkerProcesses } from './services/session-worker-stop-service.mjs';
|
|
27
|
+
import { applyCdnNoStoreHeaders } from './services/file-serving.mjs';
|
|
27
28
|
import { pickLiveTurnRowId } from './services/live-turn-picker.mjs';
|
|
28
29
|
import { buildSteerSettleFailure, steerAgentLabelForProvider } from '../shared/steer-settle-failure.mjs';
|
|
29
30
|
import { sanitizeRelayQuestionContext as sanitizeRelayQuestionContextWith } from './services/relay-question-context.mjs';
|
|
@@ -42,6 +43,7 @@ import { registerGitRoutes } from './routes/git-routes.mjs';
|
|
|
42
43
|
import { registerClaudeCloudRoutes } from './routes/claude-cloud-routes.mjs';
|
|
43
44
|
import { registerPreviewRoutes } from './routes/preview-routes.mjs';
|
|
44
45
|
import { registerRemoteRelayRoutes } from './routes/remote-relay-routes.mjs';
|
|
46
|
+
import { registerRelayEventRoutes } from './routes/relay-event-routes.mjs';
|
|
45
47
|
import { createRemoteRelayRepository } from './repositories/remote-relay-repository.mjs';
|
|
46
48
|
import { createRemoteRelayClient } from './services/remote-relay-client.mjs';
|
|
47
49
|
import { createRemoteRelayLoopbackClient } from './services/remote-relay-loopback.mjs';
|
|
@@ -67,6 +69,9 @@ import webpush from 'web-push';
|
|
|
67
69
|
import { createPushDispatchService, ensurePushVapidKeys } from './services/push-dispatch-service.mjs';
|
|
68
70
|
import { createHostSuspendService } from './services/host-suspend-service.mjs';
|
|
69
71
|
import { USAGE_LIMIT_ACCOUNT_SOCKET_EVENT, createUsageLimitPauseService } from './services/usage-limit-pause-service.mjs';
|
|
72
|
+
import { createSessionActivityTracker } from './services/session-activity-tracker.mjs';
|
|
73
|
+
import { createRelayEventService } from './services/relay-event-service.mjs';
|
|
74
|
+
import { createRelayEventLinkService } from './services/relay-event-link-service.mjs';
|
|
70
75
|
import { buildClaudeAttributionSettings, normalizeClaudeAttributionMode, resolveClaudeAttributionMode, DEFAULT_CLAUDE_ATTRIBUTION_MODE } from '../shared/claude-attribution.mjs';
|
|
71
76
|
import { createWorkerLaunchNoteService } from './services/worker-launch-note-service.mjs';
|
|
72
77
|
import { createGitHubCiActivityService } from './services/github-ci-activity-service.mjs';
|
|
@@ -115,12 +120,14 @@ import {
|
|
|
115
120
|
import { createPreviewProxyServer } from './services/preview-proxy-server.mjs';
|
|
116
121
|
import { createPreviewHealthProbe } from './services/preview-health-probe.mjs';
|
|
117
122
|
import { createTunnelWorkerPathGuard } from './services/tunnel-worker-path-guard.mjs';
|
|
123
|
+
import { guardUpgradeSocket } from './services/upgrade-socket-guard.mjs';
|
|
118
124
|
import { createWindowsAutostartService } from './services/windows-autostart-service.mjs';
|
|
119
125
|
import { createWindowsBootAutostartService } from './services/windows-boot-autostart-service.mjs';
|
|
120
126
|
import { createSessionWorkerWebSocketService } from './services/session-worker-websocket-service.mjs';
|
|
121
127
|
import { createTmuxInspectorAccessPolicy } from './services/tmux-inspector-access-policy.mjs';
|
|
122
128
|
import { createTmuxInspectorStreamService } from './services/tmux-inspector-stream-service.mjs';
|
|
123
129
|
import { createTmuxInspectorSocketService } from './services/tmux-inspector-socket-service.mjs';
|
|
130
|
+
import { createSubagentViewerRooms } from './services/subagent-viewer-rooms.mjs';
|
|
124
131
|
import {
|
|
125
132
|
resolveDefaultSessionWorkspaceRootState as resolveDefaultSessionWorkspaceRootStateFromService,
|
|
126
133
|
resolveLaunchWorkspaceRootPath,
|
|
@@ -141,6 +148,19 @@ import {
|
|
|
141
148
|
readTurnCeilingSetting,
|
|
142
149
|
turnCeilingMinutesToMs,
|
|
143
150
|
} from '../shared/turn-ceiling.mjs';
|
|
151
|
+
import {
|
|
152
|
+
SUBAGENT_RECORD_RETENTION_SETTING_KEY,
|
|
153
|
+
parseSubagentRecordRetentionUpdate,
|
|
154
|
+
readSubagentRecordRetentionSetting,
|
|
155
|
+
} from '../shared/subagent-record-retention.mjs';
|
|
156
|
+
import { createSubagentRecordRetention } from './services/subagent-record-retention.mjs';
|
|
157
|
+
import { isRelayShutdownQueued, queuedShutdownCanProceed, relayShutdownGateReply } from './services/relay-shutdown-gate.mjs';
|
|
158
|
+
import { createStaleRecoveryClockGuard } from './services/stale-recovery-clock-guard.mjs';
|
|
159
|
+
import {
|
|
160
|
+
STALE_SUBAGENT_RUN_BOOT_DELAY_MS,
|
|
161
|
+
STALE_SUBAGENT_RUN_SWEEP_INTERVAL_MS,
|
|
162
|
+
createStaleSubagentRunSweep,
|
|
163
|
+
} from './services/stale-subagent-run-sweep.mjs';
|
|
144
164
|
import {
|
|
145
165
|
normalizePwaAppName,
|
|
146
166
|
readPwaAppNameSetting,
|
|
@@ -177,6 +197,7 @@ import {
|
|
|
177
197
|
} from '../shared/model-id.mjs';
|
|
178
198
|
import { createModelVariantCatalogService } from './services/model-variant-catalog-service.mjs';
|
|
179
199
|
import { applyPwaManifestTheme, normalizePwaTheme } from './services/pwa-manifest-theme.mjs';
|
|
200
|
+
import { mapSubagentRunHeader } from './services/subagent-timeline-service.mjs';
|
|
180
201
|
|
|
181
202
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
182
203
|
const REPO_ROOT = path.resolve(__dirname, '..');
|
|
@@ -1510,6 +1531,17 @@ const WORKSPACE_CONTENT_TYPES = Object.freeze({
|
|
|
1510
1531
|
'.gif': 'image/gif',
|
|
1511
1532
|
'.webp': 'image/webp',
|
|
1512
1533
|
'.ico': 'image/x-icon',
|
|
1534
|
+
'.bmp': 'image/bmp',
|
|
1535
|
+
'.avif': 'image/avif',
|
|
1536
|
+
'.mp3': 'audio/mpeg',
|
|
1537
|
+
'.wav': 'audio/wav',
|
|
1538
|
+
'.m4a': 'audio/mp4',
|
|
1539
|
+
'.aac': 'audio/aac',
|
|
1540
|
+
'.ogg': 'audio/ogg',
|
|
1541
|
+
'.oga': 'audio/ogg',
|
|
1542
|
+
'.opus': 'audio/ogg',
|
|
1543
|
+
'.flac': 'audio/flac',
|
|
1544
|
+
'.weba': 'audio/webm',
|
|
1513
1545
|
'.mp4': 'video/mp4',
|
|
1514
1546
|
'.m4v': 'video/mp4',
|
|
1515
1547
|
'.mov': 'video/quicktime',
|
|
@@ -1650,6 +1682,7 @@ const WORKSPACE_CODE_EXTENSIONS = new Set(Object.keys(WORKSPACE_PREVIEW_LANGUAGE
|
|
|
1650
1682
|
.filter((ext) => !WORKSPACE_MARKDOWN_EXTENSIONS.has(ext)));
|
|
1651
1683
|
const WORKSPACE_IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg', '.ico', '.bmp', '.avif']);
|
|
1652
1684
|
const WORKSPACE_VIDEO_EXTENSIONS = new Set(['.mp4', '.m4v', '.mov', '.webm', '.mkv', '.avi', '.ogv', '.wmv']);
|
|
1685
|
+
const WORKSPACE_AUDIO_EXTENSIONS = new Set(['.mp3', '.wav', '.m4a', '.aac', '.ogg', '.oga', '.opus', '.flac', '.weba']);
|
|
1653
1686
|
const REPO_HEAVY_DIR_NAMES = new Set(['.git', 'node_modules']);
|
|
1654
1687
|
const MAX_REPO_TREE_NODES = 20_000;
|
|
1655
1688
|
const MAX_REPO_TREE_DEPTH = 64;
|
|
@@ -1779,6 +1812,8 @@ const OFFLINE_STALE_RECOVER_MS = 45_000;
|
|
|
1779
1812
|
// reaches the browser.
|
|
1780
1813
|
const SHUTDOWN_SOCKET_FLUSH_MS = 300;
|
|
1781
1814
|
let runtimeShutdownStarted = false;
|
|
1815
|
+
// Set once the database is open (see the retention sweep near the other sweeps).
|
|
1816
|
+
let subagentRecordRetention = null;
|
|
1782
1817
|
const runtimeTimers = {
|
|
1783
1818
|
cliStatus: null,
|
|
1784
1819
|
ownerWatchdog: null,
|
|
@@ -1987,6 +2022,22 @@ function setTurnCeilingMinutes(value) {
|
|
|
1987
2022
|
return { ok: true, ceilingMinutes: parsed.minutes };
|
|
1988
2023
|
}
|
|
1989
2024
|
|
|
2025
|
+
function getSubagentRecordRetentionDays() {
|
|
2026
|
+
return readSubagentRecordRetentionSetting(readAppSettingValue(SUBAGENT_RECORD_RETENTION_SETTING_KEY));
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
function setSubagentRecordRetentionDays(value) {
|
|
2030
|
+
if (typeof stmts?.upsertAppSetting?.run !== 'function') {
|
|
2031
|
+
return { ok: false, error: 'Sub-agent record settings are unavailable' };
|
|
2032
|
+
}
|
|
2033
|
+
const parsed = parseSubagentRecordRetentionUpdate(value);
|
|
2034
|
+
if (!parsed.ok) return parsed;
|
|
2035
|
+
stmts.upsertAppSetting.run(SUBAGENT_RECORD_RETENTION_SETTING_KEY, String(parsed.days), new Date().toISOString());
|
|
2036
|
+
// A shorter period takes effect now, not at the next daily sweep.
|
|
2037
|
+
subagentRecordRetention?.scheduleSweep(5_000);
|
|
2038
|
+
return { ok: true, retentionDays: parsed.days };
|
|
2039
|
+
}
|
|
2040
|
+
|
|
1990
2041
|
const PWA_APP_NAME_SETTING_KEY = 'pwa_app_name';
|
|
1991
2042
|
|
|
1992
2043
|
/** '' means "no custom name": the manifest falls back to the template default. */
|
|
@@ -2528,6 +2579,14 @@ const stmts = {
|
|
|
2528
2579
|
...createImageConversationRepository(db),
|
|
2529
2580
|
};
|
|
2530
2581
|
const statusEventService = createStatusEventService(db);
|
|
2582
|
+
// Relay events (built further down, next to the usage-limit pause): declared
|
|
2583
|
+
// here so recovery code that runs before then finds null, not a TDZ error.
|
|
2584
|
+
let sessionActivityTracker = null;
|
|
2585
|
+
let relayEventService = null;
|
|
2586
|
+
// Fetches the events of subscriptions on paired relays (built with the
|
|
2587
|
+
// remote relays below; the event service asks it to fetch at once when an
|
|
2588
|
+
// end or a new watcher must reach one).
|
|
2589
|
+
let relayEventLinkService = null;
|
|
2531
2590
|
// Owns the `claude auth …` child processes, so the runtime (not the route
|
|
2532
2591
|
// module) constructs it: shutdownRuntime() has to be able to dispose a login
|
|
2533
2592
|
// PTY that is still holding a process group.
|
|
@@ -3331,26 +3390,6 @@ function cancelRelayShutdown({ requestedBy = 'localhost-api' } = {}) {
|
|
|
3331
3390
|
return { cancelled: true, dropped, ...relayShutdownStatePayload() };
|
|
3332
3391
|
}
|
|
3333
3392
|
|
|
3334
|
-
const countPendingRowsByOwnerSession = db.prepare(`
|
|
3335
|
-
SELECT owner_sdk_session_id AS sdk_session_id, COUNT(*) AS n
|
|
3336
|
-
FROM queue
|
|
3337
|
-
WHERE status = 'pending' AND owner_sdk_session_id IS NOT NULL AND owner_sdk_session_id != ''
|
|
3338
|
-
GROUP BY owner_sdk_session_id
|
|
3339
|
-
`);
|
|
3340
|
-
|
|
3341
|
-
function countPendingRowsOfStoppedSessions() {
|
|
3342
|
-
try {
|
|
3343
|
-
let stalled = 0;
|
|
3344
|
-
for (const row of countPendingRowsByOwnerSession.all()) {
|
|
3345
|
-
const lifecycle = sessionWorkerSupervisor?.getLifecycleState?.(String(row.sdk_session_id || '').trim());
|
|
3346
|
-
if (lifecycle?.retriesStopped === true) stalled += Number(row.n || 0);
|
|
3347
|
-
}
|
|
3348
|
-
return stalled;
|
|
3349
|
-
} catch {
|
|
3350
|
-
return 0;
|
|
3351
|
-
}
|
|
3352
|
-
}
|
|
3353
|
-
|
|
3354
3393
|
function requestRelayShutdown({ reason = 'manual-request', requestedBy = 'localhost-api', restart = false } = {}) {
|
|
3355
3394
|
const safeReason = String(reason || 'manual-request').trim().slice(0, 140) || 'manual-request';
|
|
3356
3395
|
const safeRequestedBy = String(requestedBy || 'localhost-api').trim().slice(0, 80) || 'localhost-api';
|
|
@@ -3366,7 +3405,11 @@ function requestRelayShutdown({ reason = 'manual-request', requestedBy = 'localh
|
|
|
3366
3405
|
};
|
|
3367
3406
|
}
|
|
3368
3407
|
|
|
3369
|
-
// Shutdown is
|
|
3408
|
+
// Shutdown is deferred until no turn is processing; it never interrupts an
|
|
3409
|
+
// active turn. From here on no pending row is handed out (the dequeue paths
|
|
3410
|
+
// read relayShutdownQueued): a turn started meanwhile would postpone the
|
|
3411
|
+
// restart again. Pending and parked rows wait in the database and run
|
|
3412
|
+
// after the restart.
|
|
3370
3413
|
if (!pendingRelayShutdownRequest) {
|
|
3371
3414
|
pendingRelayShutdownRequest = {
|
|
3372
3415
|
action: requestedAction,
|
|
@@ -3387,12 +3430,7 @@ function requestRelayShutdown({ reason = 'manual-request', requestedBy = 'localh
|
|
|
3387
3430
|
const tryShutdownWhenIdle = () => {
|
|
3388
3431
|
if (runtimeShutdownStarted) return;
|
|
3389
3432
|
const counts = queueCounts();
|
|
3390
|
-
|
|
3391
|
-
// retrying makes no progress until somebody presses Retry; it must not
|
|
3392
|
-
// hold the restart (which, fresh, is itself the next launch attempt).
|
|
3393
|
-
const stalledPending = countPendingRowsOfStoppedSessions();
|
|
3394
|
-
const queueFree = (counts.pendingCount - stalledPending + counts.processingCount + counts.parkedCount) <= 0;
|
|
3395
|
-
if (!queueFree) return;
|
|
3433
|
+
if (!queuedShutdownCanProceed(counts)) return;
|
|
3396
3434
|
const requestInfo = pendingRelayShutdownRequest;
|
|
3397
3435
|
pendingRelayShutdownRequest = null;
|
|
3398
3436
|
if (runtimeTimers.shutdownDrain) {
|
|
@@ -3403,7 +3441,7 @@ function requestRelayShutdown({ reason = 'manual-request', requestedBy = 'localh
|
|
|
3403
3441
|
const exitCode = action === 'restart' ? RELAY_RESTART_EXIT_CODE : 0;
|
|
3404
3442
|
console.log(
|
|
3405
3443
|
`${runtimeLogPrefix()}Relay ${action} request accepted by ${requestInfo?.requestedBy || 'unknown'}`
|
|
3406
|
-
+ `;
|
|
3444
|
+
+ `; no turn is processing (${counts.pendingCount} pending, ${counts.parkedCount} parked wait), exiting now (reason=${requestInfo?.reason || 'manual-request'})`,
|
|
3407
3445
|
);
|
|
3408
3446
|
sessionWorkerWebSocketService.emitDraining(`api-${action}:${requestInfo?.reason || 'manual-request'}`);
|
|
3409
3447
|
void shutdownRuntime(`api-${action}:${requestInfo?.reason || 'manual-request'}`, { exitCode });
|
|
@@ -4148,6 +4186,7 @@ function readWorkspaceFileMeta(filePath) {
|
|
|
4148
4186
|
return cacheWorkspaceFileMeta(filePath, {
|
|
4149
4187
|
kind: 'file',
|
|
4150
4188
|
size: Number(stat.size || 0),
|
|
4189
|
+
mtimeMs: Number(stat.mtimeMs || 0),
|
|
4151
4190
|
contentType: workspaceContentType(filePath),
|
|
4152
4191
|
});
|
|
4153
4192
|
}
|
|
@@ -4214,6 +4253,8 @@ function workspacePreviewKindForMeta(filePath, contentType) {
|
|
|
4214
4253
|
if (WORKSPACE_MARKDOWN_EXTENSIONS.has(normalizedExt)) return 'markdown';
|
|
4215
4254
|
if (WORKSPACE_IMAGE_EXTENSIONS.has(normalizedExt) || normalizedType.startsWith('image/')) return 'image';
|
|
4216
4255
|
if (WORKSPACE_VIDEO_EXTENSIONS.has(normalizedExt) || normalizedType.startsWith('video/')) return 'video';
|
|
4256
|
+
if (WORKSPACE_AUDIO_EXTENSIONS.has(normalizedExt) || normalizedType.startsWith('audio/')) return 'audio';
|
|
4257
|
+
if (normalizedExt === '.pdf' || normalizedType === 'application/pdf') return 'pdf';
|
|
4217
4258
|
if (WORKSPACE_CODE_EXTENSIONS.has(normalizedExt) || WORKSPACE_PREVIEW_LANGUAGE_BY_FILENAME[filename]) return 'code';
|
|
4218
4259
|
if (isLikelyTextContentType(normalizedType)) return 'text';
|
|
4219
4260
|
return 'binary';
|
|
@@ -5033,19 +5074,18 @@ function subagentRunsForResponse(responseMessageId) {
|
|
|
5033
5074
|
return rows.map(mapSubagentRunRow).filter(Boolean).slice(0, 96);
|
|
5034
5075
|
}
|
|
5035
5076
|
|
|
5077
|
+
// The run as a row of the panel, the reply bubble and the viewer header. Stop
|
|
5078
|
+
// is offered while it runs: a background run through its live task, a
|
|
5079
|
+
// foreground run through the sub-agent cancel route.
|
|
5036
5080
|
function mapSubagentRunRow(row) {
|
|
5037
|
-
const
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
status: String(row?.status || 'running').trim().toLowerCase() || 'running',
|
|
5046
|
-
startedAt: row?.started_at || null,
|
|
5047
|
-
updatedAt: row?.updated_at || null,
|
|
5048
|
-
};
|
|
5081
|
+
const status = String(row?.status || 'running').trim().toLowerCase();
|
|
5082
|
+
let stoppable = false;
|
|
5083
|
+
if (status === 'running') {
|
|
5084
|
+
const tasks = backgroundTaskStore.get(row?.conversation_id);
|
|
5085
|
+
const task = tasks.find((entry) => entry?.toolUseId && entry.toolUseId === String(row?.id || ''));
|
|
5086
|
+
stoppable = task ? task.stoppable !== false : Number(row?.background || 0) !== 1;
|
|
5087
|
+
}
|
|
5088
|
+
return mapSubagentRunHeader(row, { stoppable });
|
|
5049
5089
|
}
|
|
5050
5090
|
|
|
5051
5091
|
// Final workflow digests persisted with an assistant message (the "Finished
|
|
@@ -5212,6 +5252,46 @@ const usageLimitPauseService = createUsageLimitPauseService({
|
|
|
5212
5252
|
logger: console,
|
|
5213
5253
|
});
|
|
5214
5254
|
|
|
5255
|
+
// ─── Relay events ─────────────────────────────────────────────────────────────
|
|
5256
|
+
// What each session is doing (state, tasks, quiet time) and the subscriptions
|
|
5257
|
+
// through which a watching session is woken with the events of the sessions
|
|
5258
|
+
// it watches (docs: server/README.md "Relay event subscriptions").
|
|
5259
|
+
sessionActivityTracker = createSessionActivityTracker({
|
|
5260
|
+
db,
|
|
5261
|
+
onEdge: (edge) => relayEventService?.noteTaskEdge(edge),
|
|
5262
|
+
logger: console,
|
|
5263
|
+
});
|
|
5264
|
+
relayEventService = createRelayEventService({
|
|
5265
|
+
db,
|
|
5266
|
+
stmts,
|
|
5267
|
+
tracker: sessionActivityTracker,
|
|
5268
|
+
emit: (event, payload) => io.emit(event, payload),
|
|
5269
|
+
// The event row is an ordinary pending message: the primer starts a
|
|
5270
|
+
// stopped worker, the socket service hands it to a running one.
|
|
5271
|
+
wakeWatcher: () => {
|
|
5272
|
+
void primePendingSessionWorkers('relay-event');
|
|
5273
|
+
sessionWorkerWebSocketService?.emitQueueChanged?.('relay-event');
|
|
5274
|
+
},
|
|
5275
|
+
relayName: () => remoteRelayRegistry?.selfName?.() || getPwaAppName?.() || '',
|
|
5276
|
+
onRemoteChange: (relayId) => { void relayEventLinkService?.fetchNow(relayId); },
|
|
5277
|
+
logger: console,
|
|
5278
|
+
});
|
|
5279
|
+
|
|
5280
|
+
/** A worker died: its tasks are gone, and whoever watches its sessions hears of it. */
|
|
5281
|
+
function noteWorkerDeathForEvents(sessionId, conversationIds, reason) {
|
|
5282
|
+
const ids = new Set((conversationIds || []).map((id) => String(id || '').trim()).filter(Boolean));
|
|
5283
|
+
if (!ids.size) {
|
|
5284
|
+
const conversation = resolveConversationRecord({ conversationId: sessionId, sdkSessionId: sessionId });
|
|
5285
|
+
if (conversation?.id) ids.add(String(conversation.id));
|
|
5286
|
+
}
|
|
5287
|
+
for (const conversationId of ids) {
|
|
5288
|
+
try {
|
|
5289
|
+
sessionActivityTracker?.noteWorkerDied(conversationId);
|
|
5290
|
+
relayEventService?.noteError({ conversationId, kind: 'worker_died', message: `the session's worker process ended (${reason})` });
|
|
5291
|
+
} catch {}
|
|
5292
|
+
}
|
|
5293
|
+
}
|
|
5294
|
+
|
|
5215
5295
|
// The live turn among a conversation's processing rows. Mid-turn steering can
|
|
5216
5296
|
// leave several processing at once — the running turn, steered messages the
|
|
5217
5297
|
// CLI folded into it (no output of their own), a handed-off row still settling
|
|
@@ -5314,6 +5394,9 @@ function failRecoveredRowTerminally(row, reason, { failureRecord = null, failure
|
|
|
5314
5394
|
message: { role: 'assistant', text: failureText, model: requestedModel || 'unknown', mode: String(row?.relay_mode || '').trim() || null, timestamp: now },
|
|
5315
5395
|
});
|
|
5316
5396
|
io.emit('message_status', { messageId: row.id, conversationId: row.conversation_id, status: 'failed' });
|
|
5397
|
+
try {
|
|
5398
|
+
relayEventService?.noteError({ conversationId: row.conversation_id, kind: 'turn_failed', message: failureText, queueMessageId: row.id });
|
|
5399
|
+
} catch {}
|
|
5317
5400
|
console.warn(`${runtimeLogPrefix()}${failureRecord ? 'CONSUMED FAILED' : 'RECOVERY LIMIT'} ${String(row.id).slice(0, 8)} conv=${String(row.conversation_id).slice(0, 8)} retries=${retryCount} reason=${reason}`);
|
|
5318
5401
|
}
|
|
5319
5402
|
|
|
@@ -5338,6 +5421,12 @@ function recoverProcessingOlderThan(cutoffIso, requeueAtIso, { ceilingBeforeIso
|
|
|
5338
5421
|
for (const row of staleContinuations) {
|
|
5339
5422
|
stmts.dropStaleContinuation?.run?.(row.id);
|
|
5340
5423
|
io.emit('message_status', { messageId: row.id, conversationId: row.conversation_id, status: 'failed' });
|
|
5424
|
+
// A teardown is a turn ending with no reply; it must be visible in the
|
|
5425
|
+
// console log (the only trace of a silent one was `ignored=already_failed`
|
|
5426
|
+
// hours later, when the worker's reply arrived for the dead row).
|
|
5427
|
+
const lastSeen = row.owner_last_claimed_at || row.processing_at || row.timestamp || null;
|
|
5428
|
+
const reason = lastSeen && lastSeen < cutoffIso ? `inactive since ${lastSeen}` : `ceiling (started ${row.processing_at || row.timestamp})`;
|
|
5429
|
+
console.warn(`${runtimeLogPrefix()}STALE CONTINUATION ${String(row.id).slice(0, 8)} conv=${String(row.conversation_id).slice(0, 8)} dropped: ${reason}`);
|
|
5341
5430
|
}
|
|
5342
5431
|
const listedRows = stmts.listRecoverableProcessing.all(params);
|
|
5343
5432
|
for (const row of listedRows.filter((entry) => entry?.consumed_at)) failConsumedRowTerminally(row, 'stale-recovery');
|
|
@@ -5733,6 +5822,10 @@ httpServer.prependListener('request', (req, _res) => {
|
|
|
5733
5822
|
httpServer.prependListener('upgrade', (req, _socket, _head) => {
|
|
5734
5823
|
rewriteSocketIoRequestPath(req, remotePath);
|
|
5735
5824
|
});
|
|
5825
|
+
// A client reset during an upgrade must not end the relay; see the guard.
|
|
5826
|
+
httpServer.prependListener('upgrade', (_req, socket, _head) => {
|
|
5827
|
+
guardUpgradeSocket(socket);
|
|
5828
|
+
});
|
|
5736
5829
|
// Public tunnels (Cloudflare, or a VPS/Caddy front) forward every path on the
|
|
5737
5830
|
// bound hostname to this port, including the session-worker WebSocket plumbing.
|
|
5738
5831
|
// Reject those before auth; local workers connect over 127.0.0.1 with no edge
|
|
@@ -5942,6 +6035,9 @@ async function requestSessionWorkerSocketDelivery({ sessionId, pid, reason = 'wo
|
|
|
5942
6035
|
if (runtimeState.relayPaused) {
|
|
5943
6036
|
return { message: null, paused: true, reason: 'relay_paused' };
|
|
5944
6037
|
}
|
|
6038
|
+
if (isRelayShutdownQueued(runtimeState)) {
|
|
6039
|
+
return relayShutdownGateReply(runtimeState);
|
|
6040
|
+
}
|
|
5945
6041
|
if (runtimeState.tunnelState?.blocking) {
|
|
5946
6042
|
return {
|
|
5947
6043
|
message: null,
|
|
@@ -6156,11 +6252,36 @@ async function isWorkerSessionProcessAlive(sessionId, hintPid = null) {
|
|
|
6156
6252
|
return false;
|
|
6157
6253
|
}
|
|
6158
6254
|
|
|
6255
|
+
// Runs a dead worker can no longer end: marked failed, each with the full
|
|
6256
|
+
// header and its own turn's messageId, so an open page settles the row
|
|
6257
|
+
// without a reload (the browser drops a status event it cannot place).
|
|
6258
|
+
function closeStrandedSubagentRuns(runs, queueMessageId = null) {
|
|
6259
|
+
if (!runs.length) return;
|
|
6260
|
+
const nowIso = new Date().toISOString();
|
|
6261
|
+
for (const run of runs) {
|
|
6262
|
+
stmts.updateSubagentRunStatus?.run?.('failed', nowIso, nowIso, run.id);
|
|
6263
|
+
const closed = stmts.getSubagentRun?.get?.(run.id) || { ...run, status: 'failed', completed_at: nowIso };
|
|
6264
|
+
const header = mapSubagentRunHeader(closed, { stoppable: false });
|
|
6265
|
+
if (header) {
|
|
6266
|
+
io.emit('subagent_status', {
|
|
6267
|
+
...header,
|
|
6268
|
+
timestamp: nowIso,
|
|
6269
|
+
queueMessageId: queueMessageId || header.messageId,
|
|
6270
|
+
reconciled: true,
|
|
6271
|
+
});
|
|
6272
|
+
}
|
|
6273
|
+
}
|
|
6274
|
+
}
|
|
6275
|
+
|
|
6159
6276
|
function recoverDeadWorkerProcessingRows(sessionId, reason) {
|
|
6160
6277
|
const sid = String(sessionId || '').trim();
|
|
6161
6278
|
if (!sid) return 0;
|
|
6279
|
+
// A background agent outlives its turn, so its run is closed with the
|
|
6280
|
+
// worker rather than with a processing row (its turn is usually done).
|
|
6281
|
+
closeStrandedSubagentRuns(stmts.listRunningBackgroundSubagentRunsForSession?.all?.(sid, sid) || []);
|
|
6162
6282
|
const rows = stmts.listProcessingRowsForOwner?.all?.(sid) || [];
|
|
6163
6283
|
if (!rows.length) return 0;
|
|
6284
|
+
noteWorkerDeathForEvents(sid, rows.map((row) => row.conversation_id), reason);
|
|
6164
6285
|
let recovered = 0;
|
|
6165
6286
|
const requeueAt = addMsIso(2_000);
|
|
6166
6287
|
for (const row of rows) {
|
|
@@ -6180,22 +6301,7 @@ function recoverDeadWorkerProcessingRows(sessionId, reason) {
|
|
|
6180
6301
|
}
|
|
6181
6302
|
// The dead worker's subagent runs can never be terminated by the replay
|
|
6182
6303
|
// (fresh worker, fresh call ids) — close them out now.
|
|
6183
|
-
|
|
6184
|
-
if (strandedRuns.length) {
|
|
6185
|
-
const nowIso = new Date().toISOString();
|
|
6186
|
-
stmts.closeRunningSubagentRunsByQueueMessage?.run?.('failed', nowIso, nowIso, row.id);
|
|
6187
|
-
for (const run of strandedRuns) {
|
|
6188
|
-
io.emit('subagent_status', {
|
|
6189
|
-
conversationId: run.conversation_id,
|
|
6190
|
-
subagentRunId: run.id,
|
|
6191
|
-
parentSubagentId: run.parent_subagent_id || null,
|
|
6192
|
-
displayName: run.display_name || null,
|
|
6193
|
-
status: 'failed',
|
|
6194
|
-
queueMessageId: row.id,
|
|
6195
|
-
reconciled: true,
|
|
6196
|
-
});
|
|
6197
|
-
}
|
|
6198
|
-
}
|
|
6304
|
+
closeStrandedSubagentRuns(stmts.listRunningSubagentRunsByQueueMessage?.all?.(row.id) || [], row.id);
|
|
6199
6305
|
stmts.recoverProcessingRowKeepOwner?.run?.(requeueAt, row.id);
|
|
6200
6306
|
io.emit('message_status', { messageId: row.id, conversationId: row.conversation_id, status: 'pending' });
|
|
6201
6307
|
recovered += 1;
|
|
@@ -6219,14 +6325,22 @@ function handleWorkerSocketClosed({ sessionId, pid = null } = {}) {
|
|
|
6219
6325
|
// A dead worker's steering snapshot must not keep holding the composer even
|
|
6220
6326
|
// when it owned no rows (the hold can outlive them, e.g. a continuation's).
|
|
6221
6327
|
const heldSteering = Boolean(sessionWorkerRegistry?.getWorker?.(sid)?.steering);
|
|
6222
|
-
|
|
6328
|
+
// Background agents run between turns too: a worker that dies then owns no
|
|
6329
|
+
// row, but its agents' runs still read "running".
|
|
6330
|
+
const backgroundRuns = () => stmts.listRunningBackgroundSubagentRunsForSession?.all?.(sid, sid) || [];
|
|
6331
|
+
if (!owned.length && !heldSteering && !backgroundRuns().length) return;
|
|
6223
6332
|
const timer = setTimeout(async () => {
|
|
6224
6333
|
try {
|
|
6225
6334
|
if (runtimeShutdownStarted) return;
|
|
6226
6335
|
if (await isWorkerSessionProcessAlive(sid, pid)) return; // reconnect or socket flap
|
|
6227
6336
|
sessionWorkerRegistry?.clearSteering?.(sid);
|
|
6228
6337
|
const stillOwned = stmts.listProcessingRowsForOwner?.all?.(sid) || [];
|
|
6229
|
-
if (!stillOwned.length)
|
|
6338
|
+
if (!stillOwned.length) {
|
|
6339
|
+
const stranded = backgroundRuns();
|
|
6340
|
+
closeStrandedSubagentRuns(stranded);
|
|
6341
|
+
if (stranded.length) noteWorkerDeathForEvents(sid, stranded.map((run) => run.conversation_id), 'socket-close');
|
|
6342
|
+
return;
|
|
6343
|
+
}
|
|
6230
6344
|
console.warn(`${runtimeLogPrefix()}WORKER DIED session=${sid.slice(0, 8)} rows=${stillOwned.length} trigger=socket-close — recovering owned turns`);
|
|
6231
6345
|
recoverDeadWorkerProcessingRows(sid, 'socket-close');
|
|
6232
6346
|
} catch (error) {
|
|
@@ -6341,6 +6455,10 @@ app.use((req, _res, next) => {
|
|
|
6341
6455
|
stripRequestPathPrefix(req, remotePath);
|
|
6342
6456
|
next();
|
|
6343
6457
|
});
|
|
6458
|
+
function isApiRequestPath(requestPath) {
|
|
6459
|
+
return /^\/api(?:\/|$)/.test(String(requestPath || ''));
|
|
6460
|
+
}
|
|
6461
|
+
|
|
6344
6462
|
// Baseline security headers on every response. Served-file/attachment endpoints
|
|
6345
6463
|
// layer a stricter sandbox CSP on top (see applySafeServedContentHeaders). A
|
|
6346
6464
|
// global script-src CSP is intentionally omitted for now: the SPA relies on
|
|
@@ -6349,6 +6467,14 @@ app.use((req, res, next) => {
|
|
|
6349
6467
|
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
6350
6468
|
res.setHeader('X-Frame-Options', 'SAMEORIGIN');
|
|
6351
6469
|
res.setHeader('Referrer-Policy', 'no-referrer');
|
|
6470
|
+
// Nothing this relay answers belongs in a CDN's cache: it is all behind a
|
|
6471
|
+
// login. These two headers bind the edge itself, whatever cache rule the
|
|
6472
|
+
// zone in front of a tunnel has.
|
|
6473
|
+
applyCdnNoStoreHeaders(res);
|
|
6474
|
+
// An API answer is never stored by default, errors included (a 404 or a
|
|
6475
|
+
// 401 for a file must not outlive the file's absence). A route that
|
|
6476
|
+
// serves content-addressed bytes sets its own, longer-lived value.
|
|
6477
|
+
if (isApiRequestPath(req.path)) res.setHeader('Cache-Control', 'no-store');
|
|
6352
6478
|
next();
|
|
6353
6479
|
});
|
|
6354
6480
|
app.get('/socket.io/socket.io.js', (req, res, next) => {
|
|
@@ -6409,6 +6535,9 @@ const runtimeState = {
|
|
|
6409
6535
|
set relayPaused(value) { relayPaused = value; },
|
|
6410
6536
|
get ttyConsoleActive() { return Boolean(ttyConsoleRuntime?.tty); },
|
|
6411
6537
|
get relayShutdown() { return getRelayShutdownState(); },
|
|
6538
|
+
// A shutdown or restart waits for the processing turns: no pending row
|
|
6539
|
+
// goes out meanwhile (see requestRelayShutdown).
|
|
6540
|
+
get relayShutdownQueued() { return !!pendingRelayShutdownRequest && !runtimeShutdownStarted; },
|
|
6412
6541
|
get hostSuspend() { return hostSuspendService.getState(); },
|
|
6413
6542
|
get usageLimit() {
|
|
6414
6543
|
return { account: usageLimitPauseService.getAccountState(), pauses: usageLimitPauseService.listPauses() };
|
|
@@ -6462,6 +6591,13 @@ const remoteRelayRegistry = createRemoteRelayRegistry({
|
|
|
6462
6591
|
platform: process.platform,
|
|
6463
6592
|
version: RUNNING_VERSION,
|
|
6464
6593
|
emit: (event, payload) => io.emit(event, payload),
|
|
6594
|
+
onRemove: (entry) => { void relayEventLinkService?.noteRelayRemoved(entry); },
|
|
6595
|
+
logger: console,
|
|
6596
|
+
});
|
|
6597
|
+
relayEventLinkService = createRelayEventLinkService({
|
|
6598
|
+
relayEvents: relayEventService,
|
|
6599
|
+
registry: remoteRelayRegistry,
|
|
6600
|
+
client: remoteRelayClient,
|
|
6465
6601
|
logger: console,
|
|
6466
6602
|
});
|
|
6467
6603
|
const remoteRelayPairing = createRemoteRelayPairing({
|
|
@@ -6492,7 +6628,10 @@ const remoteRelayCallerContext = createRemoteRelayCallerContext({
|
|
|
6492
6628
|
normalizeRelayMode,
|
|
6493
6629
|
defaultRelayMode: DEFAULT_RELAY_MODE,
|
|
6494
6630
|
emit: (event, payload) => io.emit(event, payload),
|
|
6495
|
-
notifyQuestion: (question) =>
|
|
6631
|
+
notifyQuestion: (question) => {
|
|
6632
|
+
try { relayEventService?.noteQuestion(question); } catch {}
|
|
6633
|
+
return pushDispatchService.notifyQuestion?.(question);
|
|
6634
|
+
},
|
|
6496
6635
|
uuid: uuidv4,
|
|
6497
6636
|
logger: console,
|
|
6498
6637
|
});
|
|
@@ -6513,6 +6652,8 @@ const remoteRelayDispatcher = createRemoteRelayDispatcher({
|
|
|
6513
6652
|
requestApproval: (request) => remoteRelayCallerContext.requestApproval(request),
|
|
6514
6653
|
emitStatusEvent: ({ type, ...details }) => statusEventService.recordEvent(type, details),
|
|
6515
6654
|
resolveConversationId: (id) => resolveConversationRecord({ conversationId: id, sdkSessionId: id })?.id || id,
|
|
6655
|
+
relayEvents: relayEventService,
|
|
6656
|
+
relayEventLink: relayEventLinkService,
|
|
6516
6657
|
logger: console,
|
|
6517
6658
|
});
|
|
6518
6659
|
|
|
@@ -6552,6 +6693,7 @@ const sharedRouteDeps = {
|
|
|
6552
6693
|
compareRepoDirEntries,
|
|
6553
6694
|
shouldSkipRepoEntryName,
|
|
6554
6695
|
readWorkspaceFileMeta,
|
|
6696
|
+
workspaceFileMetaCache,
|
|
6555
6697
|
resolveWorkspaceFilePath,
|
|
6556
6698
|
normalizeWorkspaceRelativePath,
|
|
6557
6699
|
previewLanguageForWorkspaceFile,
|
|
@@ -6700,6 +6842,8 @@ const sharedRouteDeps = {
|
|
|
6700
6842
|
resolveCursorSessionRoot: cursorSessionRootResolver.resolveCursorSessionRoot,
|
|
6701
6843
|
getTurnCeilingMinutes,
|
|
6702
6844
|
setTurnCeilingMinutes,
|
|
6845
|
+
getSubagentRecordRetentionDays,
|
|
6846
|
+
setSubagentRecordRetentionDays,
|
|
6703
6847
|
getBackgroundTaskTimeoutMinutes,
|
|
6704
6848
|
setBackgroundTaskTimeoutMinutes,
|
|
6705
6849
|
getFeatureFlagsSettingsState,
|
|
@@ -6712,6 +6856,9 @@ const sharedRouteDeps = {
|
|
|
6712
6856
|
hostSuspendService,
|
|
6713
6857
|
collectHostActivity,
|
|
6714
6858
|
usageLimitPauseService,
|
|
6859
|
+
relayEventService,
|
|
6860
|
+
sessionActivityTracker,
|
|
6861
|
+
resolveConversationRecord,
|
|
6715
6862
|
markSharedViewerPresence,
|
|
6716
6863
|
getSharedWatcherCount,
|
|
6717
6864
|
statusEventService,
|
|
@@ -6745,6 +6892,12 @@ registerRemoteRelayRoutes(app, {
|
|
|
6745
6892
|
registry: remoteRelayRegistry,
|
|
6746
6893
|
pairing: remoteRelayPairing,
|
|
6747
6894
|
dispatcher: remoteRelayDispatcher,
|
|
6895
|
+
remoteRelayInbound,
|
|
6896
|
+
});
|
|
6897
|
+
registerRelayEventRoutes(app, {
|
|
6898
|
+
auth,
|
|
6899
|
+
relayEventService,
|
|
6900
|
+
remoteRelayInbound,
|
|
6748
6901
|
});
|
|
6749
6902
|
function markCliOffline(reason = 'offline', { clearOwner = true } = {}) {
|
|
6750
6903
|
cliLastSeen = null;
|
|
@@ -6788,9 +6941,22 @@ if (managedOwnerPid) {
|
|
|
6788
6941
|
// The window is measured from the last heartbeat that named the message, not
|
|
6789
6942
|
// from when the turn began, so a turn stays alive for as long as its worker
|
|
6790
6943
|
// keeps reporting it — however many minutes of tool calls and subagents that
|
|
6791
|
-
// takes. The separate, user-configurable ceiling is the only elapsed-time cap
|
|
6792
|
-
//
|
|
6944
|
+
// takes. The separate, user-configurable ceiling is the only elapsed-time cap;
|
|
6945
|
+
// it counts from the last answered question rather than from the start, and
|
|
6946
|
+
// both exempt a turn that is waiting on an unanswered question (continuation
|
|
6947
|
+
// rows included — an interview hangs its cards on one).
|
|
6948
|
+
//
|
|
6949
|
+
// After a wall-clock jump (the host slept, the event loop was blocked) every
|
|
6950
|
+
// heartbeat looks old at once although the workers may be alive: the guard
|
|
6951
|
+
// skips recovery for a full inactivity window, so a live worker can report.
|
|
6952
|
+
const STALE_RECOVERY_INTERVAL_MS = 15_000;
|
|
6953
|
+
const staleRecoveryClockGuard = createStaleRecoveryClockGuard({
|
|
6954
|
+
intervalMs: STALE_RECOVERY_INTERVAL_MS,
|
|
6955
|
+
graceMs: Math.max(processingTimeoutMs, 60_000),
|
|
6956
|
+
log: (line) => console.log(`${runtimeLogPrefix()}${line}`),
|
|
6957
|
+
});
|
|
6793
6958
|
function recoverStaleMessages() {
|
|
6959
|
+
if (staleRecoveryClockGuard.tick().skip) return;
|
|
6794
6960
|
const staleWindowMs = cliOnline
|
|
6795
6961
|
? processingTimeoutMs
|
|
6796
6962
|
: Math.min(processingTimeoutMs, OFFLINE_STALE_RECOVER_MS);
|
|
@@ -6806,7 +6972,7 @@ function recoverStaleMessages() {
|
|
|
6806
6972
|
);
|
|
6807
6973
|
}
|
|
6808
6974
|
}
|
|
6809
|
-
runtimeTimers.staleRecovery = setInterval(recoverStaleMessages,
|
|
6975
|
+
runtimeTimers.staleRecovery = setInterval(recoverStaleMessages, STALE_RECOVERY_INTERVAL_MS);
|
|
6810
6976
|
recoverStaleMessages(); // run immediately on startup
|
|
6811
6977
|
|
|
6812
6978
|
function expirePendingQuestions() {
|
|
@@ -6836,6 +7002,40 @@ if (sweptUploads > 0) {
|
|
|
6836
7002
|
}
|
|
6837
7003
|
runtimeTimers.uploadSweep = setInterval(sweepUnreferencedUploadBlobs, 6 * 60 * 60 * 1000);
|
|
6838
7004
|
if (typeof runtimeTimers.uploadSweep.unref === 'function') runtimeTimers.uploadSweep.unref();
|
|
7005
|
+
// Old sub-agent records lose their tool inputs and output: shortly after
|
|
7006
|
+
// start (not in the boot path), then once a day.
|
|
7007
|
+
subagentRecordRetention = createSubagentRecordRetention({
|
|
7008
|
+
db,
|
|
7009
|
+
getRetentionDays: getSubagentRecordRetentionDays,
|
|
7010
|
+
isStopped: () => runtimeShutdownStarted,
|
|
7011
|
+
log: (line) => console.log(`${runtimeLogPrefix()}${line}`),
|
|
7012
|
+
warn: (line) => console.warn(`${runtimeLogPrefix()}${line}`),
|
|
7013
|
+
});
|
|
7014
|
+
subagentRecordRetention.scheduleSweep(30_000);
|
|
7015
|
+
runtimeTimers.subagentRecordRetention = setInterval(() => { void subagentRecordRetention.sweep(); }, 24 * 60 * 60 * 1000);
|
|
7016
|
+
if (typeof runtimeTimers.subagentRecordRetention.unref === 'function') runtimeTimers.subagentRecordRetention.unref();
|
|
7017
|
+
// Sub-agent runs left `running` by a worker that is gone are closed: once
|
|
7018
|
+
// the previous runtime's workers have had time to reconnect after boot, then
|
|
7019
|
+
// periodically. A live worker's runs are never touched.
|
|
7020
|
+
const staleSubagentRunSweep = createStaleSubagentRunSweep({
|
|
7021
|
+
db,
|
|
7022
|
+
isConversationWorkerAlive: (conversationId) => {
|
|
7023
|
+
if (hostActivityWorkerAlive(conversationId)) return true;
|
|
7024
|
+
const sdkSessionId = String(stmts.getRuntimeSessionByConversation?.get?.(conversationId)?.sdk_session_id || '').trim();
|
|
7025
|
+
return !!sdkSessionId && hostActivityWorkerAlive(sdkSessionId);
|
|
7026
|
+
},
|
|
7027
|
+
log: (line) => console.log(`${runtimeLogPrefix()}${line}`),
|
|
7028
|
+
warn: (line) => console.warn(`${runtimeLogPrefix()}${line}`),
|
|
7029
|
+
});
|
|
7030
|
+
runtimeTimers.staleSubagentRunsBoot = setTimeout(() => {
|
|
7031
|
+
runtimeTimers.staleSubagentRunsBoot = null;
|
|
7032
|
+
if (!runtimeShutdownStarted) staleSubagentRunSweep.sweep();
|
|
7033
|
+
}, STALE_SUBAGENT_RUN_BOOT_DELAY_MS);
|
|
7034
|
+
if (typeof runtimeTimers.staleSubagentRunsBoot.unref === 'function') runtimeTimers.staleSubagentRunsBoot.unref();
|
|
7035
|
+
runtimeTimers.staleSubagentRuns = setInterval(() => {
|
|
7036
|
+
if (!runtimeShutdownStarted) staleSubagentRunSweep.sweep();
|
|
7037
|
+
}, STALE_SUBAGENT_RUN_SWEEP_INTERVAL_MS);
|
|
7038
|
+
if (typeof runtimeTimers.staleSubagentRuns.unref === 'function') runtimeTimers.staleSubagentRuns.unref();
|
|
6839
7039
|
// Arms the 12h update poller only when the user opted in; otherwise silent.
|
|
6840
7040
|
updateCheckService?.startIfEnabled();
|
|
6841
7041
|
const runtimeBindingsBootstrapped = bootstrapRuntimeSessionBindings();
|
|
@@ -7038,6 +7238,10 @@ function touchCli() {
|
|
|
7038
7238
|
}
|
|
7039
7239
|
sessionWorkerWebSocketService.start();
|
|
7040
7240
|
void primePendingSessionWorkers('startup');
|
|
7241
|
+
// Undelivered events get their timers back; stalled, all_idle and the
|
|
7242
|
+
// heartbeat are swept from here on.
|
|
7243
|
+
relayEventService.start();
|
|
7244
|
+
relayEventLinkService.start();
|
|
7041
7245
|
runtimeTimers.pendingWorkerPrime = setInterval(() => {
|
|
7042
7246
|
void primePendingSessionWorkers('maintenance');
|
|
7043
7247
|
}, 5_000);
|
|
@@ -7084,6 +7288,10 @@ function socketAuthToken(socket) {
|
|
|
7084
7288
|
const cookies = parseCookies(socket.request.headers.cookie);
|
|
7085
7289
|
return socket.handshake.auth?.token || socket.handshake.query?.token || cookies[AUTH_COOKIE];
|
|
7086
7290
|
}
|
|
7291
|
+
// A sub-agent's record goes live only to the pages whose viewer shows it.
|
|
7292
|
+
const subagentViewerRooms = createSubagentViewerRooms({
|
|
7293
|
+
getRun: (runId) => stmts.getSubagentRun?.get?.(runId) || null,
|
|
7294
|
+
});
|
|
7087
7295
|
io.use((socket, next) => {
|
|
7088
7296
|
if (tokensMatch(socketAuthToken(socket), config.authToken)) return next();
|
|
7089
7297
|
next(new Error('Unauthorized'));
|
|
@@ -7121,6 +7329,7 @@ io.on('connection', (socket) => {
|
|
|
7121
7329
|
// deviceVisible flag restored by connectionStateRecovery.
|
|
7122
7330
|
activeDeviceTracker.registerSocket(socket);
|
|
7123
7331
|
tmuxInspectorSocketService.registerSocket(socket);
|
|
7332
|
+
subagentViewerRooms.registerSocket(socket);
|
|
7124
7333
|
});
|
|
7125
7334
|
|
|
7126
7335
|
// ─── SSH Reverse Tunnel ────────────────────────────────────────────────────────
|
|
@@ -7230,6 +7439,7 @@ function clearRuntimeTimers() {
|
|
|
7230
7439
|
if (!timer) continue;
|
|
7231
7440
|
try { clearInterval(timer); } catch {}
|
|
7232
7441
|
}
|
|
7442
|
+
subagentRecordRetention?.stop();
|
|
7233
7443
|
runtimeTimers.cliStatus = null;
|
|
7234
7444
|
runtimeTimers.ownerWatchdog = null;
|
|
7235
7445
|
runtimeTimers.staleRecovery = null;
|
|
@@ -7277,6 +7487,8 @@ function shutdownRuntime(reason = 'unknown', { exitCode = 0 } = {}) {
|
|
|
7277
7487
|
console.warn(`${runtimeLogPrefix()}Grok auth service shutdown failed: ${error?.message || error}`);
|
|
7278
7488
|
}
|
|
7279
7489
|
sessionWorkerWebSocketService.stop();
|
|
7490
|
+
try { relayEventService.stop(); } catch {}
|
|
7491
|
+
try { relayEventLinkService.stop(); } catch {}
|
|
7280
7492
|
tmuxInspectorSocketService.stop();
|
|
7281
7493
|
stopWorkspaceFileWatcher();
|
|
7282
7494
|
sshTunnelManager.stop();
|