@oar-sh/oar 0.9.21 → 0.9.23
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 +206 -0
- package/README.md +9 -9
- package/bin/oar.js +28 -0
- package/package.json +13 -14
- package/server/claude-cloud-worker/claude-cloud-session-worker.mjs +9 -1
- package/server/claude-worker/claude-cli-pinned-env.mjs +4 -1
- package/server/claude-worker/claude-sdk-adapter.mjs +149 -4
- package/server/claude-worker/claude-session-process.mjs +9 -4
- package/server/claude-worker/claude-session-worker-main.mjs +10 -1
- package/server/copilot-worker/copilot-sdk-session-process.mjs +1 -0
- package/server/copilot-worker/copilot-sdk-session-worker.mjs +10 -1
- package/server/cursor-worker/cursor-session-worker.mjs +9 -1
- package/server/db-schema.mjs +19 -0
- package/server/grok-worker/grok-background-tasks.mjs +1 -0
- package/server/grok-worker/grok-session-worker.mjs +10 -1
- package/server/grok-worker/grok-turn-runner.mjs +1 -0
- package/server/migrations/0037-relay-asks-queue-id.mjs +66 -0
- package/server/migrations/0038-turn-seq.mjs +92 -0
- package/server/public/app/activity-replay-state.mjs +160 -42
- package/server/public/app/api-client.js +7 -0
- package/server/public/app/ask-user-view.js +4 -3
- package/server/public/app/bootstrap.js +62 -4
- package/server/public/app/chat-actions-menu-state.mjs +2 -0
- package/server/public/app/context-usage-view.mjs +68 -0
- package/server/public/app/conversation-view.js +306 -49
- package/server/public/app/header-priority-actions.mjs +5 -1
- package/server/public/app/held-details.mjs +202 -0
- package/server/public/app/inbox-ui.js +70 -79
- package/server/public/app/inbox-view.mjs +35 -19
- package/server/public/app/journal-view.js +17 -0
- package/server/public/app/live-activity-cap.mjs +103 -0
- package/server/public/app/questions-panel-ui.js +634 -0
- package/server/public/app/questions-stub-view.mjs +40 -0
- package/server/public/app/questions-view.mjs +89 -0
- package/server/public/app/relay-board-view.js +12 -2
- package/server/public/app/relay-thoughts.mjs +15 -1
- package/server/public/app/session-history-ui.js +1 -0
- package/server/public/app/settings-modal.js +6 -0
- package/server/public/app/socket-handlers.js +42 -11
- package/server/public/app/stream-view-preference.mjs +60 -0
- package/server/public/app/subagent-fold.mjs +6 -3
- package/server/public/app/subagent-timeline-view.mjs +12 -5
- package/server/public/app/subagent-viewer.mjs +8 -6
- package/server/public/app/thoughts-preference.mjs +79 -0
- package/server/public/app/turn-stream-view.mjs +124 -0
- package/server/public/index.html +391 -214
- package/server/repositories/message-repository.mjs +10 -0
- package/server/repositories/question-repository.mjs +51 -7
- package/server/repositories/relay-turn-rows.mjs +56 -0
- package/server/routes/ask-user-routes.mjs +34 -5
- package/server/routes/messages-routes.mjs +46 -34
- package/server/routes/relay-ask-routes.mjs +15 -5
- package/server/routes/sessions-routes.mjs +77 -6
- package/server/server-runtime.mjs +199 -171
- package/server/services/cloudflared-tunnel-service.mjs +7 -0
- package/server/services/earlier-database-service.mjs +1 -1
- package/server/services/file-serving.mjs +13 -0
- package/server/services/idle-timers.mjs +184 -0
- package/server/services/native-addon-build-service.mjs +224 -0
- package/server/services/oar-cli-helpers.mjs +7 -4
- package/server/services/queue-wake-hook.mjs +145 -0
- package/server/services/queue-waker.mjs +127 -0
- package/server/services/relay-ask-service.mjs +64 -5
- package/server/services/relay-console-log.mjs +2 -3
- package/server/services/relay-event-service.mjs +4 -1
- package/server/services/remote-relay-dispatcher.mjs +23 -4
- package/server/services/remote-relay-loopback.mjs +3 -3
- package/server/services/sdk-session-sync-service.mjs +1 -1
- package/server/services/session-history-refresh-service.mjs +4 -0
- package/server/services/session-worker-supervisor-service.mjs +36 -0
- package/server/services/session-worker-websocket-service.mjs +143 -15
- package/server/services/ssh-tunnel-manager-service.mjs +7 -0
- package/server/services/update-install-service.mjs +32 -1
- package/shared/ask-user-bridge.mjs +9 -6
- package/shared/relay-asks.mjs +10 -0
- package/shared/relay-events.mjs +1 -0
- package/shared/worker-runtime/heartbeat.mjs +26 -0
- package/shared/worker-runtime/worker-keepalive.mjs +108 -0
- package/shared/worker-runtime/worker-websocket-link.mjs +21 -10
- package/server/services/package-data-rescue-preinstall.mjs +0 -22
- package/server/services/package-data-rescue.mjs +0 -139
- package/server/tty-console-bootstrap.mjs +0 -75
|
@@ -4,7 +4,7 @@ import express from 'express';
|
|
|
4
4
|
import http from 'http';
|
|
5
5
|
import { Server } from 'socket.io';
|
|
6
6
|
import { WebSocketServer } from 'ws';
|
|
7
|
-
import {
|
|
7
|
+
import { randomUUID as uuidv4 } from 'crypto';
|
|
8
8
|
import fs from 'fs';
|
|
9
9
|
import path from 'path';
|
|
10
10
|
import { execFile, execFileSync, spawn } from 'child_process';
|
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
} from './services/workspace-root-path-policy.mjs';
|
|
26
26
|
import { stopSessionWorkerProcesses } from './services/session-worker-stop-service.mjs';
|
|
27
27
|
import { createDetachedWorkerWatch } from './services/detached-worker-watch-service.mjs';
|
|
28
|
-
import { applyCdnNoStoreHeaders } from './services/file-serving.mjs';
|
|
28
|
+
import { applyCdnNoStoreHeaders, sendOwnFile } from './services/file-serving.mjs';
|
|
29
29
|
import { pickLiveTurnRowId } from './services/live-turn-picker.mjs';
|
|
30
30
|
import { buildSteerSettleFailure, steerAgentLabelForProvider } from '../shared/steer-settle-failure.mjs';
|
|
31
31
|
import { PLAN_BOARD_APPROVE_ONLY, planBoardShownActions } from '../shared/plan-board-actions.mjs';
|
|
@@ -36,6 +36,7 @@ import { applySchema } from './db-schema.mjs';
|
|
|
36
36
|
import { createSessionRepository } from './repositories/session-repository.mjs';
|
|
37
37
|
import { createMessageRepository } from './repositories/message-repository.mjs';
|
|
38
38
|
import { createQuestionRepository } from './repositories/question-repository.mjs';
|
|
39
|
+
import { mapRelayActivityRow, mapRelayThoughtRow, sanitizeActivityText } from './repositories/relay-turn-rows.mjs';
|
|
39
40
|
import { createImageConversationRepository } from './repositories/image-conversation-repository.mjs';
|
|
40
41
|
import { registerSessionsRoutes } from './routes/sessions-routes.mjs';
|
|
41
42
|
import { buildDequeuedRelayMessage, claudeApiRemedyText, dequeuePendingMessageForWorkerLoop, registerMessagesRoutes } from './routes/messages-routes.mjs';
|
|
@@ -195,8 +196,8 @@ import {
|
|
|
195
196
|
resolveDefaultSessionWorkspaceRootState as resolveDefaultSessionWorkspaceRootStateFromService,
|
|
196
197
|
resolveLaunchWorkspaceRootPath,
|
|
197
198
|
} from './services/workspace-root-defaults-service.mjs';
|
|
198
|
-
import { maybeStartTtyConsole } from './tty-console-bootstrap.mjs';
|
|
199
199
|
import { installRelayConsoleLog, resolveRelayConsoleLogPath } from './services/relay-console-log.mjs';
|
|
200
|
+
import { isMissingSqliteBindingError, missingSqliteBindingLines } from './services/native-addon-build-service.mjs';
|
|
200
201
|
import {
|
|
201
202
|
computeFeatureFlagState,
|
|
202
203
|
FEATURE_REGISTRY,
|
|
@@ -233,6 +234,9 @@ import {
|
|
|
233
234
|
import { createRestartPictureCollector, detectRelayHosting } from './services/relay-restart-picture.mjs';
|
|
234
235
|
import { createWorkerScope } from './services/worker-scope.mjs';
|
|
235
236
|
import { createStaleRecoveryClockGuard } from './services/stale-recovery-clock-guard.mjs';
|
|
237
|
+
import { createArmableInterval, createCliPresence } from './services/idle-timers.mjs';
|
|
238
|
+
import { installQueueWakeHook } from './services/queue-wake-hook.mjs';
|
|
239
|
+
import { createPendingWorkerPrimer, createQueueWaker } from './services/queue-waker.mjs';
|
|
236
240
|
import { createStaleTurnRecovery } from './services/stale-turn-recovery.mjs';
|
|
237
241
|
import {
|
|
238
242
|
STALE_SUBAGENT_RUN_BOOT_DELAY_MS,
|
|
@@ -270,7 +274,11 @@ import {
|
|
|
270
274
|
readTaskStallMinutesSetting,
|
|
271
275
|
} from '../shared/task-output-text.mjs';
|
|
272
276
|
import { normalizeRelayThoughtList } from './public/app/relay-thoughts.mjs';
|
|
273
|
-
import {
|
|
277
|
+
import {
|
|
278
|
+
capFinishedActivityEntries,
|
|
279
|
+
capRelayActivityEntries,
|
|
280
|
+
inFlightActivityCap,
|
|
281
|
+
} from './public/app/activity-replay-state.mjs';
|
|
274
282
|
import {
|
|
275
283
|
isOpenAIModelId,
|
|
276
284
|
isSafeClaudeModelId,
|
|
@@ -319,13 +327,15 @@ const PWA_VERSION_PLACEHOLDER = /const __PWA_VERSION = '[^']*';/;
|
|
|
319
327
|
// after a restart or an incident.
|
|
320
328
|
const relayConsoleLogPath = resolveRelayConsoleLogPath({ env: process.env, serverDir: __dirname });
|
|
321
329
|
const relayConsoleLog = installRelayConsoleLog({ logPath: relayConsoleLogPath });
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
})
|
|
327
|
-
|
|
328
|
-
|
|
330
|
+
// Where better-sqlite3 ships no binary (Termux, 32-bit ARM) the binding is
|
|
331
|
+
// built by `oar build-native`; until then say so instead of a stack trace.
|
|
332
|
+
try {
|
|
333
|
+
new Database(':memory:').close();
|
|
334
|
+
} catch (error) {
|
|
335
|
+
if (!isMissingSqliteBindingError(error)) throw error;
|
|
336
|
+
for (const line of missingSqliteBindingLines({ error })) console.error(`[startup] ${line}`);
|
|
337
|
+
process.exit(1);
|
|
338
|
+
}
|
|
329
339
|
// TEMP and TMP for this relay and everything it starts (normalizeTempEnv).
|
|
330
340
|
for (const change of normalizeTempEnv()) console.log(`[startup] ${change} (quotes taken off, or the folder is not there)`);
|
|
331
341
|
|
|
@@ -2005,12 +2015,9 @@ let runtimeShutdownStarted = false;
|
|
|
2005
2015
|
// Set once the database is open (see the retention sweep near the other sweeps).
|
|
2006
2016
|
let subagentRecordRetention = null;
|
|
2007
2017
|
const runtimeTimers = {
|
|
2008
|
-
cliStatus: null,
|
|
2009
2018
|
ownerWatchdog: null,
|
|
2010
2019
|
staleRecovery: null,
|
|
2011
2020
|
questionExpiry: null,
|
|
2012
|
-
pendingWorkerPrime: null,
|
|
2013
|
-
sharedViewerPrune: null,
|
|
2014
2021
|
restoredTaskSettle: null,
|
|
2015
2022
|
claudeApiCapClock: null,
|
|
2016
2023
|
memoryLog: null,
|
|
@@ -2019,6 +2026,25 @@ const runtimeTimers = {
|
|
|
2019
2026
|
archivedWorkerSweep: null,
|
|
2020
2027
|
};
|
|
2021
2028
|
|
|
2029
|
+
// The worker prime and the detached-worker sweep, every 5 s while a pending
|
|
2030
|
+
// row waits for its worker (rows in retry backoff included, so the worker is
|
|
2031
|
+
// warm when the backoff matures). Armed at startup, by every prime that finds
|
|
2032
|
+
// a row and by every queue snapshot with a row pending; a pass that leaves no
|
|
2033
|
+
// such row disarms it. Declared up here so a prime early in the boot can arm
|
|
2034
|
+
// it; its tick runs only after the boot.
|
|
2035
|
+
const pendingWorkerPrimeTimer = createArmableInterval({
|
|
2036
|
+
intervalMs: 5_000,
|
|
2037
|
+
tick: async () => {
|
|
2038
|
+
await Promise.allSettled([
|
|
2039
|
+
primePendingSessionWorkers('maintenance'),
|
|
2040
|
+
detachedWorkerWatch.sweep().catch((error) => {
|
|
2041
|
+
console.warn(`${runtimeLogPrefix()}WORKER DETACHED sweep failed: ${error?.message || error}`);
|
|
2042
|
+
}),
|
|
2043
|
+
]);
|
|
2044
|
+
return hasPendingWorkerOwnerRows();
|
|
2045
|
+
},
|
|
2046
|
+
});
|
|
2047
|
+
|
|
2022
2048
|
function normalizeRemotePath(value) {
|
|
2023
2049
|
if (typeof value !== 'string') return '';
|
|
2024
2050
|
const trimmed = value.trim();
|
|
@@ -2088,7 +2114,6 @@ function workspaceRootPayload() {
|
|
|
2088
2114
|
}
|
|
2089
2115
|
|
|
2090
2116
|
const ACTIVE_SESSION_CWD_STATUSES = new Set(['starting', 'ready', 'processing']);
|
|
2091
|
-
const ACTIVE_TTY_CLI_WORKER_STATUSES = new Set(['starting', 'ready', 'processing']);
|
|
2092
2117
|
|
|
2093
2118
|
function normalizeConversationWorkspaceRootPath(candidatePath) {
|
|
2094
2119
|
const normalized = normalizeWorkspaceRootPath(candidatePath);
|
|
@@ -2861,6 +2886,16 @@ const stmts = {
|
|
|
2861
2886
|
...createQuestionRepository(db),
|
|
2862
2887
|
...createImageConversationRepository(db),
|
|
2863
2888
|
};
|
|
2889
|
+
// Every queue write that can make a row deliverable wakes the delivery
|
|
2890
|
+
// (services/queue-wake-hook.mjs): installed before any route or startup
|
|
2891
|
+
// write. The waker exists once the worker socket service does; a write
|
|
2892
|
+
// before that is covered by the startup prime and the service's start.
|
|
2893
|
+
// Without the hook (it could not be installed) the queue is polled.
|
|
2894
|
+
let queueWaker = null;
|
|
2895
|
+
const queueWakeHook = installQueueWakeHook(db, {
|
|
2896
|
+
onWake: (reason) => { queueWaker?.wake(reason, { prime: false }); },
|
|
2897
|
+
log: (line) => console.warn(`[queue-wake] ${line}`),
|
|
2898
|
+
});
|
|
2864
2899
|
const statusEventService = createStatusEventService(db);
|
|
2865
2900
|
// Public shares and their access log (migration 0016).
|
|
2866
2901
|
const shareRepository = createShareRepository(db);
|
|
@@ -3422,6 +3457,8 @@ const sessionWorkerSupervisor = createSessionWorkerSupervisor({
|
|
|
3422
3457
|
diagnosticPlanReference: () => path.join(currentWorkspaceRootPath(), '.cursor', 'plans', 'worker-startup-monitoring-plan.md'),
|
|
3423
3458
|
log: (message) => console.warn(`${runtimeLogPrefix()}${message}`),
|
|
3424
3459
|
onLaunchState: (event) => workerLaunchNoteService.handle(event),
|
|
3460
|
+
// A kill's grace ended: the session's rows may go to its next worker.
|
|
3461
|
+
onKillBlockLifted: () => { wakeSessionWorkerQueue('kill-block-lifted'); },
|
|
3425
3462
|
});
|
|
3426
3463
|
// Feature flags moved from config.json into app_settings; adopt any legacy
|
|
3427
3464
|
// `features` key once, then resolve the boot snapshot (defaults < stored rows
|
|
@@ -3724,27 +3761,6 @@ function queueCounts() {
|
|
|
3724
3761
|
return { pendingCount: map.pending || 0, processingCount: map.processing || 0, parkedCount: map.parked || 0 };
|
|
3725
3762
|
}
|
|
3726
3763
|
|
|
3727
|
-
function countActiveCliWorkers() {
|
|
3728
|
-
const workers = sessionWorkerRegistry?.listWorkers?.() || [];
|
|
3729
|
-
return workers.reduce((acc, worker) => {
|
|
3730
|
-
const status = String(worker?.status || '').trim().toLowerCase();
|
|
3731
|
-
return ACTIVE_TTY_CLI_WORKER_STATUSES.has(status) ? acc + 1 : acc;
|
|
3732
|
-
}, 0);
|
|
3733
|
-
}
|
|
3734
|
-
|
|
3735
|
-
function attachTtyConsoleRuntimeTitle(runtime) {
|
|
3736
|
-
const addConsoleTitle = runtime?.tty?.addConsoleTitle;
|
|
3737
|
-
if (typeof addConsoleTitle !== 'function') return;
|
|
3738
|
-
addConsoleTitle(() => {
|
|
3739
|
-
const counts = queueCounts();
|
|
3740
|
-
const connectedClients = Number(io?.sockets?.sockets?.size || 0);
|
|
3741
|
-
const activeCliWorkers = countActiveCliWorkers();
|
|
3742
|
-
const cliState = cliOnline ? 'online' : 'offline';
|
|
3743
|
-
return `Clients: ${connectedClients} :: Pen: ${counts.pendingCount} :: Proc: ${counts.processingCount} :: parked: ${counts.parkedCount} :: CLIs: ${activeCliWorkers} :: CLI: ${cliState}`;
|
|
3744
|
-
});
|
|
3745
|
-
}
|
|
3746
|
-
attachTtyConsoleRuntimeTitle(ttyConsoleRuntime);
|
|
3747
|
-
|
|
3748
3764
|
// ─── Queued relay restart / shutdown ─────────────────────────────────────────
|
|
3749
3765
|
// A request waits until no turn is processing and holds new turns meanwhile
|
|
3750
3766
|
// (services/relay-shutdown-gate.mjs); past the maximum wait it asks instead
|
|
@@ -3822,7 +3838,11 @@ function relayShutdownStatePayload() {
|
|
|
3822
3838
|
|
|
3823
3839
|
function cancelRelayShutdown({ requestedBy = 'localhost-api' } = {}) {
|
|
3824
3840
|
const result = relayRestartQueue.cancel({ requestedBy });
|
|
3825
|
-
if (result?.cancelled)
|
|
3841
|
+
if (result?.cancelled) {
|
|
3842
|
+
relayEventService?.noteRestartCancelled();
|
|
3843
|
+
// The queued restart held deliveries back: they go out now.
|
|
3844
|
+
wakeSessionWorkerQueue('restart-cancelled');
|
|
3845
|
+
}
|
|
3826
3846
|
return result;
|
|
3827
3847
|
}
|
|
3828
3848
|
|
|
@@ -5391,52 +5411,28 @@ function createCompactedConversation(sourceConversationId) {
|
|
|
5391
5411
|
};
|
|
5392
5412
|
}
|
|
5393
5413
|
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
}
|
|
5399
|
-
|
|
5400
|
-
function mapRelayActivityRow(row) {
|
|
5401
|
-
const text = sanitizeActivityText(row?.text);
|
|
5402
|
-
if (!text) return null;
|
|
5403
|
-
const subagentRunId = row?.subagent_run_id ? String(row.subagent_run_id).trim() : null;
|
|
5404
|
-
// Structured rows (compaction boundaries) carry their payload alongside the
|
|
5405
|
-
// prose so the transcript can promote them to a break row; junk JSON is
|
|
5406
|
-
// simply dropped back to a prose-only entry.
|
|
5407
|
-
let metadata = null;
|
|
5408
|
-
if (row?.metadata_json) {
|
|
5409
|
-
try {
|
|
5410
|
-
const parsed = JSON.parse(String(row.metadata_json));
|
|
5411
|
-
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) metadata = parsed;
|
|
5412
|
-
} catch { metadata = null; }
|
|
5413
|
-
}
|
|
5414
|
-
return {
|
|
5415
|
-
text,
|
|
5416
|
-
subagentRunId: subagentRunId || null,
|
|
5417
|
-
...(metadata ? { metadata } : {}),
|
|
5418
|
-
};
|
|
5419
|
-
}
|
|
5420
|
-
|
|
5421
|
-
const RELAY_ACTIVITY_ROW_LIMIT = 48;
|
|
5422
|
-
|
|
5414
|
+
// Per thread (the main agent, each sub-agent), the newest rows; structured
|
|
5415
|
+
// rows are always kept (activity-replay-state.mjs). A finished turn: the main
|
|
5416
|
+
// agent's 200, each sub-agent's 50, and a turn whose main agent ran more says
|
|
5417
|
+
// how many it left out.
|
|
5423
5418
|
function relayActivityForResponse(responseMessageId) {
|
|
5424
|
-
return
|
|
5419
|
+
return capFinishedActivityEntries(
|
|
5425
5420
|
stmts.listActivityByResponse
|
|
5426
5421
|
.all(responseMessageId)
|
|
5427
5422
|
.map(mapRelayActivityRow)
|
|
5428
5423
|
.filter(Boolean),
|
|
5429
|
-
RELAY_ACTIVITY_ROW_LIMIT,
|
|
5430
5424
|
);
|
|
5431
5425
|
}
|
|
5432
5426
|
|
|
5433
|
-
|
|
5427
|
+
// A running turn: as many per thread as the browser shows (`activityCap`, the
|
|
5428
|
+
// request's param; 50 when it sends none, 500 at most).
|
|
5429
|
+
function relayActivityForQueueMessage(queueMessageId, activityCap = undefined) {
|
|
5434
5430
|
return capRelayActivityEntries(
|
|
5435
5431
|
stmts.listActivityByQueueMessage
|
|
5436
5432
|
.all(queueMessageId)
|
|
5437
5433
|
.map(mapRelayActivityRow)
|
|
5438
5434
|
.filter(Boolean),
|
|
5439
|
-
|
|
5435
|
+
inFlightActivityCap(activityCap),
|
|
5440
5436
|
);
|
|
5441
5437
|
}
|
|
5442
5438
|
|
|
@@ -5458,19 +5454,6 @@ function relayStreamEventsForQueueMessage(queueMessageId) {
|
|
|
5458
5454
|
.slice(0, 5000);
|
|
5459
5455
|
}
|
|
5460
5456
|
|
|
5461
|
-
function mapRelayThoughtRow(row) {
|
|
5462
|
-
const seq = Math.max(0, Math.trunc(Number(row?.seq || 0)));
|
|
5463
|
-
const subagentRunId = row?.subagent_run_id ? String(row.subagent_run_id).trim() : null;
|
|
5464
|
-
return {
|
|
5465
|
-
reasoningId: row?.reasoning_id ? String(row.reasoning_id) : null,
|
|
5466
|
-
seq,
|
|
5467
|
-
text: String(row?.text || ''),
|
|
5468
|
-
done: Number(row?.done || 0) === 1,
|
|
5469
|
-
timestamp: row?.created_at || null,
|
|
5470
|
-
subagentRunId: subagentRunId || null,
|
|
5471
|
-
};
|
|
5472
|
-
}
|
|
5473
|
-
|
|
5474
5457
|
function relayThoughtsForResponse(responseMessageId) {
|
|
5475
5458
|
const rows = stmts.listThoughtsByResponse?.all(responseMessageId) || [];
|
|
5476
5459
|
return normalizeRelayThoughtList(rows.map(mapRelayThoughtRow)).slice(0, 5000);
|
|
@@ -5654,7 +5637,7 @@ const usageLimitPauseService = createUsageLimitPauseService({
|
|
|
5654
5637
|
stmts,
|
|
5655
5638
|
emit: (event, payload) => io.emit(event, payload),
|
|
5656
5639
|
// A follow-up resumed by hand is due at once: wake its worker.
|
|
5657
|
-
onQueueChanged: (reason) => {
|
|
5640
|
+
onQueueChanged: (reason) => { wakeSessionWorkerQueue(reason); },
|
|
5658
5641
|
logger: console,
|
|
5659
5642
|
});
|
|
5660
5643
|
|
|
@@ -5686,8 +5669,7 @@ const claudeBillingSwitchService = createClaudeBillingSwitchService({
|
|
|
5686
5669
|
// What the swap waits for: a turn, a question, a compaction, background tasks.
|
|
5687
5670
|
getPending: (runtime) => claudeBillingSwapService.pendingFor(runtime?.sdk_session_id || runtime?.conversation_id),
|
|
5688
5671
|
onQueueChanged: (reason) => {
|
|
5689
|
-
|
|
5690
|
-
sessionWorkerWebSocketService?.emitQueueChanged?.(reason);
|
|
5672
|
+
wakeSessionWorkerQueue(reason);
|
|
5691
5673
|
},
|
|
5692
5674
|
logger: console,
|
|
5693
5675
|
});
|
|
@@ -5735,8 +5717,7 @@ const restartResumeService = createRestartResumeService({
|
|
|
5735
5717
|
// The note is an ordinary pending message: the primer starts a stopped
|
|
5736
5718
|
// worker, the socket service hands it to a running one.
|
|
5737
5719
|
wake: () => {
|
|
5738
|
-
|
|
5739
|
-
sessionWorkerWebSocketService?.emitQueueChanged?.('restart-resume');
|
|
5720
|
+
wakeSessionWorkerQueue('restart-resume');
|
|
5740
5721
|
},
|
|
5741
5722
|
logger: console,
|
|
5742
5723
|
});
|
|
@@ -5766,8 +5747,7 @@ relayEventService = createRelayEventService({
|
|
|
5766
5747
|
// The event row is an ordinary pending message: the primer starts a
|
|
5767
5748
|
// stopped worker, the socket service hands it to a running one.
|
|
5768
5749
|
wakeWatcher: () => {
|
|
5769
|
-
|
|
5770
|
-
sessionWorkerWebSocketService?.emitQueueChanged?.('relay-event');
|
|
5750
|
+
wakeSessionWorkerQueue('relay-event');
|
|
5771
5751
|
},
|
|
5772
5752
|
relayName: () => remoteRelayRegistry?.selfName?.() || getPwaAppName?.() || '',
|
|
5773
5753
|
onRemoteChange: (relayId) => { void relayEventLinkService?.fetchNow(relayId); },
|
|
@@ -5783,8 +5763,7 @@ const relayScheduleService = createRelayScheduleService({
|
|
|
5783
5763
|
emit: (event, payload) => io.emit(event, payload),
|
|
5784
5764
|
// The scheduled message is an ordinary pending one, like a relay event's.
|
|
5785
5765
|
wake: () => {
|
|
5786
|
-
|
|
5787
|
-
sessionWorkerWebSocketService?.emitQueueChanged?.('relay-schedule');
|
|
5766
|
+
wakeSessionWorkerQueue('relay-schedule');
|
|
5788
5767
|
},
|
|
5789
5768
|
// A firing into another session is a prompt of the conversation that set
|
|
5790
5769
|
// it: the agents' loop guard counts it (and holds it once it wants the user).
|
|
@@ -5847,7 +5826,9 @@ function resolveLiveTurnQueueRow(conversationId) {
|
|
|
5847
5826
|
return byId.get(liveId) || rows[0];
|
|
5848
5827
|
}
|
|
5849
5828
|
|
|
5850
|
-
|
|
5829
|
+
// `activityCap`: the tool lines per thread the asking page shows (the
|
|
5830
|
+
// conversation route passes its `activityCap` param; others get the default).
|
|
5831
|
+
function inFlightStateForConversation(conversationId, { activityCap = undefined } = {}) {
|
|
5851
5832
|
const row = resolveLiveTurnQueueRow(conversationId);
|
|
5852
5833
|
if (!row) return null;
|
|
5853
5834
|
const streamEvents = relayStreamEventsForQueueMessage(row.id);
|
|
@@ -5859,7 +5840,7 @@ function inFlightStateForConversation(conversationId) {
|
|
|
5859
5840
|
mode: normalizeRelayMode(row.relay_mode) || DEFAULT_RELAY_MODE,
|
|
5860
5841
|
timestamp: row.timestamp || null,
|
|
5861
5842
|
processingAt: row.processing_at || null,
|
|
5862
|
-
activities: relayActivityForQueueMessage(row.id),
|
|
5843
|
+
activities: relayActivityForQueueMessage(row.id, activityCap),
|
|
5863
5844
|
thoughts: relayThoughtsForQueueMessage(row.id),
|
|
5864
5845
|
streamEvents,
|
|
5865
5846
|
streamDone: !!lastStreamEvent?.done,
|
|
@@ -6392,6 +6373,15 @@ const SHARED_VIEWER_MAX_GLOBAL = Number.isFinite(Number(config.sharedPresenceMax
|
|
|
6392
6373
|
? Math.max(SHARED_VIEWER_MAX_PER_CONVERSATION, Math.trunc(Number(config.sharedPresenceMaxGlobal)))
|
|
6393
6374
|
: 5_000;
|
|
6394
6375
|
const sharedViewersByConversation = new Map();
|
|
6376
|
+
// Armed when the first share viewer arrives, disarmed by the prune that
|
|
6377
|
+
// leaves none: a relay nobody watches through a share link has no timer.
|
|
6378
|
+
const sharedViewerPruneTimer = createArmableInterval({
|
|
6379
|
+
intervalMs: 5_000,
|
|
6380
|
+
tick: () => {
|
|
6381
|
+
pruneSharedViewerPresence();
|
|
6382
|
+
return sharedViewersByConversation.size > 0;
|
|
6383
|
+
},
|
|
6384
|
+
});
|
|
6395
6385
|
|
|
6396
6386
|
function sanitizeSharedViewerId(value) {
|
|
6397
6387
|
const id = String(value || '').trim().replace(/[^a-zA-Z0-9:_-]+/g, '');
|
|
@@ -6532,6 +6522,7 @@ function markSharedViewerPresence({ conversationId, token, viewerId } = {}) {
|
|
|
6532
6522
|
}
|
|
6533
6523
|
const before = viewers.size;
|
|
6534
6524
|
viewers.set(key, { lastSeenAt: now });
|
|
6525
|
+
sharedViewerPruneTimer.arm();
|
|
6535
6526
|
const watcherCount = viewers.size;
|
|
6536
6527
|
if (before !== watcherCount) {
|
|
6537
6528
|
emitConversationWatcherCount(convId);
|
|
@@ -6808,7 +6799,7 @@ async function recoverUndeliveredSessionWorkerMessage({ pending = null, sessionI
|
|
|
6808
6799
|
console.warn(`${runtimeLogPrefix()}WS REQUEUE ${messageId.slice(0, 8)} session=${requesterSessionId ? requesterSessionId.slice(0, 8) : 'unknown'} retry=${retryCount} reason=${String(reason || 'ws-send-failed')}`);
|
|
6809
6800
|
io.emit('message_status', { messageId, conversationId: row.conversation_id, status: 'pending' });
|
|
6810
6801
|
io.emit('queue_updated', { recovered: 1, reason: 'worker-ws-send-failed' });
|
|
6811
|
-
|
|
6802
|
+
wakeSessionWorkerQueue('worker-ws-send-failed');
|
|
6812
6803
|
return true;
|
|
6813
6804
|
}
|
|
6814
6805
|
|
|
@@ -6898,11 +6889,10 @@ function recoverDeadWorkerProcessingRows(sessionId, reason) {
|
|
|
6898
6889
|
}
|
|
6899
6890
|
if (recovered > 0) {
|
|
6900
6891
|
io.emit('queue_updated', { recovered, reason: `worker-died:${reason}` });
|
|
6901
|
-
sessionWorkerWebSocketService?.emitQueueChanged?.(`worker-died:${reason}`);
|
|
6902
6892
|
}
|
|
6903
6893
|
sessionWorkerSupervisor?.markError?.(sid, `worker-died:${reason}`);
|
|
6904
6894
|
// The rows stay owned, so the primer is what respawns their worker.
|
|
6905
|
-
|
|
6895
|
+
wakeSessionWorkerQueue(`worker-died:${reason}`);
|
|
6906
6896
|
return recovered;
|
|
6907
6897
|
}
|
|
6908
6898
|
|
|
@@ -6981,7 +6971,19 @@ async function sweepDeadWorkerProcessingRowsOnce() {
|
|
|
6981
6971
|
// work. The worker recycle raises it; the billing swap does not, since its
|
|
6982
6972
|
// own delivery fence already keeps the other billing's rows from the old
|
|
6983
6973
|
// worker.
|
|
6984
|
-
|
|
6974
|
+
// A lifted fence (the replacement launched, or its stop failed and the old
|
|
6975
|
+
// worker serves again) hands out what waited behind it.
|
|
6976
|
+
const workerReplacementFence = (() => {
|
|
6977
|
+
const fence = createWorkerReplacementFence();
|
|
6978
|
+
return {
|
|
6979
|
+
...fence,
|
|
6980
|
+
release(sid, pid) {
|
|
6981
|
+
const released = fence.release(sid, pid);
|
|
6982
|
+
if (released) wakeSessionWorkerQueue('replacement-fence-lifted');
|
|
6983
|
+
return released;
|
|
6984
|
+
},
|
|
6985
|
+
};
|
|
6986
|
+
})();
|
|
6985
6987
|
function createRuntimeWorkerReplacement(tag) {
|
|
6986
6988
|
return createWorkerReplacement({
|
|
6987
6989
|
registry: sessionWorkerRegistry,
|
|
@@ -7288,6 +7290,21 @@ const sessionWorkerWebSocketService = createSessionWorkerWebSocketService({
|
|
|
7288
7290
|
},
|
|
7289
7291
|
onDeliverySendFailed: recoverUndeliveredSessionWorkerMessage,
|
|
7290
7292
|
onWorkerSocketClosed: handleWorkerSocketClosed,
|
|
7293
|
+
// The CLI presence window is wider while a worker socket is open.
|
|
7294
|
+
onSocketCountChange: (count) => cliPresence.noteWorkerSocketCount(count),
|
|
7295
|
+
// A row pending (in backoff too) keeps the worker prime armed, and the
|
|
7296
|
+
// poll primes now and then while rows wait.
|
|
7297
|
+
onQueueSnapshot: (snapshot) => {
|
|
7298
|
+
if (snapshot.pendingCount > 0) queueWaker?.primeFromBackstop('backstop');
|
|
7299
|
+
},
|
|
7300
|
+
// A retry time or a usage-limit hold coming due wakes the queue on time.
|
|
7301
|
+
nextDueAtMs: () => {
|
|
7302
|
+
if (runtimeShutdownStarted) return null;
|
|
7303
|
+
const due = stmts.nextPendingAttemptAt?.get?.(new Date().toISOString())?.due;
|
|
7304
|
+
const dueMs = due ? Date.parse(due) : NaN;
|
|
7305
|
+
return Number.isFinite(dueMs) ? dueMs : null;
|
|
7306
|
+
},
|
|
7307
|
+
onDue: (reason) => { wakeSessionWorkerQueue(reason); },
|
|
7291
7308
|
requestWork: requestSessionWorkerSocketDelivery,
|
|
7292
7309
|
// What a worker says it is ("claude", "claude-api", ...): the registry
|
|
7293
7310
|
// keeps it for the billing fence and for what a response ran on.
|
|
@@ -7309,9 +7326,23 @@ const sessionWorkerWebSocketService = createSessionWorkerWebSocketService({
|
|
|
7309
7326
|
buildId: BUILD_ID,
|
|
7310
7327
|
isWorkerProcessAlive: (pid) => isProcessAlive(Number(pid)),
|
|
7311
7328
|
pathPrefix: remotePath,
|
|
7312
|
-
|
|
7329
|
+
// A backstop under the queue's own wakes; the poll every 500 ms it was
|
|
7330
|
+
// when those cannot be installed.
|
|
7331
|
+
pollIntervalMs: queueWakeHook.installed ? 10_000 : 500,
|
|
7313
7332
|
logger: console,
|
|
7314
7333
|
});
|
|
7334
|
+
let queueWakeStarted = false;
|
|
7335
|
+
queueWaker = createQueueWaker({
|
|
7336
|
+
prime: (reason) => primePendingSessionWorkers(reason),
|
|
7337
|
+
notify: (reason) => {
|
|
7338
|
+
sessionWorkerWebSocketService.notifyQueueChanged(reason).catch((error) => {
|
|
7339
|
+
console.warn(`${runtimeLogPrefix()}QUEUE WAKE failed reason=${reason} err=${error?.message || error}`);
|
|
7340
|
+
});
|
|
7341
|
+
},
|
|
7342
|
+
armPrime: () => pendingWorkerPrimeTimer.arm(),
|
|
7343
|
+
isReady: () => queueWakeStarted && !runtimeShutdownStarted,
|
|
7344
|
+
warn: (line) => console.warn(`${runtimeLogPrefix()}${line}`),
|
|
7345
|
+
});
|
|
7315
7346
|
runtimeTimers.deadWorkerSweep = setInterval(() => {
|
|
7316
7347
|
sweepDeadWorkerProcessingRows().catch((error) => {
|
|
7317
7348
|
console.warn(`${runtimeLogPrefix()}WORKER SWEEP failed: ${error?.message || error}`);
|
|
@@ -7338,40 +7369,39 @@ const tmuxInspectorSocketService = createTmuxInspectorSocketService({
|
|
|
7338
7369
|
logger: console,
|
|
7339
7370
|
});
|
|
7340
7371
|
|
|
7341
|
-
|
|
7342
|
-
|
|
7343
|
-
|
|
7344
|
-
|
|
7345
|
-
|
|
7346
|
-
|
|
7347
|
-
|
|
7348
|
-
|
|
7349
|
-
|
|
7350
|
-
|
|
7351
|
-
|
|
7352
|
-
|
|
7353
|
-
|
|
7354
|
-
|
|
7355
|
-
|
|
7356
|
-
|
|
7357
|
-
|
|
7358
|
-
|
|
7359
|
-
|
|
7360
|
-
|
|
7361
|
-
|
|
7362
|
-
|
|
7363
|
-
|
|
7364
|
-
|
|
7365
|
-
|
|
7366
|
-
|
|
7367
|
-
|
|
7368
|
-
|
|
7369
|
-
|
|
7370
|
-
|
|
7371
|
-
|
|
7372
|
-
|
|
7373
|
-
|
|
7374
|
-
return requested;
|
|
7372
|
+
// No next_attempt_at filter in the listing: rows sitting in retry backoff
|
|
7373
|
+
// still need their worker warm, otherwise the poll wins the race the moment
|
|
7374
|
+
// the backoff matures and the turn executes on the wrong provider.
|
|
7375
|
+
const pendingWorkerPrimer = createPendingWorkerPrimer({
|
|
7376
|
+
isEnabled: () => featureFlags?.SESSION_WORKER_ROUTING_ENABLED === true
|
|
7377
|
+
&& !runtimeShutdownStarted
|
|
7378
|
+
&& typeof sessionWorkerSupervisor?.ensureWorker === 'function'
|
|
7379
|
+
&& typeof stmts.listPendingWorkerOwnerSessionIds?.all === 'function',
|
|
7380
|
+
listPendingOwnerSessionIds: (limit) => stmts.listPendingWorkerOwnerSessionIds.all(limit),
|
|
7381
|
+
isKillBlocked: (sid) => sessionWorkerSupervisor?.isKillBlocked?.(sid) === true,
|
|
7382
|
+
failRowsWithoutLaunch: (sid) => failClaudeApiRowsWithoutLaunch(sid),
|
|
7383
|
+
ensureWorker: (sid) => sessionWorkerSupervisor.ensureWorker(sid),
|
|
7384
|
+
// Rows waiting for their worker: the maintenance pass retries until none is.
|
|
7385
|
+
onRowsWaiting: () => pendingWorkerPrimeTimer.arm(),
|
|
7386
|
+
warn: (line) => console.warn(`${runtimeLogPrefix()}${line}`),
|
|
7387
|
+
});
|
|
7388
|
+
|
|
7389
|
+
function primePendingSessionWorkers(reason = 'pending-worker-prime') {
|
|
7390
|
+
return pendingWorkerPrimer(reason);
|
|
7391
|
+
}
|
|
7392
|
+
|
|
7393
|
+
/**
|
|
7394
|
+
* The queue may hold work for a worker now (a row written, a gate lifted, a
|
|
7395
|
+
* retry come due): hand rows to the ready workers, prime the sessions with
|
|
7396
|
+
* pending rows, keep the maintenance pass armed.
|
|
7397
|
+
*/
|
|
7398
|
+
function wakeSessionWorkerQueue(reason = 'queue-wake') {
|
|
7399
|
+
return queueWaker?.wake(reason) === true;
|
|
7400
|
+
}
|
|
7401
|
+
|
|
7402
|
+
function hasPendingWorkerOwnerRows() {
|
|
7403
|
+
if (featureFlags?.SESSION_WORKER_ROUTING_ENABLED !== true || runtimeShutdownStarted) return false;
|
|
7404
|
+
return (stmts.listPendingWorkerOwnerSessionIds?.all?.(1) || []).length > 0;
|
|
7375
7405
|
}
|
|
7376
7406
|
|
|
7377
7407
|
// Guard first, so a tunnelled request to a worker path is rejected before any
|
|
@@ -7380,6 +7410,12 @@ app.use(tunnelWorkerPathGuard.requestMiddleware);
|
|
|
7380
7410
|
// Uploads travel as raw bodies on POST /api/upload (express.raw, capped by
|
|
7381
7411
|
// MAX_UPLOAD_BYTES), so the JSON limit only bounds inline image data URLs.
|
|
7382
7412
|
app.use(express.json({ limit: '20mb' }));
|
|
7413
|
+
// Express 5 leaves req.body undefined when no parser read a body (a GET, a
|
|
7414
|
+
// POST without a JSON body); the routes read it as express 4 gave it, {}.
|
|
7415
|
+
app.use((req, _res, next) => {
|
|
7416
|
+
if (req.body === undefined) req.body = {};
|
|
7417
|
+
next();
|
|
7418
|
+
});
|
|
7383
7419
|
app.use((req, _res, next) => {
|
|
7384
7420
|
stripRequestPathPrefix(req, remotePath);
|
|
7385
7421
|
next();
|
|
@@ -7411,9 +7447,7 @@ app.use((req, res, next) => {
|
|
|
7411
7447
|
});
|
|
7412
7448
|
app.get('/socket.io/socket.io.js', (req, res, next) => {
|
|
7413
7449
|
res.setHeader('Cache-Control', 'no-store');
|
|
7414
|
-
res
|
|
7415
|
-
if (error) next(error);
|
|
7416
|
-
});
|
|
7450
|
+
sendOwnFile(res, SOCKET_IO_CLIENT_JS_PATH, next);
|
|
7417
7451
|
});
|
|
7418
7452
|
app.get('/manifest.webmanifest', (req, res, next) => {
|
|
7419
7453
|
try {
|
|
@@ -7427,9 +7461,7 @@ app.get('/manifest.webmanifest', (req, res, next) => {
|
|
|
7427
7461
|
});
|
|
7428
7462
|
app.get('/sw.js', (req, res, next) => {
|
|
7429
7463
|
res.setHeader('Cache-Control', 'no-store');
|
|
7430
|
-
res
|
|
7431
|
-
if (error) next(error);
|
|
7432
|
-
});
|
|
7464
|
+
sendOwnFile(res, PWA_SW_PATH, next);
|
|
7433
7465
|
});
|
|
7434
7466
|
app.get(['/', '/index.html'], (req, res, next) => {
|
|
7435
7467
|
try {
|
|
@@ -7475,7 +7507,14 @@ app.get(['/share-preview/:conversationId', '/share-preview/:conversationId/'], (
|
|
|
7475
7507
|
app.use(express.static(PUBLIC_DIR, { etag: false, setHeaders: (res) => res.setHeader('Cache-Control', 'no-store') }));
|
|
7476
7508
|
|
|
7477
7509
|
// ─── CLI Status Tracking ──────────────────────────────────────────────────────
|
|
7478
|
-
|
|
7510
|
+
// When the workers were last heard from, without a polling timer: the window
|
|
7511
|
+
// is 45 s while a worker socket is open (an idle worker beats every 30 s) and
|
|
7512
|
+
// 10 s once none is (services/idle-timers.mjs).
|
|
7513
|
+
const cliPresence = createCliPresence({
|
|
7514
|
+
onExpired: () => {
|
|
7515
|
+
if (cliOnline) markCliOffline('heartbeat-timeout');
|
|
7516
|
+
},
|
|
7517
|
+
});
|
|
7479
7518
|
let cliOnline = false;
|
|
7480
7519
|
let relayPaused = false;
|
|
7481
7520
|
const relayBridgeOwnerService = createRelayBridgeOwnerService();
|
|
@@ -7491,8 +7530,12 @@ const runtimeState = {
|
|
|
7491
7530
|
get cliOnline() { return cliOnline; },
|
|
7492
7531
|
set cliOnline(value) { cliOnline = value; },
|
|
7493
7532
|
get relayPaused() { return relayPaused; },
|
|
7494
|
-
set relayPaused(value) {
|
|
7495
|
-
|
|
7533
|
+
set relayPaused(value) {
|
|
7534
|
+
const wasPaused = relayPaused;
|
|
7535
|
+
relayPaused = value;
|
|
7536
|
+
// Resumed: what was queued while paused goes out now.
|
|
7537
|
+
if (wasPaused && !relayPaused) wakeSessionWorkerQueue('relay-resumed');
|
|
7538
|
+
},
|
|
7496
7539
|
get relayShutdown() { return relayRestartQueue.getState(); },
|
|
7497
7540
|
// A shutdown or restart waits for the processing turns: no pending row
|
|
7498
7541
|
// goes out meanwhile (services/relay-restart-queue.mjs).
|
|
@@ -8015,7 +8058,7 @@ registerAboutRoutes(app, {
|
|
|
8015
8058
|
}),
|
|
8016
8059
|
});
|
|
8017
8060
|
function markCliOffline(reason = 'offline', { clearOwner = true } = {}) {
|
|
8018
|
-
|
|
8061
|
+
cliPresence.clear();
|
|
8019
8062
|
const wasOnline = cliOnline;
|
|
8020
8063
|
cliOnline = false;
|
|
8021
8064
|
if (clearOwner) {
|
|
@@ -8029,20 +8072,6 @@ function markCliOffline(reason = 'offline', { clearOwner = true } = {}) {
|
|
|
8029
8072
|
}
|
|
8030
8073
|
}
|
|
8031
8074
|
|
|
8032
|
-
function checkCliStatus() {
|
|
8033
|
-
const wasOnline = cliOnline;
|
|
8034
|
-
cliOnline = cliLastSeen !== null && (Date.now() - cliLastSeen) < 10_000;
|
|
8035
|
-
if (wasOnline !== cliOnline) {
|
|
8036
|
-
if (!cliOnline) {
|
|
8037
|
-
markCliOffline('heartbeat-timeout');
|
|
8038
|
-
return;
|
|
8039
|
-
}
|
|
8040
|
-
console.log(`${runtimeLogPrefix()}CLI ONLINE`);
|
|
8041
|
-
io.emit('cli_status', { online: true });
|
|
8042
|
-
}
|
|
8043
|
-
}
|
|
8044
|
-
runtimeTimers.cliStatus = setInterval(checkCliStatus, 2000);
|
|
8045
|
-
|
|
8046
8075
|
if (managedOwnerPid) {
|
|
8047
8076
|
runtimeTimers.ownerWatchdog = setInterval(() => {
|
|
8048
8077
|
if (isProcessAlive(managedOwnerPid)) return;
|
|
@@ -8072,6 +8101,10 @@ const staleRecoveryClockGuard = createStaleRecoveryClockGuard({
|
|
|
8072
8101
|
});
|
|
8073
8102
|
function recoverStaleMessages() {
|
|
8074
8103
|
if (staleRecoveryClockGuard.tick().skip) return;
|
|
8104
|
+
// The tick itself stays: the guard reads the gaps between ticks, and a timer
|
|
8105
|
+
// that slept while nothing ran would look like a clock jump. Only the work
|
|
8106
|
+
// is skipped while no row is processing and no restart outcome is open.
|
|
8107
|
+
if (!stmts.hasProcessingRow.get() && !restartResumeService.pending().length) return;
|
|
8075
8108
|
const staleWindowMs = cliOnline
|
|
8076
8109
|
? processingTimeoutMs
|
|
8077
8110
|
: Math.min(processingTimeoutMs, OFFLINE_STALE_RECOVER_MS);
|
|
@@ -8111,8 +8144,6 @@ function expirePendingQuestions() {
|
|
|
8111
8144
|
}
|
|
8112
8145
|
runtimeTimers.questionExpiry = setInterval(expirePendingQuestions, 10_000);
|
|
8113
8146
|
expirePendingQuestions();
|
|
8114
|
-
runtimeTimers.sharedViewerPrune = setInterval(pruneSharedViewerPresence, 5_000);
|
|
8115
|
-
if (typeof runtimeTimers.sharedViewerPrune.unref === 'function') runtimeTimers.sharedViewerPrune.unref();
|
|
8116
8147
|
// Memory in the status log: now, every 10 minutes, and once per crossing of
|
|
8117
8148
|
// 80 % of the heap limit (relay-heap-guard.mjs).
|
|
8118
8149
|
{
|
|
@@ -8169,7 +8200,7 @@ if (runtimeBindingsBootstrapped > 0) {
|
|
|
8169
8200
|
|
|
8170
8201
|
function ts() { return new Date().toISOString().slice(11, 23); }
|
|
8171
8202
|
function runtimeLogPrefix() {
|
|
8172
|
-
return
|
|
8203
|
+
return `[${ts()}] `;
|
|
8173
8204
|
}
|
|
8174
8205
|
|
|
8175
8206
|
// ─── Auth Middleware ──────────────────────────────────────────────────────────
|
|
@@ -8344,7 +8375,7 @@ function emitToClientsExceptSessionId(event, payload, sessionId) {
|
|
|
8344
8375
|
// ─── CLI Routes ───────────────────────────────────────────────────────────────
|
|
8345
8376
|
|
|
8346
8377
|
function touchCli() {
|
|
8347
|
-
|
|
8378
|
+
cliPresence.touch();
|
|
8348
8379
|
relayRestartOrchestrator.noteCliOnline();
|
|
8349
8380
|
if (!cliOnline) {
|
|
8350
8381
|
cliOnline = true;
|
|
@@ -8353,6 +8384,7 @@ function touchCli() {
|
|
|
8353
8384
|
}
|
|
8354
8385
|
}
|
|
8355
8386
|
sessionWorkerWebSocketService.start();
|
|
8387
|
+
queueWakeStarted = true;
|
|
8356
8388
|
void primePendingSessionWorkers('startup');
|
|
8357
8389
|
// Undelivered events get their timers back; stalled, all_idle and the
|
|
8358
8390
|
// heartbeat are swept from here on.
|
|
@@ -8365,13 +8397,7 @@ relayScheduleService.start();
|
|
|
8365
8397
|
relayAskLinkService.start();
|
|
8366
8398
|
// Paired relays' Inboxes, every ~20 s.
|
|
8367
8399
|
remoteInboxWatchService.start();
|
|
8368
|
-
|
|
8369
|
-
void primePendingSessionWorkers('maintenance');
|
|
8370
|
-
detachedWorkerWatch.sweep().catch((error) => {
|
|
8371
|
-
console.warn(`${runtimeLogPrefix()}WORKER DETACHED sweep failed: ${error?.message || error}`);
|
|
8372
|
-
});
|
|
8373
|
-
}, 5_000);
|
|
8374
|
-
if (typeof runtimeTimers.pendingWorkerPrime.unref === 'function') runtimeTimers.pendingWorkerPrime.unref();
|
|
8400
|
+
pendingWorkerPrimeTimer.arm();
|
|
8375
8401
|
|
|
8376
8402
|
// POST /api/heartbeat — CLI sends a ping every poll interval
|
|
8377
8403
|
|
|
@@ -8500,6 +8526,7 @@ const sshTunnelManager = createSshTunnelManager({
|
|
|
8500
8526
|
logger: console,
|
|
8501
8527
|
runtimeShutdownRef: () => runtimeShutdownStarted,
|
|
8502
8528
|
configBaseDir: __dirname,
|
|
8529
|
+
onBlockingLifted: () => { wakeSessionWorkerQueue('ssh-tunnel-connected'); },
|
|
8503
8530
|
});
|
|
8504
8531
|
const tunnelState = sshTunnelManager.state;
|
|
8505
8532
|
runtimeState.tunnelState = tunnelState;
|
|
@@ -8513,6 +8540,7 @@ const cloudflaredTunnelManager = createCloudflaredTunnelManager({
|
|
|
8513
8540
|
logger: console,
|
|
8514
8541
|
runtimeShutdownRef: () => runtimeShutdownStarted,
|
|
8515
8542
|
configBaseDir: __dirname,
|
|
8543
|
+
onBlockingLifted: () => { wakeSessionWorkerQueue('cloudflared-tunnel-connected'); },
|
|
8516
8544
|
});
|
|
8517
8545
|
runtimeState.cloudflaredTunnelState = cloudflaredTunnelManager.state;
|
|
8518
8546
|
|
|
@@ -8633,15 +8661,15 @@ function clearRuntimeTimers() {
|
|
|
8633
8661
|
try { workerRecycleService.stop(); } catch {}
|
|
8634
8662
|
try { taskHealthPoller.stop(); } catch {}
|
|
8635
8663
|
try { taskOutputRooms.stop(); } catch {}
|
|
8636
|
-
runtimeTimers.cliStatus = null;
|
|
8637
8664
|
runtimeTimers.ownerWatchdog = null;
|
|
8638
8665
|
runtimeTimers.staleRecovery = null;
|
|
8639
8666
|
runtimeTimers.questionExpiry = null;
|
|
8640
|
-
|
|
8641
|
-
|
|
8667
|
+
pendingWorkerPrimeTimer.stop();
|
|
8668
|
+
sharedViewerPruneTimer.stop();
|
|
8642
8669
|
runtimeTimers.restoredTaskSettle = null;
|
|
8643
8670
|
runtimeTimers.archivedWorkerSweep = null;
|
|
8644
8671
|
try { relayRestartQueue.dispose(); } catch {}
|
|
8672
|
+
try { cliPresence.clear(); } catch {}
|
|
8645
8673
|
}
|
|
8646
8674
|
|
|
8647
8675
|
let runtimeShutdownPromise = null;
|