Add pin and unpin actions for user and assistant text messages, with clear compaction-survival labels, localized tooltips, status-info active styling, and VS Code gating where the server runtime is unavailable. Persist pinned message IDs, creation timestamps, and roles under the OpenChamber session metadata namespace using fresh-read merge updates so goal, review, and other metadata remain intact. Introduce a server runtime that reacts to OpenCode's dedicated session.compacted event, fetches pinned messages by ID, extracts and chronologically orders their text parts, and injects them as hidden synthetic context through prompt_async. The restoration prompt tells the agent to use the context silently while work remains and limits idle summaries to one short paragraph. Track the last handled compaction summary to avoid replay duplication, tolerate individually missing pinned messages, integrate runtime shutdown, document ownership and limitations, and cover metadata round trips plus compaction injection behavior with focused tests.
154 lines
4.0 KiB
JavaScript
154 lines
4.0 KiB
JavaScript
export const createGracefulShutdownRuntime = (dependencies) => {
|
|
const {
|
|
process,
|
|
shutdownTimeoutMs,
|
|
getExitOnShutdown,
|
|
getIsShuttingDown,
|
|
setIsShuttingDown,
|
|
syncToHmrState,
|
|
openCodeWatcherRuntime,
|
|
sessionRuntime,
|
|
sessionAssistRuntime,
|
|
sessionGoalRuntime,
|
|
contextObligatoryRuntime,
|
|
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?.();
|
|
contextObligatoryRuntime?.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,
|
|
};
|
|
};
|