import { type EnvironmentId, type ProviderInstanceId, type ServerConfig, type ServerConfigStreamEvent, type ServerLifecycleWelcomePayload, type ServerLifecycleStreamReadyEvent, type ServerProviderWorkspaceSnapshot, type ServerSelfUpdateProgressEvent, type ServerSelfUpdateResult, shouldRecommendDatabaseCompaction, WS_METHODS, } from "@t3tools/contracts"; import * as Cause from "effect/Cause"; import * as Duration from "effect/Duration"; import * as Equal from "effect/Equal"; import * as Deferred from "effect/Deferred"; import * as Effect from "effect/Effect"; import * as Exit from "effect/Exit"; import * as Fiber from "effect/Fiber"; import * as Option from "effect/Option"; import * as Queue from "effect/Queue"; import * as Ref from "effect/Ref"; import * as Result from "effect/Result"; import * as Schema from "effect/Schema"; import * as Stream from "effect/Stream"; import * as SubscriptionRef from "effect/SubscriptionRef"; import { AsyncResult, Atom } from "effect/reactivity"; import { createAtomCommandScheduler, createEnvironmentRpcCommand, createEnvironmentQueryAtomFamily, createEnvironmentRpcQueryAtomFamily, createEnvironmentRpcSubscriptionAtomFamily, createEnvironmentSubscriptionAtomFamily, createRuntimeCommand, scheduleAtomCommandEffect, } from "./runtime.ts"; import * as EnvironmentRegistry from "../connection/registry.ts"; import * as EnvironmentSupervisor from "../connection/supervisor.ts"; import { safeErrorLogAttributes } from "../errors/safeLog.ts"; import * as Persistence from "../platform/persistence.ts"; import { runCachePersistence } from "./cachePersistence.ts"; import { isRpcClientError, request, requestGuarded, runStream, subscribe, subscribeDynamicWithSession, type EnvironmentRpcInput, } from "../rpc/client.ts"; import type { RpcSession } from "../rpc/session.ts"; import { followStreamInEnvironment } from "./runtime.ts"; import { applyServerConfigProjection, reuseUnchanged, type ServerConfigProjection, withoutEnvironmentThemes, } from "./serverConfigProjection.ts"; export type { ServerConfigProjection } from "./serverConfigProjection.ts"; export type ServerUpdateStage = "downloading" | "installing" | "resuming"; export type ServerUpdateState = | { readonly status: "idle" } | { readonly status: "running"; readonly stage: ServerUpdateStage; readonly fromVersion: string; readonly targetVersion: string; } | { readonly status: "failed"; readonly stage: ServerUpdateStage; readonly fromVersion: string; readonly targetVersion: string; readonly message: string; }; export interface ServerUpdateTarget { readonly environmentId: EnvironmentId; readonly input: EnvironmentRpcInput; } const IDLE_SERVER_UPDATE_STATE: ServerUpdateState = { status: "idle" }; const EMPTY_SERVER_UPDATE_STATE_ATOM = Atom.make(IDLE_SERVER_UPDATE_STATE).pipe( Atom.withLabel("environment-data:server:update-state:empty"), ); export const serverUpdateStateAtom = Atom.family((environmentId: EnvironmentId) => Atom.make(IDLE_SERVER_UPDATE_STATE).pipe( Atom.withLabel(`environment-data:server:update-state:${environmentId}`), ), ); export class ServerUpdateResumeTimeoutError extends Schema.TaggedError()( "ServerUpdateResumeTimeoutError", { environmentId: Schema.String, targetVersion: Schema.String, }, ) { override get message(): string { return `Could not confirm that the server resumed on t3@${this.targetVersion}. Reconnect to check the update outcome.`; } } export class ServerUpdateProgressIncompleteError extends Schema.TaggedError()( "ServerUpdateProgressIncompleteError", { targetVersion: Schema.String, }, ) { override get message(): string { return `The t3@${this.targetVersion} update ended before the server accepted the restart.`; } } export class ServerUpdateTerminalError extends Schema.TaggedError()( "ServerUpdateTerminalError", { targetVersion: Schema.String, status: Schema.Literals(["committed", "rolled-back", "failed"]), reason: Schema.optional(Schema.String), }, ) { override get message(): string { return this.reason ?? `The t3@${this.targetVersion} update ${this.status}.`; } } // Covers the service launcher's 30-minute trial deadline (PREPARED_TIMEOUT_MS) // and a final restart of the previous version when the trial rolls back. const SERVER_UPDATE_RESUME_TIMEOUT = Duration.minutes(32); export function matchesServerUpdateReadyEvent( result: ServerSelfUpdateResult, event: ServerLifecycleStreamReadyEvent, ): boolean { return result.updateId === undefined ? event.payload.environment.serverVersion === result.targetVersion : event.payload.updateOutcome?.id === result.updateId; } export function matchesServerUpdateResumeEvent( result: ServerSelfUpdateResult, event: ServerLifecycleStreamReadyEvent, ): boolean { return ( (result.method === "desktop-app" && result.desktopUpdateToken !== undefined) || matchesServerUpdateReadyEvent(result, event) ); } export function validateServerUpdateReadyEvent( result: ServerSelfUpdateResult, event: ServerLifecycleStreamReadyEvent, ): Effect.Effect { if (result.updateId === undefined) return Effect.void; const outcome = event.payload.updateOutcome; if ( outcome?.id === result.updateId && outcome.status === "committed" && outcome.targetVersion === result.targetVersion && event.payload.environment.serverVersion === result.targetVersion ) { return Effect.void; } return Effect.fail( new ServerUpdateTerminalError({ targetVersion: result.targetVersion, status: outcome?.status ?? "failed", reason: outcome?.reason ?? "The service launcher resumed without committing the requested server version.", }), ); } /** * The sleep before each nudge is the pacer: a connection that fails instantly * re-enters backoff immediately and would otherwise spin a tight retry loop. * * Callers fork this as a child of the update command so it is interrupted as * soon as the update settles, whether it succeeds, fails, or times out. */ export function nudgeReconnectDuringUpdateRestart(input: { readonly stateChanges: Stream.Stream< { readonly phase: string; readonly lastFailure?: { readonly reason: string } | null; }, unknown >; readonly retryNow: Effect.Effect; readonly interval?: Duration.Duration; }): Effect.Effect { return input.stateChanges.pipe( Stream.filter( (state) => state.phase === "backoff" || (state.phase === "blocked" && state.lastFailure?.reason === "authentication"), ), Stream.runForEach(() => Effect.sleep(input.interval ?? Duration.seconds(1)).pipe(Effect.andThen(input.retryNow)), ), Effect.timeoutOption(SERVER_UPDATE_RESUME_TIMEOUT), Effect.ignore, ); } export function waitForNextEnvironmentReconnect( stateChanges: Stream.Stream<{ readonly phase: string }, E>, ): Effect.Effect { return stateChanges.pipe( Stream.dropWhile((state) => state.phase === "connected"), Stream.filter((state) => state.phase === "connected"), Stream.runHead, Effect.asVoid, ); } export const runDesktopCommitWithReconnectObserver = Effect.fn( "runDesktopCommitWithReconnectObserver", )(function* ( stateChanges: Stream.Stream<{ readonly phase: string }, EState>, commit: Effect.Effect, ) { const armed = yield* Deferred.make(); const reconnected = yield* Deferred.make(); const observer = yield* stateChanges.pipe( Stream.tap(() => Deferred.succeed(armed, undefined)), waitForNextEnvironmentReconnect, Effect.andThen(Deferred.succeed(reconnected, undefined)), Effect.forkChild, ); yield* Deferred.await(armed); const commitExit = yield* commit.pipe(Effect.exit); if (Exit.isSuccess(commitExit)) { yield* Fiber.interrupt(observer); return; } if (!isLegacyUpdateHandoffLoss(commitExit.cause)) { yield* Fiber.interrupt(observer); return yield* Effect.failCause(commitExit.cause); } yield* Deferred.await(reconnected).pipe(Effect.timeout(SERVER_UPDATE_RESUME_TIMEOUT)); return yield* Effect.failCause(commitExit.cause); }); export const waitForDesktopUpdateTarget = Effect.fn("waitForDesktopUpdateTarget")(function* < EReady, ECommit, >( targetVersion: string, nextReady: Effect.Effect, retryCommit: Effect.Effect, ): Effect.fn.Return { const maxCommitAttempts = 3; for (let attempt = 1; attempt <= maxCommitAttempts; attempt += 1) { const ready = yield* nextReady; if (ready.payload.environment.serverVersion === targetVersion) return ready; if (attempt === maxCommitAttempts) break; const retryExit = yield* retryCommit.pipe(Effect.exit); if (Exit.isSuccess(retryExit)) break; if (!isLegacyUpdateHandoffLoss(retryExit.cause)) { return yield* Effect.failCause(retryExit.cause); } } return yield* new ServerUpdateTerminalError({ targetVersion, status: "failed", reason: "The desktop app resumed without installing the prepared update.", }); }); export function serverUpdateStateForProgressEvent( fromVersion: string, targetVersion: string, event: ServerSelfUpdateProgressEvent, ): Extract { return { status: "running", stage: event.type !== "complete" ? event.stage : event.result.method === "desktop-app" && event.result.desktopUpdateToken !== undefined ? "installing" : "resuming", fromVersion, targetVersion, }; } export function serverUpdateStateForServerVersion( state: ServerUpdateState, serverVersion: string | null, ): ServerUpdateState { return state.status === "idle" || state.status === "running" || serverVersion === null || state.fromVersion === serverVersion ? state : IDLE_SERVER_UPDATE_STATE; } export function serverUpdateFailureMessage(error: unknown): string { return error instanceof Error ? error.message : "Server update failed."; } function isRpcSocketError(error: unknown): boolean { if (!isRpcClientError(error)) { return false; } switch (error.reason._tag) { case "SocketReadError": case "SocketWriteError": case "SocketCloseError": return true; default: return false; } } export function isLegacyUpdateHandoffLoss(cause: Cause.Cause): boolean { if (Cause.hasInterruptsOnly(cause)) { return true; } return ( cause.reasons.length > 0 && cause.reasons.every((reason) => Cause.isFailReason(reason) && isRpcSocketError(reason.error)) ); } export function resolveServerUpdateProgressResult( targetVersion: string, terminal: Option.Option, streamExit: Exit.Exit, accepted?: ServerSelfUpdateResult, ): Effect.Effect { if ( Option.isSome(terminal) && (Exit.isSuccess(streamExit) || isLegacyUpdateHandoffLoss(streamExit.cause)) ) { return Effect.succeed(terminal.value); } if (Exit.isFailure(streamExit)) { if (accepted !== undefined && isLegacyUpdateHandoffLoss(streamExit.cause)) { return Effect.succeed(accepted); } return Effect.failCause(streamExit.cause); } return Effect.fail(new ServerUpdateProgressIncompleteError({ targetVersion })); } const cachedConfigSnapshotEvent = (config: ServerConfig): ServerConfigStreamEvent => ({ version: 1, type: "snapshot", config, }); export interface ServerConfigSubscriptionOptions { readonly environmentThemes?: boolean; readonly usageLimitSources?: boolean; readonly usageLimitsCommand?: boolean; readonly providerCatalogsOnDemand?: boolean; } // Configs are JSON-shaped. Missing and undefined object fields persist identically. function sameCacheValue(previous: unknown, next: unknown): boolean { if (previous === next) return true; if ( previous === null || next === null || typeof previous !== "object" || typeof next !== "object" ) { return false; } if (Array.isArray(previous) || Array.isArray(next)) { if (!Array.isArray(previous) || !Array.isArray(next) || previous.length !== next.length) { return false; } for (let index = 0; index < previous.length; index += 1) { if (!sameCacheValue(previous[index], next[index])) return false; } return true; } if ( Object.getPrototypeOf(previous) !== Object.prototype || Object.getPrototypeOf(next) !== Object.prototype ) { return false; } const before = previous as Record; const after = next as Record; return ( Object.keys(before).every((key) => sameCacheValue(before[key], Object.hasOwn(after, key) ? after[key] : undefined), ) && Object.keys(after).every((key) => Object.hasOwn(before, key) || after[key] === undefined) ); } export const makeEnvironmentServerConfigState = Effect.fn("EnvironmentServerConfigState.make")( function* (subscription: ServerConfigSubscriptionOptions) { const supervisor = yield* EnvironmentSupervisor.EnvironmentSupervisor; const cache = yield* Persistence.EnvironmentCacheStore; const environmentId = supervisor.target.environmentId; const cachedConfig = yield* cache.loadServerConfig(environmentId).pipe( Effect.catch((error) => Effect.logWarning("Could not load cached server configuration.").pipe( Effect.annotateLogs({ environmentId, ...safeErrorLogAttributes(error), }), Effect.as(Option.none()), ), ), ); const state = yield* SubscriptionRef.make>( Option.map(cachedConfig, (cached) => ({ config: withoutEnvironmentThemes(cached), latestEvent: cachedConfigSnapshotEvent(withoutEnvironmentThemes(cached)), source: "cache" as const, })), ); const persistence = yield* Queue.sliding(1); const pendingPersistence = yield* Ref.make>(Option.none()); let lastPersisted: ServerConfig | undefined; const persist = Effect.fn("EnvironmentServerConfigState.persist")(function* ( config: ServerConfig, ) { // A cached stall would read as a fresh freeze on the next launch. const { environmentThemes: _themes, usageLimitSources: _sources, lastEventLoopStall: _stall, ...cacheable } = config; if (lastPersisted !== undefined && sameCacheValue(lastPersisted, cacheable)) { return true; } const saved = yield* cache.saveServerConfig(environmentId, cacheable).pipe( Effect.as(true), Effect.catch((error) => Effect.logWarning("Could not persist cached server configuration.").pipe( Effect.annotateLogs({ environmentId, ...safeErrorLogAttributes(error), }), Effect.as(false), ), ), ); if (saved) lastPersisted = cacheable; return saved; }); const persistPending = Effect.fn("EnvironmentServerConfigState.persistPending")(function* ( config: ServerConfig, ) { if (!(yield* persist(config))) { return; } yield* Ref.update(pendingPersistence, (pending) => Option.isSome(pending) && pending.value === config ? Option.none() : pending, ); }); yield* Effect.addFinalizer(() => Ref.get(pendingPersistence).pipe( Effect.flatMap( Option.match({ onNone: () => Effect.void, onSome: (config) => persist(config).pipe(Effect.asVoid), }), ), ), ); yield* runCachePersistence(persistence, persistPending).pipe(Effect.forkScoped); yield* subscribe(WS_METHODS.subscribeServerConfig, { ...(subscription.environmentThemes === true ? { environmentThemes: true } : {}), ...(subscription.usageLimitSources === true ? { usageLimitSources: true } : {}), ...(subscription.usageLimitsCommand === true ? { usageLimitsCommand: true } : {}), ...(subscription.providerCatalogsOnDemand === true ? { providerCatalogsOnDemand: true } : {}), }).pipe( Stream.runForEach((event) => Effect.gen(function* () { const next = applyServerConfigProjection(yield* SubscriptionRef.get(state), event); if (Option.isNone(next)) { return; } yield* Ref.set(pendingPersistence, Option.some(next.value.config)); yield* SubscriptionRef.set(state, next); yield* Queue.offer(persistence, next.value.config); }), ), Effect.forkScoped, ); return state; }, ); function serverConfigStateChanges( environmentId: EnvironmentId, subscription: ServerConfigSubscriptionOptions, ) { return followStreamInEnvironment( environmentId, Stream.unwrap( makeEnvironmentServerConfigState(subscription).pipe( Effect.map((state) => SubscriptionRef.changes(state).pipe( Stream.filterMap((projection) => Option.match(projection, { onNone: () => Result.failVoid, onSome: (value) => Result.succeed(value), }), ), ), ), ), ), ); } export function applyServerWelcomeEvent( current: EnvironmentServerWelcomeState, session: RpcSession, event: { readonly type: "welcome" | "ready" | "legacyThreadMigration"; readonly payload: unknown; }, ): EnvironmentServerWelcomeState { return event.type === "welcome" && current.currentSession === session ? { ...current, welcomeSession: session, welcome: event.payload as ServerLifecycleWelcomePayload, } : current; } export interface EnvironmentServerWelcomeState { readonly currentSession: RpcSession | null; readonly welcomeSession: RpcSession | null; readonly welcome: ServerLifecycleWelcomePayload | null; } export function resolveServerWelcomeState( state: EnvironmentServerWelcomeState, ): ServerLifecycleWelcomePayload | null { return state.currentSession === state.welcomeSession ? state.welcome : null; } export const makeEnvironmentServerWelcomeState = Effect.fn("EnvironmentServerWelcomeState.make")( function* () { const supervisor = yield* EnvironmentSupervisor.EnvironmentSupervisor; const initialSession = Option.getOrNull(yield* SubscriptionRef.get(supervisor.session)); const state = yield* SubscriptionRef.make({ currentSession: initialSession, welcomeSession: null, welcome: null, }); const updateWithCurrentSession = Effect.fn( "EnvironmentServerWelcomeState.updateWithCurrentSession", )(function* ( update: ( current: EnvironmentServerWelcomeState, currentSession: RpcSession | null, ) => EnvironmentServerWelcomeState, ) { return yield* SubscriptionRef.modifyEffect(state, (current) => SubscriptionRef.get(supervisor.session).pipe( Effect.map( (latestSession) => [undefined, update(current, Option.getOrNull(latestSession))] as const, ), ), ); }); yield* SubscriptionRef.changes(supervisor.session).pipe( Stream.runForEach(() => updateWithCurrentSession((current, currentSession) => ({ ...current, currentSession, })), ), Effect.forkScoped, ); yield* subscribeDynamicWithSession( WS_METHODS.subscribeServerLifecycle, Effect.fn("EnvironmentServerWelcomeState.makeSubscribeInput")(function* (session) { yield* updateWithCurrentSession((current, currentSession) => currentSession === session ? { ...current, currentSession, welcomeSession: session, welcome: null, } : { ...current, currentSession }, ); return {}; }), ).pipe( Stream.runForEach(([session, event]) => updateWithCurrentSession((current, currentSession) => applyServerWelcomeEvent( { ...current, currentSession, }, session, event, ), ), ), Effect.forkScoped, ); return state; }, ); function serverWelcomeStateChanges(environmentId: EnvironmentId) { return followStreamInEnvironment( environmentId, Stream.unwrap( makeEnvironmentServerWelcomeState().pipe( Effect.map((state) => SubscriptionRef.changes(state).pipe(Stream.map(resolveServerWelcomeState)), ), ), ), ); } export function resolveServerConfigValue( projection: ServerConfigProjection | null, initialConfig: ServerConfig | null, ): ServerConfig | null { if ( projection?.source === "live" && (initialConfig === null || projection.config.environment.serverVersion === initialConfig.environment.serverVersion) ) { return projection.config; } return initialConfig ?? projection?.config ?? null; } type ServerProviderEntry = ServerConfig["providers"][number]; /** * The provider-wide fields a workspace snapshot builds on. A live session, a sign-out, or a * /usage-limits change shows up here, so a snapshot fetched before it no longer applies. */ const snapshotBasis = (provider: ServerProviderEntry | undefined) => provider && { enabled: provider.enabled, slashCommands: provider.slashCommands, skills: provider.skills, }; interface FetchedWorkspaceSnapshot { readonly instanceId: ProviderInstanceId; readonly snapshot: ServerProviderWorkspaceSnapshot; readonly basis: ReturnType; } const MAX_FETCHED_WORKSPACE_SNAPSHOTS = 32; /** * Servers with `providerWorkspaceSnapshotsOnDemand` leave per-cwd snapshots out of the config, * so the ones this client fetched are merged back in. Older servers send every snapshot and * their drops, so their config is used as is. */ export function withFetchedWorkspaceSnapshots( config: ServerConfig, fetched: ReadonlyArray, ): ServerConfig { if ( fetched.length === 0 || config.environment.capabilities.providerWorkspaceSnapshotsOnDemand !== true ) { return config; } return { ...config, providers: config.providers.map((provider) => { // A dropped snapshot reads as lost, and the composer fetches it again. const own = fetched.filter( (entry) => entry.instanceId === provider.instanceId && Equal.equals(entry.basis, snapshotBasis(provider)), ); return own.length === 0 ? provider : { ...provider, workspaceSnapshots: own.map((entry) => entry.snapshot) }; }), }; } export function recordFetchedWorkspaceSnapshot( current: ReadonlyArray, input: { readonly instanceId: ProviderInstanceId; readonly cwd: string; readonly fresh: boolean }, providers: ServerConfig["providers"], configProvider: ServerProviderEntry | undefined, ): ReadonlyArray { const basis = snapshotBasis(configProvider); const snapshot = providers .find((provider) => provider.instanceId === input.instanceId) ?.workspaceSnapshots?.find((candidate) => candidate.cwd === input.cwd); const stored = current.find( (entry) => entry.instanceId === input.instanceId && entry.snapshot.cwd === input.cwd, ); // A cached answer from the server would otherwise re-render every config reader. if ( !input.fresh && snapshot && stored && Equal.equals(stored.snapshot, snapshot) && Equal.equals(stored.basis, basis) ) { return current; } // A fresh scan drops the cwd's snapshots for every provider, as the server does. const kept = current.filter( (entry) => entry.snapshot.cwd !== input.cwd || (!input.fresh && entry.instanceId !== input.instanceId), ); if (!snapshot) return kept.length === current.length ? current : kept; return [...kept, { instanceId: input.instanceId, snapshot, basis }].slice( -MAX_FETCHED_WORKSPACE_SNAPSHOTS, ); } export function createServerEnvironmentAtoms( runtime: Atom.AtomRuntime< EnvironmentRegistry.EnvironmentRegistry | Persistence.EnvironmentCacheStore | R, E >, options: { readonly initialConfigValueAtom: ( environmentId: EnvironmentId, ) => Atom.Atom; readonly environmentThemes?: boolean; readonly usageLimitSources?: boolean; readonly usageLimitsCommand?: boolean; readonly providerCatalogsOnDemand?: boolean; }, ) { const configScheduler = createAtomCommandScheduler(); const updateScheduler = createAtomCommandScheduler(); const configConcurrency = { mode: "serial" as const, key: ({ environmentId }: { readonly environmentId: string }) => environmentId, }; const configProjectionFamily = Atom.family((environmentId: EnvironmentId) => runtime .atom( serverConfigStateChanges(environmentId, { ...(options.environmentThemes === true ? { environmentThemes: true } : {}), ...(options.usageLimitSources === true ? { usageLimitSources: true } : {}), ...(options.usageLimitsCommand === true ? { usageLimitsCommand: true } : {}), ...(options.providerCatalogsOnDemand === true ? { providerCatalogsOnDemand: true } : {}), }), ) .pipe( Atom.setIdleTTL(5 * 60_000), Atom.withLabel(`environment-data:server:config-projection:${environmentId}`), ), ); const configProjection = (target: { readonly environmentId: EnvironmentId; readonly input: EnvironmentRpcInput; }) => configProjectionFamily(target.environmentId); const fetchedWorkspaceSnapshotsAtom = Atom.family((environmentId: EnvironmentId) => Atom.make>([]).pipe( Atom.keepAlive, Atom.withLabel(`environment-data:server:fetched-workspace-snapshots:${environmentId}`), ), ); const emptyConfigAtom = Atom.make(null).pipe( Atom.withLabel("environment-data:server:config:empty"), ); const configValueAtom = Atom.family((environmentId: EnvironmentId | null) => { if (environmentId === null) { return emptyConfigAtom; } let previousProviders: ServerConfig["providers"] | undefined; return Atom.make((get): ServerConfig | null => { const projection = AsyncResult.getOrElse( get(configProjection({ environmentId, input: {} })), () => null, ); const config = resolveServerConfigValue( projection, get(options.initialConfigValueAtom(environmentId)), ); if (config === null) return null; const merged = withFetchedWorkspaceSnapshots( config, get(fetchedWorkspaceSnapshotsAtom(environmentId)), ); if (merged === config) return config; // Keep provider identities stable across config events; the projection already reuses the rest. previousProviders = reuseUnchanged(previousProviders, merged.providers); return { ...merged, providers: previousProviders }; }).pipe(Atom.withLabel(`environment-data:server:config:${environmentId}`)); }); const updateStateValueAtom = Atom.family((environmentId: EnvironmentId) => Atom.make((get) => serverUpdateStateForServerVersion( get(serverUpdateStateAtom(environmentId)), get(configValueAtom(environmentId))?.environment.serverVersion ?? null, ), ).pipe(Atom.withLabel(`environment-data:server:update-state-value:${environmentId}`)), ); const updateStateAtom = (environmentId: EnvironmentId | null) => environmentId === null ? EMPTY_SERVER_UPDATE_STATE_ATOM : updateStateValueAtom(environmentId); const updateServer = createRuntimeCommand< EnvironmentRegistry.EnvironmentRegistry | Persistence.EnvironmentCacheStore | R, E, ServerUpdateTarget, ServerSelfUpdateResult, unknown >(runtime, { label: "environment-data:server:update-server", scheduler: updateScheduler, concurrency: configConcurrency, execute: Effect.fn("ServerUpdate.updateServer")(function (target, atomRegistry) { const stateAtom = serverUpdateStateAtom(target.environmentId); let targetVersion = target.input.targetVersion; let fromVersion = atomRegistry.get(configValueAtom(target.environmentId))?.environment.serverVersion ?? targetVersion; let currentStage: ServerUpdateStage = "downloading"; let desktopCommitLostTransport = false; atomRegistry.set(stateAtom, { status: "running", stage: currentStage, fromVersion, targetVersion, }); return Effect.gen(function* () { const environmentRegistry = yield* EnvironmentRegistry.EnvironmentRegistry; const desktopCommitStarting = yield* Deferred.make(); const desktopReconnectObserverArmed = yield* Deferred.make(); const desktopReconnected = yield* Deferred.make(); yield* Deferred.await(desktopCommitStarting).pipe( Effect.andThen( environmentRegistry.stateChanges(target.environmentId).pipe( Stream.tap(() => Deferred.succeed(desktopReconnectObserverArmed, undefined)), Stream.dropWhile((state) => state.phase === "connected"), Stream.filter((state) => state.phase === "connected"), Stream.runHead, ), ), Effect.andThen(Deferred.succeed(desktopReconnected, undefined)), Effect.forkChild, ); const result = yield* scheduleAtomCommandEffect( atomRegistry, configScheduler, configConcurrency, target, Effect.gen(function* () { const currentConfig = atomRegistry.get(configValueAtom(target.environmentId)); fromVersion = currentConfig?.environment.serverVersion ?? targetVersion; atomRegistry.set(stateAtom, { status: "running", stage: currentStage, fromVersion, targetVersion, }); const supportsProgress = currentConfig?.environment.capabilities.serverSelfUpdateProgress === true; const updateResult: ServerSelfUpdateResult = supportsProgress ? yield* Effect.gen(function* () { let receivedProgress = false; const terminal = yield* Ref.make>( Option.none(), ); const streamExit = yield* environmentRegistry .runStream( target.environmentId, runStream(WS_METHODS.serverUpdateServerWithProgress, target.input), ) .pipe( Stream.runForEach((event) => Effect.sync(() => { receivedProgress ||= event.type === "progress"; const state = serverUpdateStateForProgressEvent( fromVersion, targetVersion, event, ); currentStage = state.stage; atomRegistry.set(stateAtom, state); }).pipe( Effect.andThen( event.type === "complete" ? Ref.set(terminal, Option.some(event.result)) : Effect.void, ), ), ), Effect.exit, ); const selfUpdateMethod = currentConfig?.environment.capabilities.serverSelfUpdate; // Desktop preparation needs its terminal commit token. const accepted = receivedProgress && (selfUpdateMethod === "boot-service" || selfUpdateMethod === "respawn") ? { targetVersion, method: selfUpdateMethod } : undefined; return yield* resolveServerUpdateProgressResult( targetVersion, yield* Ref.get(terminal), streamExit, accepted, ); }) : yield* Effect.gen(function* () { const selfUpdateMethod = currentConfig?.environment.capabilities.serverSelfUpdate; const exit = yield* environmentRegistry .run(target.environmentId, request(WS_METHODS.serverUpdateServer, target.input)) .pipe(Effect.exit); if (Exit.isSuccess(exit)) { return exit.value; } if ( (selfUpdateMethod === "boot-service" || selfUpdateMethod === "respawn") && isLegacyUpdateHandoffLoss(exit.cause) ) { // Older servers can tear down the transport before their // unary acknowledgement arrives. Treat only that transport // loss as a handoff, then prove it by waiting for target ready. return { targetVersion, method: selfUpdateMethod }; } return yield* Effect.failCause(exit.cause); }); if ( updateResult.method === "desktop-app" && updateResult.desktopUpdateToken !== undefined ) { yield* Deferred.succeed(desktopCommitStarting, undefined); yield* Deferred.await(desktopReconnectObserverArmed); const commitExit = yield* environmentRegistry .run( target.environmentId, request(WS_METHODS.serverCommitDesktopUpdate, { requestId: updateResult.desktopUpdateToken, }), ) .pipe(Effect.exit); if (Exit.isFailure(commitExit) && !isLegacyUpdateHandoffLoss(commitExit.cause)) { return yield* Effect.failCause(commitExit.cause); } desktopCommitLostTransport = Exit.isFailure(commitExit); } targetVersion = updateResult.targetVersion; currentStage = "resuming"; atomRegistry.set(stateAtom, { status: "running", stage: currentStage, fromVersion, targetVersion, }); return updateResult; }), ); // A track switch's desktop host already on its track's newest build only saved the // track; nothing restarts. if ( result.method === "desktop-app" && result.desktopUpdateToken === undefined && result.targetVersion === fromVersion ) { atomRegistry.set(stateAtom, IDLE_SERVER_UPDATE_STATE); return result; } yield* nudgeReconnectDuringUpdateRestart({ stateChanges: environmentRegistry.stateChanges(target.environmentId), retryNow: environmentRegistry.retryNow(target.environmentId), }).pipe(Effect.forkChild); if (result.method === "desktop-app" && desktopCommitLostTransport) { yield* Deferred.await(desktopReconnected).pipe( Effect.timeout(SERVER_UPDATE_RESUME_TIMEOUT), ); } const waitForReady = environmentRegistry .followStream(target.environmentId, subscribe(WS_METHODS.subscribeServerLifecycle, {})) .pipe( Stream.filter( (event): event is ServerLifecycleStreamReadyEvent => event.type === "ready" && matchesServerUpdateResumeEvent(result, event), ), Stream.runHead, Effect.timeoutOption(SERVER_UPDATE_RESUME_TIMEOUT), Effect.map(Option.flatten), ); const nextReady = waitForReady.pipe( Effect.flatMap( Option.match({ onNone: () => new ServerUpdateResumeTimeoutError({ environmentId: target.environmentId, targetVersion, }), onSome: Effect.succeed, }), ), ); const desktopUpdateToken = result.desktopUpdateToken; const resumed = result.method === "desktop-app" && desktopUpdateToken !== undefined ? yield* waitForDesktopUpdateTarget( result.targetVersion, nextReady, runDesktopCommitWithReconnectObserver( environmentRegistry.stateChanges(target.environmentId), environmentRegistry .run( target.environmentId, request(WS_METHODS.serverCommitDesktopUpdate, { requestId: desktopUpdateToken, }), ) .pipe(Effect.asVoid), ), ) : yield* nextReady; yield* validateServerUpdateReadyEvent(result, resumed); atomRegistry.set(stateAtom, IDLE_SERVER_UPDATE_STATE); return result; }).pipe( Effect.onExit((exit) => Effect.sync(() => { if (Exit.isSuccess(exit)) { return; } if (Cause.hasInterruptsOnly(exit.cause)) { atomRegistry.set(stateAtom, IDLE_SERVER_UPDATE_STATE); return; } atomRegistry.set(stateAtom, { status: "failed", stage: currentStage, fromVersion, targetVersion, message: serverUpdateFailureMessage(Cause.squash(exit.cause)), }); }), ), ); }), }); const settingsValueAtom = Atom.family((environmentId: EnvironmentId) => Atom.make((get) => get(configValueAtom(environmentId))?.settings ?? null).pipe( Atom.withLabel(`environment-data:server:settings:${environmentId}`), ), ); const hiddenThreadTagsAtom = Atom.family((environmentId: EnvironmentId) => Atom.make((get) => { const settings = get(settingsValueAtom(environmentId)); if (settings?.hideTaggedThreads !== true) return null; // Tags are trimmed on write; trim hand-edited entries so " private" still matches. const tags = settings.hiddenThreadTags.map((tag) => tag.trim()).filter((tag) => tag !== ""); return tags.length > 0 ? tags : null; }).pipe( // Trimming makes a new array on every settings push; keep equal lists stable. Atom.withEquality( (a: ReadonlyArray | null, b: ReadonlyArray | null) => a === b || (a !== null && b !== null && a.length === b.length && a.every((tag, index) => tag === b[index])), ), Atom.withLabel(`environment-data:server:hidden-thread-tags:${environmentId}`), ), ); const usagePricesAtom = Atom.family((environmentId: EnvironmentId) => Atom.make((get) => { const overrides = get(settingsValueAtom(environmentId))?.usagePriceOverrides ?? {}; // Only changed prices should trigger another transcript scan. Settings // snapshots can recreate the same mapping in a different property order. return JSON.stringify( Object.keys(overrides) .sort() .map((model) => { const price = overrides[model]!; return [ model, price.inputCostPerMillionTokens, price.outputCostPerMillionTokens, price.cacheReadCostPerMillionTokens, price.cacheWriteCostPerMillionTokens, ]; }), ); }).pipe(Atom.withLabel(`environment-data:server:usage-prices:${environmentId}`)), ); const usageScanSettingsAtom = Atom.family((environmentId: EnvironmentId) => Atom.make((get) => { const settings = get(settingsValueAtom(environmentId)); const aliases = settings?.usageModelAliases ?? {}; return JSON.stringify([ get(usagePricesAtom(environmentId)), Object.keys(aliases) .sort() .map((model) => [model, aliases[model]]), ]); }).pipe(Atom.withLabel(`environment-data:server:usage-scan-settings:${environmentId}`)), ); // Settings snapshots rebuild the filter object; only a changed filter should touch the Usage tab. const usageModelFilterAtom = Atom.family((environmentId: EnvironmentId) => Atom.make((get) => get(settingsValueAtom(environmentId))?.usageModelFilter).pipe( Atom.withEquality((a, b) => JSON.stringify(a) === JSON.stringify(b)), Atom.withLabel(`environment-data:server:usage-model-filter:${environmentId}`), ), ); const providersValueAtom = Atom.family((environmentId: EnvironmentId) => Atom.make((get) => get(configValueAtom(environmentId))?.providers ?? null).pipe( Atom.withLabel(`environment-data:server:providers:${environmentId}`), ), ); const welcomeStateFamily = Atom.family((environmentId: EnvironmentId) => runtime .atom(serverWelcomeStateChanges(environmentId), { initialValue: null }) .pipe( Atom.setIdleTTL(5 * 60_000), Atom.withLabel(`environment-data:server:welcome-state:${environmentId}`), ), ); const welcomeFamily = Atom.family((environmentId: EnvironmentId) => Atom.make((get) => { const result = get(welcomeStateFamily(environmentId)); if (result._tag !== "Success") return result; return result.value === null ? AsyncResult.initial(result.waiting) : AsyncResult.success(result.value, result); }).pipe(Atom.withLabel(`environment-data:server:welcome:${environmentId}`)), ); const databaseStorage = createEnvironmentRpcSubscriptionAtomFamily(runtime, { label: "environment-data:server:database-storage", tag: WS_METHODS.serverGetDatabaseStorage, idleTtlMs: 0, }); // Keyed by a JSON array of environment ids so one notice can list every machine. const databaseCompactionRecommendations = Atom.family((environmentIdsKey: string) => { const sources = (JSON.parse(environmentIdsKey) as EnvironmentId[]).map((environmentId) => ({ environmentId, atom: databaseStorage({ environmentId, input: {} }), })); return Atom.make((get) => sources .flatMap(({ environmentId, atom }) => { const result = get(atom); return result._tag === "Success" && result.value !== null && shouldRecommendDatabaseCompaction(result.value) ? [{ environmentId, status: result.value }] : []; }) .sort((left, right) => right.status.reclaimableBytes - left.status.reclaimableBytes), ).pipe( Atom.withLabel( `environment-data:server:database-compaction-recommendations:${environmentIdsKey}`, ), ); }); const welcome = (target: { readonly environmentId: EnvironmentId; readonly input: EnvironmentRpcInput; }) => welcomeFamily(target.environmentId); const updateSettings = createEnvironmentRpcCommand(runtime, { label: "environment-data:server:update-settings", tag: WS_METHODS.serverUpdateSettings, scheduler: configScheduler, concurrency: configConcurrency, }); return { configValueAtom, runStorageCleanup: createEnvironmentRpcCommand(runtime, { label: "environment-data:server:run-storage-cleanup", tag: WS_METHODS.serverRunStorageCleanup, concurrency: { mode: "singleFlight", key: ({ environmentId }) => environmentId }, }), prepareTriage: createEnvironmentRpcCommand(runtime, { label: "environment-data:server:prepare-triage", tag: WS_METHODS.serverPrepareTriage, concurrency: { mode: "singleFlight", key: ({ environmentId }) => environmentId }, }), databaseStorage, databaseCompactionRecommendations, reclaimDatabaseSpace: createEnvironmentRpcCommand(runtime, { label: "environment-data:server:reclaim-database-space", tag: WS_METHODS.serverReclaimDatabaseSpace, concurrency: { mode: "singleFlight", key: ({ environmentId }) => environmentId }, }), storageCleanupReport: createEnvironmentRpcSubscriptionAtomFamily(runtime, { label: "environment-data:server:storage-cleanup-report", tag: WS_METHODS.serverGetStorageCleanupReport, idleTtlMs: 0, }), updateStateAtom, settingsValueAtom, hiddenThreadTagsAtom, usageModelFilterAtom, providersValueAtom, providerAuthState: createEnvironmentRpcSubscriptionAtomFamily(runtime, { label: "environment-data:provider:auth-state", tag: WS_METHODS.providerAuthSubscribe, idleTtlMs: 0, }), startProviderAuth: createEnvironmentRpcCommand(runtime, { label: "environment-data:provider:auth-start", tag: WS_METHODS.providerAuthStart, concurrency: { mode: "singleFlight", key: ({ environmentId, input }) => JSON.stringify([environmentId, input]), }, }), respondProviderAuth: createEnvironmentRpcCommand(runtime, { label: "environment-data:provider:auth-respond", tag: WS_METHODS.providerAuthRespond, }), completeProviderAuth: createEnvironmentRpcCommand(runtime, { label: "environment-data:provider:auth-complete", tag: WS_METHODS.providerAuthComplete, }), chatGptReconnectProfile: createEnvironmentRpcCommand(runtime, { label: "environment-data:chatgpt:reconnect-profile", tag: WS_METHODS.chatGptReconnectProfile, }), chatGptImportProfile: createEnvironmentRpcCommand(runtime, { label: "environment-data:chatgpt:import-profile", tag: WS_METHODS.chatGptImportProfile, }), chatGptHandoffState: createEnvironmentSubscriptionAtomFamily(runtime, { label: "environment-data:chatgpt:handoff", sensitiveInput: true, // OAuth must not be replayed when the connection recovers. subscribe: (input: EnvironmentRpcInput) => runStream(WS_METHODS.chatGptHandoffSubscribe, input), idleTtlMs: 0, }), cancelProviderAuth: createEnvironmentRpcCommand(runtime, { label: "environment-data:provider:auth-cancel", tag: WS_METHODS.providerAuthCancel, }), removeProviderAuthMethod: createEnvironmentRpcCommand(runtime, { label: "environment-data:provider:auth-remove-method", tag: WS_METHODS.providerAuthRemoveMethod, }), logoutProviderAuth: createEnvironmentRpcCommand(runtime, { label: "environment-data:provider:auth-logout", tag: WS_METHODS.providerAuthLogout, }), providerInstallState: createEnvironmentRpcSubscriptionAtomFamily(runtime, { label: "environment-data:provider:install-state", tag: WS_METHODS.providerInstallSubscribe, idleTtlMs: 0, }), startProviderInstall: createEnvironmentRpcCommand(runtime, { label: "environment-data:provider:install-start", tag: WS_METHODS.providerInstallStart, concurrency: { mode: "singleFlight", key: ({ environmentId, input }) => JSON.stringify([environmentId, input]), }, }), cancelProviderInstall: createEnvironmentRpcCommand(runtime, { label: "environment-data:provider:install-cancel", tag: WS_METHODS.providerInstallCancel, }), removeProviderInstallation: createEnvironmentRpcCommand(runtime, { label: "environment-data:provider:install-remove", tag: WS_METHODS.providerInstallRemove, }), traceDiagnostics: createEnvironmentRpcQueryAtomFamily(runtime, { label: "environment-data:server:trace-diagnostics", tag: WS_METHODS.serverGetTraceDiagnostics, }), npmRelease: createEnvironmentRpcQueryAtomFamily(runtime, { label: "environment-data:server:npm-release", tag: WS_METHODS.serverGetNpmRelease, staleTimeMs: 60_000, refreshIntervalMs: 5 * 60_000, }), processDiagnostics: createEnvironmentRpcQueryAtomFamily(runtime, { label: "environment-data:server:process-diagnostics", tag: WS_METHODS.serverGetProcessDiagnostics, }), hostResources: createEnvironmentQueryAtomFamily(runtime, { label: "environment-data:server:host-resources", idleTtlMs: 0, staleTimeMs: 5_000, execute: (input: EnvironmentRpcInput) => request(WS_METHODS.serverGetHostResources, input).pipe(Effect.timeout("5 seconds")), }), processResourceHistory: createEnvironmentRpcQueryAtomFamily(runtime, { label: "environment-data:server:process-resource-history", tag: WS_METHODS.serverGetProcessResourceHistory, }), scheduledTaskWebhookDeliveries: createEnvironmentRpcQueryAtomFamily(runtime, { label: "environment-data:server:scheduled-task:webhook-deliveries", tag: WS_METHODS.scheduledTasksListWebhookDeliveries, }), scheduledTaskWebhookDelivery: createEnvironmentRpcQueryAtomFamily(runtime, { label: "environment-data:server:scheduled-task:webhook-delivery", tag: WS_METHODS.scheduledTasksGetWebhookDelivery, }), scheduledTasksLive: createEnvironmentRpcSubscriptionAtomFamily(runtime, { label: "environment-data:server:scheduled-tasks:live", tag: WS_METHODS.scheduledTasksSubscribe, }), // A cold transcript scan is measured in seconds, so keep the result around // long enough that switching windows or re-rendering does not rescan. usageSummary: createEnvironmentRpcQueryAtomFamily(runtime, { label: "environment-data:server:usage-summary", tag: WS_METHODS.serverGetUsageSummary, staleTimeMs: 60_000, refreshTrigger: ({ environmentId }) => usageScanSettingsAtom(environmentId), execute: (input, emit) => request(WS_METHODS.serverGetUsageSummary, input).pipe( Effect.filterOrElse( (summary) => !summary.sources.some((source) => source.refreshing === true), (summary) => emit(summary).pipe( Effect.andThen( request(WS_METHODS.serverGetUsageSummary, { ...input, awaitRefresh: true }), ), Effect.catch((error) => Effect.logWarning("Could not refresh slow usage sources.").pipe( Effect.annotateLogs({ ...safeErrorLogAttributes(error) }), Effect.as(summary), ), ), ), ), ), }), resourceTelemetry: createEnvironmentRpcSubscriptionAtomFamily(runtime, { label: "environment-data:server:resource-telemetry", tag: WS_METHODS.subscribeResourceTelemetry, idleTtlMs: 0, }), resourceTelemetryHistory: createEnvironmentRpcQueryAtomFamily(runtime, { label: "environment-data:server:resource-telemetry-history", tag: WS_METHODS.serverGetResourceTelemetryHistory, staleTimeMs: 5_000, refreshIntervalMs: 5_000, idleTtlMs: 0, }), searchAcpRegistry: createEnvironmentRpcQueryAtomFamily(runtime, { label: "environment-data:server:acp-registry:search", tag: WS_METHODS.serverSearchAcpRegistry, // Reopening Add provider reuses recent results. Submitting the same search asks the // server again, which reuses a Registry index fetched in the last five minutes. staleTimeMs: 5 * 60_000, }), configProjection, welcome, legacyThreadMigration: createEnvironmentRpcSubscriptionAtomFamily(runtime, { label: "environment-data:server:legacy-thread-migration", tag: WS_METHODS.subscribeServerLifecycle, transform: (stream) => stream.pipe( Stream.filterMap((event) => event.type === "legacyThreadMigration" ? Result.succeed(event.payload) : Result.failVoid, ), ), }), consumeResetCredit: createEnvironmentRpcCommand(runtime, { label: "environment-data:server:consume-reset-credit", tag: WS_METHODS.providerConsumeResetCredit, concurrency: { mode: "singleFlight", // Both ids are free-form strings; a delimiter could collide. key: ({ environmentId, input }) => JSON.stringify([environmentId, input]), }, }), refreshProviders: createEnvironmentRpcCommand(runtime, { label: "environment-data:server:refresh-providers", tag: WS_METHODS.serverRefreshProviders, execute: (input) => requestGuarded(WS_METHODS.serverRefreshProviders, { ...input, workspaceSnapshotsOnDemand: true, ...(options.usageLimitsCommand === true ? { usageLimitsCommand: true } : {}), ...(options.providerCatalogsOnDemand === true ? { providerCatalogsOnDemand: true } : {}), }), onSuccess: ({ environmentId, input }, registry, result) => Effect.sync(() => { const { instanceId, cwd } = input; if (instanceId === undefined || cwd === undefined) return; registry.update(fetchedWorkspaceSnapshotsAtom(environmentId), (current) => recordFetchedWorkspaceSnapshot( current, { instanceId, cwd, fresh: input.fresh === true }, result.providers, registry .get(configValueAtom(environmentId)) ?.providers.find((provider) => provider.instanceId === instanceId), ), ); }), concurrency: { mode: "singleFlight", key: ({ environmentId, input }) => JSON.stringify([ environmentId, input.instanceId ?? null, input.cwd ?? null, input.fresh ?? false, input.refreshModels ?? false, input.modelCatalogOnly ?? false, ]), }, }), updateProvider: createEnvironmentRpcCommand(runtime, { label: "environment-data:server:update-provider", tag: WS_METHODS.serverUpdateProvider, scheduler: configScheduler, concurrency: configConcurrency, }), updateServer, upsertKeybinding: createEnvironmentRpcCommand(runtime, { label: "environment-data:server:upsert-keybinding", tag: WS_METHODS.serverUpsertKeybinding, scheduler: configScheduler, concurrency: configConcurrency, }), removeKeybinding: createEnvironmentRpcCommand(runtime, { label: "environment-data:server:remove-keybinding", tag: WS_METHODS.serverRemoveKeybinding, scheduler: configScheduler, concurrency: configConcurrency, }), updateSettings, mutateProviderInstance: updateSettings, prepareAcpRegistryAgent: createEnvironmentRpcCommand(runtime, { label: "environment-data:server:acp-registry:prepare", tag: WS_METHODS.serverPrepareAcpRegistryAgent, concurrency: { mode: "singleFlight", key: ({ environmentId, input }) => `${environmentId}:${input.agentId}`, }, }), uninstallAcpRegistryManagedBinary: createEnvironmentRpcCommand(runtime, { label: "environment-data:server:acp-registry:uninstall-managed-binary", tag: WS_METHODS.serverUninstallAcpRegistryManagedBinary, concurrency: { mode: "singleFlight", key: ({ environmentId, input }) => `${environmentId}:${input.agentId}`, }, }), acceptAcpRegistryUrlAuth: createEnvironmentRpcCommand(runtime, { label: "environment-data:server:acp-registry:accept-url-auth", tag: WS_METHODS.serverAcceptAcpRegistryUrlAuth, concurrency: { mode: "singleFlight", key: ({ environmentId, input }) => `${environmentId}:${input.instanceId}:${input.elicitationId}`, }, }), listAcpRegistrySessions: createEnvironmentRpcCommand(runtime, { label: "environment-data:server:acp-registry:list-sessions", tag: WS_METHODS.serverListAcpRegistrySessions, concurrency: { mode: "singleFlight", key: ({ environmentId, input }) => `${environmentId}:${input.instanceId}:${input.projectId}:${input.cursor ?? "first"}`, }, }), importAcpRegistrySession: createEnvironmentRpcCommand(runtime, { label: "environment-data:server:acp-registry:import-session", tag: WS_METHODS.serverImportAcpRegistrySession, concurrency: { mode: "singleFlight", key: ({ environmentId, input }) => `${environmentId}:${input.instanceId}:${input.projectId}:${input.sessionId}`, }, }), deleteAcpRegistrySession: createEnvironmentRpcCommand(runtime, { label: "environment-data:server:acp-registry:delete-session", tag: WS_METHODS.serverDeleteAcpRegistrySession, concurrency: { mode: "singleFlight", key: ({ environmentId, input }) => `${environmentId}:${input.instanceId}:${input.projectId}:${input.sessionId}`, }, }), listAcpRegistryProviders: createEnvironmentRpcCommand(runtime, { label: "environment-data:server:acp-registry:list-providers", tag: WS_METHODS.serverListAcpRegistryProviders, concurrency: { mode: "singleFlight", key: ({ environmentId, input }) => `${environmentId}:${input.instanceId}:${input.projectId}`, }, }), setAcpRegistryProvider: createEnvironmentRpcCommand(runtime, { label: "environment-data:server:acp-registry:set-provider", tag: WS_METHODS.serverSetAcpRegistryProvider, concurrency: { mode: "singleFlight", key: ({ environmentId, input }) => `${environmentId}:${input.instanceId}:${input.projectId}:${input.providerId}`, }, }), disableAcpRegistryProvider: createEnvironmentRpcCommand(runtime, { label: "environment-data:server:acp-registry:disable-provider", tag: WS_METHODS.serverDisableAcpRegistryProvider, concurrency: { mode: "singleFlight", key: ({ environmentId, input }) => `${environmentId}:${input.instanceId}:${input.projectId}:${input.providerId}`, }, }), logoutAcpRegistry: createEnvironmentRpcCommand(runtime, { label: "environment-data:server:acp-registry:logout", tag: WS_METHODS.serverLogoutAcpRegistry, concurrency: { mode: "singleFlight", key: ({ environmentId, input }) => `${environmentId}:${input.instanceId}`, }, }), signalProcess: createEnvironmentRpcCommand(runtime, { label: "environment-data:server:signal-process", tag: WS_METHODS.serverSignalProcess, }), upsertScheduledTask: createEnvironmentRpcCommand(runtime, { label: "environment-data:server:scheduled-task:upsert", tag: WS_METHODS.scheduledTasksUpsert, scheduler: configScheduler, concurrency: configConcurrency, }), setScheduledTaskEnabled: createEnvironmentRpcCommand(runtime, { label: "environment-data:server:scheduled-task:set-enabled", tag: WS_METHODS.scheduledTasksSetEnabled, scheduler: configScheduler, concurrency: configConcurrency, }), deleteScheduledTask: createEnvironmentRpcCommand(runtime, { label: "environment-data:server:scheduled-task:delete", tag: WS_METHODS.scheduledTasksDelete, scheduler: configScheduler, concurrency: configConcurrency, }), // Deliberately not on the config lane: run-now blocks until the run is // dispatched, and a slow run must not stall settings/keybinding/provider // mutations (or other scheduled-task edits) queued behind it. runScheduledTaskNow: createEnvironmentRpcCommand(runtime, { label: "environment-data:server:scheduled-task:run-now", tag: WS_METHODS.scheduledTasksRunNow, }), rotateScheduledTaskWebhookToken: createEnvironmentRpcCommand(runtime, { label: "environment-data:server:scheduled-task:rotate-webhook-token", tag: WS_METHODS.scheduledTasksRotateWebhookToken, scheduler: configScheduler, concurrency: configConcurrency, }), createGuestShare: createEnvironmentRpcCommand(runtime, { label: "environment-data:server:guest-share:create", tag: WS_METHODS.guestShareCreate, }), // Off the config lane: answering a card must not queue behind settings // edits. One answer per card at a time. answerSecretRequest: createEnvironmentRpcCommand(runtime, { label: "environment-data:server:secrets:answer-request", tag: WS_METHODS.secretsAnswerRequest, concurrency: { mode: "singleFlight", key: ({ environmentId, input }) => JSON.stringify([environmentId, input.threadId, input.turnItemId]), }, }), refreshUsageRates: createEnvironmentRpcCommand(runtime, { label: "environment-data:server:refresh-usage-rates", tag: WS_METHODS.serverRefreshUsageRates, concurrency: { mode: "singleFlight", key: ({ environmentId }) => environmentId, }, }), checkOtlpEndpoint: createEnvironmentRpcCommand(runtime, { label: "environment-data:server:check-otlp-endpoint", tag: WS_METHODS.serverCheckOtlpEndpoint, }), listTextGeneratorModels: createEnvironmentRpcCommand(runtime, { label: "environment-data:server:list-text-generator-models", tag: WS_METHODS.serverListTextGeneratorModels, }), retryResourceTelemetry: createEnvironmentRpcCommand(runtime, { label: "environment-data:server:retry-resource-telemetry", tag: WS_METHODS.serverRetryResourceTelemetry, concurrency: { mode: "singleFlight", key: ({ environmentId }) => environmentId, }, }), }; }