blob: 52fea1c65d38d76a871b3564df86efa6565b2e45 [file]
import { createHash, randomUUID } from "node:crypto";
import type { AttachmentRef, ShellRunUpdate } from "@maka/core";
import type { PlanSessionState, PlanUserControlInput } from "@maka/core";
import {
decodeStoredMessageForRead,
type StoredMessage,
} from "@maka/core";
import type { Task } from "@maka/core";
import type {
ConnectionCatalogEntry,
ConnectionCatalogSnapshot,
ConnectionVersionBasis,
CredentialLocator,
CredentialStatus,
RuntimePolicy,
RuntimePolicyMutation,
} from "@maka/core";
import {
canonicalPricingConfigsEqual,
comparePricingModelKeys,
} from "@maka/core";
import type { PricingConfig } from "@maka/core";
import {
isSessionTrace,
type SessionTrace,
} from "@maka/core";
import {
type ClientCapabilityProvider,
type DirectRequestOperationKey,
type RuntimeHostConnection,
type RuntimeHostSessionSubscription,
RuntimeHostCatalogReadError,
RuntimeHostOperationError,
readRuntimeHostConnectionCatalog,
readRuntimeHostInvocableSkills,
readRuntimeHostResources,
readRuntimeHostSessions,
readRuntimeHostSkillCatalog,
} from "@maka/runtime-host/client";
import {
ARTIFACT_INGEST_CHUNK_MAX_BYTES,
decodePricingMutateInput,
type AutomationProjection,
type ArtifactBinaryPreview,
type ArtifactProjection,
type ArtifactQueryResult,
type ArtifactTextPreview,
type EffectivePricingEntry,
type ExternalSessionCatalogQueryInput,
type ExternalSessionCatalogQueryResult,
type ExternalSessionSourceQueryResult,
type ClientCapabilityReplaceResult,
type ClientCapabilityUnregisterResult,
type InteractionAnswerInput,
type MemoryMutateInput,
type MemoryMutateResult,
type MemoryQueryInput,
type MemoryQueryResult,
type GoalControlAction,
type GoalProjection,
type OperationInput,
type OperationOutput,
type PlanProjectionItem,
type PlanQueryResult,
type PricingMutation,
type PricingQueryResult,
type QueueRetractInput,
type QueueRetractResult,
type SessionCatalogFilter,
type SessionCatalogChangedFrame,
type SessionCatalogItem,
type SessionCatalogProjection,
type SessionConfiguration,
type SessionContinuitySnapshot,
type SessionConversationCopyInput,
type SessionConversationCopyResult,
type SessionCreateInput,
type ExecutionBoundarySummary,
type SessionLifecycleState,
type SessionMetadataPatch,
type SessionUpdateResult,
type SkillCatalogLocalContext,
type SkillCatalogInvocableItem,
type SkillCatalogInvocableTarget,
type SkillCatalogMutateInput,
type SkillCatalogMutateResult,
type SkillCatalogPageItem,
type SkillCatalogPreviewUpdateInput,
type SkillCatalogPreviewUpdateResult,
type SkillCatalogRevision,
type SkillCatalogView,
type SubscriptionFrame,
type TurnInterruptInput,
type TurnInterruptResult,
type TurnMessageSubmitInput,
type TurnMessageSubmitResult,
} from "@maka/runtime-host/protocol";
const MAX_OPTIMISTIC_ATTEMPTS = 3;
const MAX_SESSION_REVISION_ATTEMPTS = 8;
const MAX_PRICING_SNAPSHOT_ATTEMPTS = 3;
export type DesktopSessionConfigurationPatch = Partial<SessionConfiguration>;
export type DesktopRuntimeHostClientErrorCode =
| "catalog_unstable"
| "client_closed"
| "projection_unstable"
| "pricing_snapshot_stale"
| "pricing_unstable"
| "revision_conflict"
| "session_not_found"
| "skill_catalog_unstable"
| "unsupported_session";
export class DesktopRuntimeHostClientError extends Error {
constructor(
readonly code: DesktopRuntimeHostClientErrorCode,
message: string,
) {
super(message);
this.name = "DesktopRuntimeHostClientError";
}
}
export interface DesktopRuntimeHostSession {
readonly snapshot: SessionContinuitySnapshot;
readonly transcript: Promise<StoredMessage[]>;
readonly events: AsyncIterable<SubscriptionFrame>;
close(): Promise<void>;
}
export interface DesktopPricingSnapshot {
readonly hostEpoch: string;
readonly connectionId: string;
readonly revision: number;
readonly entries: readonly EffectivePricingEntry[];
}
export interface DesktopSkillCatalogSnapshot {
readonly revision: SkillCatalogRevision;
readonly view: SkillCatalogView;
readonly items: readonly SkillCatalogPageItem[];
}
export interface DesktopPricingMutationInput {
readonly base: DesktopPricingSnapshot;
readonly mutation: PricingMutation;
}
export type DesktopPricingMutationOutcome =
| {
readonly kind: "saved";
readonly disposition: "committed" | "unchanged";
readonly snapshot: DesktopPricingSnapshot;
}
| {
readonly kind: "saved_refresh_failed";
readonly disposition: "committed" | "unchanged";
}
| {
readonly kind: "synchronized" | "review_required";
readonly reason: "revision_conflict" | "outcome_unknown";
readonly snapshot: DesktopPricingSnapshot;
}
| {
readonly kind: "reconciliation_unavailable";
readonly reason: "revision_conflict" | "outcome_unknown";
};
type PricingReconciliationTarget =
| { readonly kind: "upsert"; readonly pricing: Readonly<PricingConfig> }
| {
readonly kind: "delete";
readonly modelKey: string;
readonly expected: "builtin" | "unpriced" | "no_override";
};
export class DesktopRuntimeHostClient {
readonly #sessions = new Set<DesktopSessionHandle>();
#closeTask: Promise<void> | undefined;
#connectionClosed = false;
constructor(private readonly connection: RuntimeHostConnection) {
void connection.closed?.then(() => {
this.#connectionClosed = true;
});
}
get hostEpoch(): string {
return this.connection.hostEpoch;
}
get lifecycleState(): 'ready' | 'unavailable' {
return this.#connectionClosed || this.#closeTask ? 'unavailable' : 'ready';
}
subscribeConfigurationChanges(listener: (revision: number) => void): () => void {
this.#assertOpen();
return this.connection.subscribeConfigurationChanges(listener);
}
subscribeSessionCatalogChanges(
listener: (frame: SessionCatalogChangedFrame) => void,
): () => void {
this.#assertOpen();
return this.connection.subscribeSessionCatalogChanges(listener);
}
async loadConnectionCatalog(): Promise<ConnectionCatalogSnapshot> {
this.#assertOpen();
try {
return await readRuntimeHostConnectionCatalog(this.connection);
} catch (error) {
if (!(error instanceof RuntimeHostCatalogReadError)) throw error;
throw new DesktopRuntimeHostClientError(
"catalog_unstable",
"Connection catalog kept changing while Desktop read it",
);
}
}
queryCredential(
locator: CredentialLocator,
): Promise<CredentialStatus | null> {
return this.#request("credential.vault.query", { locator }).then(
(result) => (result.kind === "status" ? result.status : null),
);
}
queryRuntimePolicy(): Promise<OperationOutput<"runtime.policy.query">> {
return this.#request("runtime.policy.query", {});
}
async updateRuntimePolicy(
buildOperation: (policy: RuntimePolicy) => RuntimePolicyMutation,
): Promise<OperationOutput<"runtime.policy.query">> {
for (let attempt = 0; attempt < MAX_OPTIMISTIC_ATTEMPTS; attempt += 1) {
const current = await this.queryRuntimePolicy();
const result = await this.#request("runtime.policy.mutate", {
expectedRevision: current.revision,
operation: buildOperation(current.policy),
});
if (result.kind === "committed") return this.queryRuntimePolicy();
}
throw revisionConflict("Runtime Policy update", "workspace");
}
queryMemory(input: MemoryQueryInput): Promise<MemoryQueryResult> {
return this.#request("memory.query", input);
}
mutateMemory(input: MemoryMutateInput): Promise<MemoryMutateResult> {
return this.#request("memory.mutate", input);
}
createConnection(
expectedCatalogRevision: number,
connection: OperationInput<"connection.catalog.create">["connection"],
): Promise<OperationOutput<"connection.catalog.create">> {
return this.#request("connection.catalog.create", {
expectedCatalogRevision,
connection,
});
}
updateConnection(
expected: ConnectionVersionBasis,
changes: OperationInput<"connection.catalog.update">["changes"],
): Promise<OperationOutput<"connection.catalog.update">> {
return this.#request("connection.catalog.update", { expected, changes });
}
removeConnection(
expected: ConnectionVersionBasis,
): Promise<OperationOutput<"connection.catalog.remove">> {
return this.#request("connection.catalog.remove", { expected });
}
setDefaultConnectionTarget(
expectedCatalogRevision: number,
target: OperationInput<"connection.catalog.set-default-target">["target"],
): Promise<OperationOutput<"connection.catalog.set-default-target">> {
return this.#request("connection.catalog.set-default-target", {
expectedCatalogRevision,
target,
});
}
setCredential(
input: OperationInput<"credential.vault.set">,
): Promise<OperationOutput<"credential.vault.set">> {
return this.#request("credential.vault.set", input);
}
deleteCredential(
input: OperationInput<"credential.vault.delete">,
): Promise<OperationOutput<"credential.vault.delete">> {
return this.#request("credential.vault.delete", input);
}
getConnectionRequestHeaders(
connectionId: string,
): Promise<OperationOutput<"connection.request-headers.query">> {
return this.#request("connection.request-headers.query", { connectionId });
}
replaceConnectionRequestHeaders(
connectionId: string,
headers: OperationInput<"connection.request-headers.replace">["headers"],
): Promise<OperationOutput<"connection.request-headers.replace">> {
return this.#request("connection.request-headers.replace", { connectionId, headers });
}
fetchConnectionModels(
connectionId: string,
): Promise<OperationOutput<"connection.models.fetch">> {
return this.#request("connection.models.fetch", { connectionId });
}
testConnection(
connectionId: string,
modelId?: string,
): Promise<OperationOutput<"connection.test.run">> {
return this.#request("connection.test.run", {
connectionId,
modelId: modelId ?? null,
});
}
startOAuthLogin(
attemptId: string,
connectionId: string,
): Promise<OperationOutput<"oauth.login.start">> {
return this.#request("oauth.login.start", { attemptId, connectionId });
}
queryOAuthLogin(
attemptId: string,
): Promise<OperationOutput<"oauth.login.query">> {
return this.#request("oauth.login.query", { attemptId });
}
cancelOAuthLogin(
attemptId: string,
): Promise<OperationOutput<"oauth.login.cancel">> {
return this.#request("oauth.login.cancel", { attemptId });
}
fetchOAuthAccountUsage(
connectionId: string,
): Promise<OperationOutput<"oauth.account.usage.fetch">> {
return this.#request("oauth.account.usage.fetch", { connectionId });
}
async loadSkillCatalog(
context: SkillCatalogLocalContext,
view: SkillCatalogView,
): Promise<DesktopSkillCatalogSnapshot> {
this.#assertOpen();
try {
return await readRuntimeHostSkillCatalog(this.connection, context, view);
} catch (error) {
if (!(error instanceof RuntimeHostCatalogReadError)) throw error;
throw new DesktopRuntimeHostClientError(
"skill_catalog_unstable",
"Skill catalog kept changing while Desktop read it",
);
}
}
async listInvocableSkills(
target: SkillCatalogInvocableTarget,
): Promise<readonly SkillCatalogInvocableItem[]> {
this.#assertOpen();
try {
return await readRuntimeHostInvocableSkills(this.connection, target);
} catch (error) {
if (!(error instanceof RuntimeHostCatalogReadError)) throw error;
throw new DesktopRuntimeHostClientError(
"skill_catalog_unstable",
"Invocable Skill catalog kept changing while Desktop read it",
);
}
}
mutateSkillCatalog(
input: SkillCatalogMutateInput,
): Promise<SkillCatalogMutateResult> {
return this.#request("skill.catalog.mutate", input);
}
previewSkillUpdate(
input: SkillCatalogPreviewUpdateInput,
): Promise<SkillCatalogPreviewUpdateResult> {
return this.#request("skill.catalog.preview-update", input);
}
async loadPricingSnapshot(): Promise<DesktopPricingSnapshot> {
for (
let attempt = 0;
attempt < MAX_PRICING_SNAPSHOT_ATTEMPTS;
attempt += 1
) {
const snapshot = await this.#readPricingSnapshot();
if (snapshot) return snapshot;
}
throw new DesktopRuntimeHostClientError(
"pricing_unstable",
"Pricing kept changing while Desktop read it",
);
}
async applyPricingMutation(
input: DesktopPricingMutationInput,
): Promise<DesktopPricingMutationOutcome> {
this.#assertOpen();
if (
input.base.hostEpoch !== this.connection.hostEpoch ||
input.base.connectionId !== this.connection.connectionId
) {
throw new DesktopRuntimeHostClientError(
"pricing_snapshot_stale",
"Pricing snapshot belongs to a different Runtime Host connection",
);
}
const request = decodePricingMutateInput({
expectedRevision: input.base.revision,
mutation: input.mutation,
});
const reconciliationTarget = createPricingReconciliationTarget(
input.base,
request.mutation,
);
let result: OperationOutput<"pricing.mutate">;
try {
result = await this.#request("pricing.mutate", request);
} catch (error) {
if (
error instanceof RuntimeHostOperationError &&
error.code !== "commit_outcome_unknown"
) {
throw error;
}
return this.#reconcilePricingMutation(
reconciliationTarget,
"outcome_unknown",
);
}
if (result.kind === "revision_conflict") {
return this.#reconcilePricingMutation(
reconciliationTarget,
"revision_conflict",
);
}
try {
return {
kind: "saved",
disposition: result.kind,
snapshot: await this.loadPricingSnapshot(),
};
} catch {
return { kind: "saved_refresh_failed", disposition: result.kind };
}
}
async listSessions(
filter?: SessionCatalogFilter,
): Promise<SessionCatalogProjection[]> {
this.#assertOpen();
try {
return (await readRuntimeHostSessions(this.connection, filter)).map(requireSessionProjection);
} catch (error) {
if (error instanceof DesktopRuntimeHostClientError) throw error;
if (!(error instanceof RuntimeHostCatalogReadError)) throw error;
throw new DesktopRuntimeHostClientError(
"catalog_unstable",
"Session catalog kept changing while Desktop read it",
);
}
}
async listArtifacts(sessionId: string): Promise<ArtifactProjection[]> {
for (let attempt = 0; attempt < MAX_OPTIMISTIC_ATTEMPTS; attempt += 1) {
const first = await this.#request("artifact.query", {
kind: "list_start",
sessionId,
});
if (first.kind !== "page") throw invalidProjection("Artifact");
const artifacts = [...first.artifacts];
const cursors = new Set<string>();
let page: Extract<ArtifactQueryResult, { kind: "page" }> = first;
let retry = false;
while (page.nextCursor !== null) {
if (cursors.has(page.nextCursor)) throw repeatedCursor("Artifact");
cursors.add(page.nextCursor);
const next = await this.#request("artifact.query", {
kind: "list_continue",
sessionId,
revision: first.revision,
cursor: page.nextCursor,
});
if (next.kind === "revision_changed") {
retry = true;
break;
}
if (
next.kind !== "page" ||
next.sessionId !== sessionId ||
next.revision !== first.revision
) {
throw invalidProjection("Artifact");
}
artifacts.push(...next.artifacts);
page = next;
}
if (!retry) return artifacts;
}
throw unstableProjection("Artifact", sessionId);
}
async getArtifact(
sessionId: string,
artifactId: string,
): Promise<ArtifactProjection | null> {
const result = await this.#request("artifact.query", {
kind: "get",
sessionId,
artifactId,
});
if (result.kind !== "artifact" || result.sessionId !== sessionId) {
throw invalidProjection("Artifact");
}
return result.artifact;
}
async readArtifactText(
sessionId: string,
artifactId: string,
): Promise<ArtifactTextPreview> {
const result = await this.#request("artifact.query", {
kind: "read_text",
sessionId,
artifactId,
});
if (
result.kind !== "text" ||
result.sessionId !== sessionId ||
result.artifactId !== artifactId
) {
throw invalidProjection("Artifact");
}
return result.preview;
}
async readArtifactBinary(
sessionId: string,
artifactId: string,
): Promise<ArtifactBinaryPreview> {
const result = await this.#request("artifact.query", {
kind: "read_binary",
sessionId,
artifactId,
});
if (
result.kind !== "binary" ||
result.sessionId !== sessionId ||
result.artifactId !== artifactId
) {
throw invalidProjection("Artifact");
}
return result.preview;
}
deleteArtifact(sessionId: string, artifactId: string) {
return this.#request("artifact.delete", { sessionId, artifactId });
}
async streamArtifact(
sessionId: string,
artifactId: string,
writeChunk: (chunk: Uint8Array) => Promise<void>,
): Promise<number> {
let offset = 0;
let expectedTotal: number | undefined;
while (true) {
const result = await this.#request("artifact.query", {
kind: "read_chunk",
sessionId,
artifactId,
offset,
});
if (
result.kind !== "chunk" ||
result.sessionId !== sessionId ||
result.artifactId !== artifactId ||
result.offset !== offset ||
(expectedTotal !== undefined && result.totalBytes !== expectedTotal)
) {
throw invalidProjection("Artifact content");
}
expectedTotal ??= result.totalBytes;
const chunk = Buffer.from(result.chunkBase64, "base64");
if (chunk.byteLength > 0) await writeChunk(chunk);
if (result.nextOffset === null) return result.totalBytes;
if (result.nextOffset !== offset + chunk.byteLength) {
throw invalidProjection("Artifact content");
}
offset = result.nextOffset;
}
}
async getSession(
sessionId: string,
): Promise<SessionCatalogProjection | null> {
const result = await this.#request("session.catalog.query", {
kind: "get",
sessionId,
});
if (result.kind !== "session") {
throw new DesktopRuntimeHostClientError(
"catalog_unstable",
"Runtime Host returned an invalid Session catalog lookup",
);
}
return result.session === null
? null
: requireSessionProjection(result.session);
}
async createSession(
input: SessionCreateInput,
): Promise<SessionCatalogProjection> {
return requireSessionProjection(
await this.#request("session.create", input),
);
}
listExternalSessionSources(): Promise<ExternalSessionSourceQueryResult> {
return this.#request("external-session.source.query", {});
}
listExternalSessions(
input: ExternalSessionCatalogQueryInput,
): Promise<ExternalSessionCatalogQueryResult> {
return this.#request("external-session.catalog.query", input);
}
async importExternalSession(input: {
readonly adapterId: string;
readonly sourceSessionId: string;
}): Promise<SessionCatalogProjection> {
const result = await this.#request("external-session.import", input);
return requireSessionProjection(result.session);
}
updateSessionMetadata(
sessionId: string,
patch: SessionMetadataPatch,
): Promise<SessionCatalogProjection> {
return this.#updateSession(sessionId, (current) =>
this.#request("session.metadata.update", {
sessionId,
expectedRevision: current.revision,
patch,
}),
);
}
async updateSessionConfiguration(
sessionId: string,
patch: DesktopSessionConfigurationPatch,
): Promise<SessionCatalogProjection> {
const definedPatch = Object.fromEntries(
Object.entries(patch).filter(([, value]) => value !== undefined),
) as DesktopSessionConfigurationPatch;
if (Object.keys(definedPatch).length === 0)
return this.#requireSession(sessionId);
return this.#updateSession(sessionId, (current) =>
this.#request("session.configuration.update", {
sessionId,
expectedRevision: current.revision,
configuration: {
// An unlocked Session still follows the Host-owned default route.
// Once execution or an explicit model change locks it, the resolved
// catalog route is the explicit target that must survive this patch.
modelTarget: current.connectionLocked
? {
kind: "explicit",
connectionSlug: current.llmConnectionSlug,
model: current.model,
}
: { kind: "default" },
thinkingLevel: current.thinkingLevel ?? null,
permissionMode: current.permissionMode,
collaborationMode: current.collaborationMode,
orchestrationMode: current.orchestrationMode,
...definedPatch,
},
}),
);
}
relocateSessionCwd(
sessionId: string,
cwd: string,
projectId?: string | null,
): Promise<SessionCatalogProjection> {
return this.#updateSession(sessionId, (current) =>
this.#request("session.cwd.relocate", {
sessionId,
expectedRevision: current.revision,
cwd,
...(projectId === undefined ? {} : { projectId }),
}),
);
}
async setSessionReadMarker(
sessionId: string,
readThroughMessageId: string,
): Promise<SessionCatalogProjection> {
return requireSessionProjection(
await this.#request("session.read_marker.set", {
sessionId,
readThroughMessageId,
}),
);
}
readExecutionBoundary(sessionId: string): Promise<ExecutionBoundarySummary> {
return this.#request("session.execution_boundary.query", { sessionId });
}
async setSessionLifecycle(
sessionId: string,
state: SessionLifecycleState,
): Promise<SessionCatalogProjection> {
return requireSessionProjection(
await this.#request("session.lifecycle.set", { sessionId, state }),
);
}
async removeSession(sessionId: string): Promise<void> {
for (let attempt = 0; attempt < MAX_SESSION_REVISION_ATTEMPTS; attempt += 1) {
const current = await this.#requireSession(sessionId);
const result = await this.#request("session.remove", {
sessionId,
expectedRevision: current.revision,
});
if (result.kind === "removed") return;
}
throw revisionConflict("remove", sessionId);
}
async removeSessionCopy(sessionId: string): Promise<'removed' | 'retained'> {
try {
const current = await this.#requireSession(sessionId);
if (current.revisionOfTurnId !== undefined) {
const result = await this.#request('session.revision.abandon', {
targetSessionId: sessionId,
});
return result.kind === 'abandoned' ? 'removed' : 'retained';
}
await this.removeSession(sessionId);
return 'removed';
} catch (error) {
if (isMissingSessionError(error)) return 'removed';
throw error;
}
}
async copySession(
kind: "branch" | "revision",
input: Omit<SessionConversationCopyInput, "expectedSourceRevision">,
): Promise<SessionCatalogProjection> {
for (let attempt = 0; attempt < MAX_OPTIMISTIC_ATTEMPTS; attempt += 1) {
const source = await this.#requireSession(input.sourceSessionId);
const request = { ...input, expectedSourceRevision: source.revision };
const result: SessionConversationCopyResult =
kind === "branch"
? await this.#request("session.branch.create", request)
: await this.#request("session.revision.create", request);
if (result.kind === "committed")
return requireSessionProjection(result.session);
}
throw revisionConflict(`${kind} copy`, input.sourceSessionId);
}
async ingestAttachment(input: {
sessionId: string;
name: string;
mimeType: string;
content: Uint8Array;
uploadId?: string;
}): Promise<AttachmentRef> {
const uploadId = input.uploadId ?? randomUUID();
const digest =
`sha256:${createHash("sha256").update(input.content).digest("hex")}` as const;
let opened = false;
try {
const begin = await this.#request("artifact.ingest", {
kind: "begin",
sessionId: input.sessionId,
uploadId,
name: input.name,
mimeType: input.mimeType,
totalBytes: input.content.byteLength,
contentSha256: digest,
});
if (begin.kind === "committed") return begin.attachment;
if (begin.kind !== "upload_opened") {
throw new Error("Runtime Host did not open the Attachment upload");
}
opened = true;
let offset = begin.nextOffset;
while (offset < input.content.byteLength) {
const chunk = input.content.subarray(
offset,
Math.min(
input.content.byteLength,
offset + ARTIFACT_INGEST_CHUNK_MAX_BYTES,
),
);
const accepted = await this.#request("artifact.ingest", {
kind: "chunk",
sessionId: input.sessionId,
uploadId,
offset,
chunkBase64: Buffer.from(chunk).toString("base64"),
});
if (
accepted.kind !== "chunk_accepted" ||
accepted.nextOffset <= offset
) {
throw new Error("Runtime Host did not advance the Attachment upload");
}
offset = accepted.nextOffset;
}
const committed = await this.#request("artifact.ingest", {
kind: "commit",
sessionId: input.sessionId,
uploadId,
});
if (committed.kind !== "committed") {
throw new Error("Runtime Host did not commit the Attachment upload");
}
return committed.attachment;
} catch (error) {
if (opened) {
await this.#request("artifact.ingest", {
kind: "abort",
sessionId: input.sessionId,
uploadId,
}).catch(() => undefined);
}
throw error;
}
}
submitMessage(
input: Omit<TurnMessageSubmitInput, "originHostEpoch">,
): Promise<TurnMessageSubmitResult> {
return this.#request("turn.message.submit", {
...input,
originHostEpoch: this.connection.hostEpoch,
});
}
retractQueue(
input: Omit<QueueRetractInput, "originHostEpoch">,
): Promise<QueueRetractResult> {
return this.#request("queue.retract", {
...input,
originHostEpoch: this.connection.hostEpoch,
});
}
interruptTurn(
input: Omit<TurnInterruptInput, "originHostEpoch">,
): Promise<TurnInterruptResult> {
return this.#request("turn.interrupt", {
...input,
originHostEpoch: this.connection.hostEpoch,
});
}
answerInteraction(
input: InteractionAnswerInput,
): Promise<OperationOutput<"interaction.answer">> {
return this.#request("interaction.answer", input);
}
queryInteraction(
input: OperationInput<"interaction.query">,
): Promise<OperationOutput<"interaction.query">> {
return this.#request("interaction.query", input);
}
executeWebSearch(
input: OperationInput<"web-search.execute">,
): Promise<OperationOutput<"web-search.execute">> {
return this.#request("web-search.execute", input);
}
testNetworkProxy(
input: OperationInput<"network-proxy.test">,
): Promise<OperationOutput<"network-proxy.test">> {
return this.#request("network-proxy.test", input);
}
exportConfigurationCredentials(
input: OperationInput<"configuration.credentials.export">,
): Promise<OperationOutput<"configuration.credentials.export">> {
return this.#request("configuration.credentials.export", input);
}
startTurn(
input: OperationInput<"turn.start">,
): Promise<OperationOutput<"turn.start">> {
return this.#request("turn.start", input);
}
queryTurn(
input: OperationInput<"turn.query">,
): Promise<OperationOutput<"turn.query">> {
return this.#request("turn.query", input);
}
queryHostDiagnostics(): Promise<OperationOutput<"host.diagnostics.query">> {
return this.connection.queryHostDiagnostics(2_000);
}
stopTurn(
input: OperationInput<"turn.stop">,
): Promise<OperationOutput<"turn.stop">> {
return this.#request("turn.stop", input);
}
regenerateTurn(
input: OperationInput<"turn.regenerate">,
): Promise<OperationOutput<"turn.regenerate">> {
return this.#request("turn.regenerate", input);
}
queryTurnResume(
input: OperationInput<"turn.resume.query">,
): Promise<OperationOutput<"turn.resume.query">> {
return this.#request("turn.resume.query", input);
}
startTurnResume(
input: OperationInput<"turn.resume.start">,
): Promise<OperationOutput<"turn.resume.start">> {
return this.#request("turn.resume.start", input);
}
queryContextDiagnostics(
sessionId: string,
): Promise<OperationOutput<"context.diagnostics.query">> {
return this.#request("context.diagnostics.query", { sessionId });
}
compactContext(
input: OperationInput<"context.compact">,
): Promise<OperationOutput<"context.compact">> {
return this.#request("context.compact", input);
}
async listTasks(sessionId: string): Promise<Task[]> {
const projection = await collectStableProjection({
name: "Task ledger",
sessionId,
start: () =>
this.#request("task.ledger.query", { kind: "list_start", sessionId }),
continue: (first, cursor) =>
this.#request("task.ledger.query", {
kind: "list_continue",
sessionId,
revision: first.revision,
cursor,
}),
page(result, first) {
if (
result.kind !== "page" ||
result.sessionId !== sessionId ||
(first !== undefined && result.revision !== first.revision)
) {
throw invalidProjection("Task ledger");
}
return {
source: result,
items: result.tasks,
nextCursor: result.nextCursor,
};
},
});
return projection.items;
}
async listAutomations(sessionId: string): Promise<AutomationProjection[]> {
const projection = await collectStableProjection({
name: "Automation",
sessionId,
start: () =>
this.#request("automation.query", { kind: "list_start", sessionId }),
continue: (first, cursor) =>
this.#request("automation.query", {
kind: "list_continue",
sessionId,
revision: first.revision,
cursor,
}),
page(result, first) {
if (
result.kind !== "page" ||
result.sessionId !== sessionId ||
(first !== undefined && result.revision !== first.revision)
) {
throw invalidProjection("Automation");
}
return {
source: result,
items: result.automations,
nextCursor: result.nextCursor,
};
},
});
return projection.items;
}
mutateAutomation(
input: OperationInput<"automation.mutate">,
): Promise<OperationOutput<"automation.mutate">> {
return this.#request("automation.mutate", input);
}
queryDailyReview(
input: OperationInput<"daily-review.query">,
): Promise<OperationOutput<"daily-review.query">> {
return this.#request("daily-review.query", input);
}
mutateDailyReview(
input: OperationInput<"daily-review.mutate">,
): Promise<OperationOutput<"daily-review.mutate">> {
return this.#request("daily-review.mutate", input);
}
queryUsage(
input: OperationInput<"usage.query">,
): Promise<OperationOutput<"usage.query">> {
return this.#request("usage.query", input);
}
queryExecutionInspect(
input: OperationInput<"execution.inspect.query">,
): Promise<OperationOutput<"execution.inspect.query">> {
return this.#request("execution.inspect.query", input);
}
async loadSessionTrace(sessionId: string): Promise<SessionTrace> {
for (let attempt = 0; attempt < MAX_OPTIMISTIC_ATTEMPTS; attempt += 1) {
const first = await this.queryExecutionInspect({
kind: "session_trace_start",
sessionId,
});
if (first.kind !== "session_trace_page") {
throw invalidProjection("Session trace");
}
const turns = [...first.turns];
const offsets = new Set<number>([0]);
let nextOffset = first.nextOffset;
let retry = false;
while (nextOffset !== null) {
if (offsets.has(nextOffset)) {
throw invalidProjection("Session trace");
}
offsets.add(nextOffset);
const next = await this.queryExecutionInspect({
kind: "session_trace_continue",
sessionId,
revision: first.revision,
offset: nextOffset,
});
if (next.kind === "session_trace_revision_changed") {
retry = true;
break;
}
if (
next.kind !== "session_trace_page" ||
next.revision !== first.revision ||
next.offset !== nextOffset ||
JSON.stringify(next.totals) !== JSON.stringify(first.totals) ||
JSON.stringify(next.coverage) !== JSON.stringify(first.coverage)
) {
throw invalidProjection("Session trace");
}
turns.push(...next.turns);
nextOffset = next.nextOffset;
}
if (retry) continue;
const trace = {
schemaVersion: first.schemaVersion,
sessionId,
turns,
totals: first.totals,
coverage: first.coverage,
};
if (!isSessionTrace(trace)) throw invalidProjection("Session trace");
return trace;
}
throw unstableProjection("Session trace", sessionId);
}
queryGoal(sessionId: string): Promise<OperationOutput<"goal.query">> {
return this.#request("goal.query", { sessionId });
}
controlGoal(
goal: Pick<GoalProjection, "sessionId" | "goalId" | "revision">,
action: GoalControlAction,
): Promise<OperationOutput<"goal.control">> {
return this.#request("goal.control", {
sessionId: goal.sessionId,
goalId: goal.goalId,
expectedRevision: goal.revision,
action,
});
}
async clearGoal(sessionId: string): Promise<void> {
const initial = await this.queryGoal(sessionId);
if (initial.goal === null) return;
const goalId = initial.goal.goalId;
let goal = initial.goal;
for (let attempt = 0; attempt < MAX_OPTIMISTIC_ATTEMPTS; attempt += 1) {
try {
await this.controlGoal(goal, "clear");
return;
} catch (error) {
if (
!(error instanceof RuntimeHostOperationError) ||
error.code !== "operation_conflict"
) {
throw error;
}
}
const current = await this.queryGoal(sessionId);
if (current.goal === null || current.goal.goalId !== goalId) return;
goal = current.goal;
}
throw revisionConflict("Goal clear", sessionId);
}
async getPlanState(sessionId: string): Promise<PlanSessionState> {
const projection = await collectStableProjection({
name: "Plan",
sessionId,
start: () =>
this.#request("plan.query", { kind: "list_start", sessionId }),
continue: (first, cursor) =>
this.#request("plan.query", {
kind: "list_continue",
sessionId,
storeVersion: first.storeVersion,
cursor,
}),
page(result, first) {
if (
result.kind !== "page" ||
result.sessionId !== sessionId ||
(first !== undefined && result.storeVersion !== first.storeVersion)
) {
throw invalidProjection("Plan");
}
return {
source: result,
items: result.items,
nextCursor: result.nextCursor,
};
},
});
return planState(projection.first, projection.items);
}
controlPlan(
input: PlanUserControlInput,
): Promise<OperationOutput<"plan.control">> {
return this.#request("plan.control", input);
}
startPlanTurn(
input: OperationInput<"plan.turn.start">,
): Promise<OperationOutput<"plan.turn.start">> {
return this.#request("plan.turn.start", input);
}
queryAgentGraph(
input: OperationInput<"agent.graph.query">,
): Promise<OperationOutput<"agent.graph.query">> {
return this.#request("agent.graph.query", input);
}
queryAgentGraphOperator(
input: OperationInput<"agent.graph.operator.query">,
): Promise<OperationOutput<"agent.graph.operator.query">> {
return this.#request("agent.graph.operator.query", input);
}
stopAgentGraph(
input: OperationInput<"agent.graph.stop">,
): Promise<OperationOutput<"agent.graph.stop">> {
return this.#request("agent.graph.stop", input);
}
queryDeepResearch(
sessionId: string,
): Promise<OperationOutput<"deep-research.query">> {
return this.#request("deep-research.query", { sessionId });
}
async listRuntimeResources(sessionId: string): Promise<ShellRunUpdate[]> {
this.#assertOpen();
try {
return await readRuntimeHostResources(this.connection, sessionId);
} catch (error) {
if (!(error instanceof RuntimeHostCatalogReadError)) throw error;
throw unstableProjection("Runtime Resource", sessionId);
}
}
async getRuntimeResource(
sessionId: string,
ref: string,
): Promise<ShellRunUpdate | null> {
const result = await this.#request("runtime.resource.query", {
kind: "get",
sessionId,
ref,
});
if (result.kind !== "resource" || result.sessionId !== sessionId) {
throw invalidProjection("Runtime Resource");
}
return result.resource;
}
startRuntimeResource(
input: OperationInput<"runtime.resource.start">,
): Promise<OperationOutput<"runtime.resource.start">> {
return this.#request("runtime.resource.start", input);
}
acquireRuntimeResourceController(
input: OperationInput<"runtime.resource.controller.acquire">,
): Promise<OperationOutput<"runtime.resource.controller.acquire">> {
return this.#request("runtime.resource.controller.acquire", input);
}
controlRuntimeResource(
input: OperationInput<"runtime.resource.controller.control">,
): Promise<OperationOutput<"runtime.resource.controller.control">> {
return this.#request("runtime.resource.controller.control", input);
}
releaseRuntimeResourceController(
input: OperationInput<"runtime.resource.controller.release">,
): Promise<OperationOutput<"runtime.resource.controller.release">> {
return this.#request("runtime.resource.controller.release", input);
}
stopRuntimeResource(
input: OperationInput<"runtime.resource.stop">,
): Promise<OperationOutput<"runtime.resource.stop">> {
return this.#request("runtime.resource.stop", input);
}
replaceClientCapabilities(
provider: ClientCapabilityProvider,
timeoutMs?: number,
): Promise<ClientCapabilityReplaceResult> {
this.#assertOpen();
return this.connection.replaceClientCapabilities(provider, timeoutMs);
}
unregisterClientCapabilities(
timeoutMs?: number,
): Promise<ClientCapabilityUnregisterResult> {
this.#assertOpen();
return this.connection.unregisterClientCapabilities(timeoutMs);
}
async openSession(sessionId: string): Promise<DesktopRuntimeHostSession> {
this.#assertOpen();
const subscription = await this.connection.openSessionSubscription({
sessionId,
});
if (this.#closeTask) {
await subscription.close().catch(() => undefined);
throw clientClosed();
}
const session = new DesktopSessionHandle(subscription, () =>
this.#sessions.delete(session),
);
this.#sessions.add(session);
return session;
}
close(): Promise<void> {
this.#closeTask ??= this.#close();
return this.#closeTask;
}
async #close(): Promise<void> {
try {
await Promise.all([...this.#sessions].map((session) => session.close()));
} finally {
await this.connection.close();
}
}
async #readPricingSnapshot(): Promise<DesktopPricingSnapshot | undefined> {
this.#assertOpen();
const first = await this.#request("pricing.query", { kind: "start" });
if (first.kind !== "page" || first.offset !== 0) {
throw new DesktopRuntimeHostClientError(
"pricing_unstable",
"Runtime Host returned an invalid initial Pricing page",
);
}
const entries = [...first.entries];
const offsets = new Set<number>([0]);
let page: Extract<PricingQueryResult, { kind: "page" }> = first;
while (page.nextOffset !== null) {
const offset = page.nextOffset;
if (offset <= page.offset || offsets.has(offset)) {
throw new DesktopRuntimeHostClientError(
"pricing_unstable",
"Runtime Host repeated a Pricing page offset",
);
}
offsets.add(offset);
const next = await this.#request("pricing.query", {
kind: "continue",
revision: first.revision,
offset,
});
if (next.kind === "revision_changed") return undefined;
if (next.revision !== first.revision || next.offset !== offset) {
throw new DesktopRuntimeHostClientError(
"pricing_unstable",
"Runtime Host returned an inconsistent Pricing page",
);
}
entries.push(...next.entries);
page = next;
}
if (!pricingEntriesAreCanonical(entries)) {
throw new DesktopRuntimeHostClientError(
"pricing_unstable",
"Runtime Host returned non-canonical Pricing pages",
);
}
return {
hostEpoch: this.connection.hostEpoch,
connectionId: this.connection.connectionId,
revision: first.revision,
entries,
};
}
async #reconcilePricingMutation(
target: PricingReconciliationTarget,
reason: "revision_conflict" | "outcome_unknown",
): Promise<DesktopPricingMutationOutcome> {
try {
const snapshot = await this.loadPricingSnapshot();
return {
kind: pricingTargetMatchesSnapshot(target, snapshot)
? "synchronized"
: "review_required",
reason,
snapshot,
};
} catch {
return { kind: "reconciliation_unavailable", reason };
}
}
async #updateSession(
sessionId: string,
update: (current: SessionCatalogProjection) => Promise<SessionUpdateResult>,
): Promise<SessionCatalogProjection> {
for (let attempt = 0; attempt < MAX_SESSION_REVISION_ATTEMPTS; attempt += 1) {
const current = await this.#requireSession(sessionId);
const result = await update(current);
if (result.kind === "committed")
return requireSessionProjection(result.session);
}
throw revisionConflict("update", sessionId);
}
async #requireSession(sessionId: string): Promise<SessionCatalogProjection> {
const session = await this.getSession(sessionId);
if (session) return session;
throw new DesktopRuntimeHostClientError(
"session_not_found",
`Runtime Host Session not found: ${sessionId}`,
);
}
#request<K extends DirectRequestOperationKey>(
operation: K,
input: OperationInput<K>,
): Promise<OperationOutput<K>> {
this.#assertOpen();
return this.connection.request(operation, input);
}
#assertOpen(): void {
if (this.#closeTask) throw clientClosed();
}
}
class DesktopSessionHandle implements DesktopRuntimeHostSession {
readonly snapshot: SessionContinuitySnapshot;
readonly transcript: Promise<StoredMessage[]>;
readonly events: AsyncIterable<SubscriptionFrame>;
#closeTask: Promise<void> | undefined;
constructor(
private readonly subscription: RuntimeHostSessionSubscription,
private readonly onClose: () => void,
) {
this.snapshot = subscription.snapshot;
this.events = subscription;
this.transcript = subscription.loadTranscript(decodeStoredMessageForRead);
void this.transcript.catch(() => undefined);
}
close(): Promise<void> {
this.#closeTask ??= this.subscription.close().finally(this.onClose);
return this.#closeTask;
}
}
function requireSessionProjection(
item: SessionCatalogItem,
): SessionCatalogProjection {
if (!("kind" in item)) return item;
throw new DesktopRuntimeHostClientError(
"unsupported_session",
`Runtime Host Session is not representable by this Desktop Client: ${item.id}`,
);
}
function clientClosed(): DesktopRuntimeHostClientError {
return new DesktopRuntimeHostClientError(
"client_closed",
"Desktop Runtime Host Client is closed",
);
}
function isMissingSessionError(error: unknown): boolean {
return (
(error instanceof DesktopRuntimeHostClientError && error.code === 'session_not_found') ||
(error instanceof RuntimeHostOperationError && error.code === 'not_found')
);
}
function revisionConflict(
operation: string,
sessionId: string,
): DesktopRuntimeHostClientError {
return new DesktopRuntimeHostClientError(
"revision_conflict",
`Runtime Host Session kept changing during ${operation}: ${sessionId}`,
);
}
function planState(
first: Extract<PlanQueryResult, { kind: "page" }>,
items: readonly PlanProjectionItem[],
): PlanSessionState {
return {
schemaVersion: 1,
sessionId: first.sessionId,
storeVersion: first.storeVersion,
proposals: items.flatMap((item) =>
item.kind === "proposal" ? [item.proposal] : [],
),
executions: items.flatMap((item) =>
item.kind === "execution" ? [item.execution] : [],
),
...(first.latestProposalId === null
? {}
: { latestProposalId: first.latestProposalId }),
...(first.activeExecutionId === null
? {}
: { activeExecutionId: first.activeExecutionId }),
};
}
interface StableProjectionPage<TResult extends { kind: string }, TItem> {
source: Exclude<TResult, { kind: "revision_changed" }>;
items: readonly TItem[];
nextCursor: string | null;
}
async function collectStableProjection<
TResult extends { kind: string },
TItem,
>(options: {
name: string;
sessionId: string;
start(): Promise<TResult>;
continue(
first: Exclude<TResult, { kind: "revision_changed" }>,
cursor: string,
): Promise<TResult>;
page(
result: TResult,
first: Exclude<TResult, { kind: "revision_changed" }> | undefined,
): StableProjectionPage<TResult, TItem>;
}): Promise<{
first: Exclude<TResult, { kind: "revision_changed" }>;
items: TItem[];
}> {
for (let attempt = 0; attempt < MAX_OPTIMISTIC_ATTEMPTS; attempt += 1) {
const initial = await options.start();
if (initial.kind === "revision_changed")
throw invalidProjection(options.name);
const first = options.page(initial, undefined);
const items = [...first.items];
const cursors = new Set<string>();
let cursor = first.nextCursor;
let retry = false;
while (cursor !== null) {
if (cursors.has(cursor)) throw repeatedCursor(options.name);
cursors.add(cursor);
const result = await options.continue(first.source, cursor);
if (result.kind === "revision_changed") {
retry = true;
break;
}
const page = options.page(result, first.source);
items.push(...page.items);
cursor = page.nextCursor;
}
if (!retry) return { first: first.source, items };
}
throw unstableProjection(options.name, options.sessionId);
}
function invalidProjection(name: string): DesktopRuntimeHostClientError {
return new DesktopRuntimeHostClientError(
"projection_unstable",
`Runtime Host returned an invalid ${name} projection`,
);
}
function repeatedCursor(name: string): DesktopRuntimeHostClientError {
return new DesktopRuntimeHostClientError(
"projection_unstable",
`Runtime Host repeated a ${name} cursor`,
);
}
function unstableProjection(
name: string,
sessionId: string,
): DesktopRuntimeHostClientError {
return new DesktopRuntimeHostClientError(
"projection_unstable",
`Runtime Host ${name} kept changing while Desktop read Session ${sessionId}`,
);
}
function createPricingReconciliationTarget(
base: DesktopPricingSnapshot,
mutation: PricingMutation,
): PricingReconciliationTarget {
if (mutation.kind === "upsert")
return { kind: "upsert", pricing: mutation.pricing };
const baseEntry = base.entries.find(
({ pricing }) => pricing.modelKey === mutation.modelKey,
);
const expected =
baseEntry?.source === "custom"
? baseEntry.resetEffect === "restore_builtin"
? "builtin"
: "unpriced"
: "no_override";
return { kind: "delete", modelKey: mutation.modelKey, expected };
}
function pricingTargetMatchesSnapshot(
target: PricingReconciliationTarget,
snapshot: DesktopPricingSnapshot,
): boolean {
const current = snapshot.entries.find(
({ pricing }) => pricing.modelKey === pricingTargetModelKey(target),
);
if (target.kind === "upsert") {
return (
current?.source === "custom" &&
canonicalPricingConfigsEqual(current.pricing, target.pricing)
);
}
switch (target.expected) {
case "builtin":
return current?.source === "builtin";
case "unpriced":
return current === undefined;
case "no_override":
return current === undefined || current.source === "builtin";
}
}
function pricingTargetModelKey(target: PricingReconciliationTarget): string {
return target.kind === "upsert" ? target.pricing.modelKey : target.modelKey;
}
function pricingEntriesAreCanonical(
entries: readonly EffectivePricingEntry[],
): boolean {
return entries.every(
(entry, index) =>
index === 0 ||
comparePricingModelKeys(
entries[index - 1]!.pricing.modelKey,
entry.pricing.modelKey,
) < 0,
);
}