diff --git a/apps/server/src/routes/threads/actions.ts b/apps/server/src/routes/threads/actions.ts index a59673fa2..b27eaee91 100644 --- a/apps/server/src/routes/threads/actions.ts +++ b/apps/server/src/routes/threads/actions.ts @@ -75,6 +75,7 @@ import { import { requireThreadCommandEnvironment, requireThreadHostCommandEnvironment, + resolveThreadHostCommandEnvironment, } from "../../services/threads/thread-command-environment.js"; import { LIVE_DAEMON_COMMAND_TIMEOUT_MS, @@ -652,7 +653,7 @@ export function registerThreadActionRoutes(app: Hono, deps: AppDeps): void { environmentId: thread.environmentId, excludeThreadId: thread.id, }); - const environment = requireThreadHostCommandEnvironment({ + const environment = resolveThreadHostCommandEnvironment({ db: deps.db, thread, }); diff --git a/apps/server/src/services/threads/thread-archive.ts b/apps/server/src/services/threads/thread-archive.ts index 66b1e2dc2..aea6d67df 100644 --- a/apps/server/src/services/threads/thread-archive.ts +++ b/apps/server/src/services/threads/thread-archive.ts @@ -20,13 +20,13 @@ import { requestActiveRuntimeThreadStopIfNeeded, } from "./thread-lifecycle.js"; import { archiveThreadAndReleaseChildren } from "./thread-ownership.js"; -import { requireThreadHostCommandEnvironment } from "./thread-command-environment.js"; +import { resolveThreadHostCommandEnvironment } from "./thread-command-environment.js"; interface ArchiveThreadWithLifecycleEffectsArgs { environment: { hostId: string; id: string; - }; + } | null; thread: Pick; } @@ -53,12 +53,15 @@ export function archiveThreadWithLifecycleEffects( threadId: archivedThread.id, }); // Archive only stops active runtime work; manual stop is the pre-start - // provisioning cancellation entrypoint. - requestActiveRuntimeThreadStopIfNeeded( - deps, - archivedThread, - args.environment, - ); + // provisioning cancellation entrypoint. A thread whose environment row was + // pruned has no runtime left to stop. + if (args.environment !== null) { + requestActiveRuntimeThreadStopIfNeeded( + deps, + archivedThread, + args.environment, + ); + } dispatchSettledArchivedThreadProviderArchiveCommand(deps, { threadId: archivedThread.id, }); @@ -90,7 +93,7 @@ export function archiveThreadAndHiddenSourceForks( sourceThreadId: archivedThread.id, })) { archiveThreadWithLifecycleEffects(deps, { - environment: requireThreadHostCommandEnvironment({ + environment: resolveThreadHostCommandEnvironment({ db: deps.db, thread: fork, }), @@ -160,7 +163,7 @@ export function archiveThreadAndChildren( const affectedEnvironmentIds = new Set(); for (const thread of threads) { - const environment = requireThreadHostCommandEnvironment({ + const environment = resolveThreadHostCommandEnvironment({ db: deps.db, thread, }); @@ -172,7 +175,9 @@ export function archiveThreadAndChildren( continue; } archivedThreadIds.push(result.id); - affectedEnvironmentIds.add(environment.id); + if (environment !== null) { + affectedEnvironmentIds.add(environment.id); + } } for (const environmentId of affectedEnvironmentIds) { diff --git a/apps/server/src/services/threads/thread-command-environment.ts b/apps/server/src/services/threads/thread-command-environment.ts index e46e276b2..175aefed5 100644 --- a/apps/server/src/services/threads/thread-command-environment.ts +++ b/apps/server/src/services/threads/thread-command-environment.ts @@ -24,6 +24,25 @@ interface ThreadHostCommandEnvironment { id: string; } +/** + * Resolve the host command environment for a thread, or null when the thread + * has no environment pointer. A thread loses its pointer when its environment + * row is pruned (threads.environment_id is ON DELETE SET NULL), so callers + * that only need the environment to stop live runtime work must tolerate null. + */ +export function resolveThreadHostCommandEnvironment( + args: RequireThreadHostCommandEnvironmentArgs, +): ThreadHostCommandEnvironment | null { + if (args.thread.environmentId === null) { + return null; + } + const environment = requireEnvironment(args.db, args.thread.environmentId); + return { + id: environment.id, + hostId: environment.hostId, + }; +} + export function requireThreadHostCommandEnvironment( args: RequireThreadHostCommandEnvironmentArgs, ): ThreadHostCommandEnvironment {