Files
paseo/packages/server/src/server/session.ts
T
Mohamed Boudra 125b613242 refactor(server): the agent manager converts Codex's legacy fast_mode for the adapter behind the provider id
The rename is adapter-specific, so it lives where the provider id resolves to its adapter:
AgentManager converts an agent's own options on create, resume, import, reload, apply and
draft controls, following derivedFromProviderId, so a custom provider extending codex keeps
working with a saved or requested fast_mode. Storage, MCP, schedules, hub and profiles pass
options through. The featureValues-to-options fold stays provider-agnostic at the legacy edges.
2026-11-04 08:21:38 +01:00

9091 lines
321 KiB
TypeScript

import { type WorkspaceRuntimeStore } from "./workspace-runtime-store/index.js";
import type { AgentMessageDelivery } from "./agent/agent-messages/index.js";
import { AgentDirectory, type BackgroundVisibility } from "./agent/directory.js";
import { ConnectedHostSession } from "./connected-hosts/index.js";
import type { ConnectedHosts } from "./connected-hosts/index.js";
import { searchTimeline } from "./agent/chat-search/index.js";
import type { BrowserToolsBroker } from "./browser-tools/broker.js";
import { BrowserAutomationHostCapabilitySchema } from "@getpaseo/protocol/browser-automation/capabilities";
import type {
SessionEventSubscription,
UsageReportEntry,
ProviderUsage,
} from "@getpaseo/protocol/messages";
import { relative } from "node:path";
import { isAbsolute } from "node:path";
import { CreationService, type CreatedAgent } from "./creation/index.js";
import type { CreationSnapshot, AgentCreateRequest } from "@getpaseo/protocol/messages";
import type { MessageReceipts } from "./message-receipts/index.js";
import equal from "fast-deep-equal";
import { WorkspaceSetupObservation } from "./workspace-setup-observation.js";
import { SessionDelivery, type OwnedSubscription } from "./session/owned-subscriptions/index.js";
import { v4 as uuidv4 } from "uuid";
import { lstat, mkdir, mkdtemp, rename, rm, stat } from "node:fs/promises";
import { basename, join, resolve, sep } from "path";
import { homedir } from "node:os";
import { CLIENT_CAPS, type ClientCapability } from "@getpaseo/protocol/client-capabilities";
import { formatPluginSourceReference } from "@getpaseo/protocol/plugin-source-reference";
import {
serializeAgentStreamEvent,
type AgentSnapshotPayload,
type AgentAttachment,
type FirstAgentContext,
type SessionInboundMessage,
type SessionOutboundMessage,
type ScriptStatusUpdateMessage,
type StartWorkspaceScriptRequest,
type WorkspaceScriptListRequest,
type WorkspaceScriptStartRequest,
type WorkspaceScriptStopRequest,
type CloseItemsRequest,
type DirectorySuggestionsRequest,
type ProjectPlacementPayload,
type WorkspaceSetupSnapshot,
type WorkspaceDescriptorPayload,
} from "./messages.js";
import type {
TerminalManager,
TerminalWorkspaceContributionChangedEvent,
} from "../terminal/terminal-manager.js";
import { TerminalSessionController } from "../terminal/terminal-session-controller.js";
import type { TerminalActivity } from "@getpaseo/protocol/terminal-activity";
import type { BinaryFrame } from "@getpaseo/protocol/binary-frames/index";
import {
encodePluginEventFrame,
encodePluginOutputFrame,
PluginFrameOpcode,
} from "@getpaseo/protocol/binary-frames/index";
import { CursorError } from "./pagination/cursor.js";
import { SortablePager, type SortSpec } from "./pagination/sortable-pager.js";
import { matchesAgentHistoryQuery } from "./agent-history-search.js";
import type { SpeechToTextProvider, TextToSpeechProvider } from "./speech/speech-provider.js";
import type { TurnDetectionProvider } from "./speech/turn-detection-provider.js";
import {
buildConfigOverrides,
isStoredAgentProviderAvailable,
resolveStoredAgentUpdatedAt,
toAgentPersistenceHandle,
} from "./persistence-hooks.js";
import { ensureAgentLoaded, ensureUnarchivedAgentLoaded } from "./agent/agent-loading.js";
import {
sendPromptToAgent,
waitForAgentRunStartWithTimeout,
unarchiveAgentState,
} from "./agent/agent-prompt.js";
import {
resolveCreateAgentTitles,
resolveFirstAgentPromptTitle,
} from "./agent/create-agent-title.js";
import { respondToAgentPermission } from "./agent/permission-response.js";
import type { VoiceCallerContext, VoiceSpeakHandler } from "./voice-types.js";
import type { ScriptHealthState } from "./script-health-monitor.js";
import { spawnWorkspaceScript } from "./worktree-bootstrap.js";
import type { WorkspaceScriptRuntimeStore } from "./workspace-script-runtime-store.js";
import {
createWorkspaceScriptsService,
type WorkspaceScriptsService,
} from "./session/workspace-scripts/workspace-scripts-service.js";
import { toWireDaemonConfig, type DaemonConfigStore } from "./daemon-config-store.js";
import { releaseWorkspaceServicePortPlan } from "./workspace-service-port-registry.js";
import { getErrorMessage, getErrorMessageOr } from "@getpaseo/protocol/error-utils";
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 type { ProjectUpdate } from "./workspace-reconciliation-service.js";
import {
CLIENT_SHUTDOWN_RPC_REASON,
normalizeClientRestartRpcReason,
} from "./lifecycle-reasons.js";
import { DirectorySyncService } from "./directory-sync/index.js";
import {
assertWorkspaceAutomationAllowedForWorkspace,
clearWorkspaceAutomationBlock,
formatWorkspaceAutomationBlockedMessage,
} from "./workspace-automation-gate.js";
import {
WorkspaceLabelError,
WorkspaceLabelStorageUncertainError,
type WorkspaceLabelService,
} from "./workspace-labels/index.js";
import { AgentManager, AgentRunCancellationError } from "./agent/agent-manager.js";
import { buildTimelinePromptIndex } from "./agent/timeline-prompt-index.js";
import { ProviderSnapshotManager } from "./agent/provider-snapshot-manager.js";
import type {
AgentManagerEvent,
AgentTimelineCursor,
AgentTimelineFetchDirection,
AgentTimelineFetchResult,
ManagedAgent,
} from "./agent/agent-manager.js";
import { createAgentCommand } from "./agent/create-agent/create.js";
import {
inheritAgentBackground,
inheritWorkspaceBackground,
resolveCreateAgentIntent,
type CreateAgentIntent,
type CreateAgentCaller,
} from "./agent/create-agent/intent.js";
import {
archiveAgentCommand,
cancelAgentRunCommand,
closeAgentCommand,
detachAgentCommand,
setAgentModeCommand,
updateAgentCommand,
} from "./agent/lifecycle-command.js";
import { buildStoredAgentPayload, toAgentPayload } from "./agent/agent-projections.js";
import { assertPluginTimelineDataSize } from "./agent/agent-timeline-content.js";
import { parsePluginClientId } from "./plugins/plugin-session-identity.js";
import { buildAgentForkContextAttachment } from "./agent/activity-curator.js";
import { buildAgentPrompt } from "./agent/prompt-attachments.js";
import type { StructuredGenerationDaemonConfig } from "./agent/structured-generation-providers.js";
import {
getAgentStreamEventTurnId,
type AgentPersistenceHandle,
type AgentPermissionResponse,
type AgentRunOptions,
type AgentSessionConfig,
} from "./agent/agent-sdk-types.js";
import type { StoredAgentRecord } from "./agent/agent-storage.js";
import type { AgentStorage } from "./agent/agent-storage.js";
import {
ImportSessionsRequestError,
importProviderSession,
listImportableProviderSessions,
normalizeImportAgentRequest,
} from "./agent/import-sessions.js";
import { listProjectSuggestions, ProjectSuggestionsRequestError } from "./project-suggestions.js";
import {
checkoutLiteFromGitSnapshot,
checkoutFromPersistedWorkspacePlacement,
deriveWorkspaceDisplayName,
} from "./workspace-registry-model.js";
import { resolveWorkspaceIdForPath } from "./resolve-workspace-id-for-path.js";
import {
resolveProjectDisplayName,
resolveWorkspaceDisplayName,
resolveWorkspaceName,
type PersistedProjectRecord,
type PersistedWorkspaceRecord,
type ProjectMutation,
type ProjectRegistry,
type WorkspaceMutation,
type WorkspaceRegistry,
} from "./workspace-registry.js";
import { wrapSpokenInput } from "./voice-config.js";
import { isVoicePermissionAllowed } from "./voice-permission-policy.js";
import {
ProjectIconReader,
removeProjectCustomIcon,
setProjectCustomIcon,
} from "../utils/project-custom-icon.js";
import { VoiceSessions } from "./session/voice/index.js";
import { CheckoutSession } from "./session/checkout/checkout-session.js";
import {
createWorkspaceGitObserverService,
type WorkspaceGitObserverService,
} from "./session/workspace-git-observer/workspace-git-observer-service.js";
import {
createAgentStructuredTextGeneration,
createGitMetadataGenerator,
} from "./session/checkout/git-metadata-generator.js";
import { ScheduleSession } from "./session/schedule/schedule-session.js";
import { ProviderCatalogSession } from "./session/provider/provider-catalog-session.js";
import { UsageSession } from "./session/usage/usage-session.js";
import {
SpeechModelsSession,
type SpeechModelsSessionOptions,
} from "./session/speech/speech-models-session.js";
import { WorkspaceFilesSession } from "./session/files/workspace-files-session.js";
import { AgentConfigSession } from "./session/agent-config/agent-config-session.js";
import { ProjectConfigSession } from "./session/project-config/project-config-session.js";
import { DaemonSession, type DaemonRuntimeConfig } from "./session/daemon/daemon-session.js";
import type { DaemonWebSocketRuntimeDiagnosticSnapshot } from "./session/daemon/diagnostics.js";
import type { HubRelationshipManagement } from "./hub/relationship-controller.js";
import { HubExecutionController } from "./hub/execution-controller.js";
import type { HubExecutionAgents } from "./hub/daemon-executions.js";
import { DownloadTokenStore } from "./file-download/token-store.js";
import type { PushNotifications } from "./push/index.js";
import {
archivePersistedWorkspaceRecord,
archiveWorkspaceContents,
} from "./workspace-archive-service.js";
import type { ServiceProxySubsystem } from "./service-proxy.js";
import { renameCurrentBranch as renameCurrentBranchDefault } from "../utils/checkout-git.js";
import {
createGitMutationService,
type GitMutationService,
} from "./session/git-mutation/git-mutation-service.js";
import {
createWorkspaceProvisioningService,
type WorkspaceProvisioningService,
} from "./session/workspace-provisioning/workspace-provisioning-service.js";
import {
createWorkspaceRecoveryService,
type WorkspaceRecoveryService,
} from "./session/workspace-recovery/workspace-recovery-service.js";
import {
createAgentUpdatesService,
matchesAgentUpdatesFilter,
type AgentUpdatesService,
} from "./session/agent-updates/agent-updates-service.js";
import { expandTilde } from "../utils/path.js";
import {
searchDirectoryEntries,
WORKSPACE_SEARCH_HIDDEN_DIRECTORIES,
} from "../utils/directory-suggestions.js";
import type { CheckoutDiffManager } from "./checkout-diff-manager.js";
import type { Resolvable } from "./speech/provider-resolver.js";
import type { SpeechReadinessSnapshot } from "./speech/speech-runtime.js";
import type pino from "pino";
import { ScheduleService } from "./schedule/service.js";
import {
createGitHubService,
GitHubAuthenticationError,
GitHubCliMissingError,
GitHubCommandError,
type GitHubService,
} from "../services/github-service.js";
import type { ForgeService } from "../services/forge-service.js";
import {
resolveWorkspaceRootAgent,
summarizeFetchWorkspacesEntries,
workspaceIdsOnCheckout,
WorkspaceDirectory,
type WorkspaceUpdatesFilter,
} from "./workspace-directory.js";
import { shouldEmitPendingBootstrapUpdate } from "./workspace-bootstrap-dedupe.js";
import {
createPaseoWorktree,
type CreatePaseoWorktreeInput,
type CreatePaseoWorktreeResult,
} from "./paseo-worktree-service.js";
import { WorkspaceAutoName } from "./workspace-auto-name.js";
import {
createPaseoWorktreeWorkflow as createWorktreeWorkflow,
type CreatePaseoWorktreeWorkflowResult,
handleCreatePaseoWorktreeRequest as handleCreateWorktreeRequest,
handlePaseoWorktreeArchiveRequest as handleWorktreeArchiveRequest,
handlePaseoWorktreeListRequest as handleWorktreeListRequest,
handleWorkspaceSetupStatusRequest as handleWorkspaceSetupStatusRequestMessage,
handleWorkspaceSetupRunRequest as handleWorkspaceSetupRunRequestMessage,
} from "./worktree-session.js";
import { archiveByScope, type ActiveWorkspaceRef } from "./workspace-archive-service.js";
import { WorkspaceSetupRuntime } from "./workspace-setup-runtime.js";
import { SessionAuthorization, type DaemonPermission } from "./authorization/index.js";
function resolveWorkspaceSetupRuntime(
runtime: WorkspaceSetupRuntime | undefined,
): WorkspaceSetupRuntime {
return runtime ?? new WorkspaceSetupRuntime();
}
import { WorktreeRequestError, toWorktreeWireError } from "./worktree-errors.js";
import { parseGitRemoteLocation } from "@getpaseo/protocol/git-remote";
import {
createProjectDirectory,
ProjectDirectoryRequestError,
} from "./project-directory-service.js";
import { runGitCommand } from "../utils/run-git-command.js";
import { CreateAgentLifecycleDispatch } from "./agent/create-agent-lifecycle-dispatch.js";
import { resolveWorktreeSourceCwd } from "./workspace-source.js";
type ProviderSubagentManagerEvent = Extract<
AgentManagerEvent,
{ type: "provider_subagent" }
>["event"];
// TODO: Remove once all app store clients are on >=0.1.45 and understand arbitrary provider strings.
// Clients before 0.1.45 validate providers with z.enum(["claude", "codex", "opencode"]) and reject
// the entire session message if they encounter an unknown provider.
const LEGACY_PROVIDER_IDS = new Set(["claude", "codex", "opencode"]);
const MIN_VERSION_ALL_PROVIDERS = "0.1.45";
const MIN_VERSION_EXPLICIT_WORKSPACE_RECOVERY = "0.1.105";
function errorToFriendlyMessage(error: unknown): string {
if (error instanceof Error) return error.message;
if (typeof error === "string") return error;
return "Unknown error";
}
function isAppVersionAtLeast(appVersion: string | null, minVersion: string): boolean {
if (!appVersion) return false;
// Strip prerelease suffix: "0.1.45-beta.4" -> "0.1.45"
const base = appVersion.replace(/-.*$/, "");
const parts = base.split(".").map(Number);
const minParts = minVersion.split(".").map(Number);
for (let i = 0; i < minParts.length; i++) {
const a = parts[i] ?? 0;
const b = minParts[i] ?? 0;
if (a > b) return true;
if (a < b) return false;
}
return true;
}
function clientSupportsAllProviders(appVersion: string | null): boolean {
return isAppVersionAtLeast(appVersion, MIN_VERSION_ALL_PROVIDERS);
}
function clientUsesLegacyWorkspaceRestore(appVersion: string | null): boolean {
return (
appVersion !== null && !isAppVersionAtLeast(appVersion, MIN_VERSION_EXPLICIT_WORKSPACE_RECOVERY)
);
}
type DeleteFencedAgentStorage = AgentStorage & {
beginDelete(agentId: string): void;
};
function beginAgentDeleteIfSupported(agentStorage: AgentStorage, agentId: string): void {
if ("beginDelete" in agentStorage && typeof agentStorage.beginDelete === "function") {
(agentStorage as DeleteFencedAgentStorage).beginDelete(agentId);
}
}
const FETCH_AGENTS_SORT_KEYS = ["status_priority", "created_at", "updated_at", "title"] as const;
export function resolveWaitForFinishError(options: {
status: "permission" | "error" | "idle";
final: AgentSnapshotPayload | null;
}): string | null {
if (options.status !== "error") {
return null;
}
const message = options.final?.lastError;
return typeof message === "string" && message.trim().length > 0 ? message : "Agent failed";
}
export interface SessionRuntimeMetrics {
terminalDirectorySubscriptionCount: number;
terminalSubscriptionCount: number;
workspaceGitWatchedDirectoryCount: number;
workspaceGitWorkspaceRecordCount: number;
workspaceGitSubscriptionCount: number;
inflightRequests: number;
peakInflightRequests: number;
}
type FetchAgentsRequestMessage = Extract<SessionInboundMessage, { type: "fetch_agents_request" }>;
type FetchAgentHistoryRequestMessage = Extract<
SessionInboundMessage,
{ type: "fetch_agent_history_request" }
>;
type AgentDirectoryRequestMessage = FetchAgentsRequestMessage | FetchAgentHistoryRequestMessage;
/**
* Only history carries a query. The active-agents directory filters on
* structure and never ranks, so it always reads as no query at all.
*/
function agentDirectorySearchQuery(request: AgentDirectoryRequestMessage): string {
if (request.type !== "fetch_agent_history_request") return "";
return request.search?.trim() ?? "";
}
type FetchAgentsRequestFilter = NonNullable<FetchAgentsRequestMessage["filter"]>;
type FetchAgentsRequestSort = NonNullable<FetchAgentsRequestMessage["sort"]>[number];
type FetchAgentsResponsePayload = Extract<
SessionOutboundMessage,
{ type: "fetch_agents_response" }
>["payload"];
type FetchAgentsResponseEntry = FetchAgentsResponsePayload["entries"][number];
type FetchAgentsResponsePageInfo = FetchAgentsResponsePayload["pageInfo"];
type AgentUpdatesFilter = FetchAgentsRequestFilter;
type CreateAgentRequestMessage = Extract<SessionInboundMessage, { type: "create_agent_request" }>;
interface ResolvedSessionCreateAgentIntent {
config: AgentSessionConfig;
intent: CreateAgentIntent;
background: boolean;
createdDirectoryWorkspace: boolean;
}
type FetchWorkspacesRequestMessage = Extract<
SessionInboundMessage,
{ type: "fetch_workspaces_request" }
>;
type FetchWorkspacesRequestFilter = NonNullable<FetchWorkspacesRequestMessage["filter"]>;
type FetchWorkspacesResponsePayload = Extract<
SessionOutboundMessage,
{ type: "fetch_workspaces_response" }
>["payload"];
type FetchWorkspacesResponseEntry = FetchWorkspacesResponsePayload["entries"][number];
type FetchWorkspacesResponsePageInfo = FetchWorkspacesResponsePayload["pageInfo"];
type WorkspaceProjectDescriptorPayload = FetchWorkspacesResponsePayload["emptyProjects"][number];
type WorkspaceGithubSearchRepositoriesResponsePayload = Extract<
SessionOutboundMessage,
{ type: "workspace.github.search_repositories.response" }
>["payload"];
type WorkspaceUpdatePayload = Extract<
SessionOutboundMessage,
{ type: "workspace_update" }
>["payload"];
interface WorkspaceUpdatesSubscriptionState {
subscriptionId: string;
owner: OwnedSubscription;
syncEnabled?: boolean;
filter?: WorkspaceUpdatesFilter;
isBootstrapping: boolean;
pendingUpdatesByWorkspaceId: Map<string, WorkspaceUpdatePayload>;
lastEmittedByWorkspaceId: Map<string, WorkspaceUpdatePayload>;
visibleEmptyProjectIds?: Set<string>;
}
class SessionRequestError extends Error {
constructor(
readonly code: string,
message: string,
) {
super(message);
this.name = "SessionRequestError";
}
}
export interface SessionFileSystem {
isDirectory(path: string): Promise<boolean>;
}
const nodeSessionFileSystem: SessionFileSystem = {
async isDirectory(path) {
const stats = await stat(path).catch(() => null);
return stats?.isDirectory() ?? false;
},
};
// Stub types for features under development (modules not yet available)
type AgentMcpTransportFactory = () => Promise<unknown>;
export interface SessionOptions {
connectedHosts?: ConnectedHosts;
sourceId?: string;
routed?: boolean;
browserToolsBroker?: BrowserToolsBroker | null;
clientId: string;
permissions: readonly DaemonPermission[];
appVersion?: string | null;
clientCapabilities?: Record<string, unknown> | null;
onMessage: (msg: SessionOutboundMessage) => void;
onMessageToSource?: (source: object, msg: SessionOutboundMessage) => void | Promise<void>;
onBinaryMessage?: (frame: Uint8Array) => void;
onBinaryMessageToSource?: (source: object, frame: Uint8Array) => Promise<void>;
getTransportBufferedAmount?: (source?: object) => number | null;
getConnectedHostBufferedAmount?: () => number | null;
onLifecycleIntent?: (intent: SessionLifecycleIntent) => void;
onWorkspaceRecovered?: (workspace: PersistedWorkspaceRecord) => Promise<void>;
publishScriptStatusUpdate?: (message: ScriptStatusUpdateMessage) => void;
logger: pino.Logger;
downloadTokenStore: DownloadTokenStore;
pushNotifications: PushNotifications;
paseoHome: string;
worktreesRoot?: string;
agentManager: AgentManager;
agentStorage: AgentStorage;
messageReceipts: Pick<MessageReceipts, "send">;
agentMessages: AgentMessageDelivery;
creationService: Pick<CreationService, "create" | "subscribe">;
projectRegistry: ProjectRegistry;
workspaceRegistry: WorkspaceRegistry;
directorySync?: DirectorySyncService;
workspaceLabelService?: WorkspaceLabelService;
filesystem?: SessionFileSystem;
scheduleService: ScheduleService;
checkoutDiffManager: CheckoutDiffManager;
github?: ForgeService;
createAgentMcpTransport?: AgentMcpTransportFactory;
// Injected so tests can substitute the git branch rename without module mocks;
// defaults to the real checkout-git implementation.
renameCurrentBranch?: typeof renameCurrentBranchDefault;
workspaceGitService: WorkspaceGitService;
workspaceRuntimeStore: WorkspaceRuntimeStore;
workspaceAutoName: WorkspaceAutoName;
daemonConfigStore: DaemonConfigStore;
pluginRuntime?: {
wait: import("./plugins/lifecycle/index.js").PluginLifecycle["wait"];
before: import("./plugins/lifecycle/index.js").PluginLifecycle["before"];
emit: import("./plugins/lifecycle/index.js").PluginLifecycle["emit"];
listPlugins(): Promise<import("@getpaseo/protocol/messages").PluginListItem[]>;
getLogs(pluginId: string): import("@getpaseo/protocol/messages").PluginLogEntry[];
installDirectory(input: {
path: string;
id?: string;
}): Promise<import("@getpaseo/protocol/messages").PluginListItem>;
inspectDirectory(path: string): Promise<{ id: string }>;
installSource(input: {
source: string;
id?: string;
ref?: string;
}): Promise<import("@getpaseo/protocol/messages").PluginListItem>;
statusSources(
pluginId?: string,
): Promise<import("@getpaseo/protocol/messages").PluginSourceStatusItem[]>;
previewUpdates(input: {
pluginId?: string;
target?: import("@getpaseo/protocol/messages").PluginUpdateSelection;
}): Promise<import("@getpaseo/protocol/messages").PluginUpdatePreview[]>;
applyUpdates(
proposals: import("@getpaseo/protocol/messages").PluginUpdateProposal[],
): Promise<import("@getpaseo/protocol/messages").PluginUpdateResult[]>;
updateSources(
pluginId?: string,
): Promise<import("@getpaseo/protocol/messages").PluginSourceUpdateItem[]>;
reloadPlugin(pluginId: string): Promise<import("@getpaseo/protocol/messages").PluginListItem>;
enablePlugin(pluginId: string): Promise<import("@getpaseo/protocol/messages").PluginListItem>;
disablePlugin(pluginId: string): Promise<import("@getpaseo/protocol/messages").PluginListItem>;
removePlugin(pluginId: string): Promise<void>;
subscribe(listener: (pluginId: string) => void): () => void;
subscribeSettings?(listener: (pluginId: string, settingsId: string) => void): () => void;
catalog(): Array<{ id: string; clientBundle: string; clientBundleHash?: string }>;
invokePluginRpc(pluginId: string, method: string, input: unknown): Promise<unknown>;
subscribePluginRpc(
pluginId: string,
method: string,
input: unknown,
): import("./plugins/runtime-subscription.js").PluginRuntimeSubscription;
listUsageReports(options?: {
forceRefresh?: boolean;
reportIds?: string[];
}): Promise<UsageReportEntry[]>;
listLegacyUsage(): Promise<{ fetchedAt: string; providers: ProviderUsage[] }>;
};
orchestrationSkills?: import("./orchestration-skills/index.js").OrchestrationSkills;
mcpBaseUrl?: string | null;
stt: Resolvable<SpeechToTextProvider | null>;
sttLanguage?: string;
tts: Resolvable<TextToSpeechProvider | null>;
speechModels?: SpeechModelsSessionOptions["runtime"];
terminalManager: TerminalManager | null;
providerSnapshotManager: ProviderSnapshotManager;
hubExecutionAgents?: HubExecutionAgents;
hubRelationships?: HubRelationshipManagement;
serviceProxy?: ServiceProxySubsystem;
scriptRuntimeStore?: WorkspaceScriptRuntimeStore;
workspaceSetupObservation?: WorkspaceSetupObservation;
workspaceSetupRuntime?: WorkspaceSetupRuntime;
onBranchChanged?: (
workspaceId: string,
oldBranch: string | null,
newBranch: string | null,
) => void;
getDaemonTcpPort?: () => number | null;
getDaemonTcpHost?: () => string | null;
serviceProxyPublicBaseUrl?: string | null;
resolveScriptHealth?: (hostname: string) => ScriptHealthState | null;
voice?: {
turnDetection?: Resolvable<TurnDetectionProvider | null>;
};
voiceBridge?: {
registerVoiceSpeakHandler?: (agentId: string, handler: VoiceSpeakHandler) => void;
unregisterVoiceSpeakHandler?: (agentId: string) => void;
registerVoiceCallerContext?: (agentId: string, context: VoiceCallerContext) => void;
unregisterVoiceCallerContext?: (agentId: string) => void;
};
dictation?: {
finalTimeoutMs?: number;
stt?: Resolvable<SpeechToTextProvider | null>;
sttLanguage?: string;
getSpeechReadiness?: () => SpeechReadinessSnapshot;
};
serverId?: string;
daemonVersion?: string;
daemonRuntimeConfig?: DaemonRuntimeConfig;
getWebSocketRuntimeMetrics?: () => DaemonWebSocketRuntimeDiagnosticSnapshot | null;
}
export type SessionLifecycleIntent =
| {
type: "shutdown";
clientId: string;
requestId: string;
reason: string;
keepTerminals: boolean;
}
| {
type: "restart";
clientId: string;
requestId: string;
reason: string;
keepTerminals: boolean;
};
function parseClientCapabilities(
capabilities: Record<string, unknown> | null | undefined,
): ReadonlySet<ClientCapability> {
if (!capabilities) {
return new Set();
}
const known = new Set<ClientCapability>(Object.values(CLIENT_CAPS));
const result: ClientCapability[] = [];
for (const [key, value] of Object.entries(capabilities)) {
if (value === true && known.has(key as ClientCapability)) {
result.push(key as ClientCapability);
}
}
return new Set(result);
}
function sessionRequestId(message: SessionInboundMessage): string | null {
if ("requestId" in message && typeof message.requestId === "string") {
return message.requestId;
}
if (
"payload" in message &&
typeof message.payload === "object" &&
message.payload !== null &&
"requestId" in message.payload &&
typeof message.payload.requestId === "string"
) {
return message.payload.requestId;
}
return null;
}
type RegistryTransition = "created" | "unarchived" | "existing";
interface ArchivedRecordSnapshot {
archivedAt?: string | null;
}
interface WorkspaceUpdateOptions {
removedProjectId?: string;
optimisticStatus?: WorkspaceDescriptorPayload["status"];
}
function resolveDirectorySync(service: DirectorySyncService | undefined): DirectorySyncService {
return service ?? new DirectorySyncService();
}
function describeRegistryTransition(record: ArchivedRecordSnapshot | null): RegistryTransition {
if (!record) {
return "created";
}
return record.archivedAt ? "unarchived" : "existing";
}
/**
* Session represents a single connected client session.
* It owns all state management, orchestration logic, and message processing.
* Session has no knowledge of WebSockets - it only emits and receives messages.
*/
function resolveWorkspaceLabelService(
service: WorkspaceLabelService | undefined,
): WorkspaceLabelService | null {
return service ?? null;
}
function hasInitialCreationPrompt(
request?: Pick<AgentCreateRequest, "initialPrompt" | "images" | "attachments">,
): boolean {
return Boolean(
request &&
buildAgentPrompt(request.initialPrompt?.trim() ?? "", request.images, request.attachments)
.length,
);
}
function workspaceLabelErrorCode(error: unknown): string {
if (
error instanceof WorkspaceLabelError ||
error instanceof WorkspaceLabelStorageUncertainError ||
error instanceof SessionRequestError
) {
return error.code;
}
return "workspace_label_failed";
}
interface ClientActivity {
deviceType: "web" | "mobile";
focusedAgentId: string | null;
focusedTerminalId: string | null;
lastActivityAt: Date;
appVisible: boolean;
appVisibilityChangedAt: Date;
}
export class Session {
private readonly connectedHostSession: ConnectedHostSession | null;
private routed = false;
isRouted(): boolean {
return this.routed;
}
getHostSourceId(): string {
return this.hostSourceId;
}
private hostSourceId: string;
setHostSource(sourceId: string): void {
this.hostSourceId = sourceId;
this.connectedHostSession?.setSource(sourceId);
}
setRouted(routed: boolean): void {
this.routed = routed;
}
readonly delivery = new SessionDelivery(
(source, message) => {
if (!this.authorization.allowsOutbound(message)) return;
if (this.onMessageToSource) return this.onMessageToSource(source, message);
this.onMessage(message);
},
(source, frame) => {
if (this.onBinaryMessageToSource) {
return this.onBinaryMessageToSource(source, frame);
} else this.emitBinary(frame);
},
(source, message) => this.workspaceSetupMessageForClient(message, source),
(request, message) =>
this.sessionLogger.warn(
{
requestType: request.type,
messageType: message.type,
status: message.type === "status" ? message.payload.status : undefined,
},
"Unrecognized reply contract; application delivery denied",
),
);
private readonly browserToolsBroker: SessionOptions["browserToolsBroker"];
private readonly clientId: string;
private readonly authorization: SessionAuthorization;
private appVersion: string | null;
private clientCapabilities: ReadonlySet<ClientCapability>;
private readonly sessionId: string;
private readonly onMessage: (msg: SessionOutboundMessage) => void;
private readonly onMessageToSource:
| ((source: object, msg: SessionOutboundMessage) => void | Promise<void>)
| null;
private readonly onBinaryMessage: ((frame: Uint8Array) => void) | null;
private readonly onBinaryMessageToSource:
| ((source: object, frame: Uint8Array) => Promise<void>)
| null;
private readonly getTransportBufferedAmount: (source?: object) => number | null;
private readonly onLifecycleIntent: ((intent: SessionLifecycleIntent) => void) | null;
private readonly onWorkspaceRecovered:
| ((workspace: PersistedWorkspaceRecord) => Promise<void>)
| null;
private readonly sessionLogger: pino.Logger;
private readonly paseoHome: string;
private readonly projectIcons: ProjectIconReader;
private readonly worktreesRoot: string | undefined;
private readonly rewindInitiators = new Map<string, object | undefined>();
private agentManager: AgentManager;
private readonly agentStorage: AgentStorage;
private readonly projectRegistry: ProjectRegistry;
private readonly workspaceRegistry: WorkspaceRegistry;
private readonly agentDirectory: AgentDirectory;
private readonly directorySync: DirectorySyncService;
private readonly filesystem: SessionFileSystem;
private readonly github: ForgeService;
private readonly renameCurrentBranch: typeof renameCurrentBranchDefault;
private readonly workspaceGitService: WorkspaceGitService;
private readonly workspaceRuntimeStore: WorkspaceRuntimeStore;
private readonly workspaceAutoName: WorkspaceAutoName;
private readonly gitMutation: GitMutationService;
private readonly workspaceProvisioning: WorkspaceProvisioningService;
private readonly workspaceRecovery: WorkspaceRecoveryService;
private readonly daemonConfigStore: DaemonConfigStore;
private readonly pushNotifications: PushNotifications;
private readonly pluginRuntime: SessionOptions["pluginRuntime"];
private readonly orchestrationSkills: SessionOptions["orchestrationSkills"];
private unsubscribeAgentEvents: (() => void) | null = null;
private unsubscribeProjectMutations: (() => void) | null = null;
private unsubscribePluginChanges: (() => void) | null = null;
private unsubscribeWorkspaceMutations: (() => void) | null = null;
private registryMutationQueue: Promise<void> = Promise.resolve();
private projectUpdateQueue: Promise<void> = Promise.resolve();
private isCleanedUp = false;
private readonly timelineSubscriptions = new Map<
string,
{ owner: OwnedSubscription; agentIds: Set<string> }
>();
private readonly clientSources = new Map<
object,
{
capabilities: ReadonlySet<ClientCapability>;
appVersion: string | null;
activity: ClientActivity | null;
pushToken: string | null;
}
>();
private unsubscribeTerminalWorkspaceContributionEvents: (() => void) | null = null;
private readonly agentUpdates: AgentUpdatesService;
private readonly workspaceUpdatesSubscriptions = new Map<
string,
WorkspaceUpdatesSubscriptionState
>();
private readonly workspaceLabelService: WorkspaceLabelService | null;
private readonly eventSubscriptions = new Map<
string,
{ owner: OwnedSubscription; events: Set<SessionEventSubscription>; notifications: boolean }
>();
private readonly workspaceUpdateTails = new Map<string, Promise<void>>();
private readonly terminalManager: TerminalManager | null;
private readonly providerSnapshotManager: ProviderSnapshotManager;
private readonly serviceProxy: ServiceProxySubsystem | null;
private readonly scriptRuntimeStore: WorkspaceScriptRuntimeStore | null;
private readonly getDaemonTcpPort: (() => number | null) | null;
private readonly getDaemonTcpHost: (() => string | null) | null;
private readonly serviceProxyPublicBaseUrl: string | null;
private readonly resolveScriptHealth: ((hostname: string) => ScriptHealthState | null) | null;
private readonly terminalController: TerminalSessionController;
private inflightRequests = 0;
private peakInflightRequests = 0;
private readonly workspaceSetupObservation: WorkspaceSetupObservation;
private readonly unsubscribeSetupObservation: () => void;
private readonly workspaceSetupRuntime: WorkspaceSetupRuntime;
private readonly workspaceGitObserver: WorkspaceGitObserverService;
private readonly workspaceDirectory: WorkspaceDirectory;
private readonly voiceSessions: VoiceSessions;
private readonly checkoutSession: CheckoutSession;
private readonly scheduleSession: ScheduleSession;
private readonly providerCatalogSession: ProviderCatalogSession;
private readonly usageSession: UsageSession;
private readonly speechModelsSession: SpeechModelsSession;
private readonly workspaceFilesSession: WorkspaceFilesSession;
private readonly agentConfigSession: AgentConfigSession;
private readonly projectConfigSession: ProjectConfigSession;
private readonly daemonSession: DaemonSession;
private readonly hubExecutionController: HubExecutionController | null;
private readonly workspaceScripts: WorkspaceScriptsService;
private readonly messageReceipts: Pick<MessageReceipts, "send">;
private readonly agentMessages: AgentMessageDelivery;
private readonly createAgentLifecycleDispatch: CreateAgentLifecycleDispatch;
private readonly creationService: Pick<CreationService, "create" | "subscribe">;
constructor(options: SessionOptions) {
const {
clientId,
permissions,
appVersion,
clientCapabilities,
onMessage,
onMessageToSource,
onBinaryMessage,
onBinaryMessageToSource,
getTransportBufferedAmount,
onLifecycleIntent,
onWorkspaceRecovered,
publishScriptStatusUpdate,
logger,
downloadTokenStore,
pushNotifications,
paseoHome,
worktreesRoot,
agentManager,
agentStorage,
projectRegistry,
workspaceRegistry,
directorySync,
workspaceLabelService,
filesystem,
scheduleService,
checkoutDiffManager,
github,
renameCurrentBranch,
workspaceGitService,
workspaceRuntimeStore,
workspaceAutoName,
daemonConfigStore,
pluginRuntime,
orchestrationSkills,
stt,
sttLanguage,
tts,
speechModels,
terminalManager,
providerSnapshotManager,
serviceProxy,
scriptRuntimeStore,
workspaceSetupObservation,
workspaceSetupRuntime,
onBranchChanged,
getDaemonTcpPort,
getDaemonTcpHost,
serviceProxyPublicBaseUrl,
resolveScriptHealth,
voice,
voiceBridge,
dictation,
serverId,
daemonVersion,
daemonRuntimeConfig,
getWebSocketRuntimeMetrics,
} = options;
this.browserToolsBroker = options.browserToolsBroker;
this.clientId = clientId;
this.authorization = new SessionAuthorization(permissions);
this.appVersion = appVersion ?? null;
this.clientCapabilities = parseClientCapabilities(clientCapabilities);
this.sessionId = uuidv4();
this.hostSourceId = hostSource(options);
this.connectedHostSession = createHostSession(options, this);
this.onMessage = onMessage;
this.onMessageToSource = onMessageToSource
? (source, message) => this.observeSourceSend(() => onMessageToSource(source, message))
: null;
this.onBinaryMessage = onBinaryMessage ?? null;
this.onBinaryMessageToSource = onBinaryMessageToSource
? (source, frame) => this.observeSourceSend(() => onBinaryMessageToSource(source, frame))
: null;
this.getTransportBufferedAmount = getTransportBufferedAmount ?? (() => 0);
this.onLifecycleIntent = onLifecycleIntent ?? null;
this.onWorkspaceRecovered = onWorkspaceRecovered ?? null;
this.pushNotifications = pushNotifications;
this.paseoHome = paseoHome;
this.messageReceipts = options.messageReceipts;
this.agentMessages = options.agentMessages;
this.creationService = options.creationService;
this.projectIcons = new ProjectIconReader(paseoHome);
this.worktreesRoot = worktreesRoot;
this.pluginRuntime = pluginRuntime;
this.orchestrationSkills = orchestrationSkills;
this.sessionLogger = logger.child({
module: "session",
clientId: this.clientId,
sessionId: this.sessionId,
});
this.workspaceFilesSession = new WorkspaceFilesSession({
host: {
emit: (msg, source) => this.emitForSource(msg, source),
emitBinary: (frame, source) => this.emitBinaryReply(frame, source),
hasBinaryChannel: () => this.onBinaryMessage !== null,
},
downloadTokenStore,
paseoHome,
logger: this.sessionLogger,
});
this.agentManager = agentManager;
this.agentStorage = agentStorage;
this.projectRegistry = projectRegistry;
this.workspaceRegistry = workspaceRegistry;
this.directorySync = resolveDirectorySync(directorySync);
this.workspaceLabelService = resolveWorkspaceLabelService(workspaceLabelService);
this.filesystem = filesystem ?? nodeSessionFileSystem;
this.github = github ?? createGitHubService();
this.renameCurrentBranch = renameCurrentBranch ?? renameCurrentBranchDefault;
this.workspaceGitService = workspaceGitService;
this.workspaceRuntimeStore = workspaceRuntimeStore;
this.gitMutation = createGitMutationService({
workspaceGitService: this.workspaceGitService,
logger: this.sessionLogger,
});
this.workspaceAutoName = workspaceAutoName;
this.workspaceProvisioning = createWorkspaceProvisioningService({
lifecycle: this.pluginRuntime,
serverId,
workspaceRegistry: this.workspaceRegistry,
projectRegistry: this.projectRegistry,
workspaceGitService: this.workspaceGitService,
isDirectory: (path) => this.filesystem.isDirectory(path),
logger: this.sessionLogger,
});
this.workspaceRecovery = createWorkspaceRecoveryService({
paseoHome: this.paseoHome,
worktreesRoot: this.worktreesRoot,
getWorkspace: (workspaceId) => this.workspaceRegistry.get(workspaceId),
getProject: (projectId) => this.projectRegistry.get(projectId),
isDirectory: (path) => this.filesystem.isDirectory(path),
unarchiveWorkspace: async (workspace) => {
await this.workspaceProvisioning.ensureWorkspaceRecordUnarchived(workspace);
},
});
this.checkoutSession = new CheckoutSession({
host: {
emit: (msg) => this.emit(msg),
emitWorkspaceUpdateForCwd: (cwd) => this.emitWorkspaceUpdateForCwd(cwd),
handleWorkspaceGitBranchSnapshot: (cwd, branchName) =>
this.workspaceGitObserver.handleBranchSnapshot(cwd, branchName),
renameCurrentBranch: (cwd, branch) => this.renameCurrentBranch(cwd, branch),
},
gitMutation: this.gitMutation,
workspaceGitService: this.workspaceGitService,
github: this.github,
checkoutDiffManager,
gitMetadataGenerator: createGitMetadataGenerator({
workspaceGitService: this.workspaceGitService,
generation: createAgentStructuredTextGeneration({
agentManager: this.agentManager,
providerSnapshotManager,
readDaemonConfig: () => this.readStructuredGenerationDaemonConfig(),
getFocusedSelection: (cwd) => this.getFocusedAgentSelectionForCwd(cwd),
logger: this.sessionLogger,
}),
logger: this.sessionLogger,
}),
paseoHome: this.paseoHome,
worktreesRoot: this.worktreesRoot,
logger: this.sessionLogger,
});
this.workspaceGitObserver = createWorkspaceGitObserverService({
workspaceGitService: this.workspaceGitService,
emitWorkspaceUpdateForCwd: (cwd) => this.emitWorkspaceUpdateForCwd(cwd),
emitStatusUpdate: (cwd, snapshot) => this.checkoutSession.emitStatusUpdate(cwd, snapshot),
onBranchChanged,
logger: this.sessionLogger,
});
this.scheduleSession = new ScheduleSession({
host: { emit: (msg) => this.emit(msg) },
scheduleService,
logger: this.sessionLogger,
});
this.providerCatalogSession = new ProviderCatalogSession({
host: {
emit: (msg) => this.emit(msg),
isProviderVisibleToClient: (provider) => this.isProviderVisibleToClient(provider),
supportsCustomModeIcons: () => this.supports(CLIENT_CAPS.customModeIcons),
supportsCompactProviderSnapshots: () => this.supports(CLIENT_CAPS.compactProviderSnapshots),
publishSnapshot: (project) => {
const delivered = new Set<object>();
for (const subscription of this.eventSubscriptions.values()) {
if (!subscription.events.has("providers_snapshot_update")) continue;
const message = this.delivery.forSource(subscription.owner.source, project);
if (message) subscription.owner.emit(message);
delivered.add(subscription.owner.source);
}
// COMPAT(ownedSubscriptions): added in v0.8.0, remove implicit provider delivery after 2027-03-09.
for (const source of this.clientSources.keys()) {
if (
delivered.has(source) ||
this.delivery.isModern(source) ||
!this.wantsEvent("providers_snapshot_update", source)
)
continue;
const message = this.delivery.forSource(source, project);
if (message) this.onMessageToSource?.(source, message);
}
},
supportsProviderSnapshotReferences: () =>
this.supports(CLIENT_CAPS.providerSnapshotReferences),
listProviderAvailability: () => this.agentManager.listProviderAvailability(),
listDraftControls: (config) => this.agentManager.listDraftControls(config),
},
providerSnapshotManager,
logger: this.sessionLogger,
});
this.usageSession = new UsageSession({
isEnabled: () => daemonConfigStore.get().usage?.enabled !== false,
emit: (msg) => this.emit(msg),
runtime: pluginRuntime,
logger: this.sessionLogger,
});
this.speechModelsSession = new SpeechModelsSession({
emit: (msg) => this.emit(msg),
runtime: speechModels,
});
this.agentConfigSession = new AgentConfigSession({
host: {
emit: (msg) => this.emit(msg),
},
operations: {
ensureLoaded: async (agentId) => {
await ensureUnarchivedAgentLoaded(agentId, {
agentManager,
agentStorage,
logger: this.sessionLogger,
});
},
setMode: async (agentId, modeId) =>
(await setAgentModeCommand({ agentManager }, { agentId, modeId })).notice,
setModel: (agentId, modelId) => agentManager.setAgentModel(agentId, modelId),
setOptions: (agentId, patch) => agentManager.setAgentOptions(agentId, patch),
setThinking: (agentId, thinkingOptionId) =>
agentManager.setAgentThinkingOption(agentId, thinkingOptionId),
},
logger: this.sessionLogger,
});
this.projectConfigSession = new ProjectConfigSession({
host: {
emit: (msg) => this.emit(msg),
},
projectRegistry: this.projectRegistry,
logger: this.sessionLogger,
});
this.daemonSession = new DaemonSession({
host: {
emit: (msg) => this.emit(msg),
emitLifecycleIntent: (intent) => this.emitLifecycleIntent(intent),
},
clientId: this.clientId,
paseoHome: this.paseoHome,
serverId,
daemonVersion,
daemonRuntimeConfig,
getWebSocketRuntimeMetrics,
getObservationMetrics: () => ({
Registrations: this.delivery.registrationCount,
"Source registrations": this.delivery.sourceRegistrationCount(),
"Producer listeners": [
this.unsubscribeAgentEvents,
this.unsubscribeProjectMutations,
this.unsubscribeWorkspaceMutations,
this.unsubscribePluginChanges,
this.unsubscribeTerminalWorkspaceContributionEvents,
this.providerCatalogSession.isObserving,
this.hubExecutionController?.isObserving,
].filter(Boolean).length,
"Git observations": this.workspaceGitObserver.getMetrics().subscriptionCount,
"Terminal directories": this.terminalController.getMetrics().directorySubscriptionCount,
"Terminal output": this.terminalController.getMetrics().streamSubscriptionCount,
}),
listProviderAvailability: () => this.agentManager.listProviderAvailability(),
getTerminalsUnavailable: () => terminalManager?.getUnavailableError?.()?.message ?? null,
listAgents: () => this.agentManager.listAgents(),
listProjects: () => this.projectRegistry.list(),
// Diagnostics report both the active count and total archived history.
listWorkspaces: () => this.workspaceRegistry.listAll(),
logger: this.sessionLogger,
hubRelationships: options.hubRelationships,
reloadConfig: () => daemonConfigStore.reload(),
});
this.hubExecutionController = options.hubExecutionAgents
? new HubExecutionController({
agents: options.hubExecutionAgents,
validateAgentConfiguration: (input) =>
providerSnapshotManager.validateAgentConfiguration(input),
send: (message) => this.emit(message),
})
: null;
this.daemonConfigStore = daemonConfigStore;
this.terminalManager = terminalManager;
this.terminalController = new TerminalSessionController({
terminalManager,
emit: (msg) => this.emit(msg),
hasBinaryChannel: () => this.onBinaryMessage !== null,
supportsChunkedReplay: (source) =>
this.supportsForSource(CLIENT_CAPS.terminalChunkedReplay, source),
isPathWithinRoot: (rootPath, candidatePath) => this.isPathWithinRoot(rootPath, candidatePath),
sessionLogger: this.sessionLogger,
listTerminalWorkspaceRefs: () => this.listActiveWorkspaceRefs(),
getClientBufferedAmount: (source) => this.getTransportBufferedAmount(source),
});
this.agentDirectory = new AgentDirectory({
manager: this.agentManager,
storage: this.agentStorage,
workspaces: this.workspaceRegistry,
projectLive: (agent) => this.buildAgentPayload(agent),
projectStored: (record) => this.buildStoredAgentPayload(record),
isProviderVisible: (provider) => this.isProviderVisibleToClient(provider),
isStoredProviderAvailable: (record) =>
isStoredAgentProviderAvailable(
record,
new Set(this.providerSnapshotManager.listRegisteredProviderIds()),
),
});
this.agentUpdates = createAgentUpdatesService({
includesWorkspace: (agent, backgroundVisibility) =>
this.agentDirectory.includes(agent, backgroundVisibility),
emit: (message) => this.emit(message),
enrichAgentPayload: (payload) => this.enrichAgentPayload(payload),
buildStoredAgentPayload: (record) => this.buildStoredAgentPayload(record),
isProviderVisibleToClient: (provider) => this.isProviderVisibleToClient(provider),
buildProjectPlacementForWorkspaceId: (workspaceId) =>
this.buildProjectPlacementForWorkspaceId(workspaceId),
emitWorkspaceUpdateForWorkspaceId: (workspaceId) =>
this.emitWorkspaceUpdateForWorkspaceId(workspaceId),
sequenceAgentUpdate: (payload, agent, project, agentId, includeSequence, includeBackground) =>
this.directorySync.sequenceAgentUpdate(
payload,
agent && project ? { agent, project } : null,
agentId,
includeSequence,
includeBackground,
),
logger: this.sessionLogger,
});
this.createAgentLifecycleDispatch = new CreateAgentLifecycleDispatch({
paseoHome: this.paseoHome,
worktreesRoot: this.worktreesRoot,
agentManager: this.agentManager,
agentStorage: this.agentStorage,
github: this.github,
workspaceGitService: this.workspaceGitService,
createPaseoWorktreeWorkflow: (input, workflowOptions) =>
this.createPaseoWorktreeWorkflow(input, workflowOptions),
archiveAgentForClose: (agentId) => this.archiveAgentForClose(agentId),
findWorkspaceIdForCwd: (cwd) => this.findWorkspaceIdForCwd(cwd),
listActiveWorkspaces: () => this.listActiveWorkspaceRefs(),
archiveWorkspaceRecord: (workspaceId) => this.archiveWorkspaceRecord(workspaceId),
emit: (message) => this.emit(message),
emitAgentRemove: (agentId) => this.agentUpdates.removeAgent(agentId),
emitWorkspaceUpdatesForWorkspaceIds: (workspaceIds) =>
this.emitWorkspaceUpdatesForWorkspaceIds(workspaceIds),
markWorkspaceArchiving: (workspaceIds, archivingAt) =>
this.markWorkspaceArchiving(workspaceIds, archivingAt),
clearWorkspaceArchiving: (workspaceIds) => this.clearWorkspaceArchiving(workspaceIds),
killTerminalsForWorkspace: (workspaceId) =>
this.terminalController.killTerminalsForWorkspace(workspaceId),
logger: this.sessionLogger,
});
this.providerSnapshotManager = providerSnapshotManager;
this.serviceProxy = serviceProxy ?? null;
this.scriptRuntimeStore = scriptRuntimeStore ?? null;
this.workspaceSetupObservation = workspaceSetupObservation ?? new WorkspaceSetupObservation();
this.unsubscribeSetupObservation = this.workspaceSetupObservation.subscribe(
(workspaceId, snapshot, statusChanged) => {
if (statusChanged)
void this.emitWorkspaceUpdateForWorkspaceId(workspaceId).catch((error) =>
this.sessionLogger.warn({ err: error, workspaceId }, "Failed to publish setup status"),
);
this.emitSubscribedEvent({
type: "workspace_setup_progress",
payload: { workspaceId, ...snapshot },
});
},
);
this.workspaceSetupRuntime = resolveWorkspaceSetupRuntime(workspaceSetupRuntime);
this.getDaemonTcpPort = getDaemonTcpPort ?? null;
this.getDaemonTcpHost = getDaemonTcpHost ?? null;
this.serviceProxyPublicBaseUrl = serviceProxyPublicBaseUrl ?? null;
this.resolveScriptHealth = resolveScriptHealth ?? null;
this.workspaceScripts = createWorkspaceScriptsService({
serviceProxy: this.serviceProxy,
scriptRuntimeStore: this.scriptRuntimeStore,
terminalManager: this.terminalManager,
workspaceRegistry: this.workspaceRegistry,
projectRegistry: this.projectRegistry,
workspaceRuntimeStore: this.workspaceRuntimeStore,
getDaemonTcpPort: this.getDaemonTcpPort,
getDaemonTcpHost: this.getDaemonTcpHost,
serviceProxyPublicBaseUrl: this.serviceProxyPublicBaseUrl,
resolveScriptHealth: this.resolveScriptHealth,
logger: this.sessionLogger,
emit: (message) => this.emit(message),
publishStatusUpdate: (message) => {
if (publishScriptStatusUpdate) publishScriptStatusUpdate(message);
else this.emit(message);
},
spawnWorkspaceScript,
assertAutomationAllowed: (workspaceId) =>
assertWorkspaceAutomationAllowedForWorkspace(this.workspaceRegistry, workspaceId),
getGlobalServicePorts: () => this.daemonConfigStore.readServicePorts(),
});
this.workspaceDirectory = new WorkspaceDirectory({
logger: this.sessionLogger,
projectRegistry: this.projectRegistry,
workspaceRegistry: this.workspaceRegistry,
listAgentPayloads: (workspaceIds) =>
this.listAgentPayloads({ workspaceIds, includeBackground: true, includeAncestors: true }),
workspaceLastUserMessageTimes: (workspaceIds) =>
this.agentDirectory.workspaceLastUserMessageTimes(workspaceIds),
listProviderSubagentActivity: async () => this.agentManager.listProviderSubagentActivity(),
listTerminalActivityContributions: () => this.listTerminalActivityContributions(),
isProviderVisibleToClient: (provider) => this.isProviderVisibleToClient(provider),
buildWorkspaceDescriptor: (input) => this.buildWorkspaceDescriptor(input),
});
this.voiceSessions = new VoiceSessions(
{
host: {
emit: (msg) => this.emit(msg),
loadAgent: (agentId) =>
ensureAgentLoaded(agentId, {
agentManager: this.agentManager,
agentStorage: this.agentStorage,
logger: this.sessionLogger,
}),
reloadAgentSession: (agentId, overrides) =>
this.agentManager.reloadAgentSession(agentId, overrides),
sendSpokenInput: async (agentId, text) => {
await this.handleSendAgentMessage(
agentId,
text,
undefined,
undefined,
undefined,
undefined,
{ spokenInput: true },
);
},
interruptAgentIfRunning: (agentId) => this.interruptAgentIfRunning(agentId),
hasActiveAgentRun: (agentId) => this.hasActiveAgentRun(agentId),
},
logger: this.sessionLogger,
sessionId: this.sessionId,
sttLanguage,
tts,
stt,
voice,
voiceBridge,
dictation,
},
this.delivery,
() => this.refreshObservationProducers(),
);
this.sessionLogger.trace({}, "agent.session.lifecycle.created");
}
updateAppVersion(appVersion: string | null): void {
if (appVersion && appVersion !== this.appVersion) {
this.appVersion = appVersion;
}
}
updateClientCapabilities(
capabilities: Record<string, unknown> | null,
source?: object,
appVersion = this.appVersion,
): void {
this.clientCapabilities = parseClientCapabilities(capabilities);
if (source) {
this.delivery.attach(source, capabilities?.[CLIENT_CAPS.ownedSubscriptions] === true);
this.clientSources.set(source, {
capabilities: this.clientCapabilities,
appVersion,
activity: this.clientSources.get(source)?.activity ?? null,
pushToken: this.clientSources.get(source)?.pushToken ?? null,
});
this.refreshObservationProducers();
// COMPAT(ownedSubscriptions): added in v0.8.0, remove capability-driven legacy host registration after 2027-03-09.
if (
!this.delivery.isModern(source) &&
this.authorization.allowsPermission("workspace.write")
) {
const host = BrowserAutomationHostCapabilitySchema.safeParse(capabilities?.browser_host);
if (host.success && this.browserToolsBroker) {
const request = {
type: "browser.host.register.request" as const,
requestId: uuidv4(),
hostKind: host.data.hostKind,
supportedCommands: host.data.supportedCommands,
};
void this.delivery
.request(source, request, () => this.registerBrowserHost(request, false))
.catch((error) =>
this.sessionLogger.warn({ err: error }, "Failed to register legacy browser host"),
);
}
}
}
}
clearAgentTimelineSubscription(source: object): void {
void this.delivery
.detach(source)
.catch((err) => this.sessionLogger.error({ err }, "Failed to release source subscriptions"));
this.clientSources.delete(source);
this.refreshObservationProducers();
}
private subscribeAgentTimelines(agentIds: string[]): OwnedSubscription {
const owner = this.delivery.begin("timelines", undefined, (id) => {
this.timelineSubscriptions.delete(id);
this.refreshObservationProducers();
});
this.timelineSubscriptions.set(owner.id, { owner, agentIds: new Set(agentIds) });
this.refreshObservationProducers();
return owner;
}
// COMPAT(timelineItemCapabilities): plugin items added in v0.8.0, notifications in v0.7.2.
// Remove after 2027-03-07 once the supported client floor is >= v0.8.0.
private supportsTimelineItem(item: { type: string }, source?: object): boolean {
let capability: ClientCapability;
if (item.type === "notification") capability = CLIENT_CAPS.timelineNotifications;
else if (item.type === "plugin") capability = CLIENT_CAPS.pluginTimelineItems;
else return true;
return source ? this.supportsForSource(capability, source) : this.supports(capability);
}
private forwardAgentStream(
event: Extract<AgentManagerEvent, { type: "agent_stream" }>,
serializedEvent: Extract<SessionOutboundMessage, { type: "agent_stream" }>["payload"]["event"],
): void {
const attention = serializedEvent.type === "attention_required";
if (attention) {
this.emit({
type: "agent_attention_required",
payload: {
agentId: event.agentId,
reason: serializedEvent.reason,
timestamp: serializedEvent.timestamp,
shouldNotify: serializedEvent.shouldNotify,
...(serializedEvent.notification ? { notification: serializedEvent.notification } : {}),
},
});
}
const message: SessionOutboundMessage = {
type: "agent_stream",
payload: this.buildAgentStreamPayload(event, serializedEvent),
};
for (const subscription of this.timelineSubscriptions.values()) {
const source = subscription.owner.source;
if (!subscription.agentIds.has(event.agentId)) continue;
if (
attention &&
(this.delivery.isModern(source) ||
this.supportsForSource(CLIENT_CAPS.selectiveAgentTimeline, source))
)
continue;
if (
serializedEvent.type === "timeline" &&
!this.supportsTimelineItem(serializedEvent.item, source)
)
continue;
subscription.owner.emit(message);
}
// COMPAT(ownedSubscriptions): added in v0.8.0, remove implicit timeline delivery after 2027-03-09.
for (const [source, { capabilities }] of this.clientSources) {
if (this.delivery.isModern(source) || capabilities.has(CLIENT_CAPS.selectiveAgentTimeline))
continue;
if (
serializedEvent.type === "timeline" &&
!this.supportsTimelineItem(serializedEvent.item, source)
)
continue;
const alreadyDelivered = [...this.timelineSubscriptions.values()].some(
(subscription) =>
subscription.owner.source === source && subscription.agentIds.has(event.agentId),
);
if (!alreadyDelivered) this.onMessageToSource?.(source, message);
}
if (
this.clientSources.size === 0 &&
!this.supports(CLIENT_CAPS.selectiveAgentTimeline) &&
this.timelineSubscriptions.size === 0
)
this.emit(message);
}
supports(capability: ClientCapability): boolean {
const source = this.delivery.currentSource;
return source
? this.supportsForSource(capability, source)
: this.clientCapabilities.has(capability);
}
supportsForSource(capability: ClientCapability, source: object): boolean {
return (
this.clientSources.get(source)?.capabilities.has(capability) ??
this.clientCapabilities.has(capability)
);
}
emitProjectUpdate(update: ProjectUpdate): Promise<void> {
if (!this.wantsEvent("project.update")) return Promise.resolve();
const queued = this.projectUpdateQueue
.catch(() => undefined)
.then(() => this.publishProjectUpdate(update));
this.projectUpdateQueue = queued;
return queued;
}
private async publishProjectUpdate(update: ProjectUpdate): Promise<void> {
if (!this.wantsEvent("project.update")) return;
const projectedPayload =
update.kind === "upsert"
? { kind: "upsert" as const, project: await this.buildProjectDescriptor(update.project) }
: update;
if (!this.wantsEvent("project.update")) return;
const message: SessionOutboundMessage = {
type: "project.update",
payload: this.directorySync.sequenceProjectUpdate(projectedPayload, true),
};
this.emit(message);
}
async syncWorkspaceGitObserverForWorkspace(workspace: PersistedWorkspaceRecord): Promise<void> {
if (this.workspaceUpdatesSubscriptions.size === 0 && !this.wantsEvent("checkout_status_update"))
return;
const descriptor = await this.describeWorkspaceRecord(workspace);
const demanded = [...this.workspaceUpdatesSubscriptions.values()].some((subscription) =>
this.matchesWorkspaceFilter({ workspace: descriptor, filter: subscription.filter }),
);
if (demanded || this.wantsEvent("checkout_status_update"))
this.workspaceGitObserver.syncObservers([descriptor]);
}
async emitWorkspaceUpdateForWorkspaceId(workspaceId: string): Promise<void> {
await this.emitWorkspaceUpdatesForWorkspaceIds([workspaceId]);
}
private async emitCreatedWorkspaceUpdate(
workspace: WorkspaceDescriptorPayload,
optimisticStatus?: WorkspaceDescriptorPayload["status"],
): Promise<void> {
if (this.workspaceUpdatesSubscriptions.size > 0) {
await this.emitWorkspaceUpdatesForWorkspaceIds(
[workspace.id],
optimisticStatus ? { optimisticStatus } : undefined,
);
return;
}
// COMPAT(workspaceCreateCausalUpdate): added in v0.1.106, remove after 2027-01-12.
// Older clients create before subscribing and require the causal update beside the response.
if (workspace.background) return;
this.emit({
type: "workspace_update",
payload: {
kind: "upsert",
workspace: optimisticStatus ? { ...workspace, status: optimisticStatus } : workspace,
},
});
}
markWorkspaceArchivingForExternalMutation(
workspaceIds: Iterable<string>,
archivingAt: string,
): void {
this.markWorkspaceArchiving(workspaceIds, archivingAt);
}
clearWorkspaceArchivingForExternalMutation(workspaceIds: Iterable<string>): void {
this.clearWorkspaceArchiving(workspaceIds);
}
async emitWorkspaceUpdatesForExternalWorkspaceIds(workspaceIds: Iterable<string>): Promise<void> {
await this.emitWorkspaceUpdatesForWorkspaceIds(workspaceIds);
}
async syncWorkspaceGitObserversForExternalWorkspaceIds(
workspaceIds: Iterable<string>,
): Promise<void> {
if (!this.delivery.hasDemand("workspaces") && !this.wantsEvent("checkout_status_update"))
return;
await Promise.all(
Array.from(new Set(workspaceIds)).map(async (workspaceId) => {
const workspace = await this.workspaceRegistry.get(workspaceId);
if (workspace && !workspace.archivedAt) {
await this.syncWorkspaceGitObserverForWorkspace(workspace);
}
}),
);
}
async warmWorkspaceGitDataForWorkspace(workspace: PersistedWorkspaceRecord): Promise<void> {
await this.syncWorkspaceGitObserverForWorkspace(workspace);
await this.emitWorkspaceUpdateForWorkspaceId(workspace.workspaceId);
}
async refreshRecoveredWorkspaceForExternalMutation(
workspace: PersistedWorkspaceRecord,
): Promise<void> {
try {
await this.syncWorkspaceGitObserverForWorkspace(workspace);
await this.emitWorkspaceUpdateForWorkspaceId(workspace.workspaceId);
} catch (error) {
this.sessionLogger.warn(
{ err: error, workspaceId: workspace.workspaceId },
"Failed to warm git observer after workspace recovery",
);
try {
await this.emitWorkspaceUpdateForWorkspaceId(workspace.workspaceId);
} catch (emitError) {
this.sessionLogger.warn(
{ err: emitError, workspaceId: workspace.workspaceId },
"Failed to emit workspace update after recovery",
);
}
}
}
/**
* Get the client's current activity state
*/
private currentClientMetadata() {
const source = this.delivery.currentSource;
if (!source) throw new Error("Client control has no source");
let metadata = this.clientSources.get(source);
if (!metadata) {
// COMPAT(ownedSubscriptions): added in v0.8.0, remove socketless legacy session adapter after 2027-03-09.
metadata = {
capabilities: this.clientCapabilities,
appVersion: this.appVersion,
activity: null,
pushToken: null,
};
this.clientSources.set(source, metadata);
}
return metadata;
}
public getClientActivity(source = this.delivery.currentSource): ClientActivity | null {
return source ? (this.clientSources.get(source)?.activity ?? null) : null;
}
private getFocusedAgentSelectionForCwd(cwd: string):
| {
provider?: string | null;
model?: string | null;
thinkingOptionId?: string | null;
}
| undefined {
const focusedAgentId = this.getClientActivity()?.focusedAgentId;
if (!focusedAgentId) {
return undefined;
}
const agent = this.agentManager.getAgent(focusedAgentId);
if (!agent || agent.cwd !== cwd) {
return undefined;
}
return {
provider: agent.provider,
model: agent.runtimeInfo?.model ?? agent.config.model ?? null,
thinkingOptionId:
agent.runtimeInfo?.thinkingOptionId ?? agent.config.thinkingOptionId ?? null,
};
}
private readStructuredGenerationDaemonConfig(): StructuredGenerationDaemonConfig {
return {
metadataGeneration: this.daemonConfigStore.get().metadataGeneration,
};
}
public getRuntimeMetrics(): SessionRuntimeMetrics {
const terminalMetrics = this.terminalController.getMetrics();
const workspaceGitMetrics = this.workspaceGitObserver.getMetrics();
return {
terminalDirectorySubscriptionCount: terminalMetrics.directorySubscriptionCount,
terminalSubscriptionCount: terminalMetrics.streamSubscriptionCount,
workspaceGitWatchedDirectoryCount: workspaceGitMetrics.watchedDirectoryCount,
workspaceGitWorkspaceRecordCount: workspaceGitMetrics.workspaceRecordCount,
workspaceGitSubscriptionCount: workspaceGitMetrics.subscriptionCount,
inflightRequests: this.inflightRequests,
peakInflightRequests: this.peakInflightRequests,
};
}
public emitServerMessage(message: SessionOutboundMessage): void {
this.emit(message);
}
/**
* Send initial state to client after connection
*/
public async sendInitialState(): Promise<void> {
// No unsolicited agent list hydration. Callers must use fetch_agents_request.
}
/**
* Interrupt the agent's active run so the next prompt starts a fresh turn.
* Returns once the manager confirms the stream has been cancelled.
*/
private async interruptAgentIfRunning(agentId: string): Promise<void> {
const snapshot = this.agentManager.getAgent(agentId);
if (!snapshot) {
this.sessionLogger.trace({ agentId }, "agent.session.interrupt.not_found");
throw new Error(`Agent ${agentId} not found`);
}
const hasInFlightRun = this.agentManager.hasInFlightRun(agentId);
if (!hasInFlightRun) {
this.sessionLogger.trace(
{
agentId,
provider: snapshot.provider,
lifecycle: snapshot.lifecycle,
hasInFlightRun,
},
"agent.session.interrupt.skip_not_running",
);
return;
}
this.sessionLogger.debug(
{ agentId, lifecycle: snapshot.lifecycle, hasInFlightRun },
"interruptAgentIfRunning: interrupting",
);
const t0 = Date.now();
const cancellation = await this.agentManager.cancelAgentRun(agentId);
this.sessionLogger.debug(
{ agentId, cancellation: cancellation.status, durationMs: Date.now() - t0 },
"interruptAgentIfRunning: cancelAgentRun completed",
);
if (cancellation.status === "refused") {
this.sessionLogger.warn(
{ agentId },
"interruptAgentIfRunning: reported running but no active run was cancelled",
);
throw new AgentRunCancellationError(agentId, "stop");
}
}
private hasActiveAgentRun(agentId: string | null): boolean {
if (!agentId) {
return false;
}
return this.agentManager.hasInFlightRun(agentId);
}
private handleAgentRunError(agentId: string, error: unknown, context: string): void {
const message = errorToFriendlyMessage(error);
this.sessionLogger.error({ err: error, agentId, context }, `${context} for agent ${agentId}`);
this.emit({
type: "activity_log",
payload: {
id: uuidv4(),
timestamp: new Date(),
type: "error",
content: `${context}: ${message}`,
},
});
}
/**
* Subscribe to AgentManager events and forward them to the client
*/
private refreshObservationProducers(): void {
const legacy = this.delivery.hasLegacySources();
const workspaces =
legacy || this.delivery.hasDemand("workspaces") || this.wantsEvent("checkout_status_update");
const agents =
workspaces ||
this.delivery.hasDemand("agents") ||
this.delivery.hasDemand("timelines") ||
this.voiceSessions.hasDemand ||
this.wantsEvent("agent_attention_required") ||
this.wantsEvent("agent_permission_request") ||
this.wantsEvent("agent_permission_resolved") ||
this.wantsEvent("agent.provider_subagents.update");
if (agents && !this.unsubscribeAgentEvents) this.subscribeToAgentEvents();
if (!agents) {
this.unsubscribeAgentEvents?.();
this.unsubscribeAgentEvents = null;
}
this.refreshWorkspaceProducers(workspaces);
this.refreshCatalogProducers(legacy);
}
private refreshWorkspaceProducers(observing: boolean): void {
if (observing) {
this.unsubscribeProjectMutations ??=
this.projectRegistry.subscribeToMutations?.((mutation) =>
this.enqueueRegistryMutation(() => this.handleProjectMutation(mutation)),
) ?? null;
this.unsubscribeWorkspaceMutations ??=
this.workspaceRegistry.subscribeToMutations?.((mutation) =>
this.enqueueRegistryMutation(() => this.handleWorkspaceMutation(mutation)),
) ?? null;
this.unsubscribeTerminalWorkspaceContributionEvents ??=
this.terminalManager?.subscribeTerminalWorkspaceContributionChanged((event) => {
void this.emitWorkspaceUpdateForTerminalContribution(event).catch((error) =>
this.sessionLogger.warn(
{ err: error, terminalId: event.terminalId },
"Failed to emit workspace terminal contribution",
),
);
}) ?? null;
} else {
this.unsubscribeProjectMutations?.();
this.unsubscribeProjectMutations = null;
this.unsubscribeWorkspaceMutations?.();
this.unsubscribeWorkspaceMutations = null;
this.unsubscribeTerminalWorkspaceContributionEvents?.();
this.unsubscribeTerminalWorkspaceContributionEvents = null;
}
}
private refreshCatalogProducers(legacy: boolean): void {
this.hubExecutionController?.setObserving(
legacy ||
this.wantsEvent("hub.execution.agent.update") ||
this.wantsEvent("hub.execution.agent.stream"),
);
if (this.wantsEvent("providers_snapshot_update")) this.providerCatalogSession.start();
else this.providerCatalogSession.dispose();
if (
legacy ||
this.wantsEvent("status.plugin_catalog_changed") ||
this.wantsEvent("status.plugin_settings_changed")
)
this.unsubscribePluginChanges ??= this.subscribeToPluginChanges(this.pluginRuntime);
else {
this.unsubscribePluginChanges?.();
this.unsubscribePluginChanges = null;
}
}
private enqueueRegistryMutation(handleMutation: () => Promise<void>): Promise<void> {
const next = this.registryMutationQueue.then(handleMutation);
this.registryMutationQueue = next.catch(() => {});
return next;
}
private async handleWorkspaceMutation(mutation: WorkspaceMutation): Promise<void> {
try {
if (this.isCleanedUp) {
return;
}
if (
mutation.kind === "archive" ||
mutation.kind === "remove" ||
mutation.workspace?.archivedAt
) {
this.workspaceGitObserver.removeForWorkspaceId(mutation.workspaceId);
} else {
await this.syncWorkspaceMutationObserver(mutation);
}
if (this.isCleanedUp) {
return;
}
await this.emitWorkspaceUpdatesForWorkspaceIds(
[mutation.workspaceId],
mutation.expectsInitialAgent ? { optimisticStatus: "running" } : undefined,
);
} catch (error) {
this.sessionLogger.warn(
{ err: error, workspaceId: mutation.workspaceId, mutationKind: mutation.kind },
"Failed to apply workspace mutation to session",
);
}
}
private async syncWorkspaceMutationObserver(mutation: WorkspaceMutation): Promise<void> {
if (
!mutation.workspace ||
(this.workspaceUpdatesSubscriptions.size === 0 && !this.wantsEvent("checkout_status_update"))
)
return;
const descriptorsByWorkspaceId = await this.buildWorkspaceDescriptorMap({
workspaceIds: [mutation.workspaceId],
includeGitData: false,
});
const descriptor = descriptorsByWorkspaceId.get(mutation.workspaceId);
if (
!descriptor ||
(!this.wantsEvent("checkout_status_update") &&
![...this.workspaceUpdatesSubscriptions.values()].some((subscription) =>
this.matchesWorkspaceFilter({ workspace: descriptor, filter: subscription.filter }),
))
) {
this.workspaceGitObserver.removeForWorkspaceId(mutation.workspaceId);
return;
}
const currentWorkspace = await this.workspaceRegistry.get(mutation.workspaceId);
if (!currentWorkspace || currentWorkspace.archivedAt) {
this.workspaceGitObserver.removeForWorkspaceId(mutation.workspaceId);
return;
}
await this.syncWorkspaceGitObserverForWorkspace(currentWorkspace);
if (this.isCleanedUp) {
this.workspaceGitObserver.removeForWorkspaceId(mutation.workspaceId);
}
}
private async handleProjectMutation(mutation: ProjectMutation): Promise<void> {
for (const subscription of this.workspaceUpdatesSubscriptions.values()) {
await this.handleProjectMutationForSubscription(mutation, subscription);
}
}
private async handleProjectMutationForSubscription(
mutation: ProjectMutation,
subscription: WorkspaceUpdatesSubscriptionState,
): Promise<void> {
try {
if (this.isCleanedUp) {
return;
}
const projectWorkspaceIds = (await this.workspaceRegistry.listActive())
.filter((workspace) => workspace.projectId === mutation.projectId)
.map((workspace) => workspace.workspaceId);
if (mutation.kind === "remove") {
// Archived workspaces can still represent an empty project in this subscription.
for (const [workspaceId, update] of subscription.lastEmittedByWorkspaceId) {
const projectId =
update.kind === "upsert" ? update.workspace.projectId : update.emptyProject?.projectId;
if (projectId === mutation.projectId && !projectWorkspaceIds.includes(workspaceId)) {
projectWorkspaceIds.push(workspaceId);
}
}
const visibleWorkspaceIds = projectWorkspaceIds.filter((workspaceId) => {
const lastEmitted = subscription.lastEmittedByWorkspaceId.get(workspaceId);
return (
lastEmitted?.kind === "upsert" ||
(lastEmitted?.kind === "remove" &&
lastEmitted.emptyProject?.projectId === mutation.projectId)
);
});
let updateIds = visibleWorkspaceIds;
if (
updateIds.length === 0 &&
subscription.visibleEmptyProjectIds?.has(mutation.projectId)
) {
updateIds = [mutation.projectId];
}
if (updateIds.length === 0) {
return;
}
for (const workspaceId of projectWorkspaceIds) {
this.workspaceGitObserver.removeForWorkspaceId(workspaceId);
}
await this.enqueueWorkspaceUpdates(new Set(updateIds), subscription, {
removedProjectId: mutation.projectId,
});
return;
}
if (mutation.kind === "archive" || mutation.project?.archivedAt) {
for (const workspaceId of projectWorkspaceIds) {
this.workspaceGitObserver.removeForWorkspaceId(workspaceId);
}
}
await this.enqueueWorkspaceUpdates(new Set(projectWorkspaceIds), subscription, undefined);
} catch (error) {
this.sessionLogger.warn(
{ err: error, projectId: mutation.projectId, mutationKind: mutation.kind },
"Failed to apply project mutation to session",
);
}
}
private supportsSubagentTimelineItem(
item: AgentTimelineFetchResult["rows"][number]["item"],
source?: object,
): boolean {
// COMPAT(projectedSubagentTimeline): added after v0.8.0, remove gate after 2027-03-14.
const supportsProjection = source
? this.supportsForSource(CLIENT_CAPS.projectedSubagentTimeline, source)
: this.supports(CLIENT_CAPS.projectedSubagentTimeline);
return supportsProjection && this.supportsTimelineItem(item, source);
}
private forwardProviderSubagentUpdate(
update: Extract<AgentManagerEvent, { type: "provider_subagent" }>["event"],
): void {
let message: SessionOutboundMessage;
if (update.type === "upsert") {
message = {
type: "agent.provider_subagents.update",
payload: { kind: "upsert", subagent: update.subagent },
};
} else if (update.type === "timeline") {
message = {
type: "agent.provider_subagents.update",
payload: {
kind: "timeline",
parentAgentId: update.parentAgentId,
subagentId: update.subagentId,
provider: update.provider,
item: update.row.item,
timestamp: update.row.timestamp,
seq: update.row.seq,
epoch: update.epoch,
},
};
} else {
message = {
type: "agent.provider_subagents.update",
payload: {
kind: "remove",
parentAgentId: update.parentAgentId,
subagentId: update.subagentId,
},
};
}
const delivered = new Set<object>();
for (const subscription of this.eventSubscriptions.values()) {
if (!subscription.events.has("agent.provider_subagents.update")) continue;
if (
update.type === "timeline" &&
!this.supportsSubagentTimelineItem(update.row.item, subscription.owner.source)
)
continue;
subscription.owner.emit(message);
delivered.add(subscription.owner.source);
}
if (this.clientSources.size === 0 || !this.onMessageToSource) {
if (
this.supports(CLIENT_CAPS.providerSubagents) &&
(update.type !== "timeline" || this.supportsSubagentTimelineItem(update.row.item))
) {
this.emit(message);
}
return;
}
for (const [source, { capabilities }] of this.clientSources) {
if (
delivered.has(source) ||
this.delivery.isModern(source) ||
!capabilities.has(CLIENT_CAPS.providerSubagents)
)
continue;
if (update.type === "timeline" && !this.supportsSubagentTimelineItem(update.row.item, source))
continue;
this.onMessageToSource(source, message);
}
}
private subscribeToAgentEvents(): void {
if (this.unsubscribeAgentEvents) {
this.unsubscribeAgentEvents();
}
this.unsubscribeAgentEvents = this.agentManager.subscribe(
(event) => {
if (event.type === "timeline_replacement") {
this.deliverTimelineReplacement(event.agentId, this.rewindInitiators.get(event.agentId));
return;
}
if (event.type === "agent_removed") {
if (event.internal) return;
void this.agentUpdates
.removeAgent(event.agentId)
.then(() => {
if (event.workspaceId)
return this.emitWorkspaceUpdateForWorkspaceId(event.workspaceId);
return undefined;
})
.catch((error) => {
this.sessionLogger.error(
{ err: error, agentId: event.agentId },
"Failed to emit agent removal",
);
});
return;
}
if (event.type === "agent_state") {
this.sessionLogger.trace(
{
agentId: event.agent.id,
provider: event.agent.provider,
providerSessionId: event.agent.persistence?.sessionId ?? undefined,
turnId: event.agent.activeForegroundTurnId ?? undefined,
lifecycle: event.agent.lifecycle,
},
"agent.session.forward_update",
);
void this.agentUpdates.forwardLiveAgent(event.agent);
return;
}
if (event.type === "provider_subagent") {
// The subagents track of an internal parent stays hidden.
const parentAgentId =
event.event.type === "upsert"
? event.event.subagent.parentAgentId
: event.event.parentAgentId;
if (this.agentManager.getAgent(parentAgentId)?.internal) return;
this.emitProviderSubagentWorkspaceUpdate(event.event);
this.forwardProviderSubagentUpdate(event.event);
return;
}
if (
this.voiceSessions.isActiveForAgent(event.agentId) &&
event.event.type === "permission_requested" &&
isVoicePermissionAllowed(event.event.request)
) {
const requestId = event.event.request.id;
void this.agentManager
.respondToPermission(event.agentId, requestId, {
behavior: "allow",
})
.catch((error) => {
this.sessionLogger.warn(
{
err: error,
agentId: event.agentId,
requestId,
},
"Failed to auto-allow speak tool permission in voice mode",
);
});
}
const serializedEvent = serializeAgentStreamEvent(event.event);
if (!serializedEvent) {
return;
}
this.sessionLogger.trace(
{
agentId: event.agentId,
provider: event.event.provider,
turnId: getAgentStreamEventTurnId(event.event),
seq: event.seq,
epoch: event.epoch,
event: event.event,
},
"agent.session.forward_stream",
);
this.forwardAgentStream(event, serializedEvent);
// An internal agent streams only to clients subscribed to it by id;
// session-wide prompts would surface it everywhere.
const internal = this.agentManager.getAgent(event.agentId)?.internal === true;
if (event.event.type === "permission_requested" && !internal) {
this.emit({
type: "agent_permission_request",
payload: {
agentId: event.agentId,
request: event.event.request,
},
});
} else if (event.event.type === "permission_resolved") {
// A provider event is not the outcome of whichever request is currently running.
this.emitSubscribedEvent({
type: "agent_permission_resolved",
payload: {
agentId: event.agentId,
requestId: event.event.requestId,
resolution: event.event.resolution,
},
});
}
// Title updates may be applied asynchronously after agent creation.
},
// Internal agents flow through so exact-id subscribers and opted-in
// listings see them; everything else above filters them out.
{ replayState: false, includeInternal: true },
);
}
private emitProviderSubagentWorkspaceUpdate(event: ProviderSubagentManagerEvent): void {
if (event.type === "timeline") {
return;
}
const parentAgentId =
event.type === "upsert" ? event.subagent.parentAgentId : event.parentAgentId;
const parent = this.agentManager.getAgent(parentAgentId);
if (!parent?.workspaceId) {
return;
}
void this.emitWorkspaceUpdateForWorkspaceId(parent.workspaceId).catch((error) => {
this.sessionLogger.error(
{ err: error, parentAgentId, workspaceId: parent.workspaceId },
"Failed to emit provider subagent workspace update",
);
});
}
private buildAgentStreamPayload(
event: Extract<AgentManagerEvent, { type: "agent_stream" }>,
serializedEvent: Extract<SessionOutboundMessage, { type: "agent_stream" }>["payload"]["event"],
): Extract<SessionOutboundMessage, { type: "agent_stream" }>["payload"] {
return {
agentId: event.agentId,
event: serializedEvent,
timestamp: event.timestamp ?? new Date().toISOString(),
...(typeof event.seq === "number" ? { seq: event.seq } : {}),
...(typeof event.epoch === "string" ? { epoch: event.epoch } : {}),
};
}
private async enrichAgentPayload(payload: AgentSnapshotPayload): Promise<AgentSnapshotPayload> {
const storedRecord = await this.agentStorage.get(payload.id);
payload.title = storedRecord?.title ?? null;
payload.archivedAt = storedRecord?.archivedAt ?? null;
return payload;
}
private buildAgentPayload(agent: ManagedAgent): Promise<AgentSnapshotPayload> {
return this.enrichAgentPayload(toAgentPayload(agent));
}
private buildStoredAgentPayload(
record: StoredAgentRecord,
registeredProviderIds = new Set(this.providerSnapshotManager.listRegisteredProviderIds()),
): AgentSnapshotPayload {
return buildStoredAgentPayload(record, registeredProviderIds);
}
private isProviderVisibleToClient(provider: string): boolean {
if (
this.supports(CLIENT_CAPS.allProviders) ||
clientSupportsAllProviders(
this.delivery.currentSource
? (this.clientSources.get(this.delivery.currentSource)?.appVersion ?? null)
: this.appVersion,
)
) {
return true;
}
return LEGACY_PROVIDER_IDS.has(provider);
}
private async buildProjectPlacementForWorkspace(
workspace: PersistedWorkspaceRecord,
projectRecord?: PersistedProjectRecord | null,
): Promise<ProjectPlacementPayload> {
const project = projectRecord ?? (await this.projectRegistry.get(workspace.projectId));
if (!project) {
throw new Error(`Project not found for workspace ${workspace.workspaceId}`);
}
const snapshot = this.workspaceRuntimeStore.get(workspace.cwd);
const checkout = checkoutFromPersistedWorkspacePlacement({
workspace,
// COMPAT(workspacePlacementBackfill): added in v0.1.107, remove after 2027-01-15.
// Legacy records can lack branch and worktreeRoot because persisted registries
// are not migrated in place.
fallbackBranch: snapshot?.git.currentBranch ?? null,
fallbackWorktreeRoot: snapshot?.git.repoRoot,
});
return {
projectKey: project.projectId,
projectName: resolveProjectDisplayName(project),
workspaceName: resolveWorkspaceDisplayName(workspace),
checkout,
};
}
private async buildProjectPlacementForWorkspaceId(
workspaceId: string,
): Promise<ProjectPlacementPayload | null> {
const workspace = await this.workspaceRegistry.get(workspaceId);
if (!workspace) return null;
const project = await this.projectRegistry.get(workspace.projectId);
if (!project) return null;
return this.buildProjectPlacementForWorkspace(workspace, project);
}
/**
* Main entry point for processing session messages
*/
public async handleMessage(msg: SessionInboundMessage, source?: object): Promise<void> {
return this.delivery.request(source, msg, () => this.handleRequest(msg, source));
}
private async handleRequest(msg: SessionInboundMessage, source?: object): Promise<void> {
this.inflightRequests++;
if (this.inflightRequests > this.peakInflightRequests) {
this.peakInflightRequests = this.inflightRequests;
}
try {
// Stringifying a binary plugin input costs its size; test logger stubs lack isLevelEnabled.
if (this.sessionLogger.isLevelEnabled?.("trace"))
this.sessionLogger.trace(
{
messageType: msg.type,
payloadBytes: JSON.stringify(msg).length,
},
"agent.session.inbound",
);
if (!this.authorization.allowsInbound(msg)) {
const requestId = sessionRequestId(msg);
if (requestId) {
this.emit({
type: "rpc_error",
payload: {
requestId,
requestType: msg.type,
error: `Session is not authorized for ${msg.type}`,
code: msg.type.startsWith("hosts.") ? "permission_denied" : "access_denied",
},
});
}
return;
}
try {
await this.dispatchInboundMessage(msg, source);
} catch (error) {
const err = error instanceof Error ? error : new Error(String(error));
this.sessionLogger.error({ err }, "Error handling message");
const requestId =
"requestId" in msg && typeof msg.requestId === "string" ? msg.requestId : undefined;
if (typeof requestId === "string") {
try {
this.emit({
type: "rpc_error",
payload: {
requestId,
requestType: msg.type,
error: `Request failed: ${err.message}`,
code: "handler_error",
},
});
} catch (emitError) {
this.sessionLogger.error({ err: emitError }, "Failed to emit rpc_error");
}
}
this.emit({
type: "activity_log",
payload: {
id: uuidv4(),
timestamp: new Date(),
type: "error",
content: `Error: ${err.message}`,
},
});
}
} finally {
this.inflightRequests--;
}
}
public setPermissions(permissions: readonly DaemonPermission[]): void {
this.authorization.replacePermissions(permissions);
this.connectedHostSession?.revoke();
if (!this.authorization.allowsPermission("workspace.write")) {
void this.delivery
.releaseFamily("browser-host")
.catch((error) =>
this.sessionLogger.error({ err: error }, "Failed to release revoked browser hosting"),
);
}
// Binary plugin events skip the outbound permission check, so revocation ends the streams.
if (!this.authorization.allowsPermission("daemon.manage")) {
void this.delivery
.releaseFamily("plugin-rpc")
.catch((error) =>
this.sessionLogger.error(
{ err: error },
"Failed to release revoked plugin subscriptions",
),
);
}
}
public getPermissions(): DaemonPermission[] {
return this.authorization.listPermissions();
}
public allowsInbound(message: SessionInboundMessage): boolean {
return this.authorization.allowsInbound(message);
}
public allowsPermission(permission: DaemonPermission): boolean {
return this.authorization.allowsPermission(permission);
}
public subscribesToAgent(agent: ManagedAgent, source: object): Promise<boolean> {
return this.agentUpdates.includesLiveAgent(
agent,
new Set(this.delivery.subscriptionIds(source, "agents")),
);
}
public subscribesToTerminalDirectory(
input: {
cwd: string;
workspaceId?: string;
},
source: object,
): Promise<boolean> {
return this.terminalController.hasDirectorySubscription(input, source);
}
public publishToSource(source: object, message: SessionOutboundMessage): void {
if (!this.authorization.allowsOutbound(message)) return;
this.emitSubscribedEvent(message, source);
}
public wantsSourceEvent(source: object, event: SessionEventSubscription): boolean {
return this.wantsEvent(event, source);
}
public wantsSourceNotification(source: object, event: SessionEventSubscription): boolean {
// COMPAT(ownedSubscriptions): added in v0.8.0, remove legacy notification selection after 2027-03-09.
if (!this.delivery.isModern(source)) return true;
return [...this.eventSubscriptions.values()].some(
(subscription) =>
subscription.owner.source === source &&
subscription.notifications &&
subscription.events.has(event),
);
}
public publish(message: SessionOutboundMessage): void {
this.emit(message);
}
private async registerBrowserHost(
request: Extract<SessionInboundMessage, { type: "browser.host.register.request" }>,
respond = true,
): Promise<void> {
if (!this.browserToolsBroker) throw new Error("Browser hosting is unavailable");
let unregister: (() => void) | undefined;
const owner = this.delivery.begin("browser-host", undefined, () => unregister?.());
try {
unregister = this.browserToolsBroker.registerClient({
id: owner.id,
hostKind: request.hostKind,
supportedCommands: request.supportedCommands,
sendBrowserAutomationRequest: (message) => owner.emit(message),
});
if (respond)
this.emit({
type: "browser.host.register.response",
payload: { requestId: request.requestId, subscriptionId: owner.responseId },
});
} catch (error) {
await owner.release();
throw error;
}
}
private async subscribeWorkspaceSetup(
request: Extract<SessionInboundMessage, { type: "workspace.setup.subscribe.request" }>,
): Promise<void> {
if (
!this.supports(CLIENT_CAPS.workspaceSetupObservation) ||
!this.delivery.isModern(this.delivery.currentSource)
)
throw new Error("Workspace setup observation requires owned subscriptions");
// Resolve persisted blocked state first. Register and read the current cache in one
// synchronous turn afterward, so a setup transition during this read cannot be lost.
let initial: WorkspaceSetupSnapshot | null = null;
await handleWorkspaceSetupStatusRequestMessage(
{
workspaceSetupSnapshots: this.workspaceSetupObservation,
getWorkspace: (id) => this.workspaceRegistry.get(id),
emit: (message) => {
if (message.type === "workspace_setup_status_response")
initial = message.payload.snapshot;
},
},
{ ...request, type: "workspace_setup_status_request" },
);
let stop: (() => void) | undefined;
const owner = this.delivery.begin("workspace-setup", undefined, () => stop?.());
stop = this.workspaceSetupObservation.subscribe((workspaceId, snapshot) => {
if (workspaceId === request.workspaceId)
owner.emit({ type: "workspace_setup_progress", payload: { workspaceId, ...snapshot } });
});
this.emit({
type: "workspace.setup.subscribe.response",
payload: {
requestId: request.requestId,
subscriptionId: owner.responseId,
workspaceId: request.workspaceId,
snapshot: this.workspaceSetupObservation.get(request.workspaceId) ?? initial,
},
});
}
private dispatchSubscriptionMessage(
msg: SessionInboundMessage,
source?: object,
): Promise<void> | undefined {
if (msg.type === "workspace.setup.subscribe.request") return this.subscribeWorkspaceSetup(msg);
if (msg.type === "browser.host.register.request") return this.registerBrowserHost(msg);
if (msg.type === "browser.automation.execute.response") {
if (source)
this.browserToolsBroker?.receiveResponse(
msg,
this.delivery.subscriptionIds(source, "browser-host"),
);
return Promise.resolve();
}
if (msg.type === "subscription.release.request") {
return this.delivery.release(msg.subscriptionId).then(() => {
this.emit({
type: "subscription.release.response",
payload: { requestId: msg.requestId, subscriptionId: msg.subscriptionId },
});
return undefined;
});
}
return undefined;
}
private async dispatchInboundMessage(msg: SessionInboundMessage, source?: object): Promise<void> {
const promise =
this.connectedHostSession?.dispatch(msg) ??
this.dispatchSubscriptionMessage(msg, source) ??
this.dispatchVoiceAndControlMessage(msg) ??
this.dispatchAgentObservationMessage(msg, source) ??
this.dispatchHubExecutionMessage(msg) ??
this.dispatchCreationMessage(msg, source) ??
this.dispatchAgentLifecycleMessage(msg) ??
this.dispatchAgentConfigMessage(msg) ??
this.dispatchCheckoutMessage(msg) ??
this.dispatchWorkspaceLifecycleMessage(msg) ??
this.dispatchWorkspaceFileMessage(msg, source) ??
this.dispatchProviderMessage(msg) ??
this.dispatchOrchestrationSkillsMessage(msg) ??
this.dispatchPluginDirectoryMessage(msg) ??
this.dispatchPluginMessage(msg) ??
this.dispatchPluginRpcMessage(msg) ??
this.dispatchHostServiceMessage(msg) ??
this.dispatchMiscMessage(msg);
if (promise) await promise;
}
private dispatchAgentObservationMessage(
msg: SessionInboundMessage,
source?: object,
): Promise<void> | undefined {
return (
this.dispatchAgentRewindMessage(msg, source) ??
this.dispatchAgentRelationshipMessage(msg) ??
this.dispatchAgentTimelineMessage(msg, source)
);
}
private dispatchHostServiceMessage(msg: SessionInboundMessage): Promise<void> | undefined {
return (
this.dispatchTerminalMessage(msg) ??
this.dispatchScheduleMessage(msg) ??
this.speechModelsSession.dispatch(msg)
);
}
private dispatchWorkspaceLifecycleMessage(msg: SessionInboundMessage): Promise<void> | undefined {
return (
this.dispatchWorkspaceStateMessage(msg) ??
this.dispatchWorkspaceLabelMessage(msg) ??
this.dispatchWorkspaceSetupMessage(msg) ??
this.dispatchWorkspaceAndProjectMessage(msg)
);
}
private dispatchOrchestrationSkillsMessage(
msg: SessionInboundMessage,
): Promise<void> | undefined {
if (!this.orchestrationSkills || !msg.type.startsWith("agent.skills.")) return undefined;
const emitStatus = (
type:
| "agent.skills.get_status.response"
| "agent.skills.reconcile.response"
| "agent.skills.uninstall.response",
requestId: string,
operation: Promise<import("./orchestration-skills/index.js").SkillsSnapshot>,
) =>
operation.then((status) => {
this.emit({ type, payload: { requestId, ...status } });
return undefined;
});
switch (msg.type) {
case "agent.skills.get_status.request":
return emitStatus(
"agent.skills.get_status.response",
msg.requestId,
this.orchestrationSkills.getStatus(),
);
case "agent.skills.reconcile.request":
return emitStatus(
"agent.skills.reconcile.response",
msg.requestId,
this.orchestrationSkills.reconcile(),
);
case "agent.skills.uninstall.request":
return emitStatus(
"agent.skills.uninstall.response",
msg.requestId,
this.orchestrationSkills.uninstall(),
);
case "agent.skills.save_selection.request":
return this.orchestrationSkills
.saveSelection(msg.selection, msg.confirmedRemovals)
.then((result) => {
this.emit({
type: "agent.skills.save_selection.response",
payload: { requestId: msg.requestId, ...result },
});
return undefined;
});
case "agent.skills.import_legacy_selection.request":
return this.orchestrationSkills
.importLegacySelectionIfUnset(msg.selection)
.then((result) => {
this.emit({
type: "agent.skills.import_legacy_selection.response",
payload: { requestId: msg.requestId, ...result },
});
return undefined;
});
default:
return undefined;
}
}
private dispatchPluginMessage(msg: SessionInboundMessage): Promise<void> | undefined {
if (msg.type === "plugin.list.request") {
return (this.pluginRuntime?.listPlugins() ?? Promise.resolve([])).then((plugins) => {
this.emit({
type: "plugin.list.response",
payload: { requestId: msg.requestId, plugins },
});
return undefined;
});
}
if (msg.type === "plugin.logs.get.request") {
if (!this.pluginRuntime) throw new Error("Plugin service is unavailable");
this.emit({
type: "plugin.logs.get.response",
payload: {
requestId: msg.requestId,
pluginId: msg.pluginId,
entries: this.pluginRuntime.getLogs(msg.pluginId),
},
});
return undefined;
}
if (msg.type === "plugin.reload.request") {
if (!this.pluginRuntime) throw new Error("Plugin service is unavailable");
return this.pluginRuntime.reloadPlugin(msg.pluginId).then((plugin) => {
this.emit({
type: "plugin.reload.response",
payload: { requestId: msg.requestId, plugin },
});
return undefined;
});
}
if (msg.type === "plugin.enable.request") {
if (!this.pluginRuntime) throw new Error("Plugin service is unavailable");
return this.pluginRuntime.enablePlugin(msg.pluginId).then((plugin) => {
this.emit({
type: "plugin.enable.response",
payload: { requestId: msg.requestId, plugin },
});
return undefined;
});
}
if (msg.type === "plugin.disable.request") {
if (!this.pluginRuntime) throw new Error("Plugin service is unavailable");
return this.pluginRuntime.disablePlugin(msg.pluginId).then((plugin) => {
this.emit({
type: "plugin.disable.response",
payload: { requestId: msg.requestId, plugin },
});
return undefined;
});
}
if (msg.type === "plugin.remove.request") {
if (!this.pluginRuntime) throw new Error("Plugin service is unavailable");
return this.pluginRuntime.removePlugin(msg.pluginId).then(() => {
this.emit({ type: "plugin.remove.response", payload: { requestId: msg.requestId } });
return undefined;
});
}
if (msg.type === "plugin.catalog.get.request") {
const cached = new Map(
msg.cachedClientBundles?.map((entry) => [entry.id, entry.clientBundleHash]),
);
const plugins = (this.pluginRuntime?.catalog() ?? []).map(({ clientBundle, ...plugin }) =>
plugin.clientBundleHash !== undefined && cached.get(plugin.id) === plugin.clientBundleHash
? plugin
: Object.assign(plugin, { clientBundle }),
);
this.emit({
type: "plugin.catalog.get.response",
payload: { requestId: msg.requestId, plugins },
});
return undefined;
}
return undefined;
}
private dispatchPluginRpcMessage(msg: SessionInboundMessage): Promise<void> | undefined {
if (msg.type === "plugin.rpc.invoke.request") {
if (!this.pluginRuntime) throw new Error("Plugin service is unavailable");
return this.pluginRuntime
.invokePluginRpc(msg.pluginId, msg.method, msg.input)
.then((output) => this.replyPluginRpc(msg, output));
}
if (msg.type === "plugin.rpc.subscribe.request") return this.subscribePluginRpc(msg);
return undefined;
}
/** A `Uint8Array` output is the binary form of `plugin.rpc.invoke.response`. */
private async replyPluginRpc(
request: Extract<SessionInboundMessage, { type: "plugin.rpc.invoke.request" }>,
output: unknown,
): Promise<void> {
const response: SessionOutboundMessage = {
type: "plugin.rpc.invoke.response",
payload: { requestId: request.requestId, output },
};
if (!(output instanceof Uint8Array)) {
this.emit(response);
return;
}
// The frame is the binary form of the response, so it needs the response's permission.
if (!this.authorization.allowsOutbound(response)) return;
if (!this.supports(CLIENT_CAPS.pluginBinaryPayloads) || this.onBinaryMessage === null)
throw new Error(
`Update Paseo to receive the binary output of plugin RPC ${request.pluginId}.${request.method}`,
);
await this.emitBinaryReply(
encodePluginOutputFrame(request.requestId, output),
this.delivery.currentSource,
);
}
private async subscribePluginRpc(
request: Extract<SessionInboundMessage, { type: "plugin.rpc.subscribe.request" }>,
): Promise<void> {
if (!this.pluginRuntime) throw new Error("Plugin service is unavailable");
const subscription = this.pluginRuntime.subscribePluginRpc(
request.pluginId,
request.method,
request.input,
);
const owner = this.delivery.begin("plugin-rpc", undefined, () => subscription.release());
try {
await subscription.ready;
} catch (error) {
await owner.release();
throw error;
}
if (owner.signal.aborted) return;
this.emit({
type: "plugin.rpc.subscribe.response",
payload: { requestId: request.requestId, subscriptionId: owner.responseId },
});
const close = (reason?: string) => {
owner.emit({
type: "plugin.rpc.closed",
payload: {
subscriptionId: owner.responseId,
...(reason === undefined ? {} : { error: reason }),
},
});
void owner.release().catch(() => undefined);
};
subscription.attach({
event: (event) => {
const message: SessionOutboundMessage = {
type: "plugin.rpc.event",
payload: { subscriptionId: owner.responseId, event },
};
if (!(event instanceof Uint8Array)) {
owner.emit(message);
return;
}
if (!this.authorization.allowsOutbound(message)) return;
if (this.onBinaryMessage === null) {
subscription.release();
close("This connection cannot carry binary plugin events");
return;
}
owner.emitBinary(encodePluginEventFrame(owner.responseId, event));
},
closed: close,
});
}
private dispatchPluginDirectoryMessage(msg: SessionInboundMessage): Promise<void> | undefined {
if (msg.type === "plugin.source.install.request") {
if (!this.pluginRuntime) throw new Error("Plugin service is unavailable");
return this.pluginRuntime
.installSource({
// COMPAT(plugin-source-path): accepted for v0.7 clients; remove after 2027-09-01.
source: formatPluginSourceReference(msg.source, msg.pluginPath),
...(msg.id ? { id: msg.id } : {}),
...(msg.ref ? { ref: msg.ref } : {}),
})
.then((plugin) => {
this.emit({
type: "plugin.source.install.response",
payload: { requestId: msg.requestId, plugin },
});
return undefined;
});
}
if (msg.type === "plugin.source.status.request") {
if (!this.pluginRuntime) throw new Error("Plugin service is unavailable");
return this.pluginRuntime.statusSources(msg.pluginId).then((plugins) => {
this.emit({
type: "plugin.source.status.response",
payload: { requestId: msg.requestId, plugins },
});
return undefined;
});
}
if (msg.type === "plugin.source.update.preview.request") {
if (!this.pluginRuntime) throw new Error("Plugin service is unavailable");
return this.pluginRuntime
.previewUpdates({ pluginId: msg.pluginId, target: msg.target })
.then((plugins) => {
this.emit({
type: "plugin.source.update.preview.response",
payload: { requestId: msg.requestId, plugins },
});
return undefined;
});
}
if (msg.type === "plugin.source.update.apply.request") {
if (!this.pluginRuntime) throw new Error("Plugin service is unavailable");
return this.pluginRuntime.applyUpdates(msg.proposals).then((plugins) => {
this.emit({
type: "plugin.source.update.apply.response",
payload: { requestId: msg.requestId, plugins },
});
return undefined;
});
}
if (msg.type === "plugin.source.update.request") {
if (!this.pluginRuntime) throw new Error("Plugin service is unavailable");
return this.pluginRuntime.updateSources(msg.pluginId).then((plugins) => {
this.emit({
type: "plugin.source.update.response",
payload: { requestId: msg.requestId, plugins },
});
return undefined;
});
}
if (msg.type === "plugin.directory.install.request") {
if (!this.pluginRuntime) throw new Error("Plugin service is unavailable");
return this.pluginRuntime.installDirectory({ path: msg.path, id: msg.id }).then((plugin) => {
this.emit({
type: "plugin.directory.install.response",
payload: { requestId: msg.requestId, plugin },
});
return undefined;
});
}
if (msg.type === "plugin.directory.inspect.request") {
if (!this.pluginRuntime) throw new Error("Plugin service is unavailable");
return this.pluginRuntime.inspectDirectory(msg.path).then(({ id }) => {
this.emit({
type: "plugin.directory.inspect.response",
payload: { requestId: msg.requestId, id },
});
return undefined;
});
}
return undefined;
}
private subscribeToPluginChanges(
pluginRuntime: SessionOptions["pluginRuntime"],
): (() => void) | null {
if (!pluginRuntime) return null;
const catalog = pluginRuntime.subscribe((pluginId) => {
this.emit({ type: "status", payload: { status: "plugin_catalog_changed", pluginId } });
});
const settings = pluginRuntime.subscribeSettings?.((pluginId, settingsId) => {
this.emit({
type: "status",
payload: { status: "plugin_settings_changed", pluginId, settingsId },
});
});
return () => {
catalog();
settings?.();
};
}
private dispatchVoiceAndControlMessage(msg: SessionInboundMessage): Promise<void> | undefined {
switch (msg.type) {
case "voice_audio_chunk":
case "abort_request":
case "audio_played":
case "set_voice_mode":
case "dictation_stream_start":
case "dictation_stream_chunk":
case "dictation_stream_finish":
case "dictation_stream_cancel":
return this.voiceSessions.handleMessage(msg);
case "restart_server_request":
return this.handleRestartServerRequest(msg.requestId, msg.reason, {
keepTerminals: msg.killTerminals !== true,
});
case "shutdown_server_request":
return this.handleShutdownServerRequest(msg.requestId, {
keepTerminals: msg.keepTerminals === true,
});
case "client_heartbeat":
this.handleClientHeartbeat(msg);
return undefined;
case "ping": {
const now = Date.now();
this.emit({
type: "pong",
payload: {
requestId: msg.requestId,
clientSentAt: msg.clientSentAt,
serverReceivedAt: now,
serverSentAt: now,
},
});
return undefined;
}
default:
return undefined;
}
}
private dispatchAgentRewindMessage(
msg: SessionInboundMessage,
source?: object,
): Promise<void> | undefined {
switch (msg.type) {
case "agent.rewind.request":
return this.handleAgentRewindRequest(msg, source);
default:
return undefined;
}
}
private dispatchAgentRelationshipMessage(msg: SessionInboundMessage): Promise<void> | undefined {
switch (msg.type) {
case "agent.detach.request":
return this.handleDetachAgentRequest(msg.agentId, msg.requestId);
default:
return undefined;
}
}
private dispatchAgentTimelineMessage(
msg: SessionInboundMessage,
source?: object,
): Promise<void> | undefined {
switch (msg.type) {
case "fetch_agent_timeline_request":
return this.handleFetchAgentTimelineRequest(msg, source);
case "agent.timeline.append.request":
return this.handleAgentTimelineAppendRequest(msg);
case "agent.timeline.search.request":
return this.handleAgentTimelineSearchRequest(msg, source);
case "agent.timeline.list_prompts.request":
return this.handleAgentTimelineListPromptsRequest(msg, source);
case "agent.provider_subagents.list.request":
return this.handleProviderSubagentListRequest(msg);
case "agent.provider_subagents.timeline.get.request":
return this.handleProviderSubagentTimelineRequest(msg, source);
case "session.events.set_subscription.request": {
const owner = this.delivery.begin("events", undefined, async (id) => {
this.eventSubscriptions.delete(id);
this.refreshObservationProducers();
if (msg.events.includes("checkout_status_update"))
await this.reconcileWorkspaceGitObservers();
});
this.eventSubscriptions.set(owner.id, {
owner,
events: new Set(msg.events),
notifications: msg.notifications === true,
});
this.emitForSource(
{
type: "session.events.set_subscription.response",
payload: { requestId: msg.requestId, subscriptionId: owner.responseId },
},
source,
);
this.refreshObservationProducers();
if (!msg.events.includes("checkout_status_update")) return undefined;
return this.reconcileWorkspaceGitObservers().catch(async (error) => {
await owner.release();
throw error;
});
}
case "agent.timeline.set_subscription.request": {
const agentIds = [...new Set(msg.agentIds)].sort();
const owner = this.subscribeAgentTimelines(agentIds);
this.emitForSource(
{
type: "agent.timeline.set_subscription.response",
payload: { agentIds, requestId: msg.requestId, subscriptionId: owner.responseId },
},
source,
);
return undefined;
}
case "agent.fork_context.request":
return this.handleAgentForkContextRequest(msg);
default:
return undefined;
}
}
private dispatchHubExecutionMessage(msg: SessionInboundMessage): Promise<void> | undefined {
if (msg.type === "hub.execution.agent.create.request") {
return this.hubExecutionController?.createAgent(msg);
}
if (msg.type === "hub.execution.agent.validate.request") {
return this.hubExecutionController?.validateAgent(msg);
}
if (msg.type === "hub.execution.control.request") {
return this.hubExecutionController?.controlExecution(msg);
}
return undefined;
}
private dispatchCreationMessage(
msg: SessionInboundMessage,
source?: object,
): Promise<void> | undefined {
switch (msg.type) {
case "agent.create.request":
return this.handleAgentCreation(msg, source);
case "creation.subscribe.request":
return this.handleCreationSubscription(msg, source);
case "workspace.create.request":
return this.handleWorkspaceCreation(msg, source);
default:
return undefined;
}
}
private dispatchAgentLifecycleMessage(msg: SessionInboundMessage): Promise<void> | undefined {
switch (msg.type) {
case "fetch_agents_request":
return this.handleFetchAgents(msg);
case "fetch_agent_history_request":
return this.handleFetchAgentHistory(msg);
case "fetch_recent_provider_sessions_request":
return this.handleFetchRecentProviderSessions(msg);
case "fetch_agent_request":
return this.handleFetchAgent(msg.agentId, msg.requestId);
case "delete_agent_request":
return this.handleDeleteAgentRequest(msg.agentId, msg.requestId);
case "archive_agent_request":
return this.handleArchiveAgentRequest(msg.agentId, msg.requestId);
case "close_items_request":
return this.handleCloseItemsRequest(msg);
case "update_agent_request":
return this.handleUpdateAgentRequest(msg.agentId, msg.name, msg.labels, msg.requestId);
case "project.rename.request":
return this.handleProjectRenameRequest(msg.projectId, msg.customName, msg.requestId);
case "project.icon.set.request":
return this.handleProjectIconSetRequest(msg);
case "send_agent_message_request":
return this.handleSendAgentMessageRequest(msg);
case "wait_for_finish_request":
return this.handleWaitForFinish(msg.agentId, msg.requestId, msg.timeoutMs);
case "create_agent_request":
return this.handleCreateAgentRequest(msg);
case "resume_agent_request":
return this.handleResumeAgentRequest(msg);
case "import_agent_request":
return this.handleImportAgentRequest(msg);
case "refresh_agent_request":
return this.handleRefreshAgentRequest(msg);
case "cancel_agent_request":
return this.handleCancelAgentRequest(msg.agentId, msg.requestId);
case "agent_permission_response":
return this.handleAgentPermissionResponse(msg.agentId, msg.requestId, msg.response);
case "clear_agent_attention":
return this.handleClearAgentAttention(msg.agentId, msg.requestId);
default:
return undefined;
}
}
private dispatchAgentConfigMessage(msg: SessionInboundMessage): Promise<void> | undefined {
switch (msg.type) {
case "set_agent_mode_request":
return this.agentConfigSession.handleSetAgentModeRequest(msg);
case "set_agent_model_request":
return this.agentConfigSession.handleSetAgentModelRequest(msg);
case "set_agent_feature_request":
return this.agentConfigSession.handleSetAgentFeatureRequest(msg);
case "set_agent_thinking_request":
return this.agentConfigSession.handleSetAgentThinkingRequest(msg);
case "agent.config.apply.request":
return this.agentConfigSession.handleAgentConfigApplyRequest(msg);
case "get_daemon_config_request":
this.emit({
type: "get_daemon_config_response",
payload: {
requestId: msg.requestId,
config: toWireDaemonConfig(this.daemonConfigStore.get()),
},
});
return undefined;
case "daemon.get_status.request":
return this.daemonSession.handleGetStatusRequest(msg);
case "daemon.get_pairing_offer.request":
return this.daemonSession.handleGetPairingOfferRequest(msg);
case "daemon.config.reload.request":
return this.daemonSession.handleConfigReloadRequest(msg);
case "hub.management.daemon.connect.request":
case "hub.management.daemon.get_status.request":
case "hub.management.daemon.disconnect.request":
case "hub.management.daemon.permissions.update.request":
return this.daemonSession.handleHubRelationshipRequest(msg);
case "diagnostics.request":
return this.daemonSession.handleDiagnosticsRequest(msg);
case "daemon.update.request":
return this.daemonSession.handleUpdateRequest(msg);
case "set_daemon_config_request":
return this.daemonConfigStore.patch(msg.config).then((config) => {
return this.emit({
type: "set_daemon_config_response",
payload: {
requestId: msg.requestId,
config: toWireDaemonConfig(config),
},
});
});
case "read_project_config_request":
return this.projectConfigSession.handleReadProjectConfigRequest(msg);
case "write_project_config_request":
return this.projectConfigSession.handleWriteProjectConfigRequest(msg);
default:
return undefined;
}
}
// eslint-disable-next-line complexity
private dispatchCheckoutMessage(msg: SessionInboundMessage): Promise<void> | undefined {
switch (msg.type) {
case "checkout_status_request":
return this.checkoutSession.handleStatusRequest(msg);
case "checkout.commits.list.request":
return this.checkoutSession.handleCommitsListRequest(msg);
case "checkout.commits.file_diff.request":
return this.checkoutSession.handleCommitFileDiffRequest(msg, this.delivery.requestSignal);
case "validate_branch_request":
return this.checkoutSession.handleValidateBranchRequest(msg);
case "branch_suggestions_request":
return this.checkoutSession.handleBranchSuggestionsRequest(msg);
case "directory_suggestions_request":
return this.handleDirectorySuggestionsRequest(msg);
case "checkout.diff.get.request":
return this.checkoutSession.handleReadDiffRequest(msg);
case "subscribe_checkout_diff_request":
return this.checkoutSession.handleSubscribeDiffRequest(msg, this.delivery);
case "unsubscribe_checkout_diff_request":
return this.checkoutSession.handleUnsubscribeDiffRequest(msg, this.delivery);
case "checkout_switch_branch_request":
return this.checkoutSession.handleCheckoutSwitchBranchRequest(msg);
case "checkout.rename_branch.request":
return this.checkoutSession.handleCheckoutRenameBranchRequest(msg);
case "checkout_commit_request":
return this.checkoutSession.handleCheckoutCommitRequest(msg);
case "checkout_merge_request":
return this.checkoutSession.handleCheckoutMergeRequest(msg);
case "checkout_merge_from_base_request":
return this.checkoutSession.handleCheckoutMergeFromBaseRequest(msg);
case "checkout_pull_request":
return this.checkoutSession.handleCheckoutPullRequest(msg);
case "checkout_push_request":
return this.checkoutSession.handleCheckoutPushRequest(msg);
case "checkout.refresh.request":
return this.checkoutSession.handleRefreshRequest(msg);
case "checkout.discard_changes.request":
return this.checkoutSession.handleCheckoutDiscardChangesRequest(msg);
case "checkout_pr_create_request":
return this.checkoutSession.handleCheckoutPrCreateRequest(msg);
case "checkout_pr_merge_request":
return this.checkoutSession.handleCheckoutPrMergeRequest(msg);
case "checkout.forge.set_auto_merge.request":
case "checkout.github.set_auto_merge.request":
return this.checkoutSession.handleCheckoutForgeSetAutoMergeRequest(msg);
case "checkout.forge.get_check_details.request":
case "checkout.github.get_check_details.request":
return this.checkoutSession.handleCheckoutForgeGetCheckDetailsRequest(msg);
case "checkout_pr_status_request":
return this.checkoutSession.handleCheckoutPrStatusRequest(msg);
case "pull_request_timeline_request":
return this.checkoutSession.handlePullRequestTimelineRequest(msg);
case "forge.search.request":
case "github_search_request":
return this.checkoutSession.handleForgeSearchRequest(msg);
case "stash_save_request":
return this.checkoutSession.handleStashSaveRequest(msg);
case "stash_pop_request":
return this.checkoutSession.handleStashPopRequest(msg);
case "stash_list_request":
return this.checkoutSession.handleStashListRequest(msg);
default:
return undefined;
}
}
private dispatchWorkspaceAndProjectMessage(
msg: SessionInboundMessage,
): Promise<void> | undefined {
switch (msg.type) {
case "fetch_workspaces_request":
return this.handleFetchWorkspacesRequest(msg);
case "project.list.request":
return this.handleProjectListRequest(msg);
case "project.suggestions.request":
return this.handleProjectSuggestionsRequest(msg);
case "paseo_worktree_list_request":
return this.handlePaseoWorktreeListRequest(msg);
case "paseo_worktree_archive_request":
return this.handlePaseoWorktreeArchiveRequest(msg);
case "create_paseo_worktree_request":
return this.handleCreatePaseoWorktreeRequest(msg);
// COMPAT(desktopEditorBridge): added in v0.1.88, remove after 2026-12-03 once old clients no longer call daemon editor RPCs.
case "list_available_editors_request":
return this.handleLegacyListAvailableEditorsRequest(msg);
case "open_in_editor_request":
return this.handleLegacyOpenInEditorRequest(msg);
case "open_project_request":
return this.handleOpenProjectRequest(msg);
case "project.add.request":
return this.handleProjectAddRequest(msg);
case "project.create_directory.request":
return this.handleProjectCreateDirectoryRequest(msg);
case "workspace.github.search_repositories.request":
return this.handleWorkspaceGithubSearchRepositoriesRequest(msg);
case "project.github.clone.request":
return this.handleProjectGithubCloneRequest(msg);
case "archive_workspace_request":
return this.handleArchiveWorkspaceRequest(msg);
case "project.remove.request":
return this.handleProjectRemoveRequest(msg);
case "workspace.title.set.request":
return this.handleWorkspaceTitleSetRequest(msg.workspaceId, msg.title, msg.requestId);
case "workspace.pin.set.request":
return this.handleWorkspacePinSetRequest(msg.workspaceId, msg.pinned, msg.requestId);
default:
return undefined;
}
}
private dispatchWorkspaceSetupMessage(msg: SessionInboundMessage): Promise<void> | undefined {
if (msg.type === "workspace_setup_status_request") {
return this.handleWorkspaceSetupStatusRequest(msg);
}
if (msg.type === "workspace.setup.run.request") {
return this.handleWorkspaceSetupRunRequest(msg);
}
return undefined;
}
private dispatchWorkspaceLabelMessage(msg: SessionInboundMessage): Promise<void> | undefined {
switch (msg.type) {
case "workspace.label.list.request":
return this.handleWorkspaceLabelList(msg);
case "workspace.label.assignment.set.request":
return this.handleWorkspaceLabelAssignment(msg);
case "workspace.label.update.request":
return this.handleWorkspaceLabelUpdate(msg);
case "workspace.label.delete.request":
return this.handleWorkspaceLabelDelete(msg);
case "workspace.label.delete.inspect.request":
return this.handleWorkspaceLabelDeleteInspection(msg);
default:
return undefined;
}
}
private dispatchWorkspaceFileMessage(
msg: SessionInboundMessage,
source?: object,
): Promise<void> | undefined {
switch (msg.type) {
case "file_explorer_request":
return this.workspaceFilesSession.handleFileExplorerRequest(msg, source);
case "fs.file.subscribe.request":
return this.workspaceFilesSession.handleFileSubscribeRequest(msg, this.delivery);
case "fs.file.unsubscribe.request":
return this.workspaceFilesSession.handleFileUnsubscribeRequest(msg, this.delivery);
case "fs.file.write.request":
return this.workspaceFilesSession.handleFileWriteRequest(msg);
case "fs.entry.create.request":
return this.workspaceFilesSession.handleFileEntryCreateRequest(msg);
case "fs.entry.rename.request":
return this.workspaceFilesSession.handleFileEntryRenameRequest(msg);
case "fs.entry.duplicate.request":
return this.workspaceFilesSession.handleFileEntryDuplicateRequest(msg);
case "fs.entry.delete.request":
return this.workspaceFilesSession.handleFileEntryDeleteRequest(msg);
case "project_icon_request":
return this.workspaceFilesSession.handleProjectIconRequest(msg);
case "project.icon.get.request":
return this.handleProjectIconGetRequest(msg.projectId, msg.requestId);
case "file_download_token_request":
return this.workspaceFilesSession.handleFileDownloadTokenRequest(msg);
case "file.upload.request":
this.workspaceFilesSession.handleFileUploadRequest(msg, this.delivery);
return undefined;
default:
return undefined;
}
}
private dispatchWorkspaceStateMessage(msg: SessionInboundMessage): Promise<void> | undefined {
switch (msg.type) {
case "workspace.recovery.inspect.request":
return this.handleWorkspaceRecoveryInspectRequest(msg);
case "workspace.recovery.restore.request":
return this.handleWorkspaceRecoveryRestoreRequest(msg);
case "workspace.clear_attention.request":
return this.handleWorkspaceClearAttentionRequest(msg);
case "workspace.mark_unread.request":
return this.handleWorkspaceMarkUnreadRequest(msg);
default:
return undefined;
}
}
private dispatchProviderMessage(msg: SessionInboundMessage): Promise<void> | undefined {
switch (msg.type) {
case "list_provider_models_request":
return this.providerCatalogSession.handleListProviderModelsRequest(msg);
case "list_provider_modes_request":
return this.providerCatalogSession.handleListProviderModesRequest(msg);
case "list_provider_features_request":
return this.providerCatalogSession.handleListProviderFeaturesRequest(msg);
case "provider.controls.list.request":
return this.providerCatalogSession.handleProviderControlsListRequest(msg);
case "list_available_providers_request":
return this.providerCatalogSession.handleListAvailableProvidersRequest(msg);
case "get_providers_snapshot_request":
return this.providerCatalogSession.handleGetProvidersSnapshotRequest(msg);
case "refresh_providers_snapshot_request":
return this.providerCatalogSession.handleRefreshProvidersSnapshotRequest(msg);
case "provider_diagnostic_request":
return this.providerCatalogSession.handleProviderDiagnosticRequest(msg);
case "provider.usage.list.request":
return this.usageSession.handleLegacyList(msg);
case "usage.list_reports.request":
return this.usageSession.handleListReports(msg);
default:
return undefined;
}
}
private dispatchTerminalMessage(msg: SessionInboundMessage): Promise<void> | undefined {
switch (msg.type) {
case "start_workspace_script_request":
return this.handleStartWorkspaceScriptRequest(msg);
case "workspace.script.list.request":
return this.handleWorkspaceScriptListRequest(msg);
case "workspace.script.start.request":
return this.handleWorkspaceScriptStartRequest(msg);
case "workspace.script.stop.request":
return this.handleWorkspaceScriptStopRequest(msg);
default:
return this.terminalController.dispatch(msg, this.delivery);
}
}
private dispatchScheduleMessage(msg: SessionInboundMessage): Promise<void> | undefined {
switch (msg.type) {
case "schedule/create":
return this.scheduleSession.handleScheduleCreateRequest(msg);
case "schedule/list":
return this.scheduleSession.handleScheduleListRequest(msg);
case "schedule/inspect":
return this.scheduleSession.handleScheduleInspectRequest(msg);
case "schedule/logs":
return this.scheduleSession.handleScheduleLogsRequest(msg);
case "schedule/pause":
return this.scheduleSession.handleSchedulePauseRequest(msg);
case "schedule/resume":
return this.scheduleSession.handleScheduleResumeRequest(msg);
case "schedule/delete":
return this.scheduleSession.handleScheduleDeleteRequest(msg);
case "schedule/run-once":
return this.scheduleSession.handleScheduleRunOnceRequest(msg);
case "schedule/update":
return this.scheduleSession.handleScheduleUpdateRequest(msg);
default:
return undefined;
}
}
private async dispatchMiscMessage(msg: SessionInboundMessage): Promise<void> {
switch (msg.type) {
case "list_commands_request":
await this.handleListCommandsRequest(msg);
return;
case "register_push_token":
this.handleRegisterPushToken(msg.token);
return;
case "push.unregister.request":
this.pushNotifications.revoke(msg.token);
if (this.currentClientMetadata().pushToken?.trim() === msg.token.trim()) {
this.currentClientMetadata().pushToken = null;
}
this.emit({
type: "push.unregister.response",
payload: { requestId: msg.requestId },
});
return;
}
}
public resetPeakInflight(): void {
this.peakInflightRequests = this.inflightRequests;
}
public getSessionId(): string {
return this.sessionId;
}
public async handleBinaryFrame(binaryFrame: BinaryFrame, source: object): Promise<void> {
if (binaryFrame.kind === "host_route") {
this.connectedHostSession?.binary(binaryFrame.frame, source);
return;
}
if (binaryFrame.kind === "plugin") {
const frame = binaryFrame.frame;
// The binary form of plugin.rpc.invoke.request; it authorizes and replies like the JSON form.
if (frame.opcode !== PluginFrameOpcode.Input) return;
await this.handleMessage(
{
type: "plugin.rpc.invoke.request",
requestId: frame.requestId,
pluginId: frame.pluginId,
method: frame.method,
input: frame.payload,
},
source,
);
return;
}
if (!this.authorization.allowsPermission("workspace.write")) {
return;
}
if (binaryFrame.kind === "file_transfer") {
await this.workspaceFilesSession.handleFileTransferFrame(binaryFrame.frame, source);
return;
}
this.terminalController.handleBinaryFrame(binaryFrame.frame, source);
}
private async handleRestartServerRequest(
requestId: string,
reason: string | undefined,
options: { keepTerminals: boolean },
): Promise<void> {
const lifecycleReason = normalizeClientRestartRpcReason(reason);
const payload: { status: string } & Record<string, unknown> = {
status: "restart_requested",
clientId: this.clientId,
};
if (reason && reason.trim().length > 0) {
payload.reason = reason;
}
payload.requestId = requestId;
this.sessionLogger.warn(
{ reason: lifecycleReason, keepTerminals: options.keepTerminals },
"Restart requested via websocket",
);
this.emit({
type: "status",
payload,
});
this.emitLifecycleIntent({
type: "restart",
clientId: this.clientId,
requestId,
reason: lifecycleReason,
keepTerminals: options.keepTerminals,
});
}
private async handleShutdownServerRequest(
requestId: string,
options: { keepTerminals: boolean },
): Promise<void> {
const reason = CLIENT_SHUTDOWN_RPC_REASON;
this.sessionLogger.warn(
{ reason, keepTerminals: options.keepTerminals },
"Shutdown requested via websocket",
);
this.emit({
type: "status",
payload: {
status: "shutdown_requested",
clientId: this.clientId,
requestId,
},
});
this.emitLifecycleIntent({
type: "shutdown",
clientId: this.clientId,
requestId,
reason,
keepTerminals: options.keepTerminals,
});
}
private emitLifecycleIntent(intent: SessionLifecycleIntent): void {
if (!this.onLifecycleIntent) {
return;
}
try {
this.onLifecycleIntent(intent);
} catch (error) {
this.sessionLogger.error({ err: error, intent }, "Lifecycle intent handler failed");
}
}
private async handleDeleteAgentRequest(agentId: string, requestId: string): Promise<void> {
this.sessionLogger.info({ agentId }, `Deleting agent ${agentId} from registry`);
const knownAgent =
this.agentManager.getAgent(agentId) ?? (await this.agentStorage.get(agentId));
// File-backed storage still needs an early delete fence before closeAgent().
beginAgentDeleteIfSupported(this.agentStorage, agentId);
try {
await closeAgentCommand({ agentManager: this.agentManager }, agentId);
} catch (error) {
this.sessionLogger.warn(
{ err: error, agentId },
`Failed to close agent ${agentId} during delete`,
);
}
// Drain queued persistence from the just-closed agent before removing its
// durable snapshot, otherwise an in-flight background write can recreate it.
await this.agentManager.flush();
try {
await this.agentStorage.remove(agentId);
await this.agentManager.deleteAgentState(agentId);
// Directory removal is daemon-wide; agent_deleted below stays a source-scoped RPC reply.
if (knownAgent) this.agentManager.notifyAgentRemoved(knownAgent);
} catch (error) {
this.sessionLogger.error({ err: error, agentId }, `Failed to fully delete agent ${agentId}`);
}
this.emit({
type: "agent_deleted",
payload: {
agentId,
requestId,
},
});
}
private async handleArchiveAgentRequest(agentId: string, requestId: string): Promise<void> {
this.sessionLogger.info({ agentId }, `Archiving agent ${agentId}`);
const { archivedAt } = await this.archiveAgentForClose(agentId);
this.emit({
type: "agent_archived",
payload: {
agentId,
archivedAt,
requestId,
},
});
}
private async archiveAgentForClose(
agentId: string,
): Promise<{ agentId: string; archivedAt: string }> {
const { archivedAt, record: archivedRecord } = await archiveAgentCommand(
{
agentManager: this.agentManager,
agentStorage: this.agentStorage,
logger: this.sessionLogger,
},
agentId,
);
if (archivedRecord && this.agentUpdates.hasSubscription()) {
const payload = await this.agentUpdates.emitStoredRecord(archivedRecord);
if (payload.workspaceId) {
await this.emitWorkspaceUpdateForWorkspaceId(payload.workspaceId);
}
}
return { agentId, archivedAt };
}
private async handleDetachAgentRequest(agentId: string, requestId: string): Promise<void> {
this.sessionLogger.info({ agentId, requestId }, "Detaching agent from parent");
try {
const result = await detachAgentCommand({ agentManager: this.agentManager }, agentId);
const affectedWorkspaceIds = new Set<string>();
if (!result.live) {
const payload = await this.agentUpdates.emitStoredRecord(result.record);
if (payload.workspaceId) {
affectedWorkspaceIds.add(payload.workspaceId);
}
} else if (result.record.workspaceId) {
affectedWorkspaceIds.add(result.record.workspaceId);
}
if (result.previousParentAgentId) {
const rootWorkspaceId = await this.resolveDelegationRootWorkspaceId(
result.previousParentAgentId,
);
if (rootWorkspaceId) {
affectedWorkspaceIds.add(rootWorkspaceId);
}
}
await this.emitWorkspaceUpdatesForWorkspaceIds(affectedWorkspaceIds);
this.emit({
type: "agent.detach.response",
payload: {
requestId,
agentId,
accepted: true,
error: null,
},
});
} catch (error) {
const message = getErrorMessageOr(error, "Failed to detach agent");
this.sessionLogger.error({ err: error, agentId, requestId }, "Failed to detach agent");
this.emit({
type: "agent.detach.response",
payload: {
requestId,
agentId,
accepted: false,
error: message,
},
});
}
}
private async handleCloseItemsRequest(msg: CloseItemsRequest): Promise<void> {
const archiveResults = await Promise.allSettled(
msg.agentIds.map((agentId) => this.archiveAgentForClose(agentId)),
);
const agents = [];
for (let i = 0; i < archiveResults.length; i += 1) {
const result = archiveResults[i];
if (result.status === "fulfilled") {
agents.push(result.value);
} else {
this.sessionLogger.warn(
{ err: result.reason, agentId: msg.agentIds[i], requestId: msg.requestId },
"Failed to archive agent during close_items batch",
);
}
}
const terminals = [];
for (const terminalId of msg.terminalIds) {
try {
terminals.push(this.terminalController.killTerminalForClose(terminalId));
} catch (error) {
this.sessionLogger.warn(
{ err: error, terminalId, requestId: msg.requestId },
"Failed to kill terminal during close_items batch",
);
terminals.push({
terminalId,
success: false,
});
}
}
this.emit({
type: "close_items_response",
payload: {
agents,
terminals,
requestId: msg.requestId,
},
});
}
private async unarchiveAgentByHandle(handle: AgentPersistenceHandle): Promise<{
record: StoredAgentRecord;
didUnarchive: boolean;
originalArchivedAt: string | null;
} | null> {
const records = await this.agentStorage.listByProviderSession(
handle.provider,
handle.sessionId,
);
const matched = records.reduce<StoredAgentRecord | null>((latest, candidate) => {
if (!latest) {
return candidate;
}
const updatedDelta =
Date.parse(resolveStoredAgentUpdatedAt(candidate)) -
Date.parse(resolveStoredAgentUpdatedAt(latest));
if (updatedDelta !== 0) {
return updatedDelta > 0 ? candidate : latest;
}
return Date.parse(candidate.createdAt) > Date.parse(latest.createdAt) ? candidate : latest;
}, null);
if (!matched) {
return null;
}
const didUnarchive = await unarchiveAgentState(
this.agentStorage,
this.agentManager,
matched.id,
);
return {
record: matched,
didUnarchive,
originalArchivedAt: matched.archivedAt ?? null,
};
}
private async handleUpdateAgentRequest(
agentId: string,
name: string | undefined,
labels: Record<string, string> | undefined,
requestId: string,
): Promise<void> {
this.sessionLogger.info(
{
agentId,
requestId,
hasName: typeof name === "string",
labelCount: labels ? Object.keys(labels).length : 0,
},
"session: update_agent_request",
);
try {
const result = await updateAgentCommand(
{ agentManager: this.agentManager },
{ agentId, name, labels },
);
if (!result.accepted) {
this.emit({
type: "update_agent_response",
payload: {
requestId,
agentId,
accepted: false,
error: result.error,
},
});
return;
}
this.emit({
type: "update_agent_response",
payload: { requestId, agentId, accepted: true, error: null },
});
} catch (error) {
this.sessionLogger.error(
{ err: error, agentId, requestId },
"session: update_agent_request error",
);
this.emit({
type: "activity_log",
payload: {
id: uuidv4(),
timestamp: new Date(),
type: "error",
content: `Failed to update agent: ${getErrorMessage(error)}`,
},
});
this.emit({
type: "update_agent_response",
payload: {
requestId,
agentId,
accepted: false,
error: getErrorMessageOr(error, "Failed to update agent"),
},
});
}
}
private async handleProjectRenameRequest(
projectId: string,
customName: string | null,
requestId: string,
): Promise<void> {
this.sessionLogger.info(
{ projectId, requestId, hasCustomName: typeof customName === "string" },
"session: project.rename.request",
);
try {
const existing = await this.projectRegistry.get(projectId);
if (!existing) {
this.emit({
type: "project.rename.response",
payload: {
requestId,
projectId,
accepted: false,
customName: null,
error: "Project not found",
},
});
return;
}
const trimmed = customName?.trim() ?? "";
const nextCustomName = trimmed.length === 0 ? null : trimmed;
const updated = {
...existing,
customName: nextCustomName,
updatedAt: new Date().toISOString(),
};
await this.projectRegistry.upsert(updated);
this.emit({
type: "project.rename.response",
payload: {
requestId,
projectId,
accepted: true,
customName: nextCustomName,
error: null,
},
});
// Emit a project.update so clients that track the project as an empty
// project (no workspaces yet) receive the resolved name immediately.
await this.emitProjectUpdate({ kind: "upsert", project: updated });
// Re-emit descriptors for every workspace under this project so the new
// resolved name lands in the UI immediately.
const workspaces = await this.workspaceRegistry.listActive();
const affectedWorkspaceIds = workspaces
.filter((workspace) => workspace.projectId === existing.projectId)
.map((workspace) => workspace.workspaceId);
if (affectedWorkspaceIds.length > 0) {
await this.emitWorkspaceUpdatesForWorkspaceIds(affectedWorkspaceIds);
}
} catch (error) {
this.sessionLogger.error(
{ err: error, projectId, requestId },
"session: project.rename.request error",
);
this.emit({
type: "activity_log",
payload: {
id: uuidv4(),
timestamp: new Date(),
type: "error",
content: `Failed to rename project: ${getErrorMessage(error)}`,
},
});
this.emit({
type: "project.rename.response",
payload: {
requestId,
projectId,
accepted: false,
customName: null,
error: getErrorMessageOr(error, "Failed to rename project"),
},
});
}
}
private async handleProjectIconSetRequest(
request: Extract<SessionInboundMessage, { type: "project.icon.set.request" }>,
): Promise<void> {
const { projectId, requestId } = request;
try {
const updated = await setProjectCustomIcon({
paseoHome: this.paseoHome,
projectId,
source: request.source,
projects: this.projectRegistry,
});
this.emit({
type: "project.icon.set.response",
payload: { requestId, projectId, accepted: true, error: null },
});
await this.emitProjectUpdate({ kind: "upsert", project: updated });
const affectedWorkspaceIds = (await this.workspaceRegistry.listActive())
.filter((workspace) => workspace.projectId === projectId)
.map((workspace) => workspace.workspaceId);
if (affectedWorkspaceIds.length > 0) {
await this.emitWorkspaceUpdatesForWorkspaceIds(affectedWorkspaceIds);
}
} catch (error) {
this.emit({
type: "project.icon.set.response",
payload: {
requestId,
projectId,
accepted: false,
error: getErrorMessageOr(error, "Failed to update project icon"),
},
});
}
}
private async handleProjectIconGetRequest(projectId: string, requestId: string): Promise<void> {
try {
const project = await this.projectRegistry.get(projectId);
if (!project) throw new Error("Project not found");
const icon = await this.projectIcons.read(project);
this.emit({
type: "project.icon.get.response",
payload: { projectId, icon, error: null, requestId },
});
} catch (error) {
this.emit({
type: "project.icon.get.response",
payload: { projectId, icon: null, error: getErrorMessage(error), requestId },
});
}
}
private async handleProjectRemoveRequest(
request: Extract<SessionInboundMessage, { type: "project.remove.request" }>,
): Promise<void> {
const { projectId, requestId } = request;
this.sessionLogger.info({ projectId, requestId }, "session: project.remove.request");
try {
const project = await this.projectRegistry.get(projectId);
const resolvedProjectId = project?.projectId ?? projectId;
const projectWorkspaces = (await this.workspaceRegistry.listActive()).filter(
(workspace) => workspace.projectId === resolvedProjectId,
);
const activeWorkspaceIds = projectWorkspaces
.filter((workspace) => !workspace.archivedAt)
.map((workspace) => workspace.workspaceId);
if (activeWorkspaceIds.length > 0) {
this.markWorkspaceArchiving(activeWorkspaceIds, new Date().toISOString());
await this.emitWorkspaceUpdatesForWorkspaceIds(activeWorkspaceIds);
}
const removedWorkspaceIds: string[] = [];
try {
for (const workspaceId of activeWorkspaceIds) {
await archiveWorkspaceContents(
{
agentManager: this.agentManager,
agentStorage: this.agentStorage,
killTerminalsForWorkspace: (id) =>
this.terminalController.killTerminalsForWorkspace(id),
sessionLogger: this.sessionLogger,
},
workspaceId,
);
await this.archiveWorkspaceRecord(workspaceId);
removedWorkspaceIds.push(workspaceId);
}
await this.projectRegistry.remove(resolvedProjectId);
await removeProjectCustomIcon({
paseoHome: this.paseoHome,
projectId: resolvedProjectId,
}).catch((error) => {
this.sessionLogger.warn(
{ err: error, projectId: resolvedProjectId },
"Failed to clean up removed project icon",
);
});
} finally {
if (activeWorkspaceIds.length > 0) {
this.clearWorkspaceArchiving(activeWorkspaceIds);
}
}
const updateIds =
removedWorkspaceIds.length > 0
? removedWorkspaceIds
: [projectWorkspaces[0]?.workspaceId ?? projectId];
await this.emitWorkspaceUpdatesForWorkspaceIds(updateIds, {
removedProjectId: projectId,
});
this.emit({
type: "project.remove.response",
payload: {
requestId,
projectId,
accepted: true,
removedWorkspaceIds,
error: null,
},
});
} catch (error) {
this.sessionLogger.error(
{ err: error, projectId, requestId },
"session: project.remove.request error",
);
this.emit({
type: "activity_log",
payload: {
id: uuidv4(),
timestamp: new Date(),
type: "error",
content: `Failed to remove project: ${getErrorMessage(error)}`,
},
});
this.emit({
type: "project.remove.response",
payload: {
requestId,
projectId,
accepted: false,
removedWorkspaceIds: [],
error: getErrorMessageOr(error, "Failed to remove project"),
},
});
}
}
private async handleWorkspaceTitleSetRequest(
workspaceId: string,
title: string | null,
requestId: string,
): Promise<void> {
this.sessionLogger.info(
{ workspaceId, requestId, hasTitle: typeof title === "string" },
"session: workspace.title.set.request",
);
try {
const trimmed = title?.trim() ?? "";
const nextTitle = trimmed.length === 0 ? null : trimmed;
const updatedAt = new Date().toISOString();
const updated = await this.workspaceRegistry.update(workspaceId, (existing) => ({
...existing,
title: nextTitle,
updatedAt,
}));
if (!updated) {
this.emit({
type: "workspace.title.set.response",
payload: {
requestId,
workspaceId,
accepted: false,
title: null,
error: "Workspace not found",
},
});
return;
}
this.emit({
type: "workspace.title.set.response",
payload: {
requestId,
workspaceId,
accepted: true,
title: nextTitle,
error: null,
},
});
await this.emitWorkspaceUpdatesForWorkspaceIds([workspaceId]);
} catch (error) {
this.sessionLogger.error(
{ err: error, workspaceId, requestId },
"session: workspace.title.set.request error",
);
this.emit({
type: "activity_log",
payload: {
id: uuidv4(),
timestamp: new Date(),
type: "error",
content: `Failed to set workspace title: ${getErrorMessage(error)}`,
},
});
this.emit({
type: "workspace.title.set.response",
payload: {
requestId,
workspaceId,
accepted: false,
title: null,
error: getErrorMessageOr(error, "Failed to set workspace title"),
},
});
}
}
private async handleWorkspacePinSetRequest(
workspaceId: string,
pinned: boolean,
requestId: string,
): Promise<void> {
const logContext = { workspaceId, pinned, requestId };
this.sessionLogger.info(logContext, "session: workspace.pin.set.request");
const emitResponse = (accepted: boolean, pinnedAt: string | null, error: string | null) => {
this.emit({
type: "workspace.pin.set.response",
payload: { requestId, workspaceId, accepted, pinnedAt, error },
});
};
try {
const nextPinnedAt = pinned ? new Date().toISOString() : null;
const updatedAt = new Date().toISOString();
const updated = await this.workspaceRegistry.update(workspaceId, (existing) => ({
...existing,
pinnedAt: nextPinnedAt,
updatedAt,
}));
if (!updated) {
emitResponse(false, null, "Workspace not found");
return;
}
emitResponse(true, nextPinnedAt, null);
await this.emitWorkspaceUpdatesForWorkspaceIds([workspaceId]);
} catch (error) {
this.sessionLogger.error(
{ ...logContext, err: error },
"session: workspace.pin.set.request error",
);
this.emit({
type: "activity_log",
payload: {
id: uuidv4(),
timestamp: new Date(),
type: "error",
content: `Failed to pin workspace: ${getErrorMessage(error)}`,
},
});
emitResponse(false, null, getErrorMessageOr(error, "Failed to pin workspace"));
}
}
private async handleWorkspaceRecoveryInspectRequest(
request: Extract<SessionInboundMessage, { type: "workspace.recovery.inspect.request" }>,
): Promise<void> {
const state = await this.workspaceRecovery.inspect(request.workspaceId);
this.emit({
type: "workspace.recovery.inspect.response",
payload: {
requestId: request.requestId,
state,
},
});
}
private async handleWorkspaceRecoveryRestoreRequest(
request: Extract<SessionInboundMessage, { type: "workspace.recovery.restore.request" }>,
): Promise<void> {
try {
await this.restoreWorkspaceAndEmit(request.workspaceId);
this.emit({
type: "workspace.recovery.restore.response",
payload: {
requestId: request.requestId,
workspaceId: request.workspaceId,
accepted: true,
error: null,
},
});
} catch (error) {
const message = getErrorMessageOr(error, "Failed to recover workspace");
this.sessionLogger.warn(
{ err: error, workspaceId: request.workspaceId, requestId: request.requestId },
"session: workspace.recovery.restore.request rejected",
);
this.emit({
type: "workspace.recovery.restore.response",
payload: {
requestId: request.requestId,
workspaceId: request.workspaceId,
accepted: false,
error: message,
},
});
}
}
/**
* Handle text message to agent (with optional image attachments)
*/
private async handleSendAgentMessage(
agentId: string,
text: string,
messageId?: string,
images?: Array<{ data: string; mimeType: string }>,
attachments?: AgentAttachment[],
runOptions?: AgentRunOptions,
options?: { spokenInput?: boolean },
): Promise<{ ok: true } | { ok: false; error: string }> {
this.sessionLogger.info(
{
agentId,
textPreview: text.substring(0, 50),
imageCount: images?.length ?? 0,
attachmentCount: attachments?.length ?? 0,
},
`Sending text to agent ${agentId}${
images && images.length > 0 ? ` with ${images.length} image attachment(s)` : ""
}${
attachments && attachments.length > 0
? ` and ${attachments.length} structured attachment(s)`
: ""
}`,
);
const promptText = options?.spokenInput ? wrapSpokenInput(text) : text;
const prompt = buildAgentPrompt(promptText, images, attachments);
try {
await sendPromptToAgent({
agentManager: this.agentManager,
agentStorage: this.agentStorage,
agentId,
prompt,
messageId,
runOptions,
// A typed or spoken message from the human answers any permission the
// agent is blocked on.
clearPendingPermissions: true,
logger: this.sessionLogger,
});
return { ok: true };
} catch (error) {
this.handleAgentRunError(agentId, error, "Failed to send agent message");
return {
ok: false,
error: error instanceof Error ? error.message : String(error),
};
}
}
/**
* Handle create agent request
*/
private creationUpdate(snapshot: CreationSnapshot): SessionOutboundMessage {
return {
type: snapshot.kind === "workspace" ? "workspace.create.update" : "agent.create.update",
payload: snapshot,
};
}
private async handleCreationSubscription(
request: Extract<SessionInboundMessage, { type: "creation.subscribe.request" }>,
source?: object,
): Promise<void> {
let unsubscribe = () => {};
const owner =
request.subscribe === false
? undefined
: this.delivery.begin(
"creation",
undefined,
() => unsubscribe(),
JSON.stringify([request.kind, request.idempotencyKey]),
);
try {
const observation = await this.creationService.subscribe(
request.kind,
request.idempotencyKey,
(snapshot) => owner?.emit(this.creationUpdate(snapshot)),
);
unsubscribe = observation.unsubscribe;
if (!owner || owner.signal.aborted) unsubscribe();
this.emitForSource(
{
type: "creation.subscribe.response",
payload: {
requestId: request.requestId,
subscriptionId: owner?.responseId,
snapshot: observation.snapshot,
error: null,
},
},
source,
);
} catch (error) {
await owner?.release();
this.emitForSource(
{
type: "creation.subscribe.response",
payload: {
requestId: request.requestId,
snapshot: null,
error: error instanceof Error ? error.message : String(error),
},
},
source,
);
}
}
private creationResourceExists = async (
kind: "workspace" | "agent",
id: string,
): Promise<boolean> =>
kind === "workspace"
? (await this.workspaceRegistry.get(id)) !== null
: this.agentManager.getAgent(id) != null || (await this.agentStorage.get(id)) !== null;
private createRequestedAgent(
request: AgentCreateRequest,
observer?: (snapshot: CreationSnapshot) => void,
): Promise<CreationSnapshot> {
const { requestId, type: _type, subscribe: _subscribe, idempotencyKey, ...intent } = request;
return this.creationService.create(
{
kind: "agent",
key: idempotencyKey ?? requestId,
request: intent,
workspaceId: request.workspaceId,
agentId: request.agentId,
hasAgent: true,
hasPrompt: hasInitialCreationPrompt(request),
exists: this.creationResourceExists,
readAgent: async (id) => {
const record = await this.agentStorage.get(id);
return record ? this.buildStoredAgentPayload(record) : null;
},
createAgent: async (id, _workspace, onReady) => {
try {
return await this.createSessionAgent(
{ ...request, type: "create_agent_request" },
id,
onReady,
);
} catch (error) {
throw new WorktreeRequestError(toWorktreeWireError(error));
}
},
},
observer,
);
}
private async handleAgentCreation(request: AgentCreateRequest, source?: object): Promise<void> {
const { requestId, subscribe } = request;
const progress = subscribe
? this.delivery.operation(
(message) =>
message.type === "workspace.create.update" || message.type === "agent.create.update",
() => {},
)
: undefined;
try {
const creation = await this.createRequestedAgent(
request,
progress ? (snapshot) => progress.emit(this.creationUpdate(snapshot)) : undefined,
);
this.emitForSource(
{
type: "agent.create.response",
payload: { requestId, agent: creation.agent ?? null, error: creation.error, creation },
},
source,
);
} catch (error) {
this.emitForSource(
{
type: "agent.create.response",
payload: {
requestId,
agent: null,
error: error instanceof Error ? error.message : String(error),
},
},
source,
);
} finally {
await progress?.release();
}
}
private async handleWorkspaceCreation(
request: Extract<SessionInboundMessage, { type: "workspace.create.request" }>,
source?: object,
): Promise<void> {
const { requestId, subscribe, type: _type, idempotencyKey, ...intent } = request;
const progress = subscribe
? this.delivery.operation(
(message) =>
message.type === "workspace.create.update" || message.type === "agent.create.update",
() => {},
)
: undefined;
try {
if (
request.agent &&
!this.authorization.allowsPermission("workspace.write") &&
!this.authorization.allowsPermission("hub.execute")
) {
throw new Error("Session is not authorized to create an agent");
}
const agentInput = request.agent;
const creation = await this.creationService.create(
{
kind: "workspace",
key: idempotencyKey ?? requestId,
request: intent,
workspaceId: request.workspaceId,
agentId: agentInput?.agentId,
hasAgent: Boolean(agentInput),
hasPrompt: hasInitialCreationPrompt(agentInput),
exists: this.creationResourceExists,
provision: async (id) => {
const descriptor = await this.createRequestedWorkspace(
{
...request,
firstAgentContext: agentInput
? { prompt: agentInput.initialPrompt, attachments: agentInput.attachments }
: request.firstAgentContext,
},
id,
);
const workspace = await this.workspaceRegistry.get(descriptor.id);
return {
workspace: descriptor,
setupSkippedReason: workspace?.untrustedSource
? formatWorkspaceAutomationBlockedMessage(workspace.untrustedSource)
: undefined,
};
},
createAgent: agentInput
? async (id, workspace, onReady) => {
if (!workspace?.workspaceDirectory)
throw new Error("Created workspace has no directory");
const sourceCwd =
request.source.kind === "directory"
? request.source.path
: await resolveWorktreeSourceCwd(request.source, this.projectRegistry);
const relativeCwd = relative(resolve(sourceCwd), resolve(agentInput.config.cwd));
if (
relativeCwd === ".." ||
relativeCwd.startsWith(`..${sep}`) ||
isAbsolute(relativeCwd)
)
throw new Error("Agent directory must be inside the workspace source");
return this.createSessionAgent(
{
...agentInput,
type: "create_agent_request",
requestId,
config: {
...agentInput.config,
cwd: resolve(workspace.workspaceDirectory, relativeCwd),
},
workspaceId: workspace.id,
},
id,
onReady,
);
}
: undefined,
},
progress ? (snapshot) => progress.emit(this.creationUpdate(snapshot)) : undefined,
);
this.emitForSource(
{
type: "workspace.create.response",
payload: {
requestId,
workspace: creation.workspace ?? null,
agent: creation.agent,
creation,
setupSkippedReason: creation.setupSkippedReason,
error: creation.error,
errorCode: creation.errorCode,
setupTerminalId: null,
},
},
source,
);
} catch (error) {
this.emitForSource(
{
type: "workspace.create.response",
payload: {
requestId,
workspace: null,
error: error instanceof Error ? error.message : String(error),
errorCode:
error instanceof Error && "code" in error && typeof error.code === "string"
? error.code
: undefined,
setupTerminalId: null,
},
},
source,
);
} finally {
await progress?.release();
}
}
private async handleCreateAgentRequest(msg: CreateAgentRequestMessage): Promise<void> {
try {
let agent: AgentSnapshotPayload;
if (msg.idempotencyKey !== undefined) {
if (msg.initialPrompt !== undefined) {
throw new Error("Idempotent creation requires sending the initial prompt separately");
}
const creation = await this.createRequestedAgent({ ...msg, type: "agent.create.request" });
if (creation.error || !creation.agent)
throw new SessionRequestError(
creation.errorCode ?? "unknown",
creation.error ?? "Agent creation failed",
);
const record = await this.agentStorage.get(creation.agent.id);
if (!record) throw new Error("Previously created agent no longer exists");
agent = this.buildStoredAgentPayload(record);
} else {
({ agent } = await this.createSessionAgent(msg));
}
this.emit({
type: "status",
payload: {
status: "agent_created",
agentId: agent.id,
requestId: msg.requestId,
agent,
},
});
} catch (error) {
const wireError = error instanceof SessionRequestError ? error : toWorktreeWireError(error);
this.sessionLogger.error({ err: error }, "Failed to create agent");
this.emit({
type: "status",
payload: {
status: "agent_create_failed",
requestId: msg.requestId,
error: wireError.message,
errorCode: wireError.code,
},
});
this.emit({
type: "activity_log",
payload: {
id: uuidv4(),
timestamp: new Date(),
type: "error",
content: `Failed to create agent: ${wireError.message}`,
},
});
}
}
private validatePublicAgentWorkspaceIntent(request: CreateAgentRequestMessage): void {
if (request.git !== undefined || request.worktreeName !== undefined) {
throw new Error(
"Legacy git/worktreeName agent placement was removed. Use createWorkspace({ source, agent, idempotencyKey }) to create a workspace with an agent, or create the workspace first and pass workspaceId.",
);
}
if (request.internal)
throw new Error(
"Public internal agents were removed; create a background workspace instead.",
);
}
private async createSessionAgent(
msg: CreateAgentRequestMessage,
agentId?: string,
onAgentReady?: (agent: AgentSnapshotPayload) => Promise<void>,
): Promise<CreatedAgent> {
const {
config,
worktreeName,
initialPrompt,
clientMessageId,
outputSchema,
worktree,
autoArchive,
images,
attachments,
env,
} = msg;
this.sessionLogger.info(
{ cwd: config.cwd, provider: config.provider, worktreeName },
`Creating agent in ${config.cwd} (${config.provider})${
worktreeName ? ` with worktree ${worktreeName}` : ""
}`,
);
let createdWorktreeForCleanup: CreatePaseoWorktreeWorkflowResult | null = null;
let createdAgentId: string | null = null;
try {
const requestedCwd = resolve(config.cwd);
const needsRequestedDirectory = Boolean(worktree) || (!msg.workspaceId && !msg.callerAgentId);
if (needsRequestedDirectory && !(await this.filesystem.isDirectory(requestedCwd))) {
throw new Error(`Working directory does not exist or is not a directory: ${requestedCwd}`);
}
this.validatePublicAgentWorkspaceIntent(msg);
const trimmedPrompt = initialPrompt?.trim();
const { provisionalTitle } = resolveCreateAgentTitles({
configTitle: config.title,
initialPrompt: trimmedPrompt,
});
const firstAgentContext: FirstAgentContext = {
...(trimmedPrompt ? { prompt: trimmedPrompt } : {}),
...(attachments && attachments.length > 0 ? { attachments } : {}),
};
const workspacePromptTitle = resolveFirstAgentPromptTitle(firstAgentContext);
const createdWorktree = await this.createAgentLifecycleDispatch.createWorktreeForRequest({
cwd: config.cwd,
target: worktree,
firstAgentContext,
...(await this.workspaceBackgroundIntent(msg.background, msg.callerAgentId)),
});
createdWorktreeForCleanup = createdWorktree;
const resolvedIntent = await this.resolveSessionCreateAgentIntent({
request: msg,
createdWorktree,
workspacePromptTitle,
});
const resolvedCwd = resolve(resolvedIntent.config.cwd);
if (!(await this.filesystem.isDirectory(resolvedCwd))) {
throw new Error(`Working directory does not exist or is not a directory: ${resolvedCwd}`);
}
const { snapshot, liveSnapshot, initialPromptStarted } = await createAgentCommand(
{
agentManager: this.agentManager,
agentStorage: this.agentStorage,
logger: this.sessionLogger,
paseoHome: this.paseoHome,
worktreesRoot: this.worktreesRoot,
providerSnapshotManager: this.providerSnapshotManager,
},
{
kind: "session",
onAgentReady: async (agent) => {
createdAgentId = agent.id;
await onAgentReady?.(await this.buildAgentPayload(agent));
},
agentId,
config: resolvedIntent.config,
workspaceId: resolvedIntent.intent.workspaceId,
background: resolvedIntent.background,
initialPrompt,
source: msg.callerAgentId
? { kind: "agent-message", agentId: msg.callerAgentId }
: undefined,
clientMessageId,
outputSchema,
images,
attachments,
labels: resolvedIntent.intent.labels,
env,
provisionalTitle,
firstAgentContext,
},
);
createdAgentId = snapshot.id;
// `snapshot` predates the initial prompt; subscribers keep the last state they receive.
await this.agentUpdates.forwardLiveAgent(
this.agentManager.getAgent(snapshot.id) ?? liveSnapshot,
);
if (resolvedIntent.createdDirectoryWorkspace && trimmedPrompt) {
this.workspaceAutoName.scheduleForDirectory(
{
workspaceId: resolvedIntent.intent.workspaceId,
cwd: resolvedIntent.config.cwd,
firstAgentContext,
},
{ currentSelection: this.getFocusedAgentSelectionForCwd(resolvedIntent.config.cwd) },
);
}
this.createAgentLifecycleDispatch.registerAutoArchiveIfRequested({
autoArchive,
agentId: snapshot.id,
createdWorktree,
});
this.sessionLogger.info(
{ agentId: snapshot.id, provider: snapshot.provider },
"Created agent",
);
return { agent: await this.buildAgentPayload(liveSnapshot), initialPromptStarted };
} catch (error) {
await this.createAgentLifecycleDispatch.cleanupCreatedWorktreeAfterFailedAgentCreate({
createdWorktree: createdWorktreeForCleanup,
createdAgentId,
});
throw error;
}
}
private async resolveSessionCreateAgentIntent(input: {
request: CreateAgentRequestMessage;
createdWorktree: CreatePaseoWorktreeWorkflowResult | null;
workspacePromptTitle: string | null;
}): Promise<ResolvedSessionCreateAgentIntent> {
const { request, createdWorktree } = input;
const callerAgent = await this.resolveCreationCaller(request.callerAgentId);
let config = request.config;
const intent = await resolveCreateAgentIntent({
explicitWorkspaceId: createdWorktree?.workspace.workspaceId ?? request.workspaceId,
caller: callerAgent,
labels: request.labels,
resolveWorkspace: async (workspaceId) => {
if (createdWorktree?.workspace.workspaceId === workspaceId) {
return { workspaceId, cwd: createdWorktree.workspace.cwd };
}
const workspace = await this.workspaceRegistry.get(workspaceId);
if (!workspace || workspace.archivedAt) {
throw new Error(`Workspace ${workspaceId} not found`);
}
return { workspaceId, cwd: workspace.cwd };
},
createWorkspace: async () => ({
workspaceId: await this.workspaceProvisioning.resolveOrCreateWorkspaceIdForCreateAgent({
createdWorktree: null,
cwd: config.cwd,
initialTitle: input.workspacePromptTitle,
background: inheritWorkspaceBackground(request.background, callerAgent),
callerWorkspaceId: callerAgent?.workspaceId,
}),
cwd: config.cwd,
}),
});
config = { ...config, cwd: intent.cwd };
return {
config,
intent,
background: inheritAgentBackground(request.background, callerAgent),
createdDirectoryWorkspace: !createdWorktree && !request.workspaceId && !callerAgent,
};
}
private async handleResumeAgentRequest(
msg: Extract<SessionInboundMessage, { type: "resume_agent_request" }>,
): Promise<void> {
const { handle, overrides, requestId } = msg;
if (!handle) {
this.sessionLogger.warn("Resume request missing persistence handle");
if (requestId) {
this.emit({
type: "rpc_error",
payload: {
requestId,
requestType: msg.type,
error: "Unable to resume agent: missing persistence handle",
code: "agent_resume_failed",
},
});
}
this.emit({
type: "activity_log",
payload: {
id: uuidv4(),
timestamp: new Date(),
type: "error",
content: "Unable to resume agent: missing persistence handle",
},
});
return;
}
this.sessionLogger.info(
{ sessionId: handle.sessionId, provider: handle.provider },
`Resuming agent ${handle.sessionId} (${handle.provider})`,
);
try {
const matched = await this.unarchiveAgentByHandle(handle);
const effectiveOverrides = matched
? { ...buildConfigOverrides(matched.record), ...overrides }
: overrides;
let snapshot: ManagedAgent;
try {
snapshot = await this.agentManager.resumeAgentFromPersistence(handle, effectiveOverrides);
} catch (error) {
if (matched?.didUnarchive && matched.originalArchivedAt) {
await this.agentManager.archiveSnapshot(matched.record.id, matched.originalArchivedAt);
}
throw error;
}
await unarchiveAgentState(this.agentStorage, this.agentManager, snapshot.id);
await this.agentManager.hydrateTimelineFromProvider(snapshot.id);
await this.agentUpdates.forwardLiveAgent(snapshot);
const timelineSize = this.agentManager.getTimeline(snapshot.id).length;
if (requestId) {
const agentPayload = await this.buildAgentPayload(snapshot);
this.emit({
type: "status",
payload: {
status: "agent_resumed",
agentId: snapshot.id,
requestId,
timelineSize,
agent: agentPayload,
},
});
}
} catch (error) {
const message = getErrorMessage(error);
this.sessionLogger.error({ err: error }, "Failed to resume agent");
if (requestId) {
this.emit({
type: "rpc_error",
payload: {
requestId,
requestType: msg.type,
error: message,
code: "agent_resume_failed",
},
});
}
this.emit({
type: "activity_log",
payload: {
id: uuidv4(),
timestamp: new Date(),
type: "error",
content: `Failed to resume agent: ${message}`,
},
});
}
}
private async handleImportAgentRequest(
msg: Extract<SessionInboundMessage, { type: "import_agent_request" }>,
): Promise<void> {
const normalized = normalizeImportAgentRequest(msg);
if ("error" in normalized) {
this.emit({
type: "status",
payload: {
status: "agent_create_failed",
requestId: msg.requestId,
error: normalized.error,
},
});
return;
}
const { provider, providerHandleId, requestId } = normalized;
this.sessionLogger.info(
{ providerHandleId, provider },
`Importing agent ${providerHandleId} (${provider})`,
);
try {
if (!normalized.cwd) {
throw new Error("Import requires cwd from the selected provider session");
}
const { snapshot, timelineSize, createdWorkspace } = await importProviderSession({
request: normalized,
workspaceProvisioning: this.workspaceProvisioning,
agentManager: this.agentManager,
agentStorage: this.agentStorage,
logger: this.sessionLogger,
});
if (createdWorkspace) {
await this.registerWorkspaceForImportedAgent(createdWorkspace);
}
const agentPayload = await this.buildAgentPayload(snapshot);
this.emit({
type: "status",
payload: {
status: "agent_resumed",
agentId: snapshot.id,
requestId,
timelineSize,
agent: agentPayload,
},
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
this.sessionLogger.error({ err: error }, "Failed to import agent");
this.emit({
type: "status",
payload: {
status: "agent_create_failed",
requestId,
error: message,
},
});
this.emit({
type: "activity_log",
payload: {
id: uuidv4(),
timestamp: new Date(),
type: "error",
content: `Failed to import agent: ${message}`,
},
});
}
}
private async handleRefreshAgentRequest(
msg: Extract<SessionInboundMessage, { type: "refresh_agent_request" }>,
): Promise<void> {
const { agentId, requestId } = msg;
this.sessionLogger.info({ agentId }, `Refreshing agent ${agentId} from persistence`);
try {
await this.restoreOwningWorkspaceForLegacyAgentRefresh(agentId);
await unarchiveAgentState(this.agentStorage, this.agentManager, agentId);
let snapshot: ManagedAgent;
const existing = this.agentManager.getAgent(agentId);
if (existing) {
await this.interruptAgentIfRunning(agentId);
snapshot = await this.agentManager.reloadAgentSession(agentId, undefined, {
rehydrateFromDisk: true,
});
} else {
const record = await this.agentStorage.get(agentId);
if (!record) {
throw new Error(`Agent not found: ${agentId}`);
}
const registeredProviderIds = this.providerSnapshotManager.listRegisteredProviderIds();
if (!isStoredAgentProviderAvailable(record, registeredProviderIds)) {
throw new Error(`Agent ${agentId} references unavailable provider '${record.provider}'`);
}
if (!toAgentPersistenceHandle(registeredProviderIds, record.persistence)) {
throw new Error(`Agent ${agentId} cannot be refreshed because it lacks persistence`);
}
// Share the loader's per-agent in-flight operation with timeline fetches.
// Unarchiving publishes the record before provider resume finishes, so
// the agent pane can otherwise race this request and resume it twice.
snapshot = await ensureAgentLoaded(agentId, {
agentManager: this.agentManager,
agentStorage: this.agentStorage,
broadcastTimeline: true,
logger: this.sessionLogger,
});
}
await this.agentManager.hydrateTimelineFromProvider(agentId, { broadcast: true });
await this.agentUpdates.forwardLiveAgent(snapshot);
const timelineSize = this.agentManager.getTimeline(agentId).length;
if (requestId) {
this.emit({
type: "status",
payload: {
status: "agent_refreshed",
agentId,
requestId,
timelineSize,
},
});
}
} catch (error) {
const message = getErrorMessage(error);
this.sessionLogger.error({ err: error, agentId }, `Failed to refresh agent ${agentId}`);
if (requestId) {
this.emit({
type: "rpc_error",
payload: {
requestId,
requestType: msg.type,
error: message,
code: error instanceof WorktreeRequestError ? error.code : "agent_refresh_failed",
},
});
}
this.emit({
type: "activity_log",
payload: {
id: uuidv4(),
timestamp: new Date(),
type: "error",
content: `Failed to refresh agent: ${message}`,
},
});
}
}
private async handleCancelAgentRequest(agentId: string, requestId?: string): Promise<void> {
this.sessionLogger.info({ agentId }, `Cancel request received for agent ${agentId}`);
try {
await cancelAgentRunCommand(
{ agentManager: this.agentManager, logger: this.sessionLogger },
agentId,
);
if (requestId) {
const agent = this.agentManager.getAgent(agentId);
const payload = agent ? await this.buildAgentPayload(agent) : null;
this.emit({
type: "cancel_agent_response",
payload: {
requestId,
agentId,
agent: payload,
error: null,
},
});
}
} catch (error) {
if (requestId) {
this.sessionLogger.error(
{ err: error, agentId },
`Failed to cancel running agent on request for agent ${agentId}`,
);
const agent = this.agentManager.getAgent(agentId);
const payload = agent ? await this.buildAgentPayload(agent) : null;
this.emit({
type: "cancel_agent_response",
payload: {
requestId,
agentId,
agent: payload,
error: errorToFriendlyMessage(error),
},
});
} else {
this.handleAgentRunError(agentId, error, "Failed to cancel running agent on request");
}
}
}
private async handleAgentRewindRequest(
msg: Extract<SessionInboundMessage, { type: "agent.rewind.request" }>,
source?: object,
): Promise<void> {
try {
this.rewindInitiators.set(msg.agentId, source);
await this.agentManager.rewind(msg.agentId, msg.messageId, msg.mode);
this.emitForSource(
{
type: "agent.rewind.response",
payload: {
requestId: msg.requestId,
agentId: msg.agentId,
ok: true,
error: null,
},
},
source,
);
} catch (error) {
this.emitForSource(
{
type: "agent.rewind.response",
payload: {
requestId: msg.requestId,
agentId: msg.agentId,
ok: false,
error: error instanceof Error ? error.message : "Failed to rewind agent",
},
},
source,
);
} finally {
this.rewindInitiators.delete(msg.agentId);
}
}
private deliverTimelineReplacement(agentId: string, initiatingSource?: object): void {
const agent = this.agentManager.getAgent(agentId);
if (!agent) return;
const timeline = this.agentManager.fetchTimeline(agentId, { limit: 0 });
const epoch = timeline.epoch;
if (this.clientSources.size === 0 || !this.onMessageToSource) {
if (!this.supports(CLIENT_CAPS.timelineReplacementInvalidation)) {
this.emitReconstructedTimelineRows(agentId, agent.provider, timeline.rows, epoch);
}
return;
}
for (const subscription of this.timelineSubscriptions.values()) {
const source = subscription.owner.source;
if (!subscription.agentIds.has(agentId)) continue;
const modern = this.delivery.isModern(source);
if (
modern ||
(source !== initiatingSource &&
this.supportsForSource(CLIENT_CAPS.timelineReplacementInvalidation, source))
) {
subscription.owner.emit({
type: "agent.timeline.replacement",
payload: { agentId, epoch },
});
}
}
// COMPAT(timelineReplacementInvalidation): added in v0.5.0, remove reconstructed replay after 2027-02-21.
for (const [source, { capabilities }] of this.clientSources) {
if (
!this.delivery.isModern(source) &&
!capabilities.has(CLIENT_CAPS.timelineReplacementInvalidation)
) {
this.emitReconstructedTimelineRows(agentId, agent.provider, timeline.rows, epoch, source);
}
}
}
private emitReconstructedTimelineRows(
agentId: string,
provider: ManagedAgent["provider"],
rows: AgentTimelineFetchResult["rows"],
epoch: string,
source?: object,
): void {
for (const row of rows) {
if (!this.supportsTimelineItem(row.item, source)) {
continue;
}
const event = serializeAgentStreamEvent({
type: "timeline",
provider,
item: row.item,
...(row.turnId ? { turnId: row.turnId } : {}),
timestamp: row.timestamp,
});
if (!event) continue;
this.emitForSource(
{
type: "agent_stream",
payload: { agentId, event, timestamp: row.timestamp, seq: row.seq, epoch },
},
source,
);
}
}
private isPathWithinRoot(rootPath: string, candidatePath: string): boolean {
const resolvedRoot = resolve(rootPath);
const resolvedCandidate = resolve(candidatePath);
if (resolvedCandidate === resolvedRoot) {
return true;
}
return resolvedCandidate.startsWith(resolvedRoot + sep);
}
/**
* Handle clearing agent attention flag
*/
private async handleClearAgentAttention(
agentId: string | string[],
requestId?: string,
): Promise<void> {
const agentIds = Array.isArray(agentId) ? agentId : [agentId];
try {
await Promise.all(
agentIds.map((id) =>
ensureAgentLoaded(id, {
agentManager: this.agentManager,
agentStorage: this.agentStorage,
logger: this.sessionLogger,
}),
),
);
await Promise.all(agentIds.map((id) => this.agentManager.clearAgentAttention(id)));
if (requestId) {
const agents = (
await Promise.all(
agentIds.map(async (id) => {
const agent = this.agentManager.getAgent(id);
return agent ? this.buildAgentPayload(agent) : null;
}),
)
).filter((payload): payload is NonNullable<typeof payload> => payload !== null);
this.emit({
type: "clear_agent_attention_response",
payload: {
requestId,
agentId,
agents,
},
});
}
} catch (error) {
this.sessionLogger.error({ err: error, agentIds }, "Failed to clear agent attention");
// Don't throw - this is not critical
}
}
/**
* Handle client heartbeat for activity tracking
*/
private handleClientHeartbeat(msg: {
deviceType: "web" | "mobile";
focusedAgentId: string | null;
focusedTerminalId?: string | null;
lastActivityAt: string;
appVisible: boolean;
appVisibilityChangedAt?: string;
}): void {
const focusedTerminalId = msg.focusedTerminalId?.trim() || null;
const appVisibilityChangedAt = msg.appVisibilityChangedAt
? new Date(msg.appVisibilityChangedAt)
: new Date(msg.lastActivityAt);
const metadata = this.currentClientMetadata();
metadata.activity = {
deviceType: msg.deviceType,
focusedAgentId: msg.focusedAgentId,
focusedTerminalId,
lastActivityAt: new Date(msg.lastActivityAt),
appVisible: msg.appVisible,
appVisibilityChangedAt,
};
if (msg.appVisible && focusedTerminalId) {
void this.clearFocusedTerminalAttention(focusedTerminalId);
}
if (metadata.pushToken) {
this.pushNotifications.renew(metadata.pushToken);
}
}
private async clearFocusedTerminalAttention(terminalId: string): Promise<void> {
const terminalManager = this.terminalManager;
if (!terminalManager) {
return;
}
try {
await terminalManager.clearTerminalAttention(terminalId);
} catch (error) {
this.sessionLogger.warn({ err: error, terminalId }, "Failed to clear terminal attention");
}
}
/**
* Handle push token registration
*/
private handleRegisterPushToken(token: string): void {
this.currentClientMetadata().pushToken = token;
this.pushNotifications.renew(token);
this.sessionLogger.info("Registered push token");
}
/**
* Handle list commands request for an agent
*/
private async handleListCommandsRequest(
msg: Extract<SessionInboundMessage, { type: "list_commands_request" }>,
): Promise<void> {
const { agentId, requestId, draftConfig } = msg;
this.sessionLogger.debug(
{ agentId, draftConfig },
`Handling list commands request for agent ${agentId}`,
);
try {
const existing = this.agentManager.getAgent(agentId);
const stored = existing ? null : await this.agentStorage.get(agentId);
const agent =
existing || (stored && !stored.archivedAt)
? await ensureAgentLoaded(agentId, {
agentManager: this.agentManager,
agentStorage: this.agentStorage,
logger: this.sessionLogger,
})
: null;
if (agent?.session?.listCommands) {
const commands = await agent.session.listCommands();
this.emit({
type: "list_commands_response",
payload: {
agentId,
commands,
error: null,
requestId,
},
});
return;
}
if (!agent && draftConfig) {
const sessionConfig: AgentSessionConfig = {
provider: draftConfig.provider,
cwd: expandTilde(draftConfig.cwd),
...(draftConfig.modeId ? { modeId: draftConfig.modeId } : {}),
...(draftConfig.model ? { model: draftConfig.model } : {}),
...(draftConfig.thinkingOptionId
? { thinkingOptionId: draftConfig.thinkingOptionId }
: {}),
};
const commands = await this.agentManager.listDraftCommands(sessionConfig);
this.emit({
type: "list_commands_response",
payload: {
agentId,
commands,
error: null,
requestId,
},
});
return;
}
this.emit({
type: "list_commands_response",
payload: {
agentId,
commands: [],
error: agent ? `Agent does not support listing commands` : `Agent not found: ${agentId}`,
requestId,
},
});
} catch (error) {
this.sessionLogger.error({ err: error, agentId, draftConfig }, "Failed to list commands");
this.emit({
type: "list_commands_response",
payload: {
agentId,
commands: [],
error: getErrorMessage(error),
requestId,
},
});
}
}
/**
* Handle agent permission response from user
*/
private async handleAgentPermissionResponse(
agentId: string,
requestId: string,
response: AgentPermissionResponse,
): Promise<void> {
try {
await respondToAgentPermission({
agentManager: this.agentManager,
agentId,
requestId,
response,
logger: this.sessionLogger,
});
// COMPAT(ownedSubscriptions): added in v0.8.0, remove after 2027-03-09.
// Legacy clients consume the single domain resolution; modern request outcomes
// are independent of whether this socket (or its logical Session) observes it.
if (this.delivery.isModern(this.delivery.currentSource)) {
this.delivery.reply({
type: "agent_permission_resolved",
payload: { agentId, requestId, resolution: response },
});
}
} catch (error) {
this.sessionLogger.error(
{ err: error, agentId, requestId },
"Failed to respond to permission",
);
this.emit({
type: "activity_log",
payload: {
id: uuidv4(),
timestamp: new Date(),
type: "error",
content: `Failed to respond to permission: ${getErrorMessage(error)}`,
},
});
throw error;
}
}
private async handleDirectorySuggestionsRequest(msg: DirectorySuggestionsRequest): Promise<void> {
const { query, limit, requestId, cwd, includeFiles, includeDirectories, matchMode } = msg;
try {
const workspaceCwd = cwd?.trim();
const searchesWorkspace = Boolean(workspaceCwd);
const homeRoot = process.env.HOME ?? homedir();
// readdir on TCC-protected folders under ~/Library blocks forever for a process that cannot
// show a consent prompt, and each blocked call holds a libuv threadpool thread.
const excludedDiscoveryPaths =
!searchesWorkspace && process.platform === "darwin" ? [join(homeRoot, "Library")] : [];
const entries = await searchDirectoryEntries({
root: workspaceCwd ? expandTilde(workspaceCwd) : homeRoot,
query,
pathFormat: searchesWorkspace ? "relative" : "absolute",
pathQueryPolicy: searchesWorkspace ? "slashes" : "rooted",
blankQueryBehavior: searchesWorkspace ? "children" : "none",
rootAliases: searchesWorkspace ? [] : ["~"],
traversableHiddenDirectoryNames: searchesWorkspace
? WORKSPACE_SEARCH_HIDDEN_DIRECTORIES
: [],
confidentResultScanThreshold: searchesWorkspace ? undefined : 5_000,
respectGitIgnore: searchesWorkspace,
excludedDiscoveryPaths,
includeFiles,
includeDirectories,
matchMode,
limit,
});
const directories = entries
.filter((entry) => entry.kind === "directory")
.map((entry) => entry.path);
this.emit({
type: "directory_suggestions_response",
payload: {
directories,
entries,
error: null,
requestId,
},
});
} catch (error) {
this.emit({
type: "directory_suggestions_response",
payload: {
directories: [],
entries: [],
error: error instanceof Error ? error.message : String(error),
requestId,
},
});
}
}
private async handlePaseoWorktreeListRequest(
msg: Extract<SessionInboundMessage, { type: "paseo_worktree_list_request" }>,
): Promise<void> {
return handleWorktreeListRequest(
{
emit: (message) => this.emit(message),
paseoHome: this.paseoHome,
workspaceGitService: this.workspaceGitService,
},
msg,
);
}
private async handlePaseoWorktreeArchiveRequest(
msg: Extract<SessionInboundMessage, { type: "paseo_worktree_archive_request" }>,
): Promise<void> {
return handleWorktreeArchiveRequest(
{
paseoHome: this.paseoHome,
paseoWorktreesBaseRoot: this.worktreesRoot,
github: this.github,
workspaceGitService: this.workspaceGitService,
agentManager: this.agentManager,
agentStorage: this.agentStorage,
findWorkspaceIdForCwd: (cwd) => this.findWorkspaceIdForCwd(cwd),
listActiveWorkspaces: () => this.listActiveWorkspaceRefs(),
archiveWorkspaceRecord: (workspaceId) => this.archiveWorkspaceRecord(workspaceId),
emit: (message) => this.emit(message),
emitWorkspaceUpdatesForWorkspaceIds: (workspaceIds) =>
this.emitWorkspaceUpdatesForWorkspaceIds(workspaceIds),
markWorkspaceArchiving: (workspaceIds, archivingAt) =>
this.markWorkspaceArchiving(workspaceIds, archivingAt),
clearWorkspaceArchiving: (workspaceIds) => this.clearWorkspaceArchiving(workspaceIds),
killTerminalsForWorkspace: (workspaceId) =>
this.terminalController.killTerminalsForWorkspace(workspaceId),
sessionLogger: this.sessionLogger,
},
msg,
);
}
private async listTerminalActivityContributions(): Promise<
Array<{ cwd: string; workspaceId?: string; activity: TerminalActivity | null }>
> {
const terminalManager = this.terminalManager;
if (!terminalManager) {
return [];
}
const directories = terminalManager.listDirectories();
const terminalsByDirectory = await Promise.all(
directories.map((cwd) => terminalManager.getTerminals(cwd)),
);
return terminalsByDirectory.flat().map((session) => {
const contribution: { cwd: string; workspaceId?: string; activity: TerminalActivity | null } =
{
cwd: session.cwd,
activity: session.getActivity(),
};
if (session.workspaceId) {
contribution.workspaceId = session.workspaceId;
}
return contribution;
});
}
/**
* Build the current agent list payload (live + persisted), optionally filtered by labels.
*/
private listAgentPayloads(
filter?: Parameters<AgentDirectory["list"]>[0],
): Promise<AgentSnapshotPayload[]> {
return this.agentDirectory.list(filter);
}
private async resolveAgentIdentifier(
identifier: string,
): Promise<{ ok: true; agentId: string } | { ok: false; notFound: boolean; error: string }> {
const trimmed = identifier.trim();
if (!trimmed) {
return { ok: false, notFound: false, error: "Agent identifier cannot be empty" };
}
// An internal agent, live or recently archived, is addressable by its exact
// id and nothing else: the caller that created it holds the id, and no
// listing ever includes it.
if (this.agentManager.getAgent(trimmed) || this.agentManager.getRetiredInternalAgent(trimmed)) {
return { ok: true, agentId: trimmed };
}
// Explicit identifier resolution admits archived agents by ID prefix.
const stored = await this.agentStorage.listAll();
const storedRecords = stored.filter((record) => !record.internal);
const knownIds = new Set<string>();
for (const record of storedRecords) {
knownIds.add(record.id);
}
for (const agent of this.agentManager.listAgents()) {
knownIds.add(agent.id);
}
if (knownIds.has(trimmed)) {
return { ok: true, agentId: trimmed };
}
const prefixMatches = Array.from(knownIds).filter((id) => id.startsWith(trimmed));
if (prefixMatches.length === 1) {
return { ok: true, agentId: prefixMatches[0] };
}
if (prefixMatches.length > 1) {
return {
ok: false,
notFound: false,
error: `Agent identifier "${trimmed}" is ambiguous (${prefixMatches
.slice(0, 5)
.map((id) => id.slice(0, 8))
.join(", ")}${prefixMatches.length > 5 ? ", …" : ""})`,
};
}
const titleMatches = storedRecords.filter((record) => record.title === trimmed);
if (titleMatches.length === 1) {
return { ok: true, agentId: titleMatches[0].id };
}
if (titleMatches.length > 1) {
return {
ok: false,
notFound: false,
error: `Agent title "${trimmed}" is ambiguous (${titleMatches
.slice(0, 5)
.map((r) => r.id.slice(0, 8))
.join(", ")}${titleMatches.length > 5 ? ", …" : ""})`,
};
}
return { ok: false, notFound: true, error: `Agent not found: ${trimmed}` };
}
/**
* An agent that is no longer live, in its settled form: the retained snapshot
* of a recently archived internal agent, else its stored record. Null when
* neither exists, and for a stored internal record, which is never served.
*/
private async readSettledAgent(
agentId: string,
): Promise<{ record: StoredAgentRecord; lastMessage: string | null } | null> {
const retired = this.agentManager.getRetiredInternalAgent(agentId);
if (retired) {
return { record: retired.record, lastMessage: retired.lastMessage };
}
const record = await this.agentStorage.get(agentId);
if (!record || record.internal) {
return null;
}
return { record, lastMessage: await this.agentManager.getLastAssistantMessage(agentId) };
}
private async getAgentPayloadById(agentId: string): Promise<AgentSnapshotPayload | null> {
const live = this.agentManager.getAgent(agentId);
if (live) {
const payload = await this.buildAgentPayload(live);
return this.isProviderVisibleToClient(payload.provider) ? payload : null;
}
const settled = await this.readSettledAgent(agentId);
if (!settled) {
return null;
}
const payload = this.buildStoredAgentPayload(settled.record);
return this.isProviderVisibleToClient(payload.provider) ? payload : null;
}
private async resolveDelegationRootWorkspaceId(agentId: string): Promise<string | null> {
const seen = new Set<string>();
let currentAgentId = agentId;
while (true) {
if (seen.has(currentAgentId)) {
return null;
}
seen.add(currentAgentId);
const live = this.agentManager.getAgent(currentAgentId);
const source = live ?? (await this.agentStorage.get(currentAgentId));
if (!source) {
return null;
}
if ("archivedAt" in source && source.archivedAt) {
return null;
}
const parentAgentId = getParentAgentIdFromLabels(source.labels);
if (!parentAgentId) {
return source.workspaceId ?? null;
}
currentAgentId = parentAgentId;
}
}
private async buildActiveProjectPlacementsByWorkspaceId(): Promise<
Map<string, ProjectPlacementPayload>
> {
const [persistedWorkspaces, persistedProjects] = await Promise.all([
this.workspaceRegistry.listActive(),
this.projectRegistry.list(),
]);
const activeProjects = new Map(
persistedProjects
.filter((project) => !project.archivedAt)
.map((project) => [project.projectId, project] as const),
);
const placementsByWorkspaceId = new Map<string, ProjectPlacementPayload>();
const pairs = persistedWorkspaces.flatMap((workspace) => {
if (workspace.archivedAt) return [];
const project = activeProjects.get(workspace.projectId);
if (!project) return [];
return [{ workspace, project }];
});
const placements = await Promise.all(
pairs.map(({ workspace, project }) =>
this.buildProjectPlacementForWorkspace(workspace, project),
),
);
for (let i = 0; i < pairs.length; i += 1) {
placementsByWorkspaceId.set(pairs[i].workspace.workspaceId, placements[i]);
}
return placementsByWorkspaceId;
}
private async collectFetchAgentsEntries(params: {
candidates: AgentSnapshotPayload[];
limit: number;
getPlacement: (workspaceId: string | undefined) => Promise<ProjectPlacementPayload | null>;
filter: AgentUpdatesFilter | undefined;
search?: string;
}): Promise<FetchAgentsResponseEntry[]> {
const { candidates, limit, getPlacement, filter, search } = params;
const matchedEntries: FetchAgentsResponseEntry[] = [];
const batchSize = 25;
for (
let start = 0;
start < candidates.length && matchedEntries.length <= limit;
start += batchSize
) {
const batch = candidates.slice(start, start + batchSize);
const batchEntries = await Promise.all(
batch.map(async (agent) => {
const project = await getPlacement(agent.workspaceId);
return project ? { agent, project } : null;
}),
);
for (const entry of batchEntries) {
if (!entry) {
continue;
}
if (
!matchesAgentUpdatesFilter({
agent: entry.agent,
project: entry.project,
filter,
})
) {
continue;
}
if (search && !matchesAgentHistoryQuery(search, entry)) continue;
matchedEntries.push(entry);
if (matchedEntries.length > limit) {
break;
}
}
}
return matchedEntries;
}
/**
* Inclusive discovery for the requesting source. Exact-ID reads never pass through this.
* COMPAT(backgroundAgents): added in v0.15.0, remove after 2027-05-03 once client floor
* >= v0.15.0. Sources without the capability see background workspaces' agents only.
*/
private resolveBackgroundVisibility(
includeBackground: boolean | undefined,
): BackgroundVisibility {
if (!includeBackground) return false;
return this.supports(CLIENT_CAPS.backgroundAgents) ? true : "workspaces";
}
private async listFetchAgentsEntries(
request: AgentDirectoryRequestMessage,
backgroundVisibility: BackgroundVisibility,
): Promise<{
entries: FetchAgentsResponseEntry[];
pageInfo: FetchAgentsResponsePageInfo;
searchTruncated?: boolean;
}> {
const filter =
request.type === "fetch_agent_history_request" &&
request.filter?.includeArchived === undefined
? { ...request.filter, includeArchived: true }
: request.filter;
const scope = request.type === "fetch_agents_request" ? request.scope : undefined;
const sort = this.agentsPager.normalizeSort(request.sort);
let agents = await this.listAgentPayloads({
labels: filter?.labels,
includeArchived: filter?.includeArchived,
includeBackground: backgroundVisibility,
includeUnavailablePersisted: request.type === "fetch_agent_history_request",
});
const activePlacementsByWorkspaceId =
scope === "active" ? await this.buildActiveProjectPlacementsByWorkspaceId() : null;
if (activePlacementsByWorkspaceId) {
agents = agents.filter(
(agent) =>
!agent.archivedAt &&
agent.workspaceId != null &&
activePlacementsByWorkspaceId.has(agent.workspaceId),
);
}
const placementByWorkspaceId = new Map<string, Promise<ProjectPlacementPayload | null>>();
const getPlacement = (
workspaceId: string | undefined,
): Promise<ProjectPlacementPayload | null> => {
if (!workspaceId) {
return Promise.resolve(null);
}
if (activePlacementsByWorkspaceId) {
return Promise.resolve(activePlacementsByWorkspaceId.get(workspaceId) ?? null);
}
const existing = placementByWorkspaceId.get(workspaceId);
if (existing) {
return existing;
}
const placementPromise = this.buildProjectPlacementForWorkspaceId(workspaceId);
placementByWorkspaceId.set(workspaceId, placementPromise);
return placementPromise;
};
const search = agentDirectorySearchQuery(request);
let candidates = [...agents];
candidates.sort((left, right) => this.agentsPager.compare(left, right, sort));
const cursorToken = request.page?.cursor;
if (cursorToken) {
const cursor = this.decodeAgentCursor(cursorToken, sort);
candidates = candidates.filter(
(agent) => this.agentsPager.compareWithCursor(agent, cursor, sort) > 0,
);
}
const limit = request.page?.limit ?? 200;
const matchedEntries = await this.collectFetchAgentsEntries({
candidates,
limit,
getPlacement,
filter,
search,
});
const pagedEntries = matchedEntries.slice(0, limit);
const hasMore = matchedEntries.length > limit;
const nextCursor =
hasMore && pagedEntries.length > 0
? this.agentsPager.encode(pagedEntries[pagedEntries.length - 1].agent, sort)
: null;
return {
entries: pagedEntries,
pageInfo: {
nextCursor,
prevCursor: request.page?.cursor ?? null,
hasMore,
},
};
}
private readonly agentsPager = new SortablePager<
AgentSnapshotPayload,
FetchAgentsRequestSort["key"]
>({
validKeys: FETCH_AGENTS_SORT_KEYS,
defaultSort: [{ key: "updated_at", direction: "desc" }],
label: "fetch_agents",
getId: (agent) => agent.id,
getSortValue: (agent, key): number | string => {
switch (key) {
case "status_priority":
return getAgentStatusPriority({
status: agent.status,
pendingPermissionCount: agent.pendingPermissions?.length ?? 0,
requiresAttention: agent.requiresAttention,
attentionReason: agent.attentionReason ?? null,
});
case "created_at":
return Date.parse(agent.createdAt);
case "updated_at":
return Date.parse(agent.updatedAt);
case "title":
return agent.title?.toLocaleLowerCase() ?? "";
}
},
});
private decodeAgentCursor(token: string, sort: SortSpec<FetchAgentsRequestSort["key"]>[]) {
try {
return this.agentsPager.decode(token, sort);
} catch (error) {
if (error instanceof CursorError) {
throw new SessionRequestError("invalid_cursor", error.message);
}
throw error;
}
}
private async describeWorkspaceRecord(
workspace: PersistedWorkspaceRecord,
projectRecord?: PersistedProjectRecord | null,
): Promise<WorkspaceDescriptorPayload> {
const resolvedProjectRecord =
projectRecord ?? (await this.projectRegistry.get(workspace.projectId));
let diffStat: { additions: number; deletions: number } | null = null;
const snapshot = this.workspaceRuntimeStore.get(workspace.cwd);
if (snapshot?.git.diffStat) {
diffStat = snapshot.git.diffStat;
}
const worktreeSlug =
workspace.isPaseoOwnedWorktree && workspace.worktreeRoot
? basename(workspace.worktreeRoot)
: undefined;
return {
id: workspace.workspaceId,
projectId: workspace.projectId,
projectDisplayName: resolvedProjectRecord
? resolveProjectDisplayName(resolvedProjectRecord)
: workspace.projectId,
projectCustomName: resolvedProjectRecord?.customName ?? null,
projectCustomIconRevision: resolvedProjectRecord?.customIconRevision ?? null,
projectRootPath: resolvedProjectRecord?.rootPath ?? workspace.cwd,
workspaceDirectory: workspace.cwd,
setupStatus: this.workspaceSetupObservation.status(workspace),
worktreeSlug,
projectKind: (resolvedProjectRecord?.kind ?? "directory") === "git" ? "git" : "non_git",
workspaceKind: workspace.kind,
name: resolveWorkspaceDisplayName(workspace),
title: workspace.title,
pinnedAt: workspace.pinnedAt,
...(workspace.labels && workspace.labels.length > 0 ? { labels: workspace.labels } : {}),
background: workspace.background,
archivingAt: null,
status: "done",
statusEnteredAt: null,
activityAt: null,
createdAt: workspace.createdAt,
diffStat,
scripts: await this.buildWorkspaceScriptPayloadSnapshot(workspace, resolvedProjectRecord),
...(resolvedProjectRecord
? {
project: await this.buildProjectPlacementForWorkspace(workspace, resolvedProjectRecord),
}
: {}),
};
}
private buildWorkspaceGitRuntimePayload(
snapshot: WorkspaceGitRuntimeSnapshot,
): NonNullable<WorkspaceDescriptorPayload["gitRuntime"]> | null {
if (!snapshot.git.isGit) {
return null;
}
return {
currentBranch: snapshot.git.currentBranch,
remoteUrl: snapshot.git.remoteUrl,
isPaseoOwnedWorktree: snapshot.git.isPaseoOwnedWorktree,
isDirty: snapshot.git.isDirty,
aheadBehind: snapshot.git.aheadBehind,
aheadOfOrigin: snapshot.git.aheadOfOrigin,
behindOfOrigin: snapshot.git.behindOfOrigin,
};
}
private buildWorkspaceGitHubRuntimePayload(
snapshot: WorkspaceGitRuntimeSnapshot,
): NonNullable<WorkspaceDescriptorPayload["githubRuntime"]> {
return {
featuresEnabled: snapshot.forge.featuresEnabled,
pullRequest: snapshot.forge.pullRequest,
error: snapshot.forge.error,
};
}
private async describeWorkspaceRecordWithGitData(
workspace: PersistedWorkspaceRecord,
projectRecord?: PersistedProjectRecord | null,
): Promise<WorkspaceDescriptorPayload> {
const base = await this.describeWorkspaceRecord(workspace, projectRecord);
const snapshot = this.workspaceRuntimeStore.get(workspace.cwd);
if (!snapshot) {
return base;
}
const checkout = checkoutLiteFromGitSnapshot(workspace.cwd, snapshot.git);
const displayName = deriveWorkspaceDisplayName({ cwd: workspace.cwd, checkout });
return {
...base,
name: resolveWorkspaceName({ title: workspace.title, derivedDisplayName: displayName }),
diffStat: snapshot.git.diffStat ?? null,
gitRuntime: this.buildWorkspaceGitRuntimePayload(snapshot) ?? undefined,
githubRuntime: this.buildWorkspaceGitHubRuntimePayload(snapshot),
// Reuse the forge already resolved on the snapshot (probe-aware; GitHub-only
// resolves to "github") so the sidebar/hover-card brand mark matches the
// status projection without a second resolve.
forge: snapshot.forge.forge,
};
}
private async describeCreatedWorktreeWorkspace(
result: CreatePaseoWorktreeResult,
): Promise<WorkspaceDescriptorPayload> {
const projectRecord = await this.projectRegistry.get(result.workspace.projectId);
return {
id: result.workspace.workspaceId,
projectId: result.workspace.projectId,
projectDisplayName: projectRecord
? resolveProjectDisplayName(projectRecord)
: result.workspace.projectId,
projectCustomName: projectRecord?.customName ?? null,
projectCustomIconRevision: projectRecord?.customIconRevision ?? null,
projectRootPath: projectRecord?.rootPath ?? result.repoRoot,
workspaceDirectory: result.workspace.cwd,
setupStatus: this.workspaceSetupObservation.status(result.workspace),
worktreeSlug: basename(result.worktree.worktreePath),
projectKind: projectRecord?.kind ?? "git",
workspaceKind: result.workspace.kind,
background: result.workspace.background,
name: resolveWorkspaceName({
title: result.workspace.title,
derivedDisplayName: result.worktree.branchName || result.workspace.displayName,
}),
title: result.workspace.title,
pinnedAt: result.workspace.pinnedAt,
...(result.workspace.labels && result.workspace.labels.length > 0
? { labels: result.workspace.labels }
: {}),
archivingAt: null,
status: "done",
statusEnteredAt: result.workspace.createdAt,
activityAt: null,
createdAt: result.workspace.createdAt,
lastUserMessageAt: null,
diffStat: { additions: 0, deletions: 0 },
scripts: [],
gitRuntime: {
currentBranch: result.worktree.branchName || null,
remoteUrl: null,
isPaseoOwnedWorktree: true,
isDirty: false,
aheadBehind: null,
aheadOfOrigin: null,
behindOfOrigin: null,
},
githubRuntime: null,
};
}
private async buildWorkspaceDescriptor(input: {
workspace: PersistedWorkspaceRecord;
projectRecord?: PersistedProjectRecord | null;
includeGitData: boolean;
}): Promise<WorkspaceDescriptorPayload> {
if (input.includeGitData && input.workspace.kind !== "directory") {
return this.describeWorkspaceRecordWithGitData(input.workspace, input.projectRecord);
}
return this.describeWorkspaceRecord(input.workspace, input.projectRecord);
}
markWorkspaceArchiving(workspaceIds: Iterable<string>, archivingAt: string): void {
this.workspaceDirectory.markArchiving(workspaceIds, archivingAt);
}
clearWorkspaceArchiving(workspaceIds: Iterable<string>): void {
this.workspaceDirectory.clearArchiving(workspaceIds);
}
private async buildWorkspaceDescriptorMap(options: {
includeGitData: boolean;
workspaceIds?: Iterable<string>;
}): Promise<Map<string, WorkspaceDescriptorPayload>> {
return this.workspaceDirectory.buildDescriptorMap(options);
}
// external path→workspace adapter, not ownership. Used by archive-by-path flows
// where the request carries a worktree path (unique to one workspace) rather
// than a workspaceId. This is a directory lookup for an archive target, not a
// status/ownership attribution.
private async findWorkspaceIdForCwd(cwd: string): Promise<string | null> {
// Explicit archive administration must prefer an archived exact match to an active ancestor.
const workspaces = await this.workspaceRegistry.listAll();
return resolveWorkspaceIdForPath(cwd, workspaces);
}
private matchesWorkspaceFilter(input: {
workspace: WorkspaceDescriptorPayload;
filter: FetchWorkspacesRequestFilter | undefined;
}): boolean {
return this.workspaceDirectory.matchesFilter(input);
}
private async listFetchWorkspacesEntries(
request: Extract<SessionInboundMessage, { type: "fetch_workspaces_request" }>,
): Promise<{
entries: FetchWorkspacesResponseEntry[];
emptyProjects: WorkspaceProjectDescriptorPayload[];
pageInfo: FetchWorkspacesResponsePageInfo;
}> {
try {
return await this.workspaceDirectory.listFetchEntries({
...request,
filter: {
...request.filter,
...(request.includeBackground ? { includeBackground: true } : {}),
},
});
} catch (error) {
if (error instanceof CursorError) {
throw new SessionRequestError("invalid_cursor", error.message);
}
throw error;
}
}
private bufferOrEmitWorkspaceUpdate(
subscription: WorkspaceUpdatesSubscriptionState,
payload: WorkspaceUpdatePayload,
): void {
if (this.workspaceUpdatesSubscriptions.get(subscription.subscriptionId) !== subscription)
return;
if (payload.kind === "upsert") {
subscription.visibleEmptyProjectIds?.delete(payload.workspace.projectId);
} else {
if (payload.emptyProject) {
subscription.visibleEmptyProjectIds?.add(payload.emptyProject.projectId);
}
if (payload.removedProjectId) {
subscription.visibleEmptyProjectIds?.delete(payload.removedProjectId);
}
}
if (subscription.isBootstrapping) {
const workspaceId = payload.kind === "upsert" ? payload.workspace.id : payload.id;
subscription.pendingUpdatesByWorkspaceId.set(workspaceId, payload);
return;
}
const workspaceId = payload.kind === "upsert" ? payload.workspace.id : payload.id;
subscription.lastEmittedByWorkspaceId.set(workspaceId, payload);
subscription.owner.emit({ type: "workspace_update", payload });
}
private flushBootstrappedWorkspaceUpdates(
subscription: WorkspaceUpdatesSubscriptionState,
options?: {
snapshotByWorkspaceId?: Map<
string,
{ status: string; statusEnteredAt: string | null; activityAtMs: number | null }
>;
},
): void {
if (
this.workspaceUpdatesSubscriptions.get(subscription.subscriptionId) !== subscription ||
!subscription.isBootstrapping
) {
return;
}
subscription.isBootstrapping = false;
const pending = Array.from(subscription.pendingUpdatesByWorkspaceId.values());
subscription.pendingUpdatesByWorkspaceId.clear();
for (const payload of pending) {
if (payload.kind === "upsert") {
const snapshot = options?.snapshotByWorkspaceId?.get(payload.workspace.id);
const updateActivityAtMs = payload.workspace.activityAt
? Date.parse(payload.workspace.activityAt)
: null;
const shouldEmit = shouldEmitPendingBootstrapUpdate({
snapshot: snapshot
? {
status: snapshot.status,
statusEnteredAt: snapshot.statusEnteredAt,
activityAtMs: snapshot.activityAtMs,
}
: null,
update: {
status: payload.workspace.status,
statusEnteredAt: payload.workspace.statusEnteredAt ?? null,
activityAtMs: Number.isNaN(updateActivityAtMs) ? null : updateActivityAtMs,
},
});
if (!shouldEmit) {
continue;
}
}
const workspaceId = payload.kind === "upsert" ? payload.workspace.id : payload.id;
subscription.lastEmittedByWorkspaceId.set(workspaceId, payload);
subscription.owner.emit({ type: "workspace_update", payload });
}
}
private async buildProjectDescriptor(
project: PersistedProjectRecord,
): Promise<WorkspaceProjectDescriptorPayload> {
const icon = await this.projectIcons.snapshot(project);
return {
projectId: project.projectId,
...(project.projectKey ? { projectKey: project.projectKey } : {}),
projectDisplayName: resolveProjectDisplayName(project),
projectCustomName: project.customName ?? null,
projectCustomIconRevision: project.customIconRevision ?? null,
projectIconRevision: icon.revision,
projectRootPath: project.rootPath,
projectKind: project.kind,
};
}
private async restoreWorkspaceAndEmit(workspaceId: string): Promise<void> {
await this.workspaceRecovery.restore(workspaceId);
const workspace = await this.workspaceRegistry.get(workspaceId);
if (!workspace) {
throw new Error(`Recovered workspace record not found: ${workspaceId}`);
}
if (this.onWorkspaceRecovered) {
try {
await this.onWorkspaceRecovered(workspace);
return;
} catch (error) {
this.sessionLogger.warn(
{ err: error, workspaceId },
"Failed to publish workspace recovery to active sessions",
);
}
}
await this.refreshRecoveredWorkspaceForExternalMutation(workspace);
}
private async restoreOwningWorkspaceForLegacyAgentRefresh(agentId: string): Promise<void> {
// COMPAT(worktreeRestore): clients older than v0.1.105 used refresh_agent_request
// as their explicit recovery RPC. Remove after 2027-01-11.
if (!clientUsesLegacyWorkspaceRestore(this.appVersion)) {
return;
}
const record = await this.agentStorage.get(agentId);
if (!record?.workspaceId) {
return;
}
const recovery = await this.workspaceRecovery.inspect(record.workspaceId);
if (recovery.kind !== "recoverable") {
return;
}
await this.restoreWorkspaceAndEmit(record.workspaceId);
}
private async createPaseoWorktree(
input: CreatePaseoWorktreeInput,
options?: {
resolveDefaultBranch?: (repoRoot: string) => Promise<string>;
},
): Promise<CreatePaseoWorktreeResult> {
const result = await createPaseoWorktree(input, {
lifecycle: this.pluginRuntime,
github: this.github,
...(options?.resolveDefaultBranch
? { resolveDefaultBranch: options.resolveDefaultBranch }
: {}),
workspaceGitService: this.workspaceGitService,
workspaceProvisioning: this.workspaceProvisioning,
});
void Promise.all([
this.gitMutation.notifyGitMutation(input.cwd, "create-worktree"),
this.gitMutation.notifyGitMutation(result.worktree.worktreePath, "create-worktree"),
]).catch((error) => {
this.sessionLogger.warn(
{ err: error, cwd: input.cwd, worktreePath: result.worktree.worktreePath },
"Failed to warm git snapshots after creating worktree",
);
});
return result;
}
private async listActiveWorkspaceRefs(): Promise<ActiveWorkspaceRef[]> {
const workspaces = await this.workspaceRegistry.listActive();
return workspaces
.filter((workspace) => !workspace.archivedAt)
.map((workspace) => ({
workspaceId: workspace.workspaceId,
cwd: workspace.cwd,
kind: workspace.kind,
worktreeRoot: workspace.worktreeRoot,
isPaseoOwnedWorktree: workspace.isPaseoOwnedWorktree,
mainRepoRoot: workspace.mainRepoRoot,
}));
}
private async archiveWorkspaceRecord(workspaceId: string, archivedAt?: string): Promise<void> {
const archiveTimestamp = archivedAt ?? new Date().toISOString();
const existingWorkspace = await archivePersistedWorkspaceRecord({
workspaceId,
archivedAt: archiveTimestamp,
workspaceRegistry: this.workspaceRegistry,
});
if (!existingWorkspace) {
this.workspaceGitObserver.removeForWorkspaceId(workspaceId);
return;
}
if (!existingWorkspace.archivedAt) {
const activeSiblings = (await this.workspaceRegistry.listActive()).filter(
(workspace) => workspace.projectId === existingWorkspace.projectId && !workspace.archivedAt,
);
this.sessionLogger.info(
{
workspaceId,
workspaceCwd: existingWorkspace.cwd,
projectId: existingWorkspace.projectId,
projectArchived: activeSiblings.length === 0,
archivedAt: archiveTimestamp,
},
"Workspace archived",
);
}
await this.teardownArchivedWorkspace(existingWorkspace.workspaceId);
}
private async teardownArchivedWorkspace(workspaceId: string): Promise<void> {
this.workspaceGitObserver.removeForWorkspaceId(workspaceId);
this.scriptRuntimeStore?.removeForWorkspace(workspaceId);
releaseWorkspaceServicePortPlan(workspaceId);
}
private async emitWorkspaceUpdatesForWorkspaceIds(
workspaceIds: Iterable<string>,
options?: WorkspaceUpdateOptions,
): Promise<void> {
const uniqueWorkspaceIds = new Set(workspaceIds);
if (uniqueWorkspaceIds.size === 0) return;
for (const subscription of this.workspaceUpdatesSubscriptions.values()) {
await this.enqueueWorkspaceUpdates(uniqueWorkspaceIds, subscription, options);
}
}
private enqueueWorkspaceUpdates(
workspaceIds: ReadonlySet<string>,
subscription: WorkspaceUpdatesSubscriptionState,
options: WorkspaceUpdateOptions | undefined,
): Promise<void> {
const previous = Array.from(workspaceIds, (workspaceId) =>
(this.workspaceUpdateTails.get(workspaceId) ?? Promise.resolve()).catch(() => undefined),
);
const next = Promise.all(previous).then(() =>
this.delivery.forSource(subscription.owner.source, () =>
this.emitWorkspaceUpdateBatch(workspaceIds, subscription, options),
),
);
for (const workspaceId of workspaceIds) {
this.workspaceUpdateTails.set(workspaceId, next);
}
const clearTail = () => {
for (const workspaceId of workspaceIds) {
if (this.workspaceUpdateTails.get(workspaceId) === next) {
this.workspaceUpdateTails.delete(workspaceId);
}
}
};
void next.then(clearTail, clearTail);
return next;
}
private async emitWorkspaceUpdateBatch(
workspaceIds: ReadonlySet<string>,
subscription: WorkspaceUpdatesSubscriptionState,
options: WorkspaceUpdateOptions | undefined,
): Promise<void> {
if (this.workspaceUpdatesSubscriptions.get(subscription.subscriptionId) !== subscription) {
return;
}
const descriptorsByWorkspaceId = await this.buildWorkspaceDescriptorMap({
workspaceIds,
includeGitData: true,
});
for (const workspaceId of workspaceIds) {
if (this.workspaceUpdatesSubscriptions.get(subscription.subscriptionId) !== subscription) {
return;
}
const workspace = descriptorsByWorkspaceId.get(workspaceId);
const filteredWorkspace =
workspace && this.matchesWorkspaceFilter({ workspace, filter: subscription.filter })
? workspace
: null;
const nextWorkspace = this.applyOptimisticWorkspaceStatus(
filteredWorkspace,
options?.optimisticStatus,
);
const lastEmitted = subscription.lastEmittedByWorkspaceId.get(workspaceId);
this.workspaceGitObserver.recordDescriptorState(workspaceId, nextWorkspace);
if (!nextWorkspace) {
if (this.shouldSkipWorkspaceRemoval(lastEmitted, options?.removedProjectId)) {
continue;
}
if (this.workspaceUpdatesSubscriptions.get(subscription.subscriptionId) !== subscription) {
return;
}
subscription.lastEmittedByWorkspaceId.delete(workspaceId);
const removePayload = await this.buildWorkspaceRemoveUpdatePayload(
workspaceId,
options?.removedProjectId,
);
this.bufferOrEmitWorkspaceUpdate(
subscription,
this.directorySync.sequenceWorkspaceUpdate(
removePayload,
workspace ?? null,
workspaceId,
subscription.syncEnabled === true,
subscription.filter?.includeBackground,
),
);
continue;
}
const nextPayload: WorkspaceUpdatePayload = this.directorySync.sequenceWorkspaceUpdate(
{ kind: "upsert", workspace: nextWorkspace },
workspace ?? null,
workspaceId,
subscription.syncEnabled === true,
subscription.filter?.includeBackground,
);
if (
lastEmitted &&
lastEmitted.kind === "upsert" &&
equal(lastEmitted.workspace, nextWorkspace)
) {
continue;
}
this.bufferOrEmitWorkspaceUpdate(subscription, nextPayload);
}
}
private applyOptimisticWorkspaceStatus(
workspace: WorkspaceDescriptorPayload | null,
optimisticStatus: WorkspaceDescriptorPayload["status"] | undefined,
): WorkspaceDescriptorPayload | null {
if (!workspace || !optimisticStatus) {
return workspace;
}
return { ...workspace, status: optimisticStatus };
}
private shouldSkipWorkspaceRemoval(
lastEmitted: WorkspaceUpdatePayload | undefined,
removedProjectId: string | undefined,
): boolean {
if (lastEmitted?.kind === "remove") {
return !removedProjectId || lastEmitted.removedProjectId === removedProjectId;
}
return !lastEmitted && !removedProjectId;
}
private async buildWorkspaceRemoveUpdatePayload(
workspaceId: string,
removedProjectId?: string,
): Promise<WorkspaceUpdatePayload> {
if (removedProjectId) {
return { kind: "remove", id: workspaceId, removedProjectId };
}
return {
kind: "remove",
id: workspaceId,
...(await this.resolveProjectWithoutActiveWorkspacesForArchivedWorkspace(workspaceId)),
};
}
// When a workspace is archived its project may have no active workspaces left.
// Resolve that project parent so the `remove` update can carry it, keeping the
// sidebar in sync without a full re-hydration.
private async resolveProjectWithoutActiveWorkspacesForArchivedWorkspace(
workspaceId: string,
): Promise<{ emptyProject: WorkspaceProjectDescriptorPayload } | null> {
const archivedWorkspace = await this.workspaceRegistry.get(workspaceId);
if (!archivedWorkspace) {
return null;
}
const projectWithoutActiveWorkspaces = (await this.workspaceDirectory.listEmptyProjects()).find(
(project) => project.projectId === archivedWorkspace.projectId,
);
return projectWithoutActiveWorkspaces ? { emptyProject: projectWithoutActiveWorkspaces } : null;
}
private async emitWorkspaceUpdateForTerminalContribution(
event: TerminalWorkspaceContributionChangedEvent,
): Promise<void> {
// A terminal's activity contributes only to the workspace it carries. A
// terminal with no workspaceId attributes to nothing — status is per-id.
if (!event.workspaceId) {
return;
}
await this.emitWorkspaceUpdatesForWorkspaceIds([event.workspaceId]);
}
// A git fact (branch, diff, dirty, PR) changed at `cwd`. Every workspace whose
// OWN cwd is this folder re-derives its git facts from that folder (id → cwd)
// and emits its own per-id descriptor. This is a deliberate same-folder fan,
// not a cwd → id ownership lookup: git never resolves which workspace owns a
// path. See `workspaceIdsOnCheckout`.
private async emitWorkspaceUpdateForCwd(cwd: string, options?: {}): Promise<void> {
if (this.workspaceUpdatesSubscriptions.size === 0) return;
const workspaceIds = workspaceIdsOnCheckout(await this.workspaceRegistry.listActive(), cwd);
if (workspaceIds.length === 0) {
return;
}
await this.emitWorkspaceUpdatesForWorkspaceIds(workspaceIds, options);
}
private async handleFetchAgents(
request: Extract<SessionInboundMessage, { type: "fetch_agents_request" }>,
): Promise<void> {
const owner = request.subscribe
? this.delivery.begin("agents", request.subscribe.subscriptionId, (id) => {
this.agentUpdates.clearSubscription(id);
this.refreshObservationProducers();
})
: null;
const subscriptionId = owner?.responseId;
const backgroundVisibility = this.resolveBackgroundVisibility(
request.includeBackground || request.filter?.includeBackground,
);
try {
if (owner) {
this.agentUpdates.beginSubscription({
subscriptionId: owner.id,
isProviderVisible: (provider) =>
this.delivery.forSource(owner.source, () => this.isProviderVisibleToClient(provider)),
filter: request.filter,
backgroundVisibility,
syncEnabled: Boolean(request.sync),
emit: (message) => {
if (message.type === "agent_update") owner.emit(message);
},
});
this.refreshObservationProducers();
}
const payload = request.sync
? await this.readAgentDirectorySync(request, backgroundVisibility)
: await this.listFetchAgentsEntries(request, backgroundVisibility);
const snapshotUpdatedAtByAgentId = new Map<string, number>();
for (const entry of payload.entries) {
const parsedUpdatedAt = Date.parse(entry.agent.updatedAt);
if (!Number.isNaN(parsedUpdatedAt)) {
snapshotUpdatedAtByAgentId.set(entry.agent.id, parsedUpdatedAt);
}
}
this.emit({
type: "fetch_agents_response",
payload: {
requestId: request.requestId,
...(subscriptionId ? { subscriptionId } : {}),
...payload,
},
});
if (subscriptionId) {
this.agentUpdates.flushBootstrapped(owner!.id, { snapshotUpdatedAtByAgentId });
}
} catch (error) {
if (subscriptionId) {
await owner?.release();
}
const code = error instanceof SessionRequestError ? error.code : "fetch_agents_failed";
const message = error instanceof Error ? error.message : "Failed to fetch agents";
this.sessionLogger.error({ err: error }, "Failed to handle fetch_agents_request");
this.emit({
type: "rpc_error",
payload: {
requestId: request.requestId,
requestType: request.type,
error: message,
code,
},
});
}
}
private async handleFetchAgentHistory(
request: Extract<SessionInboundMessage, { type: "fetch_agent_history_request" }>,
): Promise<void> {
try {
const payload = await this.listFetchAgentsEntries(
request,
this.resolveBackgroundVisibility(request.filter?.includeBackground),
);
this.emit({
type: "fetch_agent_history_response",
payload: {
requestId: request.requestId,
...payload,
},
});
} catch (error) {
const code = error instanceof SessionRequestError ? error.code : "fetch_agent_history_failed";
const message = error instanceof Error ? error.message : "Failed to fetch agent history";
this.sessionLogger.error({ err: error }, "Failed to handle fetch_agent_history_request");
this.emit({
type: "rpc_error",
payload: {
requestId: request.requestId,
requestType: request.type,
error: message,
code,
},
});
}
}
private async handleFetchRecentProviderSessions(
request: Extract<SessionInboundMessage, { type: "fetch_recent_provider_sessions_request" }>,
): Promise<void> {
try {
const result = await listImportableProviderSessions({
request,
agentManager: this.agentManager,
agentStorage: this.agentStorage,
providerSnapshotManager: this.providerSnapshotManager,
});
this.emit({
type: "fetch_recent_provider_sessions_response",
payload: {
requestId: request.requestId,
entries: result.entries,
...(result.filteredAlreadyImportedCount > 0
? { filteredAlreadyImportedCount: result.filteredAlreadyImportedCount }
: {}),
...(result.providerErrors.length > 0 ? { providerErrors: result.providerErrors } : {}),
},
});
} catch (error) {
const code =
error instanceof ImportSessionsRequestError
? error.code
: "fetch_recent_provider_sessions_failed";
const message =
error instanceof Error ? error.message : "Failed to fetch recent provider sessions";
this.sessionLogger.error(
{ err: error },
"Failed to handle fetch_recent_provider_sessions_request",
);
this.emit({
type: "rpc_error",
payload: {
requestId: request.requestId,
requestType: request.type,
error: message,
code,
},
});
}
}
private async handleFetchWorkspacesRequest(
request: Extract<SessionInboundMessage, { type: "fetch_workspaces_request" }>,
): Promise<void> {
const owner = request.subscribe
? this.delivery.begin("workspaces", request.subscribe.subscriptionId, async (id) => {
this.workspaceUpdatesSubscriptions.delete(id);
this.refreshObservationProducers();
await this.reconcileWorkspaceGitObservers();
})
: null;
const subscriptionId = owner?.responseId;
const subscription: WorkspaceUpdatesSubscriptionState | null = owner
? {
subscriptionId: owner.id,
owner,
syncEnabled: Boolean(request.sync),
filter: {
...request.filter,
...(request.includeBackground ? { includeBackground: true } : {}),
},
isBootstrapping: true,
pendingUpdatesByWorkspaceId: new Map(),
lastEmittedByWorkspaceId: new Map(),
visibleEmptyProjectIds: new Set(),
}
: null;
if (subscription) {
this.workspaceUpdatesSubscriptions.set(subscription.subscriptionId, subscription);
this.refreshObservationProducers();
}
try {
this.sessionLogger.debug(
{
requestId: request.requestId,
subscribeRequested: Boolean(request.subscribe),
filter: request.filter ?? null,
sort: request.sort ?? null,
page: request.page ?? null,
},
"fetch_workspaces_request_received",
);
const payload = request.sync
? await this.readWorkspaceDirectorySync(request)
: await this.listFetchWorkspacesEntries(request);
this.sessionLogger.debug(
{
requestId: request.requestId,
subscriptionId,
pageInfo: payload.pageInfo,
payload: summarizeFetchWorkspacesEntries(payload.entries),
},
"fetch_workspaces_response_ready",
);
const snapshot = this.buildBootstrapSnapshot(payload.entries);
if (subscription) {
this.seedWorkspaceSubscriptionSnapshot(
subscription,
payload.entries,
payload.emptyProjects,
);
}
this.emit({
type: "fetch_workspaces_response",
payload: {
requestId: request.requestId,
...(subscriptionId ? { subscriptionId } : {}),
...payload,
},
});
if (subscription) {
await this.reconcileWorkspaceGitObservers();
this.flushBootstrappedWorkspaceUpdates(subscription, snapshot);
}
} catch (error) {
await owner?.release();
const code = error instanceof SessionRequestError ? error.code : "fetch_workspaces_failed";
const message = error instanceof Error ? error.message : "Failed to fetch workspaces";
this.sessionLogger.error({ err: error }, "Failed to handle fetch_workspaces_request");
this.emit({
type: "rpc_error",
payload: {
requestId: request.requestId,
requestType: request.type,
error: message,
code,
},
});
}
}
private async handleProjectListRequest(
request: Extract<SessionInboundMessage, { type: "project.list.request" }>,
): Promise<void> {
try {
const projects = await Promise.all(
(await this.projectRegistry.list())
.filter((project) => !project.archivedAt)
.map((project) => this.buildProjectDescriptor(project)),
);
const synchronized = request.sync
? this.directorySync.synchronizeProjects(projects, request.sync)
: null;
this.emit({
type: "project.list.response",
payload: {
requestId: request.requestId,
...(synchronized ?? { projects }),
},
});
} catch (error) {
this.sessionLogger.error({ err: error }, "Failed to handle project.list.request");
this.emit({
type: "rpc_error",
payload: {
requestId: request.requestId,
requestType: "project.list.request",
error: error instanceof Error ? error.message : "Failed to list projects",
code: "project_list_failed",
},
});
}
}
private async handleProjectSuggestionsRequest(
request: Extract<SessionInboundMessage, { type: "project.suggestions.request" }>,
): Promise<void> {
try {
const result = await listProjectSuggestions({
request,
agentManager: this.agentManager,
projectRegistry: this.projectRegistry,
workspaceGitService: this.workspaceGitService,
});
this.emit({
type: "project.suggestions.response",
payload: { requestId: request.requestId, ...result },
});
} catch (error) {
this.sessionLogger.error({ err: error }, "Failed to handle project.suggestions.request");
this.emit({
type: "rpc_error",
payload: {
requestId: request.requestId,
requestType: request.type,
error: error instanceof Error ? error.message : "Failed to list project suggestions",
code:
error instanceof ProjectSuggestionsRequestError
? error.code
: "project_suggestions_failed",
},
});
}
}
private requireWorkspaceLabels(): WorkspaceLabelService {
if (!this.workspaceLabelService) {
throw new SessionRequestError("workspace_labels_unavailable", "Workspace labels unavailable");
}
return this.workspaceLabelService;
}
private emitWorkspaceLabelError(
request: { requestId: string; type: string },
error: unknown,
): void {
this.emit({
type: "rpc_error",
payload: {
requestId: request.requestId,
requestType: request.type,
code: workspaceLabelErrorCode(error),
error: error instanceof Error ? error.message : "Workspace label operation failed",
},
});
}
private async handleWorkspaceLabelList(
request: Extract<SessionInboundMessage, { type: "workspace.label.list.request" }>,
): Promise<void> {
const service = this.requireWorkspaceLabels();
if (!request.subscribe) {
this.emit({
type: "workspace.label.list.response",
payload: { requestId: request.requestId, ...(await service.list(request.sync)) },
});
return;
}
let bootstrap: ReturnType<typeof service.subscribe> | undefined;
const owner = this.delivery.begin("labels", request.subscribe.subscriptionId, async () => {
await bootstrap?.then(
(subscription) => subscription.unsubscribe(),
() => undefined,
);
});
try {
type LiveChange = Parameters<Parameters<typeof service.subscribe>[0]["onChange"]>[0];
const pending = new Map<string, LiveChange>();
let ready = false;
const emitChange = (change: LiveChange): void => {
owner.emit({
type: "workspace.label.update",
payload: change,
});
};
bootstrap = service.subscribe({
cursor: request.sync,
onChange: (change) => {
if (owner.signal.aborted) return;
if (!ready)
pending.set(change.kind === "upsert" ? change.label.name : change.name, change);
else emitChange(change);
},
});
const subscription = await bootstrap;
if (owner.signal.aborted) return;
this.emit({
type: "workspace.label.list.response",
payload: {
requestId: request.requestId,
subscriptionId: owner.responseId,
...subscription.snapshot,
},
});
ready = true;
for (const change of pending.values()) {
if (change.seq > subscription.snapshot.sync.headSeq) emitChange(change);
}
pending.clear();
} catch (error) {
await owner.release();
this.emitWorkspaceLabelError(request, error);
}
}
private async handleWorkspaceLabelAssignment(
request: Extract<SessionInboundMessage, { type: "workspace.label.assignment.set.request" }>,
): Promise<void> {
try {
const result = await this.requireWorkspaceLabels().setAssignment(request);
this.emit({
type: "workspace.label.assignment.set.response",
payload: { requestId: request.requestId, ...result },
});
} catch (error) {
this.emitWorkspaceLabelError(request, error);
}
}
private async handleWorkspaceLabelUpdate(
request: Extract<SessionInboundMessage, { type: "workspace.label.update.request" }>,
): Promise<void> {
try {
const result = await this.requireWorkspaceLabels().update(request);
this.emit({
type: "workspace.label.update.response",
payload: { requestId: request.requestId, ...result },
});
} catch (error) {
this.emitWorkspaceLabelError(request, error);
}
}
private async handleWorkspaceLabelDelete(
request: Extract<SessionInboundMessage, { type: "workspace.label.delete.request" }>,
): Promise<void> {
try {
const result = await this.requireWorkspaceLabels().delete(request.name);
this.emit({
type: "workspace.label.delete.response",
payload: { requestId: request.requestId, ...result },
});
} catch (error) {
this.emitWorkspaceLabelError(request, error);
}
}
private async handleWorkspaceLabelDeleteInspection(
request: Extract<SessionInboundMessage, { type: "workspace.label.delete.inspect.request" }>,
): Promise<void> {
try {
const affectedWorkspaceCount = await this.requireWorkspaceLabels().countAffectedWorkspaces(
request.name,
);
this.emit({
type: "workspace.label.delete.inspect.response",
payload: { requestId: request.requestId, affectedWorkspaceCount },
});
} catch (error) {
this.emitWorkspaceLabelError(request, error);
}
}
private async readAgentDirectorySync(
request: Extract<SessionInboundMessage, { type: "fetch_agents_request" }>,
backgroundVisibility: BackgroundVisibility,
) {
if (request.scope !== "active" || request.filter) {
throw new SessionRequestError(
"invalid_request",
"Sequenced agent directory reads require scope=active and no filter.",
);
}
// The canonical collection is complete; only the read below is scoped.
const snapshot = await this.listFetchAgentsEntries(
{ ...request, page: { limit: Number.MAX_SAFE_INTEGER } },
true,
);
const result = this.directorySync.synchronizeAgents(
snapshot.entries,
request.sync ?? {},
backgroundVisibility,
);
const entries = await Promise.all(
result.entries.map(async (entry) =>
(await this.agentDirectory.includes(entry.agent, backgroundVisibility)) ? entry : null,
),
);
return { ...result, entries: entries.filter((entry) => entry !== null) };
}
private async readWorkspaceDirectorySync(
request: Extract<SessionInboundMessage, { type: "fetch_workspaces_request" }>,
) {
if (request.filter)
throw new SessionRequestError(
"invalid_request",
"Sequenced workspace directory reads do not support filters.",
);
const result = this.directorySync.synchronizeWorkspaces(
await this.workspaceDirectory.listDescriptors(),
request.sync ?? {},
request.includeBackground,
);
return {
...result,
entries: result.entries.filter(
(workspace) => request.includeBackground || !workspace.background,
),
};
}
// Build the bootstrap snapshot used by `flushBootstrappedWorkspaceUpdates`
// to decide which pending updates to drop. Captures the status,
// statusEnteredAt, and activityAt (parsed to ms) for each workspace entry
// so a status-only change (e.g. the unmask case), a statusEnteredAt-only
// change (e.g. a fresh unmask time), AND a fresher activity all still
// ship to the client.
private buildBootstrapSnapshot(entries: FetchWorkspacesResponseEntry[]): {
snapshotByWorkspaceId: Map<
string,
{ status: string; statusEnteredAt: string | null; activityAtMs: number | null }
>;
} {
const snapshotByWorkspaceId = new Map<
string,
{ status: string; statusEnteredAt: string | null; activityAtMs: number | null }
>();
for (const entry of entries) {
const parsedActivity = entry.activityAt ? Date.parse(entry.activityAt) : null;
snapshotByWorkspaceId.set(entry.id, {
status: entry.status,
statusEnteredAt: entry.statusEnteredAt ?? null,
activityAtMs: Number.isNaN(parsedActivity) ? null : parsedActivity,
});
}
return { snapshotByWorkspaceId };
}
private async reconcileWorkspaceGitObservers(): Promise<void> {
const all = this.wantsEvent("checkout_status_update")
? await this.workspaceDirectory.listObservationTargets()
: [];
const entries = [...this.workspaceUpdatesSubscriptions.values()].flatMap((subscription) =>
[...subscription.lastEmittedByWorkspaceId.values()].flatMap((update) =>
update.kind === "upsert" ? [update.workspace] : [],
),
);
this.workspaceGitObserver.reconcileObservers(
this.wantsEvent("checkout_status_update") ? [...entries, ...all] : entries,
);
}
private seedWorkspaceSubscriptionSnapshot(
subscription: WorkspaceUpdatesSubscriptionState,
entries: FetchWorkspacesResponseEntry[],
emptyProjects: WorkspaceProjectDescriptorPayload[],
): void {
if (this.workspaceUpdatesSubscriptions.get(subscription.subscriptionId) !== subscription)
return;
for (const entry of entries) {
subscription.lastEmittedByWorkspaceId.set(entry.id, {
kind: "upsert",
workspace: entry,
});
}
for (const project of emptyProjects) {
subscription.visibleEmptyProjectIds?.add(project.projectId);
}
}
private async registerWorkspaceForImportedAgent(
workspace: PersistedWorkspaceRecord,
): Promise<void> {
try {
await this.syncWorkspaceGitObserverForWorkspace(workspace);
await this.describeWorkspaceRecord(workspace);
await this.emitWorkspaceUpdateForWorkspaceId(workspace.workspaceId);
} catch (error) {
this.sessionLogger.warn(
{ err: error, workspaceId: workspace.workspaceId, cwd: workspace.cwd },
"Failed to register workspace for imported agent",
);
}
}
private async resolveCreationCaller(
callerAgentId?: string,
): Promise<(CreateAgentCaller & { background?: boolean }) | null> {
if (!callerAgentId) return null;
let caller: (CreateAgentCaller & { background?: boolean }) | null =
this.agentManager.getAgent(callerAgentId);
if (!caller) {
const stored = await this.agentStorage.get(callerAgentId);
if (stored && !stored.archivedAt) caller = stored;
}
if (!caller) throw new Error(`Caller agent ${callerAgentId} not found`);
return {
id: caller.id,
cwd: caller.cwd,
workspaceId: caller.workspaceId,
background: caller.background,
};
}
private async workspaceBackgroundIntent(
background: boolean | undefined,
callerAgentId: string | undefined,
): Promise<{ background?: boolean; callerWorkspaceId?: string }> {
const caller = await this.resolveCreationCaller(callerAgentId);
return {
background: inheritWorkspaceBackground(background, caller),
callerWorkspaceId: caller?.workspaceId,
};
}
private async createRequestedWorkspace(
request: Extract<SessionInboundMessage, { type: "workspace.create.request" }>,
workspaceId?: string,
): Promise<WorkspaceDescriptorPayload> {
if (request.internal)
throw new Error(
"Public internal workspaces were removed; use background workspace creation.",
);
if (request.agent?.background !== undefined)
throw new Error("Configure background on the workspace, not its initial agent.");
let creationRequest = request;
// Hooks belong to the operation: retries fingerprint the caller's input
// and must not rerun hooks or compare their potentially changing output.
if (this.pluginRuntime) {
const { type, requestId, ...input } = request;
const transformed = await this.pluginRuntime.before("workspace.create.before", input);
creationRequest = { ...transformed, type, requestId };
}
return creationRequest.source.kind === "directory"
? this.handleWorkspaceCreateLocal(creationRequest, workspaceId)
: this.handleWorkspaceCreateWorktree(creationRequest, workspaceId);
}
private async handleWorkspaceCreateLocal(
request: Extract<SessionInboundMessage, { type: "workspace.create.request" }>,
workspaceId?: string,
): Promise<WorkspaceDescriptorPayload> {
if (request.source.kind !== "directory") {
throw new Error("Unexpected workspace source");
}
const cwd = expandTilde(request.source.path);
const directoryExists = await this.filesystem.isDirectory(cwd).catch(() => false);
if (!directoryExists) {
throw new SessionRequestError("directory_not_found", `Directory not found: ${cwd}`);
}
const explicitTitle = request.title?.trim() || null;
const promptTitle = resolveFirstAgentPromptTitle(request.firstAgentContext);
const workspace = await this.workspaceProvisioning.createWorkspaceForDirectory(
cwd,
explicitTitle ?? promptTitle,
request.source.projectId,
{
expectsInitialAgent: Boolean(request.firstAgentContext),
workspaceId,
...(await this.workspaceBackgroundIntent(request.background, request.callerAgentId)),
},
);
await this.syncWorkspaceGitObserverForWorkspace(workspace);
const descriptor = await this.describeWorkspaceRecord(workspace);
await this.emitCreatedWorkspaceUpdate(
descriptor,
request.firstAgentContext ? "running" : undefined,
);
void this.workspaceGitService
.getSnapshot(workspace.cwd, { force: true, includeForge: true, reason: "open_project" })
.catch((error) => {
this.sessionLogger.warn(
{ err: error, cwd: workspace.cwd },
"Background snapshot refresh failed after workspace.create",
);
});
if (request.firstAgentContext) {
const firstAgentContext = request.firstAgentContext;
this.workspaceAutoName.scheduleForDirectory(
{
workspaceId: workspace.workspaceId,
cwd: workspace.cwd,
firstAgentContext,
},
{ currentSelection: this.getFocusedAgentSelectionForCwd(workspace.cwd) },
);
}
return descriptor;
}
private async handleWorkspaceCreateWorktree(
request: Extract<SessionInboundMessage, { type: "workspace.create.request" }>,
workspaceId?: string,
): Promise<WorkspaceDescriptorPayload> {
if (request.source.kind !== "worktree") {
throw new Error("Unexpected workspace source");
}
const source = request.source;
if (!source.cwd && !source.projectId) {
throw new SessionRequestError(
"source_required",
"cwd or projectId is required for a worktree-backed workspace",
);
}
const sourceCwd = await resolveWorktreeSourceCwd(source, this.projectRegistry);
const result = await this.createPaseoWorktreeWorkflow(
{
cwd: sourceCwd,
workspaceId,
projectId: source.projectId,
worktreeSlug: source.worktreeSlug,
action: source.action,
refName: source.refName,
branchName: source.branchName,
checkoutSource: source.checkoutSource,
githubPrNumber: source.githubPrNumber,
firstAgentContext: request.firstAgentContext,
title: request.title,
...(await this.workspaceBackgroundIntent(request.background, request.callerAgentId)),
},
source.baseBranch
? { resolveDefaultBranch: async () => source.baseBranch as string }
: undefined,
);
const descriptor = await this.describeCreatedWorktreeWorkspace(result);
await this.emitCreatedWorkspaceUpdate(
descriptor,
request.firstAgentContext ? "running" : undefined,
);
return descriptor;
}
private async handleOpenProjectRequest(
request: Extract<SessionInboundMessage, { type: "open_project_request" }>,
): Promise<void> {
const requestedCwd = request.cwd;
const cwd = expandTilde(requestedCwd);
const directoryExists = await this.filesystem.isDirectory(cwd).catch(() => false);
if (!directoryExists) {
this.sessionLogger.info(
{ requestedCwd, resolvedCwd: cwd, reason: "directory_not_found" },
"Open project rejected",
);
this.emit({
type: "open_project_response",
payload: {
requestId: request.requestId,
workspace: null,
error: `Directory not found: ${cwd}`,
errorCode: "directory_not_found",
},
});
return;
}
try {
const projectsBefore = new Map<string, PersistedProjectRecord>();
for (const project of await this.projectRegistry.list()) {
projectsBefore.set(project.projectId, project);
}
const workspacesBefore = new Map<string, PersistedWorkspaceRecord>();
for (const workspaceRecord of await this.workspaceRegistry.listActive()) {
workspacesBefore.set(workspaceRecord.workspaceId, workspaceRecord);
}
const workspace = await this.workspaceProvisioning.findOrCreateWorkspaceForDirectory(cwd);
const project = await this.projectRegistry.get(workspace.projectId);
await this.syncWorkspaceGitObserverForWorkspace(workspace);
const descriptor = await this.describeWorkspaceRecord(workspace);
await this.emitWorkspaceUpdateForWorkspaceId(workspace.workspaceId);
this.sessionLogger.info(
{
requestedCwd,
resolvedCwd: cwd,
workspaceCwd: workspace.cwd,
workspaceId: workspace.workspaceId,
workspaceKind: workspace.kind,
workspaceTransition: describeRegistryTransition(
workspacesBefore.get(workspace.workspaceId) ?? null,
),
projectId: workspace.projectId,
projectKind: project?.kind ?? null,
projectTransition: describeRegistryTransition(
projectsBefore.get(workspace.projectId) ?? null,
),
},
"Project opened",
);
this.emit({
type: "open_project_response",
payload: {
requestId: request.requestId,
workspace: descriptor,
error: null,
},
});
void this.workspaceGitService
.getSnapshot(workspace.cwd, {
force: true,
includeForge: true,
reason: "open_project",
})
.catch((error) => {
this.sessionLogger.warn(
{ err: error, cwd: workspace.cwd },
"Background snapshot refresh failed after open_project",
);
});
} catch (error) {
const message = error instanceof Error ? error.message : "Failed to open project";
this.sessionLogger.error({ err: error, cwd }, "Failed to open project");
this.emit({
type: "open_project_response",
payload: {
requestId: request.requestId,
workspace: null,
error: message,
},
});
}
}
private async handleProjectAddRequest(
request: Extract<SessionInboundMessage, { type: "project.add.request" }>,
): Promise<void> {
const requestedCwd = request.cwd;
const cwd = expandTilde(requestedCwd);
const directoryExists = await this.filesystem.isDirectory(cwd).catch(() => false);
if (!directoryExists) {
this.sessionLogger.info(
{ requestedCwd, resolvedCwd: cwd, reason: "directory_not_found" },
"Add project rejected",
);
this.emit({
type: "project.add.response",
payload: {
requestId: request.requestId,
project: null,
error: `Directory not found: ${cwd}`,
errorCode: "directory_not_found",
},
});
return;
}
try {
const projectsBefore = new Map<string, PersistedProjectRecord>();
for (const project of await this.projectRegistry.list()) {
projectsBefore.set(project.projectId, project);
}
const project = await this.workspaceProvisioning.findOrCreateProjectForDirectory(cwd);
this.sessionLogger.info(
{
requestedCwd,
resolvedCwd: cwd,
projectId: project.projectId,
projectKind: project.kind,
projectTransition: describeRegistryTransition(
projectsBefore.get(project.projectId) ?? null,
),
},
"Project added",
);
this.emit({
type: "project.add.response",
payload: {
requestId: request.requestId,
project: await this.buildProjectDescriptor(project),
error: null,
},
});
} catch (error) {
const message = error instanceof Error ? error.message : "Failed to add project";
this.sessionLogger.error({ err: error, cwd }, "Failed to add project");
this.emit({
type: "project.add.response",
payload: {
requestId: request.requestId,
project: null,
error: message,
},
});
}
}
private async handleProjectCreateDirectoryRequest(
request: Extract<SessionInboundMessage, { type: "project.create_directory.request" }>,
): Promise<void> {
try {
const result = await createProjectDirectory(
{ parentPath: request.parentPath, name: request.name },
{
registerProject: (directoryPath) =>
this.workspaceProvisioning.findOrCreateProjectForDirectory(directoryPath),
},
);
this.emit({
type: "project.create_directory.response",
payload: {
requestId: request.requestId,
directoryPath: result.directoryPath,
project: await this.buildProjectDescriptor(result.project),
error: null,
errorCode: null,
},
});
} catch (error) {
const requestError =
error instanceof ProjectDirectoryRequestError
? error
: new ProjectDirectoryRequestError(
"registration_failed",
error instanceof Error ? error.message : "Failed to create project directory",
);
this.sessionLogger.error(
{
err: error,
parentPath: request.parentPath,
name: request.name,
errorCode: requestError.code,
},
"Failed to create project directory",
);
this.emit({
type: "project.create_directory.response",
payload: {
requestId: request.requestId,
directoryPath: requestError.directoryPath,
project: null,
error: requestError.message,
errorCode: requestError.code,
},
});
}
}
private async handleWorkspaceGithubSearchRepositoriesRequest(
request: Extract<
SessionInboundMessage,
{ type: "workspace.github.search_repositories.request" }
>,
): Promise<void> {
try {
const searchRepositories = (this.github as Partial<GitHubService>).searchRepositories;
if (!searchRepositories) {
throw new Error("GitHub repository search is unavailable");
}
const repositories = await searchRepositories.call(this.github, {
cwd: homedir(),
query: request.query,
limit: request.limit,
});
this.emit({
type: "workspace.github.search_repositories.response",
payload: {
requestId: request.requestId,
repositories,
status: "success",
available: true,
error: null,
},
});
} catch (error) {
const missing = error instanceof GitHubCliMissingError;
const unauthenticated = error instanceof GitHubAuthenticationError;
const commandError = error instanceof GitHubCommandError ? error.stderr.trim() : "";
let message: string;
if (missing) {
message = "GitHub CLI (gh) is not installed or not in PATH";
} else if (unauthenticated) {
message = "GitHub CLI is not authenticated. Run gh auth login on the host.";
} else if (commandError) {
message = commandError;
} else {
message = error instanceof Error ? error.message : "GitHub search failed";
}
let payload: WorkspaceGithubSearchRepositoriesResponsePayload;
if (missing) {
payload = {
status: "unavailable",
requestId: request.requestId,
repositories: [],
reason: "gh_missing",
available: false,
error: message,
};
} else if (unauthenticated) {
payload = {
status: "unauthenticated",
requestId: request.requestId,
repositories: [],
available: false,
error: message,
};
} else {
payload = {
status: "error",
requestId: request.requestId,
repositories: [],
available: true,
error: message,
};
}
this.sessionLogger.warn({ err: error }, "GitHub repository search failed");
this.emit({
type: "workspace.github.search_repositories.response",
payload,
});
}
}
private async handleProjectGithubCloneRequest(
request: Extract<SessionInboundMessage, { type: "project.github.clone.request" }>,
): Promise<void> {
let normalizedRepo = request.repo;
let checkoutPath: string | null = null;
try {
const repo = normalizeCloneRepository({
repo: request.repo,
cloneProtocol: request.cloneProtocol,
});
normalizedRepo = repo.displayName;
const targetParent = resolve(expandTilde(request.targetDirectory.trim()));
checkoutPath = resolve(targetParent, repo.name);
if (!this.isPathWithinRoot(targetParent, checkoutPath)) {
throw new Error("Resolved checkout path must stay inside the target directory");
}
await mkdir(targetParent, { recursive: true });
try {
await lstat(checkoutPath);
throw new Error(`Checkout path already exists: ${checkoutPath}`);
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
throw error;
}
}
const cloneStagingPath = await mkdtemp(resolve(targetParent, ".paseo-clone-"));
try {
await runGitCommand(["clone", repo.cloneUrl, cloneStagingPath], {
cwd: targetParent,
timeout: 5 * 60 * 1000,
maxOutputBytes: 1024 * 1024,
logger: this.sessionLogger,
});
await rename(cloneStagingPath, checkoutPath);
} catch (error) {
await rm(cloneStagingPath, { recursive: true, force: true }).catch((cleanupError) => {
this.sessionLogger.warn(
{ err: cleanupError, cloneStagingPath },
"Failed to clean up partial GitHub clone",
);
});
throw error;
}
const project =
await this.workspaceProvisioning.findOrCreateProjectForDirectory(checkoutPath);
this.emit({
type: "project.github.clone.response",
payload: {
requestId: request.requestId,
repo: repo.displayName,
checkoutPath,
project: await this.buildProjectDescriptor(project),
error: null,
},
});
} catch (error) {
const message = error instanceof Error ? error.message : "Failed to clone GitHub repo";
this.sessionLogger.error(
{ err: error, repo: request.repo, targetDirectory: request.targetDirectory },
"Failed to clone GitHub project",
);
this.emit({
type: "project.github.clone.response",
payload: {
requestId: request.requestId,
repo: normalizedRepo,
checkoutPath,
project: null,
error: message,
},
});
}
}
// Named accessor: the workspace descriptor builder and the git-watch test both read a workspace's
// scripts snapshot through here; the workspace-scripts module owns the payload assembly.
private buildWorkspaceScriptPayloadSnapshot(
workspace: PersistedWorkspaceRecord,
project: PersistedProjectRecord | null,
): Promise<WorkspaceDescriptorPayload["scripts"]> {
return this.workspaceScripts.buildSnapshot(workspace, project);
}
private handleStartWorkspaceScriptRequest(request: StartWorkspaceScriptRequest): Promise<void> {
return this.workspaceScripts.start(request);
}
private async handleWorkspaceScriptListRequest(
request: WorkspaceScriptListRequest,
): Promise<void> {
try {
const scripts = await this.workspaceScripts.list(request.workspaceId);
this.emit({
type: "workspace.script.list.response",
payload: {
requestId: request.requestId,
workspaceId: request.workspaceId,
scripts,
error: null,
},
});
} catch (error) {
this.emit({
type: "workspace.script.list.response",
payload: {
requestId: request.requestId,
workspaceId: request.workspaceId,
scripts: [],
error: error instanceof Error ? error.message : "Failed to list workspace scripts",
},
});
}
}
private async handleWorkspaceScriptStartRequest(
request: WorkspaceScriptStartRequest,
): Promise<void> {
try {
const script = await this.workspaceScripts.launch(request);
this.emit({
type: "workspace.script.start.response",
payload: {
requestId: request.requestId,
workspaceId: request.workspaceId,
scriptName: request.scriptName,
script,
error: null,
},
});
} catch (error) {
this.emit({
type: "workspace.script.start.response",
payload: {
requestId: request.requestId,
workspaceId: request.workspaceId,
scriptName: request.scriptName,
script: null,
error: error instanceof Error ? error.message : "Failed to start workspace script",
},
});
}
}
private async handleWorkspaceScriptStopRequest(
request: WorkspaceScriptStopRequest,
): Promise<void> {
try {
const script = await this.workspaceScripts.stop(request);
this.emit({
type: "workspace.script.stop.response",
payload: {
requestId: request.requestId,
workspaceId: request.workspaceId,
scriptName: request.scriptName,
script,
error: null,
},
});
} catch (error) {
this.emit({
type: "workspace.script.stop.response",
payload: {
requestId: request.requestId,
workspaceId: request.workspaceId,
scriptName: request.scriptName,
script: null,
error: error instanceof Error ? error.message : "Failed to stop workspace script",
},
});
}
}
// COMPAT(desktopEditorBridge): added in v0.1.88, remove after 2026-12-03 once old clients no longer call daemon editor RPCs.
private async handleLegacyListAvailableEditorsRequest(
request: Extract<SessionInboundMessage, { type: "list_available_editors_request" }>,
): Promise<void> {
this.emit({
type: "list_available_editors_response",
payload: {
requestId: request.requestId,
editors: [],
error: "Editor opening moved to the desktop app and is no longer supported by the daemon",
},
});
}
private async handleLegacyOpenInEditorRequest(
request: Extract<SessionInboundMessage, { type: "open_in_editor_request" }>,
): Promise<void> {
this.emit({
type: "open_in_editor_response",
payload: {
requestId: request.requestId,
error: "Editor opening moved to the desktop app and is no longer supported by the daemon",
},
});
}
private async handleCreatePaseoWorktreeRequest(
request: Extract<SessionInboundMessage, { type: "create_paseo_worktree_request" }>,
): Promise<void> {
return handleCreateWorktreeRequest(
{
paseoHome: this.paseoHome,
worktreesRoot: this.worktreesRoot,
describeWorkspaceRecord: (result) => this.describeCreatedWorktreeWorkspace(result),
emit: (message) => this.emit(message),
sessionLogger: this.sessionLogger,
createPaseoWorktreeWorkflow: (input) => this.createPaseoWorktreeWorkflow(input),
},
request,
);
}
private async createPaseoWorktreeWorkflow(
input: CreatePaseoWorktreeInput,
options?: {
resolveDefaultBranch?: (repoRoot: string) => Promise<string>;
},
): Promise<CreatePaseoWorktreeWorkflowResult> {
return createWorktreeWorkflow(
{
paseoHome: this.paseoHome,
worktreesRoot: this.worktreesRoot,
createPaseoWorktree: (workflowInput, serviceOptions) =>
this.createPaseoWorktree(workflowInput, serviceOptions),
warmWorkspaceGitData: (workspace) => this.warmWorkspaceGitDataForWorkspace(workspace),
autoNameWorkspaceBranchForFirstAgent: (autoNameInput) =>
this.workspaceAutoName.scheduleForWorktree(autoNameInput, {
currentSelection: this.getFocusedAgentSelectionForCwd(autoNameInput.workspace.cwd),
}),
startWorkspaceSetup: (workspaceId, operation) =>
this.workspaceSetupRuntime.start(workspaceId, operation),
assertWorkspaceAutomationAllowed: (workspaceId) =>
assertWorkspaceAutomationAllowedForWorkspace(this.workspaceRegistry, workspaceId),
emitWorkspaceUpdateForWorkspaceId: (workspaceId) =>
this.emitWorkspaceUpdateForWorkspaceId(workspaceId),
cacheWorkspaceSetupSnapshot: (workspaceId, snapshot) => {
this.workspaceSetupObservation.cache(workspaceId, snapshot);
},
emit: (message) => this.emit(message),
sessionLogger: this.sessionLogger,
terminalManager: this.terminalManager,
serviceProxy: this.serviceProxy,
scriptRuntimeStore: this.scriptRuntimeStore,
getDaemonTcpPort: this.getDaemonTcpPort,
getDaemonTcpHost: this.getDaemonTcpHost,
serviceProxyPublicBaseUrl: this.serviceProxyPublicBaseUrl,
onScriptsChanged: (workspaceId, workspaceDirectory) => {
this.workspaceScripts.emitStatusUpdate(workspaceId, workspaceDirectory);
},
},
input,
options,
);
}
private async handleWorkspaceSetupStatusRequest(
request: Extract<SessionInboundMessage, { type: "workspace_setup_status_request" }>,
): Promise<void> {
return handleWorkspaceSetupStatusRequestMessage(
{
emit: (message) => this.emit(message),
workspaceSetupSnapshots: this.workspaceSetupObservation,
getWorkspace: (workspaceId) => this.workspaceRegistry.get(workspaceId),
},
request,
);
}
private async handleWorkspaceSetupRunRequest(
request: Extract<SessionInboundMessage, { type: "workspace.setup.run.request" }>,
): Promise<void> {
return handleWorkspaceSetupRunRequestMessage(
{
getWorkspace: (workspaceId) => this.workspaceRegistry.get(workspaceId),
clearAutomationBlock: (workspaceId) =>
clearWorkspaceAutomationBlock(this.workspaceRegistry, workspaceId),
startWorkspaceSetup: (workspaceId, operation) =>
this.workspaceSetupRuntime.start(workspaceId, operation),
paseoHome: this.paseoHome,
worktreesRoot: this.worktreesRoot,
emitWorkspaceUpdateForWorkspaceId: (workspaceId) =>
this.emitWorkspaceUpdateForWorkspaceId(workspaceId),
cacheWorkspaceSetupSnapshot: (workspaceId, snapshot) =>
this.workspaceSetupObservation.cache(workspaceId, snapshot),
emit: (message) => this.emit(message),
sessionLogger: this.sessionLogger,
terminalManager: this.terminalManager,
serviceProxy: this.serviceProxy,
scriptRuntimeStore: this.scriptRuntimeStore,
getDaemonTcpPort: this.getDaemonTcpPort,
getDaemonTcpHost: this.getDaemonTcpHost,
serviceProxyPublicBaseUrl: this.serviceProxyPublicBaseUrl,
onScriptsChanged: (workspaceId, workspaceDirectory) =>
this.workspaceScripts.emitStatusUpdate(workspaceId, workspaceDirectory),
},
request,
);
}
private async handleArchiveWorkspaceRequest(
request: Extract<SessionInboundMessage, { type: "archive_workspace_request" }>,
): Promise<void> {
try {
const existing = await this.workspaceRegistry.get(request.workspaceId);
if (!existing) {
throw new Error(`Workspace not found: ${request.workspaceId}`);
}
await archiveByScope(
{
paseoHome: this.paseoHome,
paseoWorktreesBaseRoot: this.worktreesRoot,
github: this.github,
workspaceGitService: this.workspaceGitService,
agentManager: this.agentManager,
agentStorage: this.agentStorage,
findWorkspaceIdForCwd: (cwd) => this.findWorkspaceIdForCwd(cwd),
getWorkspace: (workspaceId) => this.workspaceRegistry.get(workspaceId),
listActiveWorkspaces: () => this.listActiveWorkspaceRefs(),
archiveWorkspaceRecord: (workspaceId) => this.archiveWorkspaceRecord(workspaceId),
emitWorkspaceUpdatesForWorkspaceIds: (workspaceIds) =>
this.emitWorkspaceUpdatesForWorkspaceIds(workspaceIds),
markWorkspaceArchiving: (workspaceIds, archivingAt) =>
this.markWorkspaceArchiving(workspaceIds, archivingAt),
clearWorkspaceArchiving: (workspaceIds) => this.clearWorkspaceArchiving(workspaceIds),
assertWorkspaceAutomationAllowed: (workspaceId) =>
assertWorkspaceAutomationAllowedForWorkspace(this.workspaceRegistry, workspaceId),
killTerminalsForWorkspace: (workspaceId) =>
this.terminalController.killTerminalsForWorkspace(workspaceId),
stopWorkspaceSetup: (workspaceId) => this.workspaceSetupRuntime.stop(workspaceId),
sessionLogger: this.sessionLogger,
},
{
scope: { kind: "workspace", workspaceId: existing.workspaceId },
requestId: request.requestId,
},
);
const archivedWorkspace = await this.workspaceRegistry.get(request.workspaceId);
const archivedAt = archivedWorkspace?.archivedAt ?? new Date().toISOString();
this.emit({
type: "archive_workspace_response",
payload: {
requestId: request.requestId,
workspaceId: request.workspaceId,
archivedAt,
error: null,
},
});
} catch (error) {
const message = error instanceof Error ? error.message : "Failed to archive workspace";
this.sessionLogger.error(
{ err: error, workspaceId: request.workspaceId },
"Failed to archive workspace",
);
this.emit({
type: "archive_workspace_response",
payload: {
requestId: request.requestId,
workspaceId: request.workspaceId,
archivedAt: null,
error: message,
},
});
}
}
private async handleWorkspaceClearAttentionRequest(
request: Extract<SessionInboundMessage, { type: "workspace.clear_attention.request" }>,
): Promise<void> {
const { requestId, workspaceId } = request;
const requestedWorkspaceIds = Array.isArray(workspaceId) ? workspaceId : [workspaceId];
let agents: AgentSnapshotPayload[];
try {
agents = await this.listAgentPayloads({
workspaceIds: requestedWorkspaceIds,
includeBackground: true,
});
} catch (error) {
const message = getErrorMessage(error);
const results = requestedWorkspaceIds.map((requestedWorkspaceId) => ({
workspaceId: requestedWorkspaceId,
clearedAgentIds: [],
success: false,
error: message,
}));
this.emit({
type: "workspace.clear_attention.response",
payload: {
requestId,
workspaceId,
clearedAgentIds: [],
results,
success: false,
error: message,
},
});
return;
}
const results: Array<{
workspaceId: string;
clearedAgentIds: string[];
success: boolean;
error: string | null;
}> = [];
for (const requestedWorkspaceId of requestedWorkspaceIds) {
const clearedAgentIds: string[] = [];
try {
const workspace = await this.workspaceRegistry.get(requestedWorkspaceId);
if (!workspace || workspace.archivedAt) {
throw new Error(`Workspace not found: ${requestedWorkspaceId}`);
}
// Clearing attention is scoped to the workspace that OWNS the agent, by
// workspaceId — never by comparing cwd strings. A sibling workspace
// sharing the same directory keeps its own agents' attention.
const clearableAgentIds = agents
.filter((agent) => !agent.archivedAt)
.filter((agent) => agent.workspaceId === workspace.workspaceId)
.filter((agent) => agent.requiresAttention === true)
.filter((agent) => (agent.pendingPermissions?.length ?? 0) === 0)
.filter((agent) => agent.attentionReason !== "permission")
.map((agent) => agent.id);
for (const agentId of clearableAgentIds) {
const liveAgent = this.agentManager.getAgent(agentId);
if (liveAgent) {
await this.agentManager.clearAgentAttention(agentId);
clearedAgentIds.push(agentId);
continue;
}
const record = await this.agentStorage.get(agentId);
if (
!record ||
record.internal ||
record.archivedAt ||
record.requiresAttention !== true
) {
continue;
}
const nextRecord: StoredAgentRecord = {
...record,
updatedAt: new Date().toISOString(),
requiresAttention: false,
attentionReason: null,
attentionTimestamp: null,
};
await this.agentStorage.upsert(nextRecord);
const agent = this.buildStoredAgentPayload(nextRecord);
const project = await this.buildProjectPlacementForWorkspace(workspace);
this.emit({
type: "agent_update",
payload: {
kind: "upsert",
agent,
project,
},
});
clearedAgentIds.push(agentId);
}
await this.emitWorkspaceUpdateForWorkspaceId(workspace.workspaceId);
results.push({
workspaceId: requestedWorkspaceId,
clearedAgentIds,
success: true,
error: null,
});
} catch (error) {
const message = getErrorMessage(error);
this.sessionLogger.error(
{ err: error, workspaceId: requestedWorkspaceId },
"Failed to clear workspace attention",
);
results.push({
workspaceId: requestedWorkspaceId,
clearedAgentIds,
success: false,
error: message,
});
}
}
const clearedAgentIds = results.flatMap((result) => result.clearedAgentIds);
const failedResults = results.filter((result) => !result.success);
this.emit({
type: "workspace.clear_attention.response",
payload: {
requestId,
workspaceId,
clearedAgentIds,
results,
success: failedResults.length === 0,
error:
failedResults.length === 0
? null
: failedResults
.map((result) => result.error)
.filter((error) => error !== null)
.join("; "),
},
});
}
private async handleWorkspaceMarkUnreadRequest(
request: Extract<SessionInboundMessage, { type: "workspace.mark_unread.request" }>,
): Promise<void> {
const { requestId, workspaceId } = request;
let markedAgentId: string | null = null;
try {
const workspace = await this.workspaceRegistry.get(workspaceId);
if (!workspace || workspace.archivedAt) {
throw new Error(`Workspace not found: ${workspaceId}`);
}
const agents = (
await this.listAgentPayloads({
workspaceIds: [workspaceId],
includeBackground: true,
includeAncestors: true,
})
).filter((agent) => this.isProviderVisibleToClient(agent.provider));
const agentsById = new Map(agents.map((agent) => [agent.id, agent] as const));
const candidates = agents
.filter((agent) => !agent.archivedAt && !agent.background)
.filter((agent) => agent.workspaceId === workspace.workspaceId)
.filter((agent) => resolveWorkspaceRootAgent(agent, agentsById)?.id === agent.id)
.filter((agent) => agent.status === "idle" || agent.status === "closed")
.filter((agent) => agent.requiresAttention !== true)
.filter((agent) => (agent.pendingPermissions?.length ?? 0) === 0)
.sort(
(left, right) =>
right.updatedAt.localeCompare(left.updatedAt) || left.id.localeCompare(right.id),
);
const candidate = candidates[0];
if (!candidate) {
throw new Error(`Workspace has no finished agent to mark unread: ${workspaceId}`);
}
await this.agentManager.markAgentUnread(candidate.id);
markedAgentId = candidate.id;
this.emit({
type: "workspace.mark_unread.response",
payload: {
requestId,
workspaceId,
markedAgentId,
success: true,
error: null,
},
});
} catch (error) {
const message = getErrorMessage(error);
this.sessionLogger.error({ err: error, workspaceId }, "Failed to mark workspace unread");
this.emit({
type: "workspace.mark_unread.response",
payload: {
requestId,
workspaceId,
markedAgentId,
success: false,
error: message,
},
});
}
}
private async handleFetchAgent(agentIdOrIdentifier: string, requestId: string): Promise<void> {
// An unknown agent is a null agent, not an error. Errors are for empty or ambiguous identifiers.
const resolved = await this.resolveAgentIdentifier(agentIdOrIdentifier);
if (!resolved.ok) {
this.emit({
type: "fetch_agent_response",
payload: {
requestId,
agent: null,
project: null,
error: resolved.notFound ? null : resolved.error,
},
});
return;
}
const agent = await this.getAgentPayloadById(resolved.agentId);
if (!agent) {
this.emit({
type: "fetch_agent_response",
payload: { requestId, agent: null, project: null, error: null },
});
return;
}
const project = agent.workspaceId
? await this.buildProjectPlacementForWorkspaceId(agent.workspaceId)
: null;
this.emit({
type: "fetch_agent_response",
payload: { requestId, agent, project, error: null },
});
}
private async handleFetchAgentTimelineRequest(
msg: Extract<SessionInboundMessage, { type: "fetch_agent_timeline_request" }>,
source?: object,
): Promise<void> {
const direction: AgentTimelineFetchDirection = msg.direction ?? (msg.cursor ? "after" : "tail");
const projection = "projected" as const;
const cursor: AgentTimelineCursor | undefined = msg.cursor
? {
epoch: msg.cursor.epoch,
seq: msg.cursor.seq,
}
: undefined;
try {
const snapshot = await ensureAgentLoaded(msg.agentId, {
agentManager: this.agentManager,
agentStorage: this.agentStorage,
logger: this.sessionLogger,
purpose: "history",
});
const agentPayload = await this.buildAgentPayload(snapshot);
const fetchedControlTimeline = this.agentManager.fetchTimeline(msg.agentId, {
direction,
cursor,
limit: msg.limit,
wholeTurns: msg.wholeTurns,
});
const selectedTimeline = {
timeline: fetchedControlTimeline,
entries: fetchedControlTimeline.rows,
startSeq: fetchedControlTimeline.startSeq,
endSeq: fetchedControlTimeline.endSeq,
hasOlder: fetchedControlTimeline.hasOlder,
hasNewer: fetchedControlTimeline.hasNewer,
};
const startCursor =
selectedTimeline.startSeq !== null
? { epoch: selectedTimeline.timeline.epoch, seq: selectedTimeline.startSeq }
: null;
const endCursor =
selectedTimeline.endSeq !== null
? { epoch: selectedTimeline.timeline.epoch, seq: selectedTimeline.endSeq }
: null;
const entries = selectedTimeline.entries.filter((entry) =>
this.supportsTimelineItem(entry.item, source),
);
this.emitForSource(
{
type: "fetch_agent_timeline_response",
payload: {
requestId: msg.requestId,
agentId: msg.agentId,
agent: agentPayload,
direction,
projection,
epoch: selectedTimeline.timeline.epoch,
reset: fetchedControlTimeline.reset,
staleCursor: fetchedControlTimeline.staleCursor,
gap: fetchedControlTimeline.gap,
window: selectedTimeline.timeline.window,
startCursor,
endCursor,
hasOlder: selectedTimeline.hasOlder,
hasNewer: selectedTimeline.hasNewer,
...(msg.mergeWindow === true ? { mergeWindow: true } : {}),
entries: entries.map((entry) => {
const payloadEntry = {
provider: snapshot.provider,
item: entry.item,
timestamp: entry.timestamp,
seqStart: entry.seqStart,
seqEnd: entry.seqEnd,
sourceSeqRanges: entry.sourceSeqRanges,
turnId: undefined as string | undefined,
collapsed: (
source
? this.supportsForSource(CLIENT_CAPS.reasoningMergeEnum, source)
: this.supports(CLIENT_CAPS.reasoningMergeEnum)
)
? entry.collapsed
: entry.collapsed.filter((value) => value !== "reasoning_merge"),
};
payloadEntry.turnId = entry.turnId;
return payloadEntry;
}),
error: null,
},
},
source,
);
} catch (error) {
this.sessionLogger.error(
{ err: error, agentId: msg.agentId },
"Failed to handle fetch_agent_timeline_request",
);
this.emitForSource(
{
type: "fetch_agent_timeline_response",
payload: {
requestId: msg.requestId,
agentId: msg.agentId,
agent: null,
direction,
projection,
epoch: "",
reset: false,
staleCursor: false,
gap: false,
window: { minSeq: 0, maxSeq: 0, nextSeq: 0 },
startCursor: null,
endCursor: null,
hasOlder: false,
hasNewer: false,
...(msg.mergeWindow === true ? { mergeWindow: true } : {}),
entries: [],
error: error instanceof Error ? error.message : String(error),
},
},
source,
);
}
}
private async handleAgentTimelineAppendRequest(
msg: Extract<SessionInboundMessage, { type: "agent.timeline.append.request" }>,
): Promise<void> {
const pluginId = parsePluginClientId(this.clientId);
if (!pluginId) throw new Error("Only plugin sessions can append plugin timeline items");
assertPluginTimelineDataSize(msg.item.data);
const { seq, epoch } = await this.agentManager.appendTimelineItem(msg.agentId, {
...msg.item,
pluginId,
});
this.emit({
type: "agent.timeline.append.response",
payload: { requestId: msg.requestId, seq, epoch },
});
}
private async handleAgentTimelineSearchRequest(
msg: Extract<SessionInboundMessage, { type: "agent.timeline.search.request" }>,
source?: object,
): Promise<void> {
try {
await ensureAgentLoaded(msg.agentId, {
agentManager: this.agentManager,
agentStorage: this.agentStorage,
logger: this.sessionLogger,
});
const rows = await this.agentManager.getTimelineRows(msg.agentId);
const { epoch } = this.agentManager.fetchTimeline(msg.agentId, {
direction: "tail",
limit: 1,
});
const result = await searchTimeline({ rows, query: msg.query, cursor: msg.cursor });
if (
this.agentManager.fetchTimeline(msg.agentId, { direction: "tail", limit: 1 }).epoch !==
epoch
) {
throw new Error("History changed; search again");
}
this.emitForSource(
{
type: "agent.timeline.search.response",
payload: {
requestId: msg.requestId,
agentId: msg.agentId,
epoch,
...result,
error: null,
},
},
source,
);
} catch (error) {
this.emitForSource(
{
type: "agent.timeline.search.response",
payload: {
requestId: msg.requestId,
agentId: msg.agentId,
epoch: "",
locations: [],
nextCursor: null,
error: error instanceof Error ? error.message : String(error),
},
},
source,
);
}
}
private async handleAgentTimelineListPromptsRequest(
msg: Extract<SessionInboundMessage, { type: "agent.timeline.list_prompts.request" }>,
source?: object,
): Promise<void> {
try {
await ensureAgentLoaded(msg.agentId, {
agentManager: this.agentManager,
agentStorage: this.agentStorage,
logger: this.sessionLogger,
});
const rows = await this.agentManager.getTimelineRows(msg.agentId);
const timeline = this.agentManager.fetchTimeline(msg.agentId, {
direction: "tail",
limit: 1,
});
const index = buildTimelinePromptIndex(timeline.epoch, rows);
this.emitForSource(
{
type: "agent.timeline.list_prompts.response",
payload: {
requestId: msg.requestId,
agentId: msg.agentId,
...index,
error: null,
},
},
source,
);
} catch (error) {
this.sessionLogger.error(
{ err: error, agentId: msg.agentId },
"Failed to handle agent.timeline.list_prompts.request",
);
this.emitForSource(
{
type: "agent.timeline.list_prompts.response",
payload: {
requestId: msg.requestId,
agentId: msg.agentId,
epoch: "",
prompts: [],
error: error instanceof Error ? error.message : String(error),
},
},
source,
);
}
}
private async handleProviderSubagentListRequest(
msg: Extract<SessionInboundMessage, { type: "agent.provider_subagents.list.request" }>,
): Promise<void> {
try {
await ensureUnarchivedAgentLoaded(msg.parentAgentId, {
agentManager: this.agentManager,
agentStorage: this.agentStorage,
logger: this.sessionLogger,
});
this.emit({
type: "agent.provider_subagents.list.response",
payload: {
requestId: msg.requestId,
parentAgentId: msg.parentAgentId,
subagents: this.agentManager.listProviderSubagents(msg.parentAgentId),
error: null,
},
});
} catch (error) {
this.emit({
type: "agent.provider_subagents.list.response",
payload: {
requestId: msg.requestId,
parentAgentId: msg.parentAgentId,
subagents: [],
error: error instanceof Error ? error.message : String(error),
},
});
}
}
private async handleProviderSubagentTimelineRequest(
msg: Extract<SessionInboundMessage, { type: "agent.provider_subagents.timeline.get.request" }>,
source?: object,
): Promise<void> {
const direction: AgentTimelineFetchDirection = msg.direction ?? (msg.cursor ? "after" : "tail");
try {
await ensureUnarchivedAgentLoaded(msg.parentAgentId, {
agentManager: this.agentManager,
agentStorage: this.agentStorage,
logger: this.sessionLogger,
});
const descriptor = this.agentManager.getProviderSubagent(msg.parentAgentId, msg.subagentId);
if (!descriptor) {
throw new Error("Provider subagent not found");
}
// COMPAT(projectedSubagentTimeline): added after v0.8.0, remove after 2027-03-14.
const supportsProjection = source
? this.supportsForSource(CLIENT_CAPS.projectedSubagentTimeline, source)
: this.supports(CLIENT_CAPS.projectedSubagentTimeline);
const timeline = this.agentManager.fetchProviderSubagentTimeline(
msg.parentAgentId,
msg.subagentId,
{
direction,
cursor: msg.cursor,
limit: msg.limit ?? (direction === "after" ? 0 : 200),
},
);
const rows = timeline.rows.filter((row) => this.supportsTimelineItem(row.item, source));
this.emitForSource(
{
type: "agent.provider_subagents.timeline.get.response",
payload: {
requestId: msg.requestId,
parentAgentId: msg.parentAgentId,
subagentId: msg.subagentId,
provider: descriptor.provider,
direction,
epoch: timeline.epoch,
projection: "projected",
startCursor:
timeline.startSeq === null ? null : { epoch: timeline.epoch, seq: timeline.startSeq },
endCursor:
timeline.endSeq === null ? null : { epoch: timeline.epoch, seq: timeline.endSeq },
reset: timeline.reset,
staleCursor: timeline.staleCursor,
gap: timeline.gap,
window: timeline.window,
hasOlder: supportsProjection && timeline.hasOlder,
hasNewer: supportsProjection && timeline.hasNewer,
rows: supportsProjection
? rows.map((row) => ({
item: row.item,
timestamp: row.timestamp,
seq: row.seqEnd,
seqStart: row.seqStart,
seqEnd: row.seqEnd,
sourceSeqRanges: row.sourceSeqRanges,
}))
: [
{
seq: timeline.window.maxSeq,
timestamp: new Date().toISOString(),
item: {
type: "assistant_message",
text: "Please upgrade the Paseo app to view this subagent conversation.",
},
},
],
error: null,
},
},
source,
);
} catch (error) {
this.emitForSource(
{
type: "agent.provider_subagents.timeline.get.response",
payload: {
requestId: msg.requestId,
parentAgentId: msg.parentAgentId,
subagentId: msg.subagentId,
provider: null,
direction,
epoch: "",
reset: false,
staleCursor: false,
gap: false,
window: { minSeq: 0, maxSeq: 0, nextSeq: 0 },
hasOlder: false,
hasNewer: false,
rows: [],
error: error instanceof Error ? error.message : String(error),
},
},
source,
);
}
}
private async handleAgentForkContextRequest(
msg: Extract<SessionInboundMessage, { type: "agent.fork_context.request" }>,
): Promise<void> {
try {
const snapshot = await ensureAgentLoaded(msg.agentId, {
agentManager: this.agentManager,
agentStorage: this.agentStorage,
logger: this.sessionLogger,
});
const agentPayload = await this.buildAgentPayload(snapshot);
const timeline = this.agentManager.fetchTimeline(msg.agentId, {
direction: "tail",
limit: 0,
});
const forkContext = buildAgentForkContextAttachment({
rows: timeline.rows,
cursorBoundary: msg.boundaryCursor
? { timelineEpoch: timeline.epoch, cursor: msg.boundaryCursor }
: null,
boundaryMessageId: msg.boundaryMessageId,
agentTitle: agentPayload.title,
cwd: snapshot.cwd,
});
this.emit({
type: "agent.fork_context.response",
payload: {
requestId: msg.requestId,
agentId: msg.agentId,
attachment: forkContext.attachment,
itemCount: forkContext.itemCount,
boundaryCursor: forkContext.boundaryCursor,
boundaryMessageId: forkContext.boundaryMessageId,
error: null,
},
});
} catch (error) {
this.sessionLogger.error(
{ err: error, agentId: msg.agentId },
"Failed to handle agent.fork_context.request",
);
this.emit({
type: "agent.fork_context.response",
payload: {
requestId: msg.requestId,
agentId: msg.agentId,
attachment: null,
itemCount: 0,
boundaryCursor: msg.boundaryCursor ?? null,
boundaryMessageId: msg.boundaryMessageId ?? null,
error: error instanceof Error ? error.message : String(error),
},
});
}
}
private async prepareAgentMessage(agentId: string, text: string): Promise<void> {
await ensureAgentLoaded(agentId, {
agentManager: this.agentManager,
agentStorage: this.agentStorage,
logger: this.sessionLogger,
});
const stored = await this.agentStorage.get(agentId);
if (stored && !stored.title && !stored.lastUserMessageAt) {
const { provisionalTitle } = resolveCreateAgentTitles({ initialPrompt: text });
if (provisionalTitle) await this.agentManager.setTitle(agentId, provisionalTitle);
}
}
private async handleSendAgentMessageRequest(
msg: Extract<SessionInboundMessage, { type: "send_agent_message_request" }>,
): Promise<void> {
const resolved = await this.resolveAgentIdentifier(msg.agentId);
if (!resolved.ok) {
this.emit({
type: "send_agent_message_response",
payload: {
requestId: msg.requestId,
agentId: msg.agentId,
accepted: false,
error: resolved.error,
},
});
return;
}
try {
const agentId = resolved.agentId;
const prompt = buildAgentPrompt(msg.text, msg.images, msg.attachments);
this.sessionLogger.trace(
{
agentId,
messageId: msg.messageId,
activeTurnBehavior: msg.activeTurnBehavior,
textPrefix: msg.text.slice(0, 80),
},
"agent.session.send_agent_message",
);
let queued!: () => void;
const admitted = new Promise<void>((acceptAdmission) => {
queued = acceptAdmission;
});
let deliveryStatus: "queued" | undefined;
const send = async () => {
if (msg.sourceAgentId) {
const receipt = this.agentMessages.enqueue({
recipientId: agentId,
messageId: msg.messageId,
prompt,
source: {
kind: "agent-message",
agentId: msg.sourceAgentId,
serverId: msg.sourceAgentServerId,
title: !msg.sourceAgentServerId
? (this.agentManager.getAgent(msg.sourceAgentId)?.config.title ?? undefined)
: undefined,
},
activeTurnBehavior: msg.activeTurnBehavior ?? "steer",
sessionMode: msg.sessionMode,
clearPendingPermissions: false,
});
deliveryStatus = "queued";
queued();
await receipt.dispatched;
return;
}
const result = await sendPromptToAgent({
agentManager: this.agentManager,
agentStorage: this.agentStorage,
agentId,
prompt,
messageId: msg.messageId,
activeTurnBehavior: msg.activeTurnBehavior ?? "interrupt",
sessionMode: msg.sessionMode,
clearPendingPermissions: true,
logger: this.sessionLogger,
});
if (result.disposition === "turn_started") {
await waitForAgentRunStartWithTimeout(
this.agentManager,
agentId,
this.delivery.requestSignal,
);
}
};
const sent = msg.messageId
? this.messageReceipts.send({
agentId,
messageId: msg.messageId,
request: {
prompt,
sourceAgentId: msg.sourceAgentId,
sourceAgentServerId: msg.sourceAgentServerId,
sessionMode: msg.sessionMode,
activeTurnBehavior:
msg.activeTurnBehavior ?? (msg.sourceAgentId ? "steer" : "interrupt"),
},
prepare: async () => {
if (!msg.sourceAgentId) await this.prepareAgentMessage(agentId, msg.text);
},
send,
})
: send();
void sent.catch((error) => {
if (deliveryStatus === "queued") {
this.handleAgentRunError(agentId, error, "Failed to dispatch queued agent message");
}
});
await Promise.race([sent, admitted]);
this.emit({
type: "send_agent_message_response",
payload: {
requestId: msg.requestId,
agentId,
accepted: true,
...(deliveryStatus ? { deliveryStatus } : {}),
error: null,
},
});
} catch (error) {
if (this.delivery.requestSignal.aborted) return;
this.handleAgentRunError(resolved.agentId, error, "Failed to send agent message");
this.emit({
type: "send_agent_message_response",
payload: {
requestId: msg.requestId,
agentId: resolved.agentId,
accepted: false,
error: errorToFriendlyMessage(error),
},
});
}
}
private async handleWaitForFinish(
agentIdOrIdentifier: string,
requestId: string,
timeoutMs?: number,
): Promise<void> {
const sourceSignal = this.delivery.requestSignal;
sourceSignal.throwIfAborted();
const resolved = await this.resolveAgentIdentifier(agentIdOrIdentifier);
if (!resolved.ok) {
this.emit({
type: "wait_for_finish_response",
payload: {
requestId,
status: "error",
final: null,
error: resolved.error,
lastMessage: null,
},
});
return;
}
const agentId = resolved.agentId;
const abortController = new AbortController();
const hasTimeout = typeof timeoutMs === "number" && timeoutMs > 0;
const timeoutHandle = hasTimeout
? setTimeout(() => {
abortController.abort("timeout");
}, timeoutMs)
: null;
try {
await this.agentMessages.waitForDispatch(
agentId,
AbortSignal.any([abortController.signal, sourceSignal]),
);
const live = this.agentManager.getAgent(agentId);
if (!live) {
const settled = await this.readSettledAgent(agentId);
if (!settled) {
this.emit({
type: "wait_for_finish_response",
payload: {
requestId,
status: "error",
final: null,
error: `Agent not found: ${agentId}`,
lastMessage: null,
},
});
return;
}
const { record, lastMessage } = settled;
const final = this.buildStoredAgentPayload(record);
let status: "permission" | "error" | "idle";
if (record.attentionReason === "permission") {
status = "permission";
} else if (record.lastStatus === "error") {
status = "error";
} else {
status = "idle";
}
const error = resolveWaitForFinishError({ status, final });
this.emit({
type: "wait_for_finish_response",
payload: { requestId, status, final, error, lastMessage },
});
return;
}
let result = await this.agentManager.waitForAgentEvent(agentId, {
signal: AbortSignal.any([abortController.signal, sourceSignal]),
waitForActive: true,
});
let final = await this.getAgentPayloadById(agentId);
if (!final) {
throw new Error(`Agent ${agentId} disappeared while waiting`);
}
let status: "permission" | "error" | "idle";
if (result.permission) {
status = "permission";
} else if (result.status === "error") {
status = "error";
} else {
status = "idle";
}
const error = resolveWaitForFinishError({ status, final });
this.emit({
type: "wait_for_finish_response",
payload: { requestId, status, final, error, lastMessage: result.lastMessage },
});
} catch (error) {
if (sourceSignal.aborted) return;
const isAbort =
error instanceof Error &&
(error.name === "AbortError" || error.message.toLowerCase().includes("aborted"));
if (!isAbort) {
const message = errorToFriendlyMessage(error);
this.sessionLogger.error({ err: error, agentId }, "wait_for_finish_request failed");
const final = await this.getAgentPayloadById(agentId);
this.emit({
type: "wait_for_finish_response",
payload: {
requestId,
status: "error",
final,
error: message,
lastMessage: null,
},
});
return;
}
const final = await this.getAgentPayloadById(agentId);
if (!final) {
throw new Error(`Agent ${agentId} disappeared while waiting`, { cause: error });
}
this.emit({
type: "wait_for_finish_response",
payload: { requestId, status: "timeout", final, error: null, lastMessage: null },
});
} finally {
if (timeoutHandle) {
clearTimeout(timeoutHandle);
}
}
}
/**
* Emit a message to the client
*/
// COMPAT(explicitEventSubscriptions): added in v0.8.0, remove legacy broadcasts after 2027-03-08.
private wantsEvent(event: SessionEventSubscription, source?: object): boolean {
// COMPAT(workspaceSetupObservation): added in v0.12.0, remove global setup delivery after 2027-04-19 once client floor >= v0.12.0.
if (
event === "workspace_setup_progress" &&
source &&
this.supportsForSource(CLIENT_CAPS.workspaceSetupObservation, source)
)
return false;
for (const subscription of this.eventSubscriptions.values()) {
if (
(!source || subscription.owner.source === source) &&
subscription.events.has(event) &&
!(
event === "workspace_setup_progress" &&
this.supportsForSource(CLIENT_CAPS.workspaceSetupObservation, subscription.owner.source)
)
)
return true;
}
if (!source && this.clientSources.size > 0) {
return [...this.clientSources.keys()].some((candidate) => this.wantsEvent(event, candidate));
}
if (source && this.delivery.isModern(source)) return false;
if (!source && !this.delivery.hasLegacySources()) return false;
// COMPAT(ownedSubscriptions): added in v0.8.0, remove implicit event delivery after 2027-03-09.
const capabilities = source
? (this.clientSources.get(source)?.capabilities ?? this.clientCapabilities)
: this.clientCapabilities;
return legacyWantsEvent(event, capabilities);
}
private emitEventToOwner(
subscription: { owner: OwnedSubscription; notifications: boolean },
message: SessionOutboundMessage,
notified: Set<object>,
): void {
if (
message.type !== "agent_attention_required" &&
message.type !== "terminal_attention_required"
) {
subscription.owner.emit(message);
return;
}
const shouldNotify =
message.payload.shouldNotify &&
subscription.notifications &&
!notified.has(subscription.owner.source);
subscription.owner.emit(
message.type === "agent_attention_required"
? { ...message, payload: { ...message.payload, shouldNotify } }
: { ...message, payload: { ...message.payload, shouldNotify } },
);
if (shouldNotify) notified.add(subscription.owner.source);
}
private emitSubscribedEvent(message: SessionOutboundMessage, onlySource?: object): boolean {
const event = sessionEventCategory(message);
if (!event) return false;
if (!this.authorization.allowsOutbound(message)) return true;
const delivered = new Set<object>();
const notified = new Set<object>();
for (const subscription of this.eventSubscriptions.values()) {
if (
!subscription.events.has(event) ||
(event === "workspace_setup_progress" &&
this.supportsForSource(
CLIENT_CAPS.workspaceSetupObservation,
subscription.owner.source,
)) ||
(onlySource && subscription.owner.source !== onlySource)
)
continue;
this.emitEventToOwner(subscription, message, notified);
delivered.add(subscription.owner.source);
}
// COMPAT(ownedSubscriptions): added in v0.8.0, remove implicit event delivery after 2027-03-09.
if (this.onMessageToSource && this.clientSources.size > 0) {
for (const source of this.clientSources.keys()) {
if (
(!onlySource || source === onlySource) &&
!delivered.has(source) &&
!this.delivery.isModern(source) &&
this.wantsEvent(event, source)
)
this.onMessageToSource(source, this.workspaceSetupMessageForClient(message, source));
}
} else if (delivered.size === 0 && this.wantsEvent(event)) this.onMessage(message);
return true;
}
private emit(msg: SessionOutboundMessage): void {
if (!this.authorization.allowsOutbound(msg)) return;
if (msg.type === "workspace_setup_progress") {
const { workspaceId, subscriptionId: _subscriptionId, ...snapshot } = msg.payload;
this.workspaceSetupObservation.publish(workspaceId, snapshot);
return;
}
if (this.delivery.reply(msg)) return;
if (this.emitSubscribedEvent(msg)) return;
// JSON.stringify(msg) is only computed when trace is enabled — it runs for
// every outbound message otherwise, and trace is disabled by default.
// Optional-chained because test logger stubs don't implement isLevelEnabled.
if (this.sessionLogger.isLevelEnabled?.("trace")) {
this.sessionLogger.trace(
{
messageType: msg.type,
payloadBytes: JSON.stringify(msg).length,
},
"agent.session.outbound",
);
}
if (msg.type === "workspace_setup_status_response") {
if (this.clientSources.size > 0 && this.onMessageToSource) {
for (const [source] of this.clientSources) {
this.onMessageToSource(source, this.workspaceSetupMessageForClient(msg, source));
}
return;
}
msg = this.workspaceSetupMessageForClient(msg);
}
this.onMessage(msg);
}
// COMPAT(workspaceSetupBlocked): added in v0.8.0, remove after 2027-03-07 once client floor >= v0.8.0.
private workspaceSetupMessageForClient(
message: SessionOutboundMessage,
source?: object,
): SessionOutboundMessage {
if (
message.type !== "workspace_setup_progress" &&
message.type !== "workspace_setup_status_response"
)
return message;
const supportsBlocked = source
? this.supportsForSource(CLIENT_CAPS.workspaceSetupBlocked, source)
: this.supports(CLIENT_CAPS.workspaceSetupBlocked);
const snapshot =
message.type === "workspace_setup_progress" ? message.payload : message.payload.snapshot;
if (supportsBlocked || snapshot?.status !== "blocked") return message;
const legacySnapshot = {
...snapshot,
status: "failed" as const,
error:
"Workspace setup is blocked pending approval of code from a fork pull request. Update Paseo to review and run setup.",
};
return message.type === "workspace_setup_progress"
? { ...message, payload: { ...message.payload, ...legacySnapshot } }
: { ...message, payload: { ...message.payload, snapshot: legacySnapshot } };
}
private emitBinary(frame: Uint8Array): void {
if (!this.onBinaryMessage) {
return;
}
try {
this.onBinaryMessage(frame);
} catch (error) {
this.sessionLogger.error({ err: error }, "Failed to emit binary frame");
}
}
private async emitBinaryReply(frame: Uint8Array, source?: object): Promise<void> {
if (source && this.onBinaryMessageToSource) {
this.delivery.authorizeBinaryReply(frame, source);
await this.onBinaryMessageToSource(source, frame);
return;
}
this.emitBinary(frame);
}
private observeSourceSend(send: () => void | Promise<void>): Promise<void> {
let pending: Promise<void>;
try {
pending = Promise.resolve(send());
} catch (error) {
pending = Promise.reject(error);
}
// Legacy producers fire and forget; owned streams await the same failure.
void pending.catch((error) =>
this.sessionLogger.warn({ err: error }, "Failed to emit source frame"),
);
return pending;
}
private emitForSource(msg: SessionOutboundMessage, source?: object): void {
if (source && this.onMessageToSource) {
msg = this.delivery.authorizeReply(msg, source);
this.onMessageToSource(source, msg);
return;
}
this.emit(msg);
}
/**
* Clean up session resources
*/
public async cleanup(): Promise<void> {
this.sessionLogger.trace({}, "agent.session.lifecycle.cleanup");
this.isCleanedUp = true;
this.unsubscribeSetupObservation();
await this.delivery.close();
if (this.unsubscribeAgentEvents) {
this.unsubscribeAgentEvents();
this.unsubscribeAgentEvents = null;
}
this.unsubscribeProjectMutations?.();
this.unsubscribeProjectMutations = null;
this.unsubscribePluginChanges?.();
this.unsubscribePluginChanges = null;
this.unsubscribeWorkspaceMutations?.();
this.unsubscribeWorkspaceMutations = null;
this.agentUpdates.dispose();
await this.hubExecutionController?.cleanup();
if (this.unsubscribeTerminalWorkspaceContributionEvents) {
this.unsubscribeTerminalWorkspaceContributionEvents();
this.unsubscribeTerminalWorkspaceContributionEvents = null;
}
this.providerCatalogSession.dispose();
this.terminalController.dispose();
this.checkoutSession.cleanup();
this.workspaceGitObserver.dispose();
}
}
interface CloneRepositoryInput {
name: string;
displayName: string;
cloneUrl: string;
}
function normalizeCloneRepository(input: {
repo: string;
cloneProtocol?: "https" | "ssh";
}): CloneRepositoryInput {
const trimmed = input.repo.trim();
if (!trimmed) {
throw new Error("Repository is required");
}
const remote = parseGitRemoteLocation(trimmed);
if (remote) {
const segments = remote.path.split("/").filter(Boolean);
const name = segments.at(-1);
if (!name || !isValidGitHubRepoSegment(name)) {
throw new Error("Repository name contains invalid characters");
}
return { name, displayName: remote.path, cloneUrl: trimmed };
}
const [owner, rawName, ...extra] = trimmed.split("/");
if (!owner || !rawName || extra.length > 0) {
throw new Error("Repository must use owner/repo format or a git remote URL");
}
const name = rawName.endsWith(".git") ? rawName.slice(0, -4) : rawName;
if (!isValidGitHubRepoSegment(owner) || !isValidGitHubRepoSegment(name)) {
throw new Error("Repository contains invalid characters");
}
if (!input.cloneProtocol) {
throw new Error("Clone protocol is required for owner/repo repository names");
}
const cloneUrl =
input.cloneProtocol === "ssh"
? `git@github.com:${owner}/${name}.git`
: `https://github.com/${owner}/${name}.git`;
return {
name,
displayName: `${owner}/${name}`,
cloneUrl,
};
}
function isValidGitHubRepoSegment(value: string): boolean {
return /^[A-Za-z0-9._-]+$/u.test(value);
}
function sessionEventCategory(message: SessionOutboundMessage): SessionEventSubscription | null {
switch (message.type) {
case "project.update":
case "providers_snapshot_update":
case "agent_attention_required":
case "agent_permission_request":
case "agent_permission_resolved":
case "checkout_status_update":
case "script_status_update":
case "workspace_setup_progress":
case "agent.provider_subagents.update":
case "terminal_attention_required":
case "activity_log":
case "hub.execution.agent.update":
case "hub.execution.agent.stream":
return message.type;
case "status":
switch (message.payload.status) {
case "server_info":
return "status.server_info";
case "daemon_config_changed":
return "status.daemon_config_changed";
case "plugin_catalog_changed":
return "status.plugin_catalog_changed";
case "plugin_settings_changed":
return "status.plugin_settings_changed";
default:
return null;
}
default:
return null;
}
}
// COMPAT(ownedSubscriptions): added in v0.8.0, remove implicit event delivery after 2027-03-09.
function legacyWantsEvent(
event: SessionEventSubscription,
capabilities: ReadonlySet<ClientCapability>,
): boolean {
if (event === "project.update" && !capabilities.has(CLIENT_CAPS.projectUpdates)) return false;
switch (event) {
case "project.update":
case "providers_snapshot_update":
case "agent_attention_required":
case "agent_permission_request":
case "agent_permission_resolved":
return !capabilities.has(CLIENT_CAPS.explicitEventSubscriptions);
case "agent.provider_subagents.update":
return capabilities.has(CLIENT_CAPS.providerSubagents);
default:
return true;
}
}
function createHostSession(options: SessionOptions, session: Session): ConnectedHostSession | null {
if (!options.connectedHosts) return null;
return new ConnectedHostSession(
options.connectedHosts,
session.delivery,
{ sourceId: options.sourceId ?? "owner", manageSources: false },
() => session.getPermissions(),
(message) => session.publish(message),
() => session.isRouted(),
session.getSessionId(),
() => options.getConnectedHostBufferedAmount?.() ?? 0,
);
}
function hostSource(options: SessionOptions): string {
return options.sourceId ?? "owner";
}