Arm the target button in the composer and the next prompt becomes a goal: the server keeps the session working toward it (idle tick -> small-model audit -> continuation) until the objective is verifiably complete, blocked, or out of budget — even with the UI closed. Server (packages/web/server/lib/session-goal): - event-driven loop on the global SSE hub; goal state lives in session.metadata.openchamber.goal (merge-safe patches, stale-write guard by goal id), so it survives restarts and syncs to every client for free - the small-model audit (objective + last assistant turn only, language pinned to the objective) is the sole termination authority; blocked needs 3 consecutive verdicts, audit outages tolerate one unaudited continuation then stop the goal as resumable-blocked - hard stops: optional token budget, auto-continuation cap (Resume grants a fresh allowance), turn errors; user abort pauses the goal instead of blocking it, and resuming over an aborted tail nudges immediately - token accounting as a snapshot of the latest turn (input + cache.read + output), goal-relative via a creation baseline and segmented across compactions; a compaction summary skips the audit and continues - continuations reuse the session's own provider/model/agent/variant UI: - three-mode target button (arm / disarm / manage dialog), informational goal strip with inline pause/resume and an Evaluating indicator, sidebar state glyph, objective length counter (2000-char server clamp), read-only completed goals - goal entry points: composer (sessions and drafts), start-new-session- from-answer dialog, plan implement dialog (plan content becomes the objective), scheduled tasks (Run as goal + budget) - Settings -> Chat -> Goal: feature toggle + default token budget with three-layer parity (web server, client persistence, VS Code bridge); VS Code renders goal state but hides the entry points (the loop runs in the web server only) Notifications: per-turn "ready" notifications are suppressed while a goal is active; settling sends one final notification (desktop, web-push, APNs generic titles with the session name as body) honoring the completion toggle. Error/question/permission notifications are untouched. Docs: user guide (session-goals) in all 9 locales + sidebar entry, scheduled-tasks cross-reference, server module DOCUMENTATION.md.
152 lines
3.9 KiB
JavaScript
152 lines
3.9 KiB
JavaScript
export const createGracefulShutdownRuntime = (dependencies) => {
|
|
const {
|
|
process,
|
|
shutdownTimeoutMs,
|
|
getExitOnShutdown,
|
|
getIsShuttingDown,
|
|
setIsShuttingDown,
|
|
syncToHmrState,
|
|
openCodeWatcherRuntime,
|
|
sessionRuntime,
|
|
sessionAssistRuntime,
|
|
sessionGoalRuntime,
|
|
scheduledTasksRuntime,
|
|
getHealthCheckInterval,
|
|
clearHealthCheckInterval,
|
|
getTerminalRuntime,
|
|
setTerminalRuntime,
|
|
getMessageStreamRuntime,
|
|
setMessageStreamRuntime,
|
|
shouldSkipOpenCodeStop,
|
|
getOpenCodePort,
|
|
getOpenCodeProcess,
|
|
setOpenCodeProcess,
|
|
killProcessOnPort,
|
|
waitForPortRelease,
|
|
getServer,
|
|
getUiAuthController,
|
|
setUiAuthController,
|
|
getActiveTunnelController,
|
|
setActiveTunnelController,
|
|
tunnelAuthController,
|
|
} = dependencies;
|
|
|
|
let shutdownPromise = null;
|
|
|
|
const runShutdown = async (options = {}) => {
|
|
if (getIsShuttingDown()) return;
|
|
|
|
setIsShuttingDown(true);
|
|
syncToHmrState();
|
|
console.log('Starting graceful shutdown...');
|
|
const exitProcess = typeof options.exitProcess === 'boolean' ? options.exitProcess : getExitOnShutdown();
|
|
|
|
openCodeWatcherRuntime.stop();
|
|
sessionRuntime.dispose();
|
|
sessionAssistRuntime?.stop?.();
|
|
sessionGoalRuntime?.stop?.();
|
|
scheduledTasksRuntime?.stop?.();
|
|
|
|
const healthCheckInterval = getHealthCheckInterval();
|
|
if (healthCheckInterval) {
|
|
clearHealthCheckInterval(healthCheckInterval);
|
|
}
|
|
|
|
const terminalRuntime = getTerminalRuntime();
|
|
if (terminalRuntime) {
|
|
try {
|
|
await terminalRuntime.shutdown();
|
|
} catch {
|
|
} finally {
|
|
setTerminalRuntime(null);
|
|
}
|
|
}
|
|
|
|
const messageStreamRuntime = getMessageStreamRuntime();
|
|
if (messageStreamRuntime) {
|
|
try {
|
|
await messageStreamRuntime.close();
|
|
} catch {
|
|
} finally {
|
|
setMessageStreamRuntime(null);
|
|
}
|
|
}
|
|
|
|
if (!shouldSkipOpenCodeStop()) {
|
|
const portToKill = getOpenCodePort();
|
|
const openCodeProcess = getOpenCodeProcess();
|
|
|
|
if (openCodeProcess) {
|
|
console.log('Stopping OpenCode process...');
|
|
try {
|
|
await openCodeProcess.close();
|
|
} catch (error) {
|
|
console.warn('Error closing OpenCode process:', error);
|
|
}
|
|
setOpenCodeProcess(null);
|
|
}
|
|
|
|
killProcessOnPort(portToKill);
|
|
if (!(await waitForPortRelease(portToKill, 5000))) {
|
|
console.warn(`Timed out waiting for OpenCode port ${portToKill} to be released during shutdown`);
|
|
}
|
|
} else {
|
|
console.log('Skipping OpenCode shutdown (external server)');
|
|
}
|
|
|
|
const server = getServer();
|
|
if (server) {
|
|
let closeTimeout = null;
|
|
try {
|
|
await Promise.race([
|
|
new Promise((resolve) => {
|
|
server.close(() => {
|
|
console.log('HTTP server closed');
|
|
resolve();
|
|
});
|
|
}),
|
|
new Promise((resolve) => {
|
|
closeTimeout = setTimeout(() => {
|
|
console.warn('Server close timeout reached, forcing shutdown');
|
|
resolve();
|
|
}, shutdownTimeoutMs);
|
|
}),
|
|
]);
|
|
} finally {
|
|
if (closeTimeout) {
|
|
clearTimeout(closeTimeout);
|
|
}
|
|
}
|
|
}
|
|
|
|
const uiAuthController = getUiAuthController();
|
|
if (uiAuthController) {
|
|
uiAuthController.dispose();
|
|
setUiAuthController(null);
|
|
}
|
|
|
|
const activeTunnelController = getActiveTunnelController();
|
|
if (activeTunnelController) {
|
|
console.log('Stopping active tunnel...');
|
|
activeTunnelController.stop();
|
|
setActiveTunnelController(null);
|
|
tunnelAuthController.clearActiveTunnel();
|
|
}
|
|
|
|
console.log('Graceful shutdown complete');
|
|
if (exitProcess) {
|
|
process.exit(0);
|
|
}
|
|
};
|
|
|
|
const gracefulShutdown = (options = {}) => {
|
|
if (shutdownPromise) return shutdownPromise;
|
|
shutdownPromise = runShutdown(options);
|
|
return shutdownPromise;
|
|
};
|
|
|
|
return {
|
|
gracefulShutdown,
|
|
};
|
|
};
|