Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/improve-session-initialization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@cloudflare/sandbox': patch
---

Fix session initialization to eliminate noisy error logs during hot reloads
4 changes: 4 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,10 @@ Turbo handles task orchestration (`turbo.json`) with dependency-aware builds.

**Subject line should stand alone** - don't require reading the body to understand the change. Body is optional and only needed for non-obvious context.

**Focus on the change, not how it was discovered** - never reference "review feedback", "PR comments", or "code review" in commit messages. Describe what the change does and why, not that someone asked for it.

**Avoid bullet points** - write prose, not lists. If you need bullets to explain a change, you're either committing too much at once or over-explaining implementation details. The body should be a brief paragraph, not a changelog.

Good examples:

```
Expand Down
7 changes: 3 additions & 4 deletions packages/sandbox-container/src/services/session-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,10 @@ export class SessionManager {
success: false,
error: {
message: `Session '${options.id}' already exists`,
code: ErrorCode.INTERNAL_ERROR,
code: ErrorCode.SESSION_ALREADY_EXISTS,
details: {
sessionId: options.id,
originalError: 'Session already exists'
} satisfies InternalErrorContext
sessionId: options.id
}
}
};
}
Expand Down
8 changes: 8 additions & 0 deletions packages/sandbox/src/errors/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import type {
PortNotExposedContext,
ProcessErrorContext,
ProcessNotFoundContext,
SessionAlreadyExistsContext,
ValidationFailedContext
} from '@repo/shared/errors';
import { ErrorCode } from '@repo/shared/errors';
Expand Down Expand Up @@ -58,6 +59,7 @@ import {
ProcessNotFoundError,
SandboxError,
ServiceNotRespondingError,
SessionAlreadyExistsError,
ValidationFailedError
} from './classes';

Expand Down Expand Up @@ -123,6 +125,12 @@ export function createErrorFromResponse(errorResponse: ErrorResponse): Error {
errorResponse as unknown as ErrorResponse<ProcessErrorContext>
);

// Session Errors
case ErrorCode.SESSION_ALREADY_EXISTS:
return new SessionAlreadyExistsError(
errorResponse as unknown as ErrorResponse<SessionAlreadyExistsContext>
);

// Port Errors
case ErrorCode.PORT_ALREADY_EXPOSED:
return new PortAlreadyExposedError(
Expand Down
20 changes: 20 additions & 0 deletions packages/sandbox/src/errors/classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import type {
ProcessExitedBeforeReadyContext,
ProcessNotFoundContext,
ProcessReadyTimeoutContext,
SessionAlreadyExistsContext,
ValidationFailedContext
} from '@repo/shared/errors';

Expand Down Expand Up @@ -236,6 +237,25 @@ export class ProcessError extends SandboxError<ProcessErrorContext> {
}
}

// ============================================================================
// Session Errors
// ============================================================================

/**
* Error thrown when a session already exists
*/
export class SessionAlreadyExistsError extends SandboxError<SessionAlreadyExistsContext> {
constructor(errorResponse: ErrorResponse<SessionAlreadyExistsContext>) {
super(errorResponse);
this.name = 'SessionAlreadyExistsError';
}

// Type-safe accessors
get sessionId() {
return this.context.sessionId;
}
}

// ============================================================================
// Port Errors
// ============================================================================
Expand Down
2 changes: 2 additions & 0 deletions packages/sandbox/src/errors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ export {
ProcessReadyTimeoutError,
SandboxError,
ServiceNotRespondingError,
// Session Errors
SessionAlreadyExistsError,
// Validation Errors
ValidationFailedError
} from './classes';
56 changes: 28 additions & 28 deletions packages/sandbox/src/sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ import {
CustomDomainRequiredError,
ErrorCode,
ProcessExitedBeforeReadyError,
ProcessReadyTimeoutError
ProcessReadyTimeoutError,
SessionAlreadyExistsError
} from './errors';
import { CodeInterpreter } from './interpreter';
import { isLocalhostPattern } from './request-handler';
Expand Down Expand Up @@ -994,39 +995,38 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
* we reuse it instead of trying to create a new one.
*/
private async ensureDefaultSession(): Promise<string> {
if (!this.defaultSession) {
const sessionId = `sandbox-${this.sandboxName || 'default'}`;
const sessionId = `sandbox-${this.sandboxName || 'default'}`;

try {
// Try to create session in container
await this.client.utils.createSession({
id: sessionId,
env: this.envVars || {},
cwd: '/workspace'
});
// Fast path: session already initialized in this instance
if (this.defaultSession === sessionId) {
return this.defaultSession;
}

// Create session in container
try {
await this.client.utils.createSession({
id: sessionId,
env: this.envVars || {},
cwd: '/workspace'
});

this.defaultSession = sessionId;
await this.ctx.storage.put('defaultSession', sessionId);
this.logger.debug('Default session initialized', { sessionId });
} catch (error: unknown) {
// Session may already exist (e.g., after hot reload or concurrent request)
if (error instanceof SessionAlreadyExistsError) {
this.logger.debug(
'Session exists in container but not in DO state, syncing',
{ sessionId }
);
this.defaultSession = sessionId;
// Persist to storage so it survives hot reloads
await this.ctx.storage.put('defaultSession', sessionId);
this.logger.debug('Default session initialized', { sessionId });
} catch (error: unknown) {
// If session already exists (e.g., after hot reload), reuse it
if (
error instanceof Error &&
error.message.includes('already exists')
) {
this.logger.debug('Reusing existing session after reload', {
sessionId
});
this.defaultSession = sessionId;
// Persist to storage in case it wasn't saved before
await this.ctx.storage.put('defaultSession', sessionId);
} else {
// Re-throw other errors
throw error;
}
} else {
throw error;
}
}

return this.defaultSession;
}

Expand Down
3 changes: 3 additions & 0 deletions packages/shared/src/errors/codes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ export const ErrorCode = {
PROCESS_PERMISSION_DENIED: 'PROCESS_PERMISSION_DENIED',
PROCESS_ERROR: 'PROCESS_ERROR',

// Session Errors (409)
SESSION_ALREADY_EXISTS: 'SESSION_ALREADY_EXISTS',

// Port Errors (409)
PORT_ALREADY_EXPOSED: 'PORT_ALREADY_EXPOSED',
PORT_IN_USE: 'PORT_IN_USE',
Expand Down
4 changes: 4 additions & 0 deletions packages/shared/src/errors/contexts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ export interface ProcessErrorContext {
stderr?: string;
}

export interface SessionAlreadyExistsContext {
sessionId: string;
}

/**
* Process readiness error contexts
*/
Expand Down
1 change: 1 addition & 0 deletions packages/shared/src/errors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export type {
ProcessExitedBeforeReadyContext,
ProcessNotFoundContext,
ProcessReadyTimeoutContext,
SessionAlreadyExistsContext,
ValidationFailedContext
} from './contexts';
// Export utility functions
Expand Down
1 change: 1 addition & 0 deletions packages/shared/src/errors/status-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export const ERROR_STATUS_MAP: Record<ErrorCode, number> = {
[ErrorCode.PORT_ALREADY_EXPOSED]: 409,
[ErrorCode.PORT_IN_USE]: 409,
[ErrorCode.RESOURCE_BUSY]: 409,
[ErrorCode.SESSION_ALREADY_EXISTS]: 409,

// 502 Bad Gateway
[ErrorCode.SERVICE_NOT_RESPONDING]: 502,
Expand Down
3 changes: 3 additions & 0 deletions packages/shared/src/errors/suggestions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ export function getSuggestion(
case ErrorCode.PORT_IN_USE:
return `Port ${context.port} is already in use by another service. Choose a different port`;

case ErrorCode.SESSION_ALREADY_EXISTS:
return `Session "${context.sessionId}" already exists. Use a different session ID or reuse the existing session`;

case ErrorCode.INVALID_PORT:
return `Port must be between 1 and 65535. Port ${context.port} is ${context.reason}`;

Expand Down
Loading