@oar-sh/oar 0.9.11 → 0.9.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +332 -0
- package/README.md +10 -10
- package/package.json +1 -1
- package/server/README.md +201 -12
- package/server/claude-worker/claude-remote-relay-tool.mjs +2 -0
- package/server/claude-worker/claude-sdk-adapter.mjs +57 -4
- package/server/claude-worker/claude-session-process.mjs +179 -0
- package/server/claude-worker/claude-session-worker.mjs +17 -0
- package/server/claude-worker/task-output-discovery.mjs +152 -0
- package/server/copilot-worker/copilot-continuation-signals.mjs +11 -5
- package/server/copilot-worker/copilot-sdk-session-process.mjs +169 -3
- package/server/copilot-worker/copilot-sdk-session-worker.mjs +2 -0
- package/server/copilot-worker/copilot-shell-output-mirror.mjs +94 -0
- package/server/copilot-worker/copilot-task-settle.mjs +96 -0
- package/server/db-schema.mjs +19 -0
- package/server/grok-worker/acp-host-services.mjs +48 -6
- package/server/grok-worker/grok-background-tasks.mjs +248 -0
- package/server/grok-worker/grok-sdk-adapter.mjs +3 -1
- package/server/grok-worker/grok-session-worker.mjs +33 -1
- package/server/grok-worker/grok-turn-runner.mjs +11 -0
- package/server/migrations/0017-conversation-history-indexes.mjs +89 -0
- package/server/migrations/0018-background-task-rows.mjs +101 -0
- package/server/public/app/action-confirmations.js +12 -15
- package/server/public/app/api-client.js +70 -3
- package/server/public/app/background-tasks-view.mjs +334 -16
- package/server/public/app/bootstrap.js +102 -5
- package/server/public/app/claude-background-command-limit-options.mjs +21 -0
- package/server/public/app/conversation-payload-cache.mjs +357 -0
- package/server/public/app/conversation-preferences.mjs +10 -4
- package/server/public/app/conversation-turn-state.mjs +1 -1
- package/server/public/app/conversation-view.js +462 -8
- package/server/public/app/conversation-window-retention.mjs +96 -0
- package/server/public/app/host-suspend-ui.js +45 -44
- package/server/public/app/journal-view.js +259 -67
- package/server/public/app/push-settings.js +1 -0
- package/server/public/app/relay-prompt-sanitizer.mjs +20 -6
- package/server/public/app/relay-restart-note-view.mjs +36 -0
- package/server/public/app/relay-restart-ui.js +568 -0
- package/server/public/app/settings-modal.js +162 -2
- package/server/public/app/socket-handlers.js +59 -7
- package/server/public/app/store.js +25 -0
- package/server/public/app/subagent-viewer.mjs +7 -1
- package/server/public/app/task-console-render.mjs +401 -0
- package/server/public/app/task-console-watch.mjs +42 -0
- package/server/public/app/task-console.mjs +730 -0
- package/server/public/app/task-output-text.mjs +243 -0
- package/server/public/app/transcript-island.mjs +73 -0
- package/server/public/app/transcript-jump-bottom-view.mjs +42 -8
- package/server/public/app/transcript-jump-bottom.mjs +68 -11
- package/server/public/app/transcript-reading-anchor.mjs +256 -0
- package/server/public/app/transcript-reload-freshness.mjs +17 -1
- package/server/public/app/transcript-viewport-anchor.mjs +20 -1
- package/server/public/index.html +306 -6
- package/server/relay-tools.md +6 -0
- package/server/repositories/message-repository.mjs +11 -0
- package/server/repositories/question-repository.mjs +4 -0
- package/server/repositories/remote-relay-repository.mjs +4 -2
- package/server/routes/messages-routes.mjs +397 -23
- package/server/routes/relay-board-routes.mjs +19 -2
- package/server/routes/relay-restart-routes.mjs +73 -0
- package/server/routes/sessions-routes.mjs +208 -66
- package/server/server-runtime.mjs +269 -137
- package/server/services/about-service.mjs +13 -12
- package/server/services/background-task-persistence.mjs +133 -0
- package/server/services/background-task-store.mjs +470 -6
- package/server/services/cli-process-runner.mjs +4 -21
- package/server/services/conversation-history-page.mjs +199 -0
- package/server/services/conversation-model-fallback.mjs +57 -0
- package/server/services/host-suspend-command.mjs +2 -1
- package/server/services/preview-proxy-server.mjs +14 -1
- package/server/services/process-list-service.mjs +2 -1
- package/server/services/provider-model-selection.mjs +195 -0
- package/server/services/push-dispatch-service.mjs +9 -2
- package/server/services/relay-event-service.mjs +27 -9
- package/server/services/relay-prompt-sanitizer.mjs +18 -4
- package/server/services/relay-restart-picture.mjs +209 -0
- package/server/services/relay-restart-queue.mjs +380 -0
- package/server/services/relay-shutdown-gate.mjs +4 -1
- package/server/services/remote-relay-dispatcher.mjs +316 -20
- package/server/services/restart-resume-service.mjs +303 -0
- package/server/services/session-activity-tracker.mjs +6 -2
- package/server/services/session-worker-process-service.mjs +6 -4
- package/server/services/task-output-rooms.mjs +259 -0
- package/server/services/task-output-service.mjs +761 -0
- package/shared/claude-background-command-limit.mjs +83 -0
- package/shared/relay-events.mjs +55 -2
- package/shared/remote-relay-contract.mjs +47 -10
- package/shared/task-output-text.mjs +850 -0
- package/shared/worker-runtime/task-output-spool.mjs +181 -0
- package/shared/worker-runtime/worker-websocket-link.mjs +96 -20
|
@@ -75,16 +75,29 @@ import { createHostSuspendService } from './services/host-suspend-service.mjs';
|
|
|
75
75
|
import { USAGE_LIMIT_ACCOUNT_SOCKET_EVENT, createUsageLimitPauseService } from './services/usage-limit-pause-service.mjs';
|
|
76
76
|
import { createSessionActivityTracker, resolveBootReportingWindowMs } from './services/session-activity-tracker.mjs';
|
|
77
77
|
import { createBackgroundTaskStore } from './services/background-task-store.mjs';
|
|
78
|
+
import { createSqliteBackgroundTaskPersistence } from './services/background-task-persistence.mjs';
|
|
79
|
+
import {
|
|
80
|
+
RESTART_RESUME_SETTING_KEY,
|
|
81
|
+
createRestartResumeService,
|
|
82
|
+
parseRestartResumeUpdate,
|
|
83
|
+
readRestartResumeSetting,
|
|
84
|
+
} from './services/restart-resume-service.mjs';
|
|
78
85
|
import { createBackgroundWorkView } from './services/background-work-view.mjs';
|
|
79
86
|
import { createRelayEventService } from './services/relay-event-service.mjs';
|
|
80
87
|
import { createRelayEventLinkService } from './services/relay-event-link-service.mjs';
|
|
81
88
|
import { buildClaudeAttributionSettings, normalizeClaudeAttributionMode, resolveClaudeAttributionMode, DEFAULT_CLAUDE_ATTRIBUTION_MODE } from '../shared/claude-attribution.mjs';
|
|
89
|
+
import {
|
|
90
|
+
claudeBackgroundCommandLimitMinutesToMs,
|
|
91
|
+
parseClaudeBackgroundCommandLimitUpdate,
|
|
92
|
+
readClaudeBackgroundCommandLimitSetting,
|
|
93
|
+
} from '../shared/claude-background-command-limit.mjs';
|
|
82
94
|
import { createWorkerLaunchNoteService } from './services/worker-launch-note-service.mjs';
|
|
83
95
|
import { createGitHubCiActivityService } from './services/github-ci-activity-service.mjs';
|
|
84
96
|
import { runHostSuspendToRam } from './services/host-suspend-command.mjs';
|
|
85
97
|
import { createActiveDeviceTracker } from './services/push-active-device-service.mjs';
|
|
86
98
|
import { registerPushRoutes } from './routes/push-routes.mjs';
|
|
87
99
|
import { registerAboutRoutes } from './routes/about-routes.mjs';
|
|
100
|
+
import { registerRelayRestartRoutes } from './routes/relay-restart-routes.mjs';
|
|
88
101
|
import { createAboutService, resolveBuildCommit } from './services/about-service.mjs';
|
|
89
102
|
import { createImageOperationService } from './services/image-operation-service.mjs';
|
|
90
103
|
import {
|
|
@@ -145,6 +158,8 @@ import { createTmuxInspectorAccessPolicy } from './services/tmux-inspector-acces
|
|
|
145
158
|
import { createTmuxInspectorStreamService } from './services/tmux-inspector-stream-service.mjs';
|
|
146
159
|
import { createTmuxInspectorSocketService } from './services/tmux-inspector-socket-service.mjs';
|
|
147
160
|
import { createSubagentViewerRooms } from './services/subagent-viewer-rooms.mjs';
|
|
161
|
+
import { createTaskOutputRooms } from './services/task-output-rooms.mjs';
|
|
162
|
+
import { createTaskHealthPoller, createTaskOutputSnapshots } from './services/task-output-service.mjs';
|
|
148
163
|
import {
|
|
149
164
|
resolveDefaultSessionWorkspaceRootState as resolveDefaultSessionWorkspaceRootStateFromService,
|
|
150
165
|
resolveLaunchWorkspaceRootPath,
|
|
@@ -176,7 +191,15 @@ import {
|
|
|
176
191
|
parseSharePathPrivacyUpdate,
|
|
177
192
|
readSharePathPrivacySetting,
|
|
178
193
|
} from '../shared/share-path-privacy.mjs';
|
|
179
|
-
import { isRelayShutdownQueued,
|
|
194
|
+
import { isRelayShutdownQueued, relayShutdownGateReply } from './services/relay-shutdown-gate.mjs';
|
|
195
|
+
import {
|
|
196
|
+
createRelayRestartQueue,
|
|
197
|
+
parseRelayRestartMaxWaitUpdate,
|
|
198
|
+
readRelayRestartMaxWaitSetting,
|
|
199
|
+
resolveRelayRestartMaxWaitOverrideMs,
|
|
200
|
+
RELAY_RESTART_MAX_WAIT_SETTING_KEY,
|
|
201
|
+
} from './services/relay-restart-queue.mjs';
|
|
202
|
+
import { createRestartPictureCollector } from './services/relay-restart-picture.mjs';
|
|
180
203
|
import { createStaleRecoveryClockGuard } from './services/stale-recovery-clock-guard.mjs';
|
|
181
204
|
import {
|
|
182
205
|
STALE_SUBAGENT_RUN_BOOT_DELAY_MS,
|
|
@@ -208,6 +231,11 @@ import {
|
|
|
208
231
|
readBackgroundTaskTimeoutSetting,
|
|
209
232
|
backgroundTaskTimeoutMinutesToMs,
|
|
210
233
|
} from '../shared/background-task-timeout.mjs';
|
|
234
|
+
import {
|
|
235
|
+
TASK_STALL_MINUTES_SETTING_KEY,
|
|
236
|
+
parseTaskStallMinutesUpdate,
|
|
237
|
+
readTaskStallMinutesSetting,
|
|
238
|
+
} from '../shared/task-output-text.mjs';
|
|
211
239
|
import { normalizeRelayThoughtList } from './public/app/relay-thoughts.mjs';
|
|
212
240
|
import { capRelayActivityEntries } from './public/app/activity-replay-state.mjs';
|
|
213
241
|
import {
|
|
@@ -504,10 +532,15 @@ function getClaudeProviderSettings() {
|
|
|
504
532
|
effortsByModel,
|
|
505
533
|
modelsDiscovered,
|
|
506
534
|
attributionMode: normalizeClaudeAttributionMode(readAppSettingValue(CLAUDE_ATTRIBUTION_SETTING_KEY)) || DEFAULT_CLAUDE_ATTRIBUTION_MODE,
|
|
535
|
+
backgroundCommandLimitMinutes: getClaudeBackgroundCommandLimitMinutes(),
|
|
507
536
|
providerType: 'claude',
|
|
508
537
|
};
|
|
509
538
|
}
|
|
510
539
|
|
|
540
|
+
function getClaudeBackgroundCommandLimitMinutes() {
|
|
541
|
+
return readClaudeBackgroundCommandLimitSetting(readAppSettingValue(CLAUDE_BACKGROUND_COMMAND_LIMIT_SETTING_KEY));
|
|
542
|
+
}
|
|
543
|
+
|
|
511
544
|
function setClaudeProviderSettings(update = {}) {
|
|
512
545
|
const payload = update && typeof update === 'object' ? update : {};
|
|
513
546
|
const enabled = payload.enabled;
|
|
@@ -528,6 +561,12 @@ function setClaudeProviderSettings(update = {}) {
|
|
|
528
561
|
if (attributionMode === null) {
|
|
529
562
|
return { ok: false, error: 'Invalid Claude attribution mode' };
|
|
530
563
|
}
|
|
564
|
+
const backgroundCommandLimit = payload.backgroundCommandLimitMinutes === undefined
|
|
565
|
+
? null
|
|
566
|
+
: parseClaudeBackgroundCommandLimitUpdate(payload.backgroundCommandLimitMinutes);
|
|
567
|
+
if (backgroundCommandLimit && !backgroundCommandLimit.ok) {
|
|
568
|
+
return { ok: false, error: backgroundCommandLimit.error };
|
|
569
|
+
}
|
|
531
570
|
const nowIso = new Date().toISOString();
|
|
532
571
|
const nextEnabled = typeof enabled === 'boolean' ? enabled : existing.enabled;
|
|
533
572
|
stmts.upsertAppSetting.run(CLAUDE_ENABLED_SETTING_KEY, nextEnabled ? 'true' : 'false', nowIso);
|
|
@@ -535,6 +574,9 @@ function setClaudeProviderSettings(update = {}) {
|
|
|
535
574
|
if (attributionMode) {
|
|
536
575
|
stmts.upsertAppSetting.run(CLAUDE_ATTRIBUTION_SETTING_KEY, attributionMode, nowIso);
|
|
537
576
|
}
|
|
577
|
+
if (backgroundCommandLimit) {
|
|
578
|
+
stmts.upsertAppSetting.run(CLAUDE_BACKGROUND_COMMAND_LIMIT_SETTING_KEY, String(backgroundCommandLimit.minutes), nowIso);
|
|
579
|
+
}
|
|
538
580
|
if (Array.isArray(models)) {
|
|
539
581
|
const normalizedModels = models
|
|
540
582
|
.map((value) => String(value || '').trim())
|
|
@@ -1404,6 +1446,9 @@ const CLAUDE_MODEL_EFFORTS_SETTING_KEY = 'claude_model_efforts';
|
|
|
1404
1446
|
// oar | vanilla | off — what commits and PRs made in Claude sessions say
|
|
1405
1447
|
// (shared/claude-attribution.mjs); absent means oar.
|
|
1406
1448
|
const CLAUDE_ATTRIBUTION_SETTING_KEY = 'claude_attribution_mode';
|
|
1449
|
+
// Minutes a backgrounded Bash command of a Claude session may run without its
|
|
1450
|
+
// own timeout (shared/claude-background-command-limit.mjs); absent means 2 h.
|
|
1451
|
+
const CLAUDE_BACKGROUND_COMMAND_LIMIT_SETTING_KEY = 'claude_background_command_limit_minutes';
|
|
1407
1452
|
// The set of levels the SDK itself can report/accept as EffortLevel values.
|
|
1408
1453
|
// Discovery results are filtered through this, so it must NOT contain the
|
|
1409
1454
|
// relay's derived 'ultracode' sentinel — supportedModels() never reports it
|
|
@@ -1848,7 +1893,7 @@ const runtimeTimers = {
|
|
|
1848
1893
|
questionExpiry: null,
|
|
1849
1894
|
pendingWorkerPrime: null,
|
|
1850
1895
|
sharedViewerPrune: null,
|
|
1851
|
-
|
|
1896
|
+
restoredTaskSettle: null,
|
|
1852
1897
|
};
|
|
1853
1898
|
|
|
1854
1899
|
function normalizeRemotePath(value) {
|
|
@@ -2158,6 +2203,36 @@ function setBackgroundTaskTimeoutMinutes(value) {
|
|
|
2158
2203
|
return { ok: true, timeoutMinutes: parsed.minutes };
|
|
2159
2204
|
}
|
|
2160
2205
|
|
|
2206
|
+
// "Mark a background task as quiet after N minutes": the panel's stall hint.
|
|
2207
|
+
function getTaskStallMinutes() {
|
|
2208
|
+
return readTaskStallMinutesSetting(readAppSettingValue(TASK_STALL_MINUTES_SETTING_KEY));
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
function setTaskStallMinutes(value) {
|
|
2212
|
+
if (typeof stmts?.upsertAppSetting?.run !== 'function') {
|
|
2213
|
+
return { ok: false, error: 'Task stall settings are unavailable' };
|
|
2214
|
+
}
|
|
2215
|
+
const parsed = parseTaskStallMinutesUpdate(value);
|
|
2216
|
+
if (!parsed.ok) return parsed;
|
|
2217
|
+
stmts.upsertAppSetting.run(TASK_STALL_MINUTES_SETTING_KEY, String(parsed.minutes), new Date().toISOString());
|
|
2218
|
+
return { ok: true, minutes: parsed.minutes };
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
// "After a relay restart, tell interrupted sessions to carry on" (off by default).
|
|
2222
|
+
function getRestartResumeEnabled() {
|
|
2223
|
+
return readRestartResumeSetting(readAppSettingValue(RESTART_RESUME_SETTING_KEY));
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
function setRestartResumeEnabled(value) {
|
|
2227
|
+
if (typeof stmts?.upsertAppSetting?.run !== 'function') {
|
|
2228
|
+
return { ok: false, error: 'Restart settings are unavailable' };
|
|
2229
|
+
}
|
|
2230
|
+
const parsed = parseRestartResumeUpdate(value);
|
|
2231
|
+
if (!parsed.ok) return parsed;
|
|
2232
|
+
stmts.upsertAppSetting.run(RESTART_RESUME_SETTING_KEY, parsed.enabled ? '1' : '0', new Date().toISOString());
|
|
2233
|
+
return { ok: true, enabled: parsed.enabled };
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2161
2236
|
function resolveDefaultSessionWorkspaceRootState() {
|
|
2162
2237
|
return resolveDefaultSessionWorkspaceRootStateFromService({
|
|
2163
2238
|
storedPath: readAppSettingValue(DEFAULT_SESSION_WORKSPACE_ROOT_KEY),
|
|
@@ -3407,140 +3482,87 @@ function attachTtyConsoleRuntimeTitle(runtime) {
|
|
|
3407
3482
|
}
|
|
3408
3483
|
attachTtyConsoleRuntimeTitle(ttyConsoleRuntime);
|
|
3409
3484
|
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3485
|
+
// ─── Queued relay restart / shutdown ─────────────────────────────────────────
|
|
3486
|
+
// A request waits until no turn is processing and holds new turns meanwhile
|
|
3487
|
+
// (services/relay-shutdown-gate.mjs); past the maximum wait it asks instead
|
|
3488
|
+
// of acting (services/relay-restart-queue.mjs). Every client shows a banner
|
|
3489
|
+
// with what it waits for, from `relay_shutdown_state` (sent on each change
|
|
3490
|
+
// and on connect).
|
|
3491
|
+
const relayRestartMaxWaitOverrideMs = resolveRelayRestartMaxWaitOverrideMs(process.env);
|
|
3492
|
+
if (relayRestartMaxWaitOverrideMs !== null) {
|
|
3493
|
+
console.log(`${runtimeLogPrefix()}Relay restart max wait pinned to ${relayRestartMaxWaitOverrideMs} ms (OAR_RELAY_RESTART_MAX_WAIT_MS)`);
|
|
3494
|
+
}
|
|
3495
|
+
function getRelayRestartMaxWaitMinutes() {
|
|
3496
|
+
return readRelayRestartMaxWaitSetting(readAppSettingValue(RELAY_RESTART_MAX_WAIT_SETTING_KEY));
|
|
3497
|
+
}
|
|
3498
|
+
function setRelayRestartMaxWaitMinutes(value) {
|
|
3499
|
+
if (!stmts.upsertAppSetting) return { ok: false, error: 'Relay restart settings are unavailable' };
|
|
3500
|
+
const parsed = parseRelayRestartMaxWaitUpdate(value);
|
|
3501
|
+
if (!parsed.ok) return parsed;
|
|
3502
|
+
stmts.upsertAppSetting.run(RELAY_RESTART_MAX_WAIT_SETTING_KEY, String(parsed.minutes), new Date().toISOString());
|
|
3503
|
+
// A queued request measures its current period against the new value.
|
|
3504
|
+
relayRestartQueue.refresh();
|
|
3505
|
+
return { ok: true, maxWaitMinutes: parsed.minutes };
|
|
3506
|
+
}
|
|
3507
|
+
let restartPictureCollector = null;
|
|
3508
|
+
// What a restart would wait for, hold and leave running (the confirmation
|
|
3509
|
+
// asks before anything is queued). Built on first use: the background-work
|
|
3510
|
+
// view is set up further down.
|
|
3511
|
+
function collectRestartPicture({ action = 'restart' } = {}) {
|
|
3512
|
+
if (!restartPictureCollector) {
|
|
3513
|
+
restartPictureCollector = createRestartPictureCollector({
|
|
3514
|
+
db,
|
|
3515
|
+
backgroundWorkView,
|
|
3516
|
+
listHeldTasks: () => backgroundTaskStore.listRestored().map(({ key, task }) => ({
|
|
3517
|
+
conversationId: resolveConversationRecord({ conversationId: key, sdkSessionId: key })?.id || key,
|
|
3518
|
+
description: task?.description || task?.taskType || '',
|
|
3519
|
+
})),
|
|
3520
|
+
providerOf: (conversationId) => stmts.getRuntimeSessionByConversation?.get?.(conversationId)?.provider_type,
|
|
3521
|
+
titleOf: (conversationId) => hostActivityTitle(conversationId),
|
|
3522
|
+
});
|
|
3429
3523
|
}
|
|
3430
|
-
return {
|
|
3431
|
-
status: runtimeShutdownStarted ? 'shutting_down' : 'queued',
|
|
3432
|
-
action: pending.action || 'shutdown',
|
|
3433
|
-
restart: pending.action === 'restart',
|
|
3434
|
-
reason: pending.reason || null,
|
|
3435
|
-
requestedBy: pending.requestedBy || null,
|
|
3436
|
-
requestedAt: pending.requestedAt || null,
|
|
3437
|
-
};
|
|
3524
|
+
return restartPictureCollector({ action });
|
|
3438
3525
|
}
|
|
3526
|
+
const relayRestartQueue = createRelayRestartQueue({
|
|
3527
|
+
getCounts: () => queueCounts(),
|
|
3528
|
+
collectPicture: collectRestartPicture,
|
|
3529
|
+
startShutdown: ({ action, reason }) => {
|
|
3530
|
+
const exitCode = action === 'restart' ? RELAY_RESTART_EXIT_CODE : 0;
|
|
3531
|
+
sessionWorkerWebSocketService.emitDraining(`api-${action}:${reason}`);
|
|
3532
|
+
void shutdownRuntime(`api-${action}:${reason}`, { exitCode });
|
|
3533
|
+
},
|
|
3534
|
+
getMaxWaitMs: () => (relayRestartMaxWaitOverrideMs !== null
|
|
3535
|
+
? relayRestartMaxWaitOverrideMs
|
|
3536
|
+
: getRelayRestartMaxWaitMinutes() * 60_000),
|
|
3537
|
+
isShuttingDown: () => runtimeShutdownStarted,
|
|
3538
|
+
onStateChange: (state) => {
|
|
3539
|
+
try { io.emit('relay_shutdown_state', state); } catch {}
|
|
3540
|
+
},
|
|
3541
|
+
onMaxWaitReached: (state) => {
|
|
3542
|
+
const sessions = Array.isArray(state.waitingFor) ? state.waitingFor.length : 0;
|
|
3543
|
+
const minutes = Math.max(1, Math.round(Number(state.maxWaitMs || 0) / 60_000));
|
|
3544
|
+
void pushDispatchService.notifyRelayRestart({
|
|
3545
|
+
body: `The queued relay ${state.action} has waited ${minutes} min for ${sessions} session${sessions === 1 ? '' : 's'}. Restart now, keep waiting or cancel it.`,
|
|
3546
|
+
});
|
|
3547
|
+
},
|
|
3548
|
+
logger: console,
|
|
3549
|
+
logPrefix: () => runtimeLogPrefix(),
|
|
3550
|
+
});
|
|
3439
3551
|
|
|
3440
|
-
// Every client shows a banner while a shutdown/restart is queued, so the
|
|
3441
|
-
// state goes out on each transition (request, cancel, drain) and on connect.
|
|
3442
3552
|
function relayShutdownStatePayload() {
|
|
3443
|
-
return
|
|
3444
|
-
}
|
|
3445
|
-
function emitRelayShutdownState() {
|
|
3446
|
-
try { io.emit('relay_shutdown_state', relayShutdownStatePayload()); } catch {}
|
|
3553
|
+
return relayRestartQueue.getState();
|
|
3447
3554
|
}
|
|
3448
3555
|
|
|
3449
3556
|
function cancelRelayShutdown({ requestedBy = 'localhost-api' } = {}) {
|
|
3450
|
-
|
|
3451
|
-
return { cancelled: false, status: 'shutting_down', ...relayShutdownStatePayload() };
|
|
3452
|
-
}
|
|
3453
|
-
if (!pendingRelayShutdownRequest) {
|
|
3454
|
-
return { cancelled: false, ...relayShutdownStatePayload() };
|
|
3455
|
-
}
|
|
3456
|
-
const dropped = pendingRelayShutdownRequest;
|
|
3457
|
-
pendingRelayShutdownRequest = null;
|
|
3458
|
-
if (runtimeTimers.shutdownDrain) {
|
|
3459
|
-
try { clearInterval(runtimeTimers.shutdownDrain); } catch {}
|
|
3460
|
-
runtimeTimers.shutdownDrain = null;
|
|
3461
|
-
}
|
|
3462
|
-
console.log(
|
|
3463
|
-
`${runtimeLogPrefix()}Relay ${dropped.action || 'shutdown'} request cancelled by `
|
|
3464
|
-
+ `${String(requestedBy || 'localhost-api').trim().slice(0, 80) || 'localhost-api'}`,
|
|
3465
|
-
);
|
|
3466
|
-
emitRelayShutdownState();
|
|
3467
|
-
return { cancelled: true, dropped, ...relayShutdownStatePayload() };
|
|
3557
|
+
return relayRestartQueue.cancel({ requestedBy });
|
|
3468
3558
|
}
|
|
3469
3559
|
|
|
3470
|
-
function
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
const restartRequested = normalizeRestartRequestFlag(restart);
|
|
3474
|
-
const requestedAction = restartRequested ? 'restart' : 'shutdown';
|
|
3475
|
-
if (runtimeShutdownStarted) {
|
|
3476
|
-
return {
|
|
3477
|
-
accepted: false,
|
|
3478
|
-
status: 'shutting_down',
|
|
3479
|
-
action: requestedAction,
|
|
3480
|
-
restart: restartRequested,
|
|
3481
|
-
reason: safeReason,
|
|
3482
|
-
};
|
|
3483
|
-
}
|
|
3484
|
-
|
|
3485
|
-
// Shutdown is deferred until no turn is processing; it never interrupts an
|
|
3486
|
-
// active turn. From here on no pending row is handed out (the dequeue paths
|
|
3487
|
-
// read relayShutdownQueued): a turn started meanwhile would postpone the
|
|
3488
|
-
// restart again. Pending and parked rows wait in the database and run
|
|
3489
|
-
// after the restart.
|
|
3490
|
-
if (!pendingRelayShutdownRequest) {
|
|
3491
|
-
pendingRelayShutdownRequest = {
|
|
3492
|
-
action: requestedAction,
|
|
3493
|
-
reason: safeReason,
|
|
3494
|
-
requestedBy: safeRequestedBy,
|
|
3495
|
-
requestedAt: new Date().toISOString(),
|
|
3496
|
-
};
|
|
3497
|
-
} else if (restartRequested && pendingRelayShutdownRequest.action !== 'restart') {
|
|
3498
|
-
pendingRelayShutdownRequest = {
|
|
3499
|
-
...pendingRelayShutdownRequest,
|
|
3500
|
-
action: 'restart',
|
|
3501
|
-
reason: safeReason,
|
|
3502
|
-
requestedBy: safeRequestedBy,
|
|
3503
|
-
requestedAt: new Date().toISOString(),
|
|
3504
|
-
};
|
|
3505
|
-
}
|
|
3506
|
-
|
|
3507
|
-
const tryShutdownWhenIdle = () => {
|
|
3508
|
-
if (runtimeShutdownStarted) return;
|
|
3509
|
-
const counts = queueCounts();
|
|
3510
|
-
if (!queuedShutdownCanProceed(counts)) return;
|
|
3511
|
-
const requestInfo = pendingRelayShutdownRequest;
|
|
3512
|
-
pendingRelayShutdownRequest = null;
|
|
3513
|
-
if (runtimeTimers.shutdownDrain) {
|
|
3514
|
-
try { clearInterval(runtimeTimers.shutdownDrain); } catch {}
|
|
3515
|
-
runtimeTimers.shutdownDrain = null;
|
|
3516
|
-
}
|
|
3517
|
-
const action = requestInfo?.action === 'restart' ? 'restart' : 'shutdown';
|
|
3518
|
-
const exitCode = action === 'restart' ? RELAY_RESTART_EXIT_CODE : 0;
|
|
3519
|
-
console.log(
|
|
3520
|
-
`${runtimeLogPrefix()}Relay ${action} request accepted by ${requestInfo?.requestedBy || 'unknown'}`
|
|
3521
|
-
+ `; no turn is processing (${counts.pendingCount} pending, ${counts.parkedCount} parked wait), exiting now (reason=${requestInfo?.reason || 'manual-request'})`,
|
|
3522
|
-
);
|
|
3523
|
-
sessionWorkerWebSocketService.emitDraining(`api-${action}:${requestInfo?.reason || 'manual-request'}`);
|
|
3524
|
-
void shutdownRuntime(`api-${action}:${requestInfo?.reason || 'manual-request'}`, { exitCode });
|
|
3525
|
-
emitRelayShutdownState();
|
|
3526
|
-
};
|
|
3560
|
+
function keepWaitingForRelayShutdown({ requestedBy = 'localhost-api' } = {}) {
|
|
3561
|
+
return relayRestartQueue.keepWaiting({ requestedBy });
|
|
3562
|
+
}
|
|
3527
3563
|
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
}
|
|
3531
|
-
tryShutdownWhenIdle();
|
|
3532
|
-
if (pendingRelayShutdownRequest) emitRelayShutdownState();
|
|
3533
|
-
const counts = queueCounts();
|
|
3534
|
-
return {
|
|
3535
|
-
accepted: true,
|
|
3536
|
-
status: 'queued',
|
|
3537
|
-
action: pendingRelayShutdownRequest?.action || requestedAction,
|
|
3538
|
-
restart: (pendingRelayShutdownRequest?.action || requestedAction) === 'restart',
|
|
3539
|
-
requestedAt: pendingRelayShutdownRequest?.requestedAt || new Date().toISOString(),
|
|
3540
|
-
reason: pendingRelayShutdownRequest?.reason || safeReason,
|
|
3541
|
-
requestedBy: pendingRelayShutdownRequest?.requestedBy || safeRequestedBy,
|
|
3542
|
-
queue: counts,
|
|
3543
|
-
};
|
|
3564
|
+
function requestRelayShutdown(options = {}) {
|
|
3565
|
+
return relayRestartQueue.request(options);
|
|
3544
3566
|
}
|
|
3545
3567
|
|
|
3546
3568
|
function toNullableInt(value) {
|
|
@@ -5204,10 +5226,17 @@ function previewCardsForResponse(responseMessageId) {
|
|
|
5204
5226
|
.slice(0, 5);
|
|
5205
5227
|
}
|
|
5206
5228
|
|
|
5207
|
-
// Live background-task sets, as the session workers publish them.
|
|
5208
|
-
// only: after a runtime start the heartbeat's digest brings each
|
|
5209
|
-
// (
|
|
5210
|
-
|
|
5229
|
+
// Live background-task sets, as the session workers publish them. The sets
|
|
5230
|
+
// are memory only: after a runtime start the heartbeat's digest brings each
|
|
5231
|
+
// set back. The console rows (the output file of a running shell, the ended
|
|
5232
|
+
// rows of the last hour) are stored (migration 0018) and restored here; a
|
|
5233
|
+
// running one is held until its worker reports (services/background-task-store.mjs).
|
|
5234
|
+
const backgroundTaskStore = createBackgroundTaskStore({
|
|
5235
|
+
persistence: createSqliteBackgroundTaskPersistence(db, { logger: console }),
|
|
5236
|
+
});
|
|
5237
|
+
// The output a worker posts for a task with no output file (the console's
|
|
5238
|
+
// fallback); memory only, an hour.
|
|
5239
|
+
const taskOutputSnapshots = createTaskOutputSnapshots();
|
|
5211
5240
|
|
|
5212
5241
|
// ─── Deferred host suspend ───────────────────────────────────────────────────
|
|
5213
5242
|
// "💤 Suspend host" waits until nothing runs any more: no queued/processing
|
|
@@ -5319,6 +5348,29 @@ const usageLimitPauseService = createUsageLimitPauseService({
|
|
|
5319
5348
|
logger: console,
|
|
5320
5349
|
});
|
|
5321
5350
|
|
|
5351
|
+
// Turns a restart cut off for good (their worker went with the relay, and a
|
|
5352
|
+
// second delivery would run the prompt twice): with the setting on, their
|
|
5353
|
+
// session gets one note to carry on. The rows processing right now are the
|
|
5354
|
+
// ones the runtime before left; the recovery paths below report what became
|
|
5355
|
+
// of each (services/restart-resume-service.mjs).
|
|
5356
|
+
const restartResumeService = createRestartResumeService({
|
|
5357
|
+
db,
|
|
5358
|
+
stmts,
|
|
5359
|
+
isEnabled: getRestartResumeEnabled,
|
|
5360
|
+
emit: (event, payload) => io.emit(event, payload),
|
|
5361
|
+
// The note is an ordinary pending message: the primer starts a stopped
|
|
5362
|
+
// worker, the socket service hands it to a running one.
|
|
5363
|
+
wake: () => {
|
|
5364
|
+
void primePendingSessionWorkers('restart-resume');
|
|
5365
|
+
sessionWorkerWebSocketService?.emitQueueChanged?.('restart-resume');
|
|
5366
|
+
},
|
|
5367
|
+
logger: console,
|
|
5368
|
+
});
|
|
5369
|
+
restartResumeService.captureBoot();
|
|
5370
|
+
function noteRestartTurnOutcome(row, outcome) {
|
|
5371
|
+
try { restartResumeService.noteOutcome(row, outcome); } catch {}
|
|
5372
|
+
}
|
|
5373
|
+
|
|
5322
5374
|
// ─── Relay events ─────────────────────────────────────────────────────────────
|
|
5323
5375
|
// What each session is doing (state, tasks, quiet time) and the subscriptions
|
|
5324
5376
|
// through which a watching session is woken with the events of the sessions
|
|
@@ -5492,6 +5544,7 @@ function recoverProcessingOlderThan(cutoffIso, requeueAtIso, { ceilingBeforeIso
|
|
|
5492
5544
|
for (const row of staleContinuations) {
|
|
5493
5545
|
stmts.dropStaleContinuation?.run?.(row.id);
|
|
5494
5546
|
io.emit('message_status', { messageId: row.id, conversationId: row.conversation_id, status: 'failed' });
|
|
5547
|
+
noteRestartTurnOutcome(row, 'cut');
|
|
5495
5548
|
// A teardown is a turn ending with no reply; it must be visible in the
|
|
5496
5549
|
// console log (the only trace of a silent one was `ignored=already_failed`
|
|
5497
5550
|
// hours later, when the worker's reply arrived for the dead row).
|
|
@@ -5500,13 +5553,19 @@ function recoverProcessingOlderThan(cutoffIso, requeueAtIso, { ceilingBeforeIso
|
|
|
5500
5553
|
console.warn(`${runtimeLogPrefix()}STALE CONTINUATION ${String(row.id).slice(0, 8)} conv=${String(row.conversation_id).slice(0, 8)} dropped: ${reason}`);
|
|
5501
5554
|
}
|
|
5502
5555
|
const listedRows = stmts.listRecoverableProcessing.all(params);
|
|
5503
|
-
for (const row of listedRows.filter((entry) => entry?.consumed_at))
|
|
5556
|
+
for (const row of listedRows.filter((entry) => entry?.consumed_at)) {
|
|
5557
|
+
failConsumedRowTerminally(row, 'stale-recovery');
|
|
5558
|
+
noteRestartTurnOutcome(row, 'cut');
|
|
5559
|
+
}
|
|
5504
5560
|
const allRows = listedRows.filter((entry) => !entry?.consumed_at);
|
|
5505
5561
|
if (!allRows.length) return [];
|
|
5506
5562
|
// Enforce the retry budget before requeueing: failing these rows first
|
|
5507
5563
|
// flips them out of 'processing' so the recovery UPDATE below skips them.
|
|
5508
5564
|
const overBudget = allRows.filter((row) => Number(row?.retry_count || 0) + 1 >= MAX_REQUEUE_RETRIES);
|
|
5509
|
-
for (const row of overBudget)
|
|
5565
|
+
for (const row of overBudget) {
|
|
5566
|
+
failRecoveredRowTerminally(row, 'stale-recovery');
|
|
5567
|
+
noteRestartTurnOutcome(row, 'failed');
|
|
5568
|
+
}
|
|
5510
5569
|
const rows = allRows.filter((row) => Number(row?.retry_count || 0) + 1 < MAX_REQUEUE_RETRIES);
|
|
5511
5570
|
if (!rows.length) return [];
|
|
5512
5571
|
|
|
@@ -5531,6 +5590,7 @@ function recoverProcessingOlderThan(cutoffIso, requeueAtIso, { ceilingBeforeIso
|
|
|
5531
5590
|
|
|
5532
5591
|
for (const row of rows) {
|
|
5533
5592
|
io.emit('message_status', { messageId: row.id, conversationId: row.conversation_id, status: 'pending' });
|
|
5593
|
+
noteRestartTurnOutcome(row, 'requeued');
|
|
5534
5594
|
}
|
|
5535
5595
|
io.emit('queue_updated', { recovered: rows.length });
|
|
5536
5596
|
return rows;
|
|
@@ -6274,6 +6334,12 @@ async function requestSessionWorkerSocketDelivery({ sessionId, pid, reason = 'wo
|
|
|
6274
6334
|
attribution: claudeAttributionForConversation(conv, out.providerModel || out.model, {
|
|
6275
6335
|
relayFolderFallback: deliveredProvider === 'claude',
|
|
6276
6336
|
}),
|
|
6337
|
+
// The Claude tab's background command limit; the worker
|
|
6338
|
+
// hands it to the CLI it spawns next. Claude Cloud runs no
|
|
6339
|
+
// local CLI, so it does not get one.
|
|
6340
|
+
...(deliveredProvider === 'claude'
|
|
6341
|
+
? { claudeBackgroundCommandLimitMs: claudeBackgroundCommandLimitMinutesToMs(getClaudeBackgroundCommandLimitMinutes()) }
|
|
6342
|
+
: {}),
|
|
6277
6343
|
};
|
|
6278
6344
|
})()),
|
|
6279
6345
|
};
|
|
@@ -6389,14 +6455,17 @@ function recoverDeadWorkerProcessingRows(sessionId, reason) {
|
|
|
6389
6455
|
// No user prompt to replay: a dead worker's continuation is torn down.
|
|
6390
6456
|
stmts.dropStaleContinuation?.run?.(row.id);
|
|
6391
6457
|
io.emit('message_status', { messageId: row.id, conversationId: row.conversation_id, status: 'failed' });
|
|
6458
|
+
noteRestartTurnOutcome(row, 'cut');
|
|
6392
6459
|
continue;
|
|
6393
6460
|
}
|
|
6394
6461
|
if (row.consumed_at) {
|
|
6395
6462
|
failConsumedRowTerminally(row, `worker-died-${reason}`);
|
|
6463
|
+
noteRestartTurnOutcome(row, 'cut');
|
|
6396
6464
|
continue;
|
|
6397
6465
|
}
|
|
6398
6466
|
if (Number(row.retry_count || 0) + 1 >= MAX_REQUEUE_RETRIES) {
|
|
6399
6467
|
failRecoveredRowTerminally(row, `worker-died-${reason}`);
|
|
6468
|
+
noteRestartTurnOutcome(row, 'failed');
|
|
6400
6469
|
continue;
|
|
6401
6470
|
}
|
|
6402
6471
|
// The dead worker's subagent runs can never be terminated by the replay
|
|
@@ -6404,6 +6473,7 @@ function recoverDeadWorkerProcessingRows(sessionId, reason) {
|
|
|
6404
6473
|
closeStrandedSubagentRuns(stmts.listRunningSubagentRunsByQueueMessage?.all?.(row.id) || [], row.id);
|
|
6405
6474
|
stmts.recoverProcessingRowKeepOwner?.run?.(requeueAt, row.id);
|
|
6406
6475
|
io.emit('message_status', { messageId: row.id, conversationId: row.conversation_id, status: 'pending' });
|
|
6476
|
+
noteRestartTurnOutcome(row, 'requeued');
|
|
6407
6477
|
recovered += 1;
|
|
6408
6478
|
}
|
|
6409
6479
|
if (recovered > 0) {
|
|
@@ -6516,7 +6586,7 @@ function sessionHasBackgroundWork(sid) {
|
|
|
6516
6586
|
const detachedWorkerWatch = createDetachedWorkerWatch({
|
|
6517
6587
|
isEnabled: () => featureFlags?.SESSION_WORKER_ROUTING_ENABLED === true
|
|
6518
6588
|
&& !runtimeShutdownStarted
|
|
6519
|
-
&& !
|
|
6589
|
+
&& !relayRestartQueue.isQueued(),
|
|
6520
6590
|
listWaitingSessionIds: () => (stmts.listPendingWorkerOwnerSessionIds?.all?.(25) || [])
|
|
6521
6591
|
.map((row) => row?.sdk_session_id),
|
|
6522
6592
|
hasWorkerSocket: (sid) => sessionWorkerWebSocketService.hasWorkerSocket(sid) === true,
|
|
@@ -6713,10 +6783,10 @@ const runtimeState = {
|
|
|
6713
6783
|
get relayPaused() { return relayPaused; },
|
|
6714
6784
|
set relayPaused(value) { relayPaused = value; },
|
|
6715
6785
|
get ttyConsoleActive() { return Boolean(ttyConsoleRuntime?.tty); },
|
|
6716
|
-
get relayShutdown() { return
|
|
6786
|
+
get relayShutdown() { return relayRestartQueue.getState(); },
|
|
6717
6787
|
// A shutdown or restart waits for the processing turns: no pending row
|
|
6718
|
-
// goes out meanwhile (
|
|
6719
|
-
get relayShutdownQueued() { return
|
|
6788
|
+
// goes out meanwhile (services/relay-restart-queue.mjs).
|
|
6789
|
+
get relayShutdownQueued() { return relayRestartQueue.isQueued(); },
|
|
6720
6790
|
get hostSuspend() { return hostSuspendService.getState(); },
|
|
6721
6791
|
get usageLimit() {
|
|
6722
6792
|
return { account: usageLimitPauseService.getAccountState(), pauses: usageLimitPauseService.listPauses() };
|
|
@@ -6959,6 +7029,14 @@ const sharedRouteDeps = {
|
|
|
6959
7029
|
resolveLiveTurnQueueRow,
|
|
6960
7030
|
backgroundTaskStore,
|
|
6961
7031
|
backgroundWorkView,
|
|
7032
|
+
taskOutputSnapshots,
|
|
7033
|
+
// A closure: the poller is built further down the file.
|
|
7034
|
+
pokeTaskHealth: () => taskHealthPoller.poke(),
|
|
7035
|
+
getTaskStallMinutes,
|
|
7036
|
+
setTaskStallMinutes,
|
|
7037
|
+
getRestartResumeEnabled,
|
|
7038
|
+
setRestartResumeEnabled,
|
|
7039
|
+
noteRestartTurnOutcome,
|
|
6962
7040
|
// A closure, not the registry itself: this deps object is built before the
|
|
6963
7041
|
// preview lane is constructed further down the file.
|
|
6964
7042
|
listConversationPreviews: (conversationId) => previewRegistry.listForConversation(conversationId),
|
|
@@ -7088,6 +7166,16 @@ registerRelayEventRoutes(app, {
|
|
|
7088
7166
|
relayEventService,
|
|
7089
7167
|
remoteRelayInbound,
|
|
7090
7168
|
});
|
|
7169
|
+
registerRelayRestartRoutes(app, {
|
|
7170
|
+
auth,
|
|
7171
|
+
// Read only: a GET neither fires the restart nor asks the question.
|
|
7172
|
+
getState: () => relayRestartQueue.getState(),
|
|
7173
|
+
collectRestartPicture,
|
|
7174
|
+
keepWaiting: keepWaitingForRelayShutdown,
|
|
7175
|
+
getMaxWaitMinutes: getRelayRestartMaxWaitMinutes,
|
|
7176
|
+
setMaxWaitMinutes: setRelayRestartMaxWaitMinutes,
|
|
7177
|
+
maxWaitOverrideMs: relayRestartMaxWaitOverrideMs,
|
|
7178
|
+
});
|
|
7091
7179
|
registerAboutRoutes(app, {
|
|
7092
7180
|
auth,
|
|
7093
7181
|
aboutService: createAboutService({
|
|
@@ -7178,6 +7266,8 @@ function recoverStaleMessages() {
|
|
|
7178
7266
|
const ceilingBeforeIso = ceilingMs > 0 ? new Date(Date.now() - ceilingMs).toISOString() : null;
|
|
7179
7267
|
const requeueAt = addMsIso(cliOnline ? 30_000 : 2_000);
|
|
7180
7268
|
const recoveredRows = recoverProcessingOlderThan(cutoff, requeueAt, { ceilingBeforeIso });
|
|
7269
|
+
// Turns of the start that ended on their own decide their session's note.
|
|
7270
|
+
try { restartResumeService.tick(); } catch {}
|
|
7181
7271
|
if (recoveredRows.length > 0) {
|
|
7182
7272
|
const ceilingNote = ceilingBeforeIso ? `, ceiling ${Math.round(ceilingMs / 60_000)}min` : '';
|
|
7183
7273
|
console.log(
|
|
@@ -7487,6 +7577,43 @@ function sanitizeRelayQuestionContext(rawContext) {
|
|
|
7487
7577
|
const subagentViewerRooms = createSubagentViewerRooms({
|
|
7488
7578
|
getRun: (runId) => stmts.getSubagentRun?.get?.(runId) || null,
|
|
7489
7579
|
});
|
|
7580
|
+
// A background task's output goes live only to the pages whose console shows
|
|
7581
|
+
// it; the task must be in the conversation's live or ended set, under the
|
|
7582
|
+
// conversation's id or its worker's session id (as the output route checks).
|
|
7583
|
+
const taskOutputRooms = createTaskOutputRooms({
|
|
7584
|
+
io,
|
|
7585
|
+
resolveTask: (conversationId, taskId) => {
|
|
7586
|
+
const conversation = stmts.getConvAnyStatus?.get?.(conversationId) || null;
|
|
7587
|
+
if (!conversation) return null;
|
|
7588
|
+
const keys = [conversationId, String(conversation.sdk_session_id || conversationId)];
|
|
7589
|
+
return backgroundTaskStore.findTask(keys, taskId);
|
|
7590
|
+
},
|
|
7591
|
+
});
|
|
7592
|
+
// The stall hint and the pass/fail chip of the panel's rows: every 10 s over
|
|
7593
|
+
// the live tasks with an output file.
|
|
7594
|
+
const taskHealthPoller = createTaskHealthPoller({ store: backgroundTaskStore, io });
|
|
7595
|
+
taskHealthPoller.start();
|
|
7596
|
+
// Console tasks held since the restart whose worker has not reported within
|
|
7597
|
+
// twice the boot window (at least 15 s: a test relay has none): that worker
|
|
7598
|
+
// did not survive the restart. They are marked ended (their output stays
|
|
7599
|
+
// readable for the hour), never running.
|
|
7600
|
+
runtimeTimers.restoredTaskSettle = setTimeout(() => {
|
|
7601
|
+
runtimeTimers.restoredTaskSettle = null;
|
|
7602
|
+
const keys = backgroundTaskStore.endUnconfirmed();
|
|
7603
|
+
if (!keys.length) return;
|
|
7604
|
+
console.log(`${runtimeLogPrefix()}BG-TASKS ${keys.length} session(s) did not report after the restart; their console tasks are marked ended`);
|
|
7605
|
+
for (const key of keys) {
|
|
7606
|
+
try {
|
|
7607
|
+
io.emit('background_tasks', {
|
|
7608
|
+
conversationId: key,
|
|
7609
|
+
tasks: backgroundTaskStore.withHealth(backgroundTaskStore.get(key), key),
|
|
7610
|
+
endedTasks: backgroundTaskStore.ended(key),
|
|
7611
|
+
});
|
|
7612
|
+
} catch {}
|
|
7613
|
+
}
|
|
7614
|
+
taskHealthPoller.poke();
|
|
7615
|
+
}, Math.max(15_000, 2 * resolveBootReportingWindowMs(process.env)));
|
|
7616
|
+
runtimeTimers.restoredTaskSettle?.unref?.();
|
|
7490
7617
|
// The handshake presents the token as `auth.token` or the cookie; a query
|
|
7491
7618
|
// `token` is not read (see services/relay-auth.mjs).
|
|
7492
7619
|
io.use(relayAuth.socketMiddleware);
|
|
@@ -7524,6 +7651,7 @@ io.on('connection', (socket) => {
|
|
|
7524
7651
|
activeDeviceTracker.registerSocket(socket);
|
|
7525
7652
|
tmuxInspectorSocketService.registerSocket(socket);
|
|
7526
7653
|
subagentViewerRooms.registerSocket(socket);
|
|
7654
|
+
taskOutputRooms.registerSocket(socket);
|
|
7527
7655
|
});
|
|
7528
7656
|
|
|
7529
7657
|
// ─── SSH Reverse Tunnel ────────────────────────────────────────────────────────
|
|
@@ -7654,15 +7782,19 @@ function clearRuntimeTimers() {
|
|
|
7654
7782
|
for (const timer of Object.values(runtimeTimers)) {
|
|
7655
7783
|
if (!timer) continue;
|
|
7656
7784
|
try { clearInterval(timer); } catch {}
|
|
7785
|
+
try { clearTimeout(timer); } catch {}
|
|
7657
7786
|
}
|
|
7658
7787
|
subagentRecordRetention?.stop();
|
|
7788
|
+
try { taskHealthPoller.stop(); } catch {}
|
|
7789
|
+
try { taskOutputRooms.stop(); } catch {}
|
|
7659
7790
|
runtimeTimers.cliStatus = null;
|
|
7660
7791
|
runtimeTimers.ownerWatchdog = null;
|
|
7661
7792
|
runtimeTimers.staleRecovery = null;
|
|
7662
7793
|
runtimeTimers.questionExpiry = null;
|
|
7663
7794
|
runtimeTimers.pendingWorkerPrime = null;
|
|
7664
7795
|
runtimeTimers.sharedViewerPrune = null;
|
|
7665
|
-
runtimeTimers.
|
|
7796
|
+
runtimeTimers.restoredTaskSettle = null;
|
|
7797
|
+
try { relayRestartQueue.dispose(); } catch {}
|
|
7666
7798
|
}
|
|
7667
7799
|
|
|
7668
7800
|
let runtimeShutdownPromise = null;
|