@oar-sh/oar 0.9.21 → 0.9.22
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 +113 -0
- package/README.md +9 -9
- package/package.json +1 -1
- package/server/claude-cloud-worker/claude-cloud-session-worker.mjs +9 -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 +35 -14
- package/server/routes/relay-ask-routes.mjs +15 -5
- package/server/routes/sessions-routes.mjs +76 -5
- package/server/server-runtime.mjs +178 -131
- package/server/services/cloudflared-tunnel-service.mjs +7 -0
- package/server/services/idle-timers.mjs +184 -0
- 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-event-service.mjs +4 -1
- package/server/services/remote-relay-dispatcher.mjs +23 -4
- 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/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
|
@@ -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';
|
|
@@ -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,
|
|
@@ -2005,12 +2013,9 @@ let runtimeShutdownStarted = false;
|
|
|
2005
2013
|
// Set once the database is open (see the retention sweep near the other sweeps).
|
|
2006
2014
|
let subagentRecordRetention = null;
|
|
2007
2015
|
const runtimeTimers = {
|
|
2008
|
-
cliStatus: null,
|
|
2009
2016
|
ownerWatchdog: null,
|
|
2010
2017
|
staleRecovery: null,
|
|
2011
2018
|
questionExpiry: null,
|
|
2012
|
-
pendingWorkerPrime: null,
|
|
2013
|
-
sharedViewerPrune: null,
|
|
2014
2019
|
restoredTaskSettle: null,
|
|
2015
2020
|
claudeApiCapClock: null,
|
|
2016
2021
|
memoryLog: null,
|
|
@@ -2019,6 +2024,25 @@ const runtimeTimers = {
|
|
|
2019
2024
|
archivedWorkerSweep: null,
|
|
2020
2025
|
};
|
|
2021
2026
|
|
|
2027
|
+
// The worker prime and the detached-worker sweep, every 5 s while a pending
|
|
2028
|
+
// row waits for its worker (rows in retry backoff included, so the worker is
|
|
2029
|
+
// warm when the backoff matures). Armed at startup, by every prime that finds
|
|
2030
|
+
// a row and by every queue snapshot with a row pending; a pass that leaves no
|
|
2031
|
+
// such row disarms it. Declared up here so a prime early in the boot can arm
|
|
2032
|
+
// it; its tick runs only after the boot.
|
|
2033
|
+
const pendingWorkerPrimeTimer = createArmableInterval({
|
|
2034
|
+
intervalMs: 5_000,
|
|
2035
|
+
tick: async () => {
|
|
2036
|
+
await Promise.allSettled([
|
|
2037
|
+
primePendingSessionWorkers('maintenance'),
|
|
2038
|
+
detachedWorkerWatch.sweep().catch((error) => {
|
|
2039
|
+
console.warn(`${runtimeLogPrefix()}WORKER DETACHED sweep failed: ${error?.message || error}`);
|
|
2040
|
+
}),
|
|
2041
|
+
]);
|
|
2042
|
+
return hasPendingWorkerOwnerRows();
|
|
2043
|
+
},
|
|
2044
|
+
});
|
|
2045
|
+
|
|
2022
2046
|
function normalizeRemotePath(value) {
|
|
2023
2047
|
if (typeof value !== 'string') return '';
|
|
2024
2048
|
const trimmed = value.trim();
|
|
@@ -2861,6 +2885,16 @@ const stmts = {
|
|
|
2861
2885
|
...createQuestionRepository(db),
|
|
2862
2886
|
...createImageConversationRepository(db),
|
|
2863
2887
|
};
|
|
2888
|
+
// Every queue write that can make a row deliverable wakes the delivery
|
|
2889
|
+
// (services/queue-wake-hook.mjs): installed before any route or startup
|
|
2890
|
+
// write. The waker exists once the worker socket service does; a write
|
|
2891
|
+
// before that is covered by the startup prime and the service's start.
|
|
2892
|
+
// Without the hook (it could not be installed) the queue is polled.
|
|
2893
|
+
let queueWaker = null;
|
|
2894
|
+
const queueWakeHook = installQueueWakeHook(db, {
|
|
2895
|
+
onWake: (reason) => { queueWaker?.wake(reason, { prime: false }); },
|
|
2896
|
+
log: (line) => console.warn(`[queue-wake] ${line}`),
|
|
2897
|
+
});
|
|
2864
2898
|
const statusEventService = createStatusEventService(db);
|
|
2865
2899
|
// Public shares and their access log (migration 0016).
|
|
2866
2900
|
const shareRepository = createShareRepository(db);
|
|
@@ -3422,6 +3456,8 @@ const sessionWorkerSupervisor = createSessionWorkerSupervisor({
|
|
|
3422
3456
|
diagnosticPlanReference: () => path.join(currentWorkspaceRootPath(), '.cursor', 'plans', 'worker-startup-monitoring-plan.md'),
|
|
3423
3457
|
log: (message) => console.warn(`${runtimeLogPrefix()}${message}`),
|
|
3424
3458
|
onLaunchState: (event) => workerLaunchNoteService.handle(event),
|
|
3459
|
+
// A kill's grace ended: the session's rows may go to its next worker.
|
|
3460
|
+
onKillBlockLifted: () => { wakeSessionWorkerQueue('kill-block-lifted'); },
|
|
3425
3461
|
});
|
|
3426
3462
|
// Feature flags moved from config.json into app_settings; adopt any legacy
|
|
3427
3463
|
// `features` key once, then resolve the boot snapshot (defaults < stored rows
|
|
@@ -3822,7 +3858,11 @@ function relayShutdownStatePayload() {
|
|
|
3822
3858
|
|
|
3823
3859
|
function cancelRelayShutdown({ requestedBy = 'localhost-api' } = {}) {
|
|
3824
3860
|
const result = relayRestartQueue.cancel({ requestedBy });
|
|
3825
|
-
if (result?.cancelled)
|
|
3861
|
+
if (result?.cancelled) {
|
|
3862
|
+
relayEventService?.noteRestartCancelled();
|
|
3863
|
+
// The queued restart held deliveries back: they go out now.
|
|
3864
|
+
wakeSessionWorkerQueue('restart-cancelled');
|
|
3865
|
+
}
|
|
3826
3866
|
return result;
|
|
3827
3867
|
}
|
|
3828
3868
|
|
|
@@ -5391,52 +5431,28 @@ function createCompactedConversation(sourceConversationId) {
|
|
|
5391
5431
|
};
|
|
5392
5432
|
}
|
|
5393
5433
|
|
|
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
|
-
|
|
5434
|
+
// Per thread (the main agent, each sub-agent), the newest rows; structured
|
|
5435
|
+
// rows are always kept (activity-replay-state.mjs). A finished turn: the main
|
|
5436
|
+
// agent's 200, each sub-agent's 50, and a turn whose main agent ran more says
|
|
5437
|
+
// how many it left out.
|
|
5423
5438
|
function relayActivityForResponse(responseMessageId) {
|
|
5424
|
-
return
|
|
5439
|
+
return capFinishedActivityEntries(
|
|
5425
5440
|
stmts.listActivityByResponse
|
|
5426
5441
|
.all(responseMessageId)
|
|
5427
5442
|
.map(mapRelayActivityRow)
|
|
5428
5443
|
.filter(Boolean),
|
|
5429
|
-
RELAY_ACTIVITY_ROW_LIMIT,
|
|
5430
5444
|
);
|
|
5431
5445
|
}
|
|
5432
5446
|
|
|
5433
|
-
|
|
5447
|
+
// A running turn: as many per thread as the browser shows (`activityCap`, the
|
|
5448
|
+
// request's param; 50 when it sends none, 500 at most).
|
|
5449
|
+
function relayActivityForQueueMessage(queueMessageId, activityCap = undefined) {
|
|
5434
5450
|
return capRelayActivityEntries(
|
|
5435
5451
|
stmts.listActivityByQueueMessage
|
|
5436
5452
|
.all(queueMessageId)
|
|
5437
5453
|
.map(mapRelayActivityRow)
|
|
5438
5454
|
.filter(Boolean),
|
|
5439
|
-
|
|
5455
|
+
inFlightActivityCap(activityCap),
|
|
5440
5456
|
);
|
|
5441
5457
|
}
|
|
5442
5458
|
|
|
@@ -5458,19 +5474,6 @@ function relayStreamEventsForQueueMessage(queueMessageId) {
|
|
|
5458
5474
|
.slice(0, 5000);
|
|
5459
5475
|
}
|
|
5460
5476
|
|
|
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
5477
|
function relayThoughtsForResponse(responseMessageId) {
|
|
5475
5478
|
const rows = stmts.listThoughtsByResponse?.all(responseMessageId) || [];
|
|
5476
5479
|
return normalizeRelayThoughtList(rows.map(mapRelayThoughtRow)).slice(0, 5000);
|
|
@@ -5654,7 +5657,7 @@ const usageLimitPauseService = createUsageLimitPauseService({
|
|
|
5654
5657
|
stmts,
|
|
5655
5658
|
emit: (event, payload) => io.emit(event, payload),
|
|
5656
5659
|
// A follow-up resumed by hand is due at once: wake its worker.
|
|
5657
|
-
onQueueChanged: (reason) => {
|
|
5660
|
+
onQueueChanged: (reason) => { wakeSessionWorkerQueue(reason); },
|
|
5658
5661
|
logger: console,
|
|
5659
5662
|
});
|
|
5660
5663
|
|
|
@@ -5686,8 +5689,7 @@ const claudeBillingSwitchService = createClaudeBillingSwitchService({
|
|
|
5686
5689
|
// What the swap waits for: a turn, a question, a compaction, background tasks.
|
|
5687
5690
|
getPending: (runtime) => claudeBillingSwapService.pendingFor(runtime?.sdk_session_id || runtime?.conversation_id),
|
|
5688
5691
|
onQueueChanged: (reason) => {
|
|
5689
|
-
|
|
5690
|
-
sessionWorkerWebSocketService?.emitQueueChanged?.(reason);
|
|
5692
|
+
wakeSessionWorkerQueue(reason);
|
|
5691
5693
|
},
|
|
5692
5694
|
logger: console,
|
|
5693
5695
|
});
|
|
@@ -5735,8 +5737,7 @@ const restartResumeService = createRestartResumeService({
|
|
|
5735
5737
|
// The note is an ordinary pending message: the primer starts a stopped
|
|
5736
5738
|
// worker, the socket service hands it to a running one.
|
|
5737
5739
|
wake: () => {
|
|
5738
|
-
|
|
5739
|
-
sessionWorkerWebSocketService?.emitQueueChanged?.('restart-resume');
|
|
5740
|
+
wakeSessionWorkerQueue('restart-resume');
|
|
5740
5741
|
},
|
|
5741
5742
|
logger: console,
|
|
5742
5743
|
});
|
|
@@ -5766,8 +5767,7 @@ relayEventService = createRelayEventService({
|
|
|
5766
5767
|
// The event row is an ordinary pending message: the primer starts a
|
|
5767
5768
|
// stopped worker, the socket service hands it to a running one.
|
|
5768
5769
|
wakeWatcher: () => {
|
|
5769
|
-
|
|
5770
|
-
sessionWorkerWebSocketService?.emitQueueChanged?.('relay-event');
|
|
5770
|
+
wakeSessionWorkerQueue('relay-event');
|
|
5771
5771
|
},
|
|
5772
5772
|
relayName: () => remoteRelayRegistry?.selfName?.() || getPwaAppName?.() || '',
|
|
5773
5773
|
onRemoteChange: (relayId) => { void relayEventLinkService?.fetchNow(relayId); },
|
|
@@ -5783,8 +5783,7 @@ const relayScheduleService = createRelayScheduleService({
|
|
|
5783
5783
|
emit: (event, payload) => io.emit(event, payload),
|
|
5784
5784
|
// The scheduled message is an ordinary pending one, like a relay event's.
|
|
5785
5785
|
wake: () => {
|
|
5786
|
-
|
|
5787
|
-
sessionWorkerWebSocketService?.emitQueueChanged?.('relay-schedule');
|
|
5786
|
+
wakeSessionWorkerQueue('relay-schedule');
|
|
5788
5787
|
},
|
|
5789
5788
|
// A firing into another session is a prompt of the conversation that set
|
|
5790
5789
|
// it: the agents' loop guard counts it (and holds it once it wants the user).
|
|
@@ -5847,7 +5846,9 @@ function resolveLiveTurnQueueRow(conversationId) {
|
|
|
5847
5846
|
return byId.get(liveId) || rows[0];
|
|
5848
5847
|
}
|
|
5849
5848
|
|
|
5850
|
-
|
|
5849
|
+
// `activityCap`: the tool lines per thread the asking page shows (the
|
|
5850
|
+
// conversation route passes its `activityCap` param; others get the default).
|
|
5851
|
+
function inFlightStateForConversation(conversationId, { activityCap = undefined } = {}) {
|
|
5851
5852
|
const row = resolveLiveTurnQueueRow(conversationId);
|
|
5852
5853
|
if (!row) return null;
|
|
5853
5854
|
const streamEvents = relayStreamEventsForQueueMessage(row.id);
|
|
@@ -5859,7 +5860,7 @@ function inFlightStateForConversation(conversationId) {
|
|
|
5859
5860
|
mode: normalizeRelayMode(row.relay_mode) || DEFAULT_RELAY_MODE,
|
|
5860
5861
|
timestamp: row.timestamp || null,
|
|
5861
5862
|
processingAt: row.processing_at || null,
|
|
5862
|
-
activities: relayActivityForQueueMessage(row.id),
|
|
5863
|
+
activities: relayActivityForQueueMessage(row.id, activityCap),
|
|
5863
5864
|
thoughts: relayThoughtsForQueueMessage(row.id),
|
|
5864
5865
|
streamEvents,
|
|
5865
5866
|
streamDone: !!lastStreamEvent?.done,
|
|
@@ -6392,6 +6393,15 @@ const SHARED_VIEWER_MAX_GLOBAL = Number.isFinite(Number(config.sharedPresenceMax
|
|
|
6392
6393
|
? Math.max(SHARED_VIEWER_MAX_PER_CONVERSATION, Math.trunc(Number(config.sharedPresenceMaxGlobal)))
|
|
6393
6394
|
: 5_000;
|
|
6394
6395
|
const sharedViewersByConversation = new Map();
|
|
6396
|
+
// Armed when the first share viewer arrives, disarmed by the prune that
|
|
6397
|
+
// leaves none: a relay nobody watches through a share link has no timer.
|
|
6398
|
+
const sharedViewerPruneTimer = createArmableInterval({
|
|
6399
|
+
intervalMs: 5_000,
|
|
6400
|
+
tick: () => {
|
|
6401
|
+
pruneSharedViewerPresence();
|
|
6402
|
+
return sharedViewersByConversation.size > 0;
|
|
6403
|
+
},
|
|
6404
|
+
});
|
|
6395
6405
|
|
|
6396
6406
|
function sanitizeSharedViewerId(value) {
|
|
6397
6407
|
const id = String(value || '').trim().replace(/[^a-zA-Z0-9:_-]+/g, '');
|
|
@@ -6532,6 +6542,7 @@ function markSharedViewerPresence({ conversationId, token, viewerId } = {}) {
|
|
|
6532
6542
|
}
|
|
6533
6543
|
const before = viewers.size;
|
|
6534
6544
|
viewers.set(key, { lastSeenAt: now });
|
|
6545
|
+
sharedViewerPruneTimer.arm();
|
|
6535
6546
|
const watcherCount = viewers.size;
|
|
6536
6547
|
if (before !== watcherCount) {
|
|
6537
6548
|
emitConversationWatcherCount(convId);
|
|
@@ -6808,7 +6819,7 @@ async function recoverUndeliveredSessionWorkerMessage({ pending = null, sessionI
|
|
|
6808
6819
|
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
6820
|
io.emit('message_status', { messageId, conversationId: row.conversation_id, status: 'pending' });
|
|
6810
6821
|
io.emit('queue_updated', { recovered: 1, reason: 'worker-ws-send-failed' });
|
|
6811
|
-
|
|
6822
|
+
wakeSessionWorkerQueue('worker-ws-send-failed');
|
|
6812
6823
|
return true;
|
|
6813
6824
|
}
|
|
6814
6825
|
|
|
@@ -6898,11 +6909,10 @@ function recoverDeadWorkerProcessingRows(sessionId, reason) {
|
|
|
6898
6909
|
}
|
|
6899
6910
|
if (recovered > 0) {
|
|
6900
6911
|
io.emit('queue_updated', { recovered, reason: `worker-died:${reason}` });
|
|
6901
|
-
sessionWorkerWebSocketService?.emitQueueChanged?.(`worker-died:${reason}`);
|
|
6902
6912
|
}
|
|
6903
6913
|
sessionWorkerSupervisor?.markError?.(sid, `worker-died:${reason}`);
|
|
6904
6914
|
// The rows stay owned, so the primer is what respawns their worker.
|
|
6905
|
-
|
|
6915
|
+
wakeSessionWorkerQueue(`worker-died:${reason}`);
|
|
6906
6916
|
return recovered;
|
|
6907
6917
|
}
|
|
6908
6918
|
|
|
@@ -6981,7 +6991,19 @@ async function sweepDeadWorkerProcessingRowsOnce() {
|
|
|
6981
6991
|
// work. The worker recycle raises it; the billing swap does not, since its
|
|
6982
6992
|
// own delivery fence already keeps the other billing's rows from the old
|
|
6983
6993
|
// worker.
|
|
6984
|
-
|
|
6994
|
+
// A lifted fence (the replacement launched, or its stop failed and the old
|
|
6995
|
+
// worker serves again) hands out what waited behind it.
|
|
6996
|
+
const workerReplacementFence = (() => {
|
|
6997
|
+
const fence = createWorkerReplacementFence();
|
|
6998
|
+
return {
|
|
6999
|
+
...fence,
|
|
7000
|
+
release(sid, pid) {
|
|
7001
|
+
const released = fence.release(sid, pid);
|
|
7002
|
+
if (released) wakeSessionWorkerQueue('replacement-fence-lifted');
|
|
7003
|
+
return released;
|
|
7004
|
+
},
|
|
7005
|
+
};
|
|
7006
|
+
})();
|
|
6985
7007
|
function createRuntimeWorkerReplacement(tag) {
|
|
6986
7008
|
return createWorkerReplacement({
|
|
6987
7009
|
registry: sessionWorkerRegistry,
|
|
@@ -7288,6 +7310,21 @@ const sessionWorkerWebSocketService = createSessionWorkerWebSocketService({
|
|
|
7288
7310
|
},
|
|
7289
7311
|
onDeliverySendFailed: recoverUndeliveredSessionWorkerMessage,
|
|
7290
7312
|
onWorkerSocketClosed: handleWorkerSocketClosed,
|
|
7313
|
+
// The CLI presence window is wider while a worker socket is open.
|
|
7314
|
+
onSocketCountChange: (count) => cliPresence.noteWorkerSocketCount(count),
|
|
7315
|
+
// A row pending (in backoff too) keeps the worker prime armed, and the
|
|
7316
|
+
// poll primes now and then while rows wait.
|
|
7317
|
+
onQueueSnapshot: (snapshot) => {
|
|
7318
|
+
if (snapshot.pendingCount > 0) queueWaker?.primeFromBackstop('backstop');
|
|
7319
|
+
},
|
|
7320
|
+
// A retry time or a usage-limit hold coming due wakes the queue on time.
|
|
7321
|
+
nextDueAtMs: () => {
|
|
7322
|
+
if (runtimeShutdownStarted) return null;
|
|
7323
|
+
const due = stmts.nextPendingAttemptAt?.get?.(new Date().toISOString())?.due;
|
|
7324
|
+
const dueMs = due ? Date.parse(due) : NaN;
|
|
7325
|
+
return Number.isFinite(dueMs) ? dueMs : null;
|
|
7326
|
+
},
|
|
7327
|
+
onDue: (reason) => { wakeSessionWorkerQueue(reason); },
|
|
7291
7328
|
requestWork: requestSessionWorkerSocketDelivery,
|
|
7292
7329
|
// What a worker says it is ("claude", "claude-api", ...): the registry
|
|
7293
7330
|
// keeps it for the billing fence and for what a response ran on.
|
|
@@ -7309,9 +7346,23 @@ const sessionWorkerWebSocketService = createSessionWorkerWebSocketService({
|
|
|
7309
7346
|
buildId: BUILD_ID,
|
|
7310
7347
|
isWorkerProcessAlive: (pid) => isProcessAlive(Number(pid)),
|
|
7311
7348
|
pathPrefix: remotePath,
|
|
7312
|
-
|
|
7349
|
+
// A backstop under the queue's own wakes; the poll every 500 ms it was
|
|
7350
|
+
// when those cannot be installed.
|
|
7351
|
+
pollIntervalMs: queueWakeHook.installed ? 10_000 : 500,
|
|
7313
7352
|
logger: console,
|
|
7314
7353
|
});
|
|
7354
|
+
let queueWakeStarted = false;
|
|
7355
|
+
queueWaker = createQueueWaker({
|
|
7356
|
+
prime: (reason) => primePendingSessionWorkers(reason),
|
|
7357
|
+
notify: (reason) => {
|
|
7358
|
+
sessionWorkerWebSocketService.notifyQueueChanged(reason).catch((error) => {
|
|
7359
|
+
console.warn(`${runtimeLogPrefix()}QUEUE WAKE failed reason=${reason} err=${error?.message || error}`);
|
|
7360
|
+
});
|
|
7361
|
+
},
|
|
7362
|
+
armPrime: () => pendingWorkerPrimeTimer.arm(),
|
|
7363
|
+
isReady: () => queueWakeStarted && !runtimeShutdownStarted,
|
|
7364
|
+
warn: (line) => console.warn(`${runtimeLogPrefix()}${line}`),
|
|
7365
|
+
});
|
|
7315
7366
|
runtimeTimers.deadWorkerSweep = setInterval(() => {
|
|
7316
7367
|
sweepDeadWorkerProcessingRows().catch((error) => {
|
|
7317
7368
|
console.warn(`${runtimeLogPrefix()}WORKER SWEEP failed: ${error?.message || error}`);
|
|
@@ -7338,40 +7389,39 @@ const tmuxInspectorSocketService = createTmuxInspectorSocketService({
|
|
|
7338
7389
|
logger: console,
|
|
7339
7390
|
});
|
|
7340
7391
|
|
|
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;
|
|
7392
|
+
// No next_attempt_at filter in the listing: rows sitting in retry backoff
|
|
7393
|
+
// still need their worker warm, otherwise the poll wins the race the moment
|
|
7394
|
+
// the backoff matures and the turn executes on the wrong provider.
|
|
7395
|
+
const pendingWorkerPrimer = createPendingWorkerPrimer({
|
|
7396
|
+
isEnabled: () => featureFlags?.SESSION_WORKER_ROUTING_ENABLED === true
|
|
7397
|
+
&& !runtimeShutdownStarted
|
|
7398
|
+
&& typeof sessionWorkerSupervisor?.ensureWorker === 'function'
|
|
7399
|
+
&& typeof stmts.listPendingWorkerOwnerSessionIds?.all === 'function',
|
|
7400
|
+
listPendingOwnerSessionIds: (limit) => stmts.listPendingWorkerOwnerSessionIds.all(limit),
|
|
7401
|
+
isKillBlocked: (sid) => sessionWorkerSupervisor?.isKillBlocked?.(sid) === true,
|
|
7402
|
+
failRowsWithoutLaunch: (sid) => failClaudeApiRowsWithoutLaunch(sid),
|
|
7403
|
+
ensureWorker: (sid) => sessionWorkerSupervisor.ensureWorker(sid),
|
|
7404
|
+
// Rows waiting for their worker: the maintenance pass retries until none is.
|
|
7405
|
+
onRowsWaiting: () => pendingWorkerPrimeTimer.arm(),
|
|
7406
|
+
warn: (line) => console.warn(`${runtimeLogPrefix()}${line}`),
|
|
7407
|
+
});
|
|
7408
|
+
|
|
7409
|
+
function primePendingSessionWorkers(reason = 'pending-worker-prime') {
|
|
7410
|
+
return pendingWorkerPrimer(reason);
|
|
7411
|
+
}
|
|
7412
|
+
|
|
7413
|
+
/**
|
|
7414
|
+
* The queue may hold work for a worker now (a row written, a gate lifted, a
|
|
7415
|
+
* retry come due): hand rows to the ready workers, prime the sessions with
|
|
7416
|
+
* pending rows, keep the maintenance pass armed.
|
|
7417
|
+
*/
|
|
7418
|
+
function wakeSessionWorkerQueue(reason = 'queue-wake') {
|
|
7419
|
+
return queueWaker?.wake(reason) === true;
|
|
7420
|
+
}
|
|
7421
|
+
|
|
7422
|
+
function hasPendingWorkerOwnerRows() {
|
|
7423
|
+
if (featureFlags?.SESSION_WORKER_ROUTING_ENABLED !== true || runtimeShutdownStarted) return false;
|
|
7424
|
+
return (stmts.listPendingWorkerOwnerSessionIds?.all?.(1) || []).length > 0;
|
|
7375
7425
|
}
|
|
7376
7426
|
|
|
7377
7427
|
// Guard first, so a tunnelled request to a worker path is rejected before any
|
|
@@ -7475,7 +7525,14 @@ app.get(['/share-preview/:conversationId', '/share-preview/:conversationId/'], (
|
|
|
7475
7525
|
app.use(express.static(PUBLIC_DIR, { etag: false, setHeaders: (res) => res.setHeader('Cache-Control', 'no-store') }));
|
|
7476
7526
|
|
|
7477
7527
|
// ─── CLI Status Tracking ──────────────────────────────────────────────────────
|
|
7478
|
-
|
|
7528
|
+
// When the workers were last heard from, without a polling timer: the window
|
|
7529
|
+
// is 45 s while a worker socket is open (an idle worker beats every 30 s) and
|
|
7530
|
+
// 10 s once none is (services/idle-timers.mjs).
|
|
7531
|
+
const cliPresence = createCliPresence({
|
|
7532
|
+
onExpired: () => {
|
|
7533
|
+
if (cliOnline) markCliOffline('heartbeat-timeout');
|
|
7534
|
+
},
|
|
7535
|
+
});
|
|
7479
7536
|
let cliOnline = false;
|
|
7480
7537
|
let relayPaused = false;
|
|
7481
7538
|
const relayBridgeOwnerService = createRelayBridgeOwnerService();
|
|
@@ -7491,7 +7548,12 @@ const runtimeState = {
|
|
|
7491
7548
|
get cliOnline() { return cliOnline; },
|
|
7492
7549
|
set cliOnline(value) { cliOnline = value; },
|
|
7493
7550
|
get relayPaused() { return relayPaused; },
|
|
7494
|
-
set relayPaused(value) {
|
|
7551
|
+
set relayPaused(value) {
|
|
7552
|
+
const wasPaused = relayPaused;
|
|
7553
|
+
relayPaused = value;
|
|
7554
|
+
// Resumed: what was queued while paused goes out now.
|
|
7555
|
+
if (wasPaused && !relayPaused) wakeSessionWorkerQueue('relay-resumed');
|
|
7556
|
+
},
|
|
7495
7557
|
get ttyConsoleActive() { return Boolean(ttyConsoleRuntime?.tty); },
|
|
7496
7558
|
get relayShutdown() { return relayRestartQueue.getState(); },
|
|
7497
7559
|
// A shutdown or restart waits for the processing turns: no pending row
|
|
@@ -8015,7 +8077,7 @@ registerAboutRoutes(app, {
|
|
|
8015
8077
|
}),
|
|
8016
8078
|
});
|
|
8017
8079
|
function markCliOffline(reason = 'offline', { clearOwner = true } = {}) {
|
|
8018
|
-
|
|
8080
|
+
cliPresence.clear();
|
|
8019
8081
|
const wasOnline = cliOnline;
|
|
8020
8082
|
cliOnline = false;
|
|
8021
8083
|
if (clearOwner) {
|
|
@@ -8029,20 +8091,6 @@ function markCliOffline(reason = 'offline', { clearOwner = true } = {}) {
|
|
|
8029
8091
|
}
|
|
8030
8092
|
}
|
|
8031
8093
|
|
|
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
8094
|
if (managedOwnerPid) {
|
|
8047
8095
|
runtimeTimers.ownerWatchdog = setInterval(() => {
|
|
8048
8096
|
if (isProcessAlive(managedOwnerPid)) return;
|
|
@@ -8072,6 +8120,10 @@ const staleRecoveryClockGuard = createStaleRecoveryClockGuard({
|
|
|
8072
8120
|
});
|
|
8073
8121
|
function recoverStaleMessages() {
|
|
8074
8122
|
if (staleRecoveryClockGuard.tick().skip) return;
|
|
8123
|
+
// The tick itself stays: the guard reads the gaps between ticks, and a timer
|
|
8124
|
+
// that slept while nothing ran would look like a clock jump. Only the work
|
|
8125
|
+
// is skipped while no row is processing and no restart outcome is open.
|
|
8126
|
+
if (!stmts.hasProcessingRow.get() && !restartResumeService.pending().length) return;
|
|
8075
8127
|
const staleWindowMs = cliOnline
|
|
8076
8128
|
? processingTimeoutMs
|
|
8077
8129
|
: Math.min(processingTimeoutMs, OFFLINE_STALE_RECOVER_MS);
|
|
@@ -8111,8 +8163,6 @@ function expirePendingQuestions() {
|
|
|
8111
8163
|
}
|
|
8112
8164
|
runtimeTimers.questionExpiry = setInterval(expirePendingQuestions, 10_000);
|
|
8113
8165
|
expirePendingQuestions();
|
|
8114
|
-
runtimeTimers.sharedViewerPrune = setInterval(pruneSharedViewerPresence, 5_000);
|
|
8115
|
-
if (typeof runtimeTimers.sharedViewerPrune.unref === 'function') runtimeTimers.sharedViewerPrune.unref();
|
|
8116
8166
|
// Memory in the status log: now, every 10 minutes, and once per crossing of
|
|
8117
8167
|
// 80 % of the heap limit (relay-heap-guard.mjs).
|
|
8118
8168
|
{
|
|
@@ -8344,7 +8394,7 @@ function emitToClientsExceptSessionId(event, payload, sessionId) {
|
|
|
8344
8394
|
// ─── CLI Routes ───────────────────────────────────────────────────────────────
|
|
8345
8395
|
|
|
8346
8396
|
function touchCli() {
|
|
8347
|
-
|
|
8397
|
+
cliPresence.touch();
|
|
8348
8398
|
relayRestartOrchestrator.noteCliOnline();
|
|
8349
8399
|
if (!cliOnline) {
|
|
8350
8400
|
cliOnline = true;
|
|
@@ -8353,6 +8403,7 @@ function touchCli() {
|
|
|
8353
8403
|
}
|
|
8354
8404
|
}
|
|
8355
8405
|
sessionWorkerWebSocketService.start();
|
|
8406
|
+
queueWakeStarted = true;
|
|
8356
8407
|
void primePendingSessionWorkers('startup');
|
|
8357
8408
|
// Undelivered events get their timers back; stalled, all_idle and the
|
|
8358
8409
|
// heartbeat are swept from here on.
|
|
@@ -8365,13 +8416,7 @@ relayScheduleService.start();
|
|
|
8365
8416
|
relayAskLinkService.start();
|
|
8366
8417
|
// Paired relays' Inboxes, every ~20 s.
|
|
8367
8418
|
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();
|
|
8419
|
+
pendingWorkerPrimeTimer.arm();
|
|
8375
8420
|
|
|
8376
8421
|
// POST /api/heartbeat — CLI sends a ping every poll interval
|
|
8377
8422
|
|
|
@@ -8500,6 +8545,7 @@ const sshTunnelManager = createSshTunnelManager({
|
|
|
8500
8545
|
logger: console,
|
|
8501
8546
|
runtimeShutdownRef: () => runtimeShutdownStarted,
|
|
8502
8547
|
configBaseDir: __dirname,
|
|
8548
|
+
onBlockingLifted: () => { wakeSessionWorkerQueue('ssh-tunnel-connected'); },
|
|
8503
8549
|
});
|
|
8504
8550
|
const tunnelState = sshTunnelManager.state;
|
|
8505
8551
|
runtimeState.tunnelState = tunnelState;
|
|
@@ -8513,6 +8559,7 @@ const cloudflaredTunnelManager = createCloudflaredTunnelManager({
|
|
|
8513
8559
|
logger: console,
|
|
8514
8560
|
runtimeShutdownRef: () => runtimeShutdownStarted,
|
|
8515
8561
|
configBaseDir: __dirname,
|
|
8562
|
+
onBlockingLifted: () => { wakeSessionWorkerQueue('cloudflared-tunnel-connected'); },
|
|
8516
8563
|
});
|
|
8517
8564
|
runtimeState.cloudflaredTunnelState = cloudflaredTunnelManager.state;
|
|
8518
8565
|
|
|
@@ -8633,15 +8680,15 @@ function clearRuntimeTimers() {
|
|
|
8633
8680
|
try { workerRecycleService.stop(); } catch {}
|
|
8634
8681
|
try { taskHealthPoller.stop(); } catch {}
|
|
8635
8682
|
try { taskOutputRooms.stop(); } catch {}
|
|
8636
|
-
runtimeTimers.cliStatus = null;
|
|
8637
8683
|
runtimeTimers.ownerWatchdog = null;
|
|
8638
8684
|
runtimeTimers.staleRecovery = null;
|
|
8639
8685
|
runtimeTimers.questionExpiry = null;
|
|
8640
|
-
|
|
8641
|
-
|
|
8686
|
+
pendingWorkerPrimeTimer.stop();
|
|
8687
|
+
sharedViewerPruneTimer.stop();
|
|
8642
8688
|
runtimeTimers.restoredTaskSettle = null;
|
|
8643
8689
|
runtimeTimers.archivedWorkerSweep = null;
|
|
8644
8690
|
try { relayRestartQueue.dispose(); } catch {}
|
|
8691
|
+
try { cliPresence.clear(); } catch {}
|
|
8645
8692
|
}
|
|
8646
8693
|
|
|
8647
8694
|
let runtimeShutdownPromise = null;
|
|
@@ -211,6 +211,9 @@ export function createCloudflaredTunnelManager({
|
|
|
211
211
|
io = null,
|
|
212
212
|
logger = console,
|
|
213
213
|
runtimeShutdownRef = () => false,
|
|
214
|
+
// The tunnel stopped blocking queue delivery (it connected): the relay
|
|
215
|
+
// hands out what waited for it.
|
|
216
|
+
onBlockingLifted = () => {},
|
|
214
217
|
platform = process.platform,
|
|
215
218
|
spawnImpl = spawn,
|
|
216
219
|
nowIso = () => new Date().toISOString(),
|
|
@@ -267,7 +270,11 @@ export function createCloudflaredTunnelManager({
|
|
|
267
270
|
};
|
|
268
271
|
|
|
269
272
|
const updateBlockingState = () => {
|
|
273
|
+
const wasBlocking = state.blocking;
|
|
270
274
|
state.blocking = state.required && state.mode === 'managed' && !state.connected;
|
|
275
|
+
if (wasBlocking && !state.blocking) {
|
|
276
|
+
try { onBlockingLifted(); } catch {}
|
|
277
|
+
}
|
|
271
278
|
};
|
|
272
279
|
|
|
273
280
|
const scheduleReconnect = (spawnTunnel, { slowest = false } = {}) => {
|