Make daemon shutdowns easier to diagnose (#1790)

* fix(daemon): log stop reasons and client identity

Record websocket client identity, process memory/uptime, and shutdown reasons across CLI, desktop, supervisor, and worker paths so daemon drops can be traced from the triggering client to worker termination.

* fix(daemon): keep shutdown diagnostics in sync

Test drift: supervisor and relay tests still asserted the old log text and metadata shape after shutdown diagnostics started logging structured reasons and relay connection ids. Update those assertions to the new diagnostic contract.

Also centralize client lifecycle reason normalization and derive desktop daemon stop reasons from one tuple so future changes cannot silently drift.
This commit is contained in:
Mohamed Boudra
2026-06-29 23:31:11 +08:00
committed by GitHub
parent b613bea9f6
commit e63a971968
27 changed files with 567 additions and 86 deletions
+2 -1
View File
@@ -311,12 +311,13 @@ export type DaemonLifecycleIntent =
type: "shutdown";
clientId: string;
requestId: string;
reason: string;
}
| {
type: "restart";
clientId: string;
requestId: string;
reason?: string;
reason: string;
};
export interface PaseoDaemonConfig {
+24 -7
View File
@@ -5,12 +5,14 @@ import { loadConfig } from "./config.js";
import { resolvePaseoHome } from "./paseo-home.js";
import { createRootLogger } from "./logger.js";
import type { DaemonLifecycleIntent } from "./bootstrap.js";
import { getProcessDiagnostics } from "./process-diagnostics.js";
process.title = "Paseo Daemon";
type SupervisorLifecycleMessage =
| {
type: "paseo:shutdown";
reason: string;
}
| {
type: "paseo:ready";
@@ -123,15 +125,23 @@ async function main() {
const beginShutdown = (
signal: string,
options?: {
reason?: string;
successExitCode?: number;
},
) => {
const reason = options?.reason ?? `worker_received_${signal}`;
if (!shutdownPromise) {
logger.info(`${signal} received, shutting down gracefully...`);
logger.info(
{ signal, reason, ...getProcessDiagnostics() },
`${signal} received, shutting down gracefully...`,
);
shutdownPromise = (async () => {
const forceExit = setTimeout(() => {
logger.warn("Forcing shutdown - HTTP server didn't close in time");
logger.warn(
{ signal, reason, ...getProcessDiagnostics() },
"Forcing shutdown - HTTP server didn't close in time",
);
process.exit(1);
}, 10000);
@@ -152,7 +162,10 @@ async function main() {
}
})();
} else {
logger.info(`${signal} received while shutdown is already in progress`);
logger.info(
{ signal, reason, ...getProcessDiagnostics() },
`${signal} received while shutdown is already in progress`,
);
}
installExitHook();
@@ -174,13 +187,13 @@ async function main() {
const handleLifecycleIntent = (intent: DaemonLifecycleIntent) => {
if (intent.type === "shutdown") {
logger.warn(
{ clientId: intent.clientId, requestId: intent.requestId },
{ clientId: intent.clientId, requestId: intent.requestId, reason: intent.reason },
"Shutdown requested via websocket",
);
if (sendSupervisorLifecycleMessage({ type: "paseo:shutdown" })) {
if (sendSupervisorLifecycleMessage({ type: "paseo:shutdown", reason: intent.reason })) {
return;
}
beginShutdown("shutdown lifecycle intent");
beginShutdown("shutdown lifecycle intent", { reason: intent.reason });
return;
}
@@ -196,7 +209,10 @@ async function main() {
) {
return;
}
beginShutdown("restart lifecycle intent", { successExitCode: 0 });
beginShutdown("restart lifecycle intent", {
reason: intent.reason,
successExitCode: 0,
});
};
const installSupervisorLivenessGuard = () => {
@@ -215,6 +231,7 @@ async function main() {
writeWorkerLifecycleLog(paseoHome, "Supervisor liveness lost; worker exiting", {
reason,
...getProcessDiagnostics(),
supervisorPid,
currentParentPid: process.ppid,
ipcConnected: typeof process.connected === "boolean" ? process.connected : null,
@@ -0,0 +1,6 @@
export const CLIENT_SHUTDOWN_RPC_REASON = "client_shutdown_rpc";
export const DEFAULT_CLIENT_RESTART_RPC_REASON = "client_restart_rpc";
export function normalizeClientRestartRpcReason(reason: string | undefined): string {
return reason?.trim() || DEFAULT_CLIENT_RESTART_RPC_REASON;
}
@@ -0,0 +1,38 @@
export interface ProcessMemoryDiagnostics {
rss: number;
heapTotal: number;
heapUsed: number;
external: number;
arrayBuffers: number;
}
export interface ProcessDiagnostics {
pid: number;
ppid: number;
uptimeSeconds: number;
memory: ProcessMemoryDiagnostics;
}
export function getProcessMemoryDiagnostics(): ProcessMemoryDiagnostics {
const memory = process.memoryUsage();
return {
rss: memory.rss,
heapTotal: memory.heapTotal,
heapUsed: memory.heapUsed,
external: memory.external,
arrayBuffers: memory.arrayBuffers,
};
}
export function getProcessUptimeSeconds(): number {
return Math.round(process.uptime() * 1000) / 1000;
}
export function getProcessDiagnostics(): ProcessDiagnostics {
return {
pid: process.pid,
ppid: process.ppid,
uptimeSeconds: getProcessUptimeSeconds(),
memory: getProcessMemoryDiagnostics(),
};
}
@@ -234,6 +234,7 @@ describe("relay-transport control lifecycle", () => {
{
transport: "relay",
externalSessionKey: "session:clt_test",
relayConnectionId: "clt_test",
},
]);
});
@@ -387,6 +387,7 @@ export function startRelayTransport({
const externalMetadata: ExternalSocketMetadata = {
transport: "relay",
externalSessionKey: `session:${connectionId}`,
relayConnectionId: connectionId,
};
if (daemonKeyPair) {
void attachEncryptedSocket(
+16 -4
View File
@@ -61,6 +61,10 @@ import { getErrorMessage, getErrorMessageOr } from "@getpaseo/protocol/error-uti
import { getAgentStatusPriority } from "@getpaseo/protocol/agent-state-bucket";
import { getParentAgentIdFromLabels } from "@getpaseo/protocol/agent-labels";
import type { WorkspaceGitRuntimeSnapshot, WorkspaceGitService } from "./workspace-git-service.js";
import {
CLIENT_SHUTDOWN_RPC_REASON,
normalizeClientRestartRpcReason,
} from "./lifecycle-reasons.js";
import { AgentManager } from "./agent/agent-manager.js";
import { ProviderSnapshotManager } from "./agent/provider-snapshot-manager.js";
@@ -472,12 +476,13 @@ export type SessionLifecycleIntent =
type: "shutdown";
clientId: string;
requestId: string;
reason: string;
}
| {
type: "restart";
clientId: string;
requestId: string;
reason?: string;
reason: string;
};
function parseClientCapabilities(
@@ -1731,6 +1736,10 @@ export class Session {
this.peakInflightRequests = this.inflightRequests;
}
public getSessionId(): string {
return this.sessionId;
}
public async handleBinaryFrame(binaryFrame: BinaryFrame): Promise<void> {
if (binaryFrame.kind === "file_transfer") {
await this.workspaceFilesSession.handleFileTransferFrame(binaryFrame.frame);
@@ -1740,6 +1749,7 @@ export class Session {
}
private async handleRestartServerRequest(requestId: string, reason?: string): Promise<void> {
const lifecycleReason = normalizeClientRestartRpcReason(reason);
const payload: { status: string } & Record<string, unknown> = {
status: "restart_requested",
clientId: this.clientId,
@@ -1749,7 +1759,7 @@ export class Session {
}
payload.requestId = requestId;
this.sessionLogger.warn({ reason }, "Restart requested via websocket");
this.sessionLogger.warn({ reason: lifecycleReason }, "Restart requested via websocket");
this.emit({
type: "status",
payload,
@@ -1759,12 +1769,13 @@ export class Session {
type: "restart",
clientId: this.clientId,
requestId,
...(reason ? { reason } : {}),
reason: lifecycleReason,
});
}
private async handleShutdownServerRequest(requestId: string): Promise<void> {
this.sessionLogger.warn("Shutdown requested via websocket");
const reason = CLIENT_SHUTDOWN_RPC_REASON;
this.sessionLogger.warn({ reason }, "Shutdown requested via websocket");
this.emit({
type: "status",
payload: {
@@ -1778,6 +1789,7 @@ export class Session {
type: "shutdown",
clientId: this.clientId,
requestId,
reason,
});
}
@@ -18,7 +18,7 @@ interface DaemonSelfUpdateRestartIntent {
type: "restart";
clientId: string;
requestId: string;
reason?: string;
reason: string;
}
export interface DaemonSelfUpdateSessionControllerOptions {
@@ -261,6 +261,14 @@ describe("DaemonSession", () => {
getWebSocketRuntimeMetrics: () => ({
collectedAt: "2026-01-02T03:04:05.000Z",
windowMs: 30_000,
uptimeSeconds: 12.345,
memory: {
rss: 1024 * 1024 * 64,
heapTotal: 1024 * 1024 * 32,
heapUsed: 1024 * 1024 * 12,
external: 1024 * 1024 * 3,
arrayBuffers: 1024 * 512,
},
final: false,
sessions: {
activeConnections: 2,
@@ -349,6 +357,10 @@ describe("DaemonSession", () => {
}
expect(message.payload.diagnostic).toContain("WebSocket runtime metrics");
expect(message.payload.diagnostic).toContain("Collected at: 2026-01-02T03:04:05.000Z");
expect(message.payload.diagnostic).toContain("Process uptime: 12s");
expect(message.payload.diagnostic).toContain(
"Process memory: rss=64.0 MiB, heap=12.0 MiB / 32.0 MiB",
);
expect(message.payload.diagnostic).toContain(
"Sessions: active=2, externalKeys=3, reconnectGrace=1",
);
@@ -29,7 +29,7 @@ export interface DaemonSessionHost {
type: "restart";
clientId: string;
requestId: string;
reason?: string;
reason: string;
}): void;
}
@@ -248,6 +248,16 @@ function collectWebSocketRuntimeEntries(options: DaemonDiagnosticsOptions): Diag
return [
{ label: "Collected at", value: snapshot.collectedAt },
{ label: "Window", value: formatDurationMs(snapshot.windowMs) },
{ label: "Process uptime", value: formatDurationMs(snapshot.uptimeSeconds * 1000) },
{
label: "Process memory",
value: [
`rss=${formatBytes(snapshot.memory.rss)}`,
`heap=${formatBytes(snapshot.memory.heapUsed)} / ${formatBytes(snapshot.memory.heapTotal)}`,
`external=${formatBytes(snapshot.memory.external)}`,
`arrayBuffers=${formatBytes(snapshot.memory.arrayBuffers)}`,
].join(", "),
},
{ label: "Final", value: String(snapshot.final) },
{
label: "Sessions",
@@ -52,6 +52,7 @@ const sessionMock = vi.hoisted(() => {
handleBinaryFrame = vi.fn((_frame: unknown) => {});
supports = vi.fn((capability: string) => this.args.clientCapabilities?.[capability] === true);
getClientActivity = vi.fn(() => null);
getSessionId = vi.fn(() => "mock-session-id");
resetPeakInflight = vi.fn(() => {});
getRuntimeMetrics = vi.fn(() => ({
checkoutDiffTargetCount: 0,
@@ -214,14 +215,18 @@ function createLogger() {
return logger;
}
function createServer(options?: { speechReadiness?: SpeechReadinessSnapshot | null }) {
function createServer(options?: {
speechReadiness?: SpeechReadinessSnapshot | null;
logger?: ReturnType<typeof createLogger>;
}) {
const speechReadiness = options?.speechReadiness ?? null;
const daemonConfigStore = {
onChange: vi.fn(() => () => {}),
};
const logger = options?.logger ?? createLogger();
return new VoiceAssistantWebSocketServer(
createStub<HTTPServer>({}),
createStub<pino.Logger>(createLogger()),
createStub<pino.Logger>(logger),
"srv_test",
createStub<AgentManager>({
subscribe: vi.fn(() => () => {}),
@@ -596,6 +601,45 @@ describe("relay external socket reconnect behavior", () => {
await server.close();
});
test("logs control RPCs with the socket identity", async () => {
const logger = createLogger();
const server = createServer({ logger });
const socket = new MockSocket();
await server.attachExternalSocket(socket, {
transport: "relay",
relayConnectionId: "relay-conn-1",
});
socket.emit("message", JSON.stringify(createHelloMessage("cid-control-log")));
socket.emit(
"message",
JSON.stringify({
type: "session",
message: {
type: "shutdown_server_request",
requestId: "shutdown-1",
},
}),
);
await Promise.resolve();
expect(logger.warn).toHaveBeenCalledWith(
expect.objectContaining({
connectionId: expect.stringMatching(/^conn_/),
transport: "relay",
relayConnectionId: "relay-conn-1",
clientId: "cid-control-log",
sessionId: "mock-session-id",
requestType: "shutdown_server_request",
requestId: "shutdown-1",
reason: "client_shutdown_rpc",
}),
"ws_control_rpc_received",
);
await server.close();
});
test("responds to top-level ping while provider diagnostic is still running", async () => {
const server = createServer();
const socket = new MockSocket();
+135 -28
View File
@@ -2,6 +2,7 @@ import { WebSocket, WebSocketServer } from "ws";
import type { IncomingMessage, Server as HTTPServer } from "http";
import { basename, join } from "path";
import { hostname as getHostname } from "node:os";
import { randomUUID } from "node:crypto";
import { monitorEventLoopDelay } from "node:perf_hooks";
import type { AgentManager, AgentMetricsSnapshot } from "./agent/agent-manager.js";
import type { AgentStorage } from "./agent/agent-storage.js";
@@ -63,17 +64,37 @@ import {
type WebSocketRuntimeDiagnosticSnapshot,
} from "./websocket/runtime-metrics.js";
import { ProviderUsageService } from "../services/quota-fetcher/service.js";
import { getProcessMemoryDiagnostics, getProcessUptimeSeconds } from "./process-diagnostics.js";
import {
CLIENT_SHUTDOWN_RPC_REASON,
normalizeClientRestartRpcReason,
} from "./lifecycle-reasons.js";
const WS_CLOSE_DAEMON_AUTH_FAILED = 4401;
export interface ExternalSocketMetadata {
transport: "relay";
externalSessionKey?: string;
relayConnectionId?: string;
}
interface PendingConnection {
connectionLogger: pino.Logger;
helloTimeout: ReturnType<typeof setTimeout> | null;
identity: WebSocketConnectionIdentity;
}
interface WebSocketConnectionIdentity {
connectionId: string;
transport: "direct" | "relay";
host?: string;
origin?: string;
userAgent?: string;
remoteAddress?: string;
relayConnectionId?: string;
clientId?: string;
sessionId?: string;
appVersion?: string;
}
interface WebSocketServerConfig {
@@ -355,6 +376,7 @@ export class VoiceAssistantWebSocketServer {
private readonly wss: WebSocketServer;
private readonly pendingConnections: Map<WebSocketLike, PendingConnection> = new Map();
private readonly sessions: Map<WebSocketLike, SessionConnection> = new Map();
private readonly socketIdentities: Map<WebSocketLike, WebSocketConnectionIdentity> = new Map();
private readonly externalSessionsByKey: Map<string, SessionConnection> = new Map();
private readonly serverId: string;
private readonly daemonVersion: string;
@@ -818,6 +840,7 @@ export class VoiceAssistantWebSocketServer {
this.workspaceGitService.dispose();
this.pendingConnections.clear();
this.sessions.clear();
this.socketIdentities.clear();
this.externalSessionsByKey.clear();
this.wss.close();
}
@@ -867,26 +890,14 @@ export class VoiceAssistantWebSocketServer {
metadata?: ExternalSocketMetadata,
): Promise<void> {
const requestMetadata = extractSocketRequestMetadata(request);
const connectionLoggerFields: Record<string, string> = {
transport: metadata?.transport === "relay" ? "relay" : "direct",
};
if (requestMetadata.host) {
connectionLoggerFields.host = requestMetadata.host;
}
if (requestMetadata.origin) {
connectionLoggerFields.origin = requestMetadata.origin;
}
if (requestMetadata.userAgent) {
connectionLoggerFields.userAgent = requestMetadata.userAgent;
}
if (requestMetadata.remoteAddress) {
connectionLoggerFields.remoteAddress = requestMetadata.remoteAddress;
}
const connectionLogger = this.logger.child(connectionLoggerFields);
const identity = createWebSocketConnectionIdentity(requestMetadata, metadata);
this.socketIdentities.set(ws, identity);
const connectionLogger = this.logger.child(toConnectionLogFields(identity));
const pending: PendingConnection = {
connectionLogger,
helloTimeout: null,
identity,
};
const timeout = setTimeout(() => {
if (this.pendingConnections.get(ws) !== pending) {
@@ -895,7 +906,7 @@ export class VoiceAssistantWebSocketServer {
pending.helloTimeout = null;
this.pendingConnections.delete(ws);
pending.connectionLogger.warn(
{ timeoutMs: HELLO_TIMEOUT_MS },
{ ...toConnectionLogFields(identity), timeoutMs: HELLO_TIMEOUT_MS },
"Closing connection due to missing hello",
);
try {
@@ -911,8 +922,9 @@ export class VoiceAssistantWebSocketServer {
this.incrementRuntimeCounter("connectedAwaitingHello");
this.bindSocketHandlers(ws);
pending.connectionLogger.trace(
pending.connectionLogger.info(
{
...toConnectionLogFields(identity),
totalPendingConnections: this.pendingConnections.size,
},
"Client connected; awaiting hello",
@@ -1089,6 +1101,10 @@ export class VoiceAssistantWebSocketServer {
}
this.clearPendingConnection(ws);
pending.identity.clientId = clientId;
if (message.appVersion) {
pending.identity.appVersion = message.appVersion;
}
const existing = this.externalSessionsByKey.get(clientId);
if (existing) {
this.incrementRuntimeCounter("helloResumed");
@@ -1111,10 +1127,11 @@ export class VoiceAssistantWebSocketServer {
}
existing.sockets.add(ws);
this.sessions.set(ws, existing);
pending.identity.sessionId = existing.session.getSessionId();
this.sendToClient(ws, this.createServerInfoMessage());
existing.connectionLogger.trace(
pending.connectionLogger.info(
{
clientId,
...toConnectionLogFields(pending.identity),
resumed: true,
totalSessions: this.sessions.size,
},
@@ -1134,10 +1151,11 @@ export class VoiceAssistantWebSocketServer {
});
this.sessions.set(ws, connection);
this.externalSessionsByKey.set(clientId, connection);
pending.identity.sessionId = connection.session.getSessionId();
this.sendToClient(ws, this.createServerInfoMessage());
connection.connectionLogger.trace(
connection.connectionLogger.info(
{
clientId,
...toConnectionLogFields(pending.identity),
resumed: false,
totalSessions: this.sessions.size,
},
@@ -1256,26 +1274,42 @@ export class VoiceAssistantWebSocketServer {
error?: Error;
},
): Promise<void> {
const identity = this.socketIdentities.get(ws);
const identityFields = identity ? toConnectionLogFields(identity) : {};
const pending = this.clearPendingConnection(ws);
if (pending) {
this.incrementRuntimeCounter("pendingDisconnected");
pending.connectionLogger.trace(
pending.connectionLogger.info(
{
...identityFields,
code: details.code,
reason: stringifyCloseReason(details.reason),
},
"Pending client disconnected",
);
this.socketIdentities.delete(ws);
return;
}
const connection = this.sessions.get(ws);
if (!connection) {
if (identity) {
this.logger.info(
{
...identityFields,
code: details.code,
reason: stringifyCloseReason(details.reason),
},
"Client socket closed without active session",
);
this.socketIdentities.delete(ws);
}
return;
}
this.sessions.delete(ws);
connection.sockets.delete(ws);
this.socketIdentities.delete(ws);
if (connection.sockets.size === 0) {
this.incrementRuntimeCounter("sessionDisconnectedWaitingReconnect");
@@ -1291,9 +1325,9 @@ export class VoiceAssistantWebSocketServer {
}, EXTERNAL_SESSION_DISCONNECT_GRACE_MS);
connection.externalDisconnectCleanupTimeout = timeout;
connection.connectionLogger.trace(
connection.connectionLogger.info(
{
clientId: connection.clientId,
...identityFields,
code: details.code,
reason: stringifyCloseReason(details.reason),
reconnectGraceMs: EXTERNAL_SESSION_DISCONNECT_GRACE_MS,
@@ -1305,9 +1339,9 @@ export class VoiceAssistantWebSocketServer {
if (connection.sockets.size > 0) {
this.incrementRuntimeCounter("sessionSocketDisconnectedAttached");
connection.connectionLogger.trace(
connection.connectionLogger.info(
{
clientId: connection.clientId,
...identityFields,
remainingSockets: connection.sockets.size,
code: details.code,
reason: stringifyCloseReason(details.reason),
@@ -1332,6 +1366,7 @@ export class VoiceAssistantWebSocketServer {
for (const socket of connection.sockets) {
this.sessions.delete(socket);
this.socketIdentities.delete(socket);
}
connection.sockets.clear();
const existing = this.externalSessionsByKey.get(connection.clientId);
@@ -1562,7 +1597,7 @@ export class VoiceAssistantWebSocketServer {
}
if (message.type === "session") {
void this.dispatchSessionMessage(activeConnection, message).catch((error: unknown) => {
void this.dispatchSessionMessage(ws, activeConnection, message).catch((error: unknown) => {
this.handleRawMessageError({ ws, data, error, log: activeConnection.connectionLogger });
});
}
@@ -1572,10 +1607,22 @@ export class VoiceAssistantWebSocketServer {
}
private async dispatchSessionMessage(
ws: WebSocketLike,
activeConnection: SessionConnection,
message: Extract<WSInboundMessage, { type: "session" }>,
): Promise<void> {
this.recordInboundSessionRequestType(message.message.type);
const controlRpc = getControlRpcLogInfo(message.message);
if (controlRpc) {
const identity = this.socketIdentities.get(ws);
activeConnection.connectionLogger.warn(
{
...(identity ? toConnectionLogFields(identity) : { clientId: activeConnection.clientId }),
...controlRpc,
},
"ws_control_rpc_received",
);
}
const startMs = performance.now();
await activeConnection.session.handleMessage(message.message);
const durationMs = performance.now() - startMs;
@@ -1750,6 +1797,8 @@ export class VoiceAssistantWebSocketServer {
outboundBinaryFrameTypesTop: runtimeMetrics.outboundBinaryFrameTypesTop,
bufferedAmount: runtimeMetrics.bufferedAmount,
eventLoopDelay: this.snapshotEventLoopDelay(),
uptimeSeconds: getProcessUptimeSeconds(),
memory: getProcessMemoryDiagnostics(),
runtime: sessionMetrics,
latency: runtimeMetrics.latency,
agents: agentSnapshot,
@@ -1926,6 +1975,36 @@ interface SocketRequestMetadata {
remoteAddress?: string;
}
function createWebSocketConnectionIdentity(
requestMetadata: SocketRequestMetadata,
metadata: ExternalSocketMetadata | undefined,
): WebSocketConnectionIdentity {
return {
connectionId: `conn_${randomUUID().replaceAll("-", "")}`,
transport: metadata?.transport === "relay" ? "relay" : "direct",
...(requestMetadata.host ? { host: requestMetadata.host } : {}),
...(requestMetadata.origin ? { origin: requestMetadata.origin } : {}),
...(requestMetadata.userAgent ? { userAgent: requestMetadata.userAgent } : {}),
...(requestMetadata.remoteAddress ? { remoteAddress: requestMetadata.remoteAddress } : {}),
...(metadata?.relayConnectionId ? { relayConnectionId: metadata.relayConnectionId } : {}),
};
}
function toConnectionLogFields(identity: WebSocketConnectionIdentity): Record<string, string> {
return {
connectionId: identity.connectionId,
transport: identity.transport,
...(identity.host ? { host: identity.host } : {}),
...(identity.origin ? { origin: identity.origin } : {}),
...(identity.userAgent ? { userAgent: identity.userAgent } : {}),
...(identity.remoteAddress ? { remoteAddress: identity.remoteAddress } : {}),
...(identity.relayConnectionId ? { relayConnectionId: identity.relayConnectionId } : {}),
...(identity.clientId ? { clientId: identity.clientId } : {}),
...(identity.sessionId ? { sessionId: identity.sessionId } : {}),
...(identity.appVersion ? { appVersion: identity.appVersion } : {}),
};
}
function extractSocketRequestMetadata(request: unknown): SocketRequestMetadata {
if (!request || typeof request !== "object") {
return {};
@@ -2091,6 +2170,34 @@ function stringifyCloseReason(reason: unknown): string | null {
return text.length > 0 ? text : null;
}
function getControlRpcLogInfo(
message: Extract<WSInboundMessage, { type: "session" }>["message"],
): { requestType: string; requestId: string; reason?: string } | null {
if (message.type === "shutdown_server_request") {
return {
requestType: message.type,
requestId: message.requestId,
reason: CLIENT_SHUTDOWN_RPC_REASON,
};
}
if (message.type === "restart_server_request") {
const reason = normalizeClientRestartRpcReason(message.reason);
return {
requestType: message.type,
requestId: message.requestId,
reason,
};
}
if (message.type === "daemon.update.request") {
return {
requestType: message.type,
requestId: message.requestId,
reason: "daemon_update",
};
}
return null;
}
function extractRequestInfoFromUnknownWsInbound(
payload: unknown,
): { requestId: string; requestType?: string } | null {
@@ -1,4 +1,5 @@
import type { SessionOutboundMessage, WSOutboundMessage } from "../messages.js";
import type { ProcessMemoryDiagnostics } from "../process-diagnostics.js";
export interface WebSocketRuntimeCounters {
connectedAwaitingHello: number;
@@ -62,6 +63,8 @@ export interface WebSocketRuntimeDiagnosticSnapshot<
p99Ms: number;
maxMs: number;
} | null;
uptimeSeconds: number;
memory: ProcessMemoryDiagnostics;
runtime: TRuntime;
agents: TAgents;
}