Move project and host queries into native MCP with host-owned OAuth/context, while retaining memory, skills, actions and publication plugins. Add nearby-model queries and explicit chat/task/learning model defaults. Adopt Responses HTTP/SSE and native steer/queue inbox delivery, preserving scoped session ownership, pending input edits and investigation communication. Disable local-provider discovery: Server /health allowed native vLLM to probe /v1/models on port 8000. A native before/after regression prevents recurrence. Validation: 170 focused Bun tests; service/plugin typechecks; native discovery, provider-discovery and input-inbox smoke checks; no paid model inference.
184 lines
5.3 KiB
TypeScript
184 lines
5.3 KiB
TypeScript
import { randomUUID } from "node:crypto";
|
|
|
|
import { logger } from "../logger.js";
|
|
import { type OpencodeRuntimeAdapter } from "../runtime/opencode.js";
|
|
import {
|
|
removeRuntimeSessionContext,
|
|
setRuntimeSessionContext,
|
|
} from "../runtime/sessionContext.js";
|
|
import { toActorKey, toProjectKey } from "../utils/fileStore.js";
|
|
|
|
export type SessionBinding = {
|
|
clientSessionId: string;
|
|
sessionId: string;
|
|
startedAt: number;
|
|
};
|
|
|
|
export type SessionContext = {
|
|
actorKey?: string;
|
|
clientSessionId: string;
|
|
network?: string;
|
|
projectId?: string;
|
|
userId?: string;
|
|
projectKey?: string;
|
|
};
|
|
|
|
export type ChatRequestContext = SessionContext & {
|
|
actorKey: string;
|
|
projectKey: string;
|
|
traceId: string;
|
|
};
|
|
|
|
export class ChatSessionBridge {
|
|
private readonly abortControllers = new Map<string, AbortController>();
|
|
|
|
constructor(private readonly runtime: OpencodeRuntimeAdapter) {}
|
|
|
|
async resolve(context: {
|
|
sessionId?: string;
|
|
network?: string;
|
|
projectId?: string;
|
|
traceId?: string;
|
|
userId?: string;
|
|
actorKey?: string;
|
|
projectKey?: string;
|
|
}): Promise<{
|
|
binding: SessionBinding;
|
|
requestContext: ChatRequestContext;
|
|
created: boolean;
|
|
}> {
|
|
let requestContext = this.buildRequestContext(context);
|
|
const existingSessionId = context.sessionId?.trim();
|
|
await this.abortActiveRuntime(requestContext.clientSessionId, existingSessionId);
|
|
|
|
let sessionId = existingSessionId;
|
|
let created = false;
|
|
if (!sessionId) {
|
|
const session = await this.runtime.createSession(undefined, requestContext);
|
|
sessionId = session.id;
|
|
requestContext = {
|
|
...requestContext,
|
|
clientSessionId: sessionId,
|
|
};
|
|
created = true;
|
|
}
|
|
const binding: SessionBinding = {
|
|
clientSessionId: requestContext.clientSessionId,
|
|
sessionId,
|
|
startedAt: Date.now(),
|
|
};
|
|
const workspaceDirectory = await this.runtime.ensureSessionWorkspace?.(sessionId, requestContext);
|
|
setRuntimeSessionContext({
|
|
workspaceDirectory,
|
|
actorKey: requestContext.actorKey,
|
|
allowLearningWrite: true,
|
|
clientSessionId: requestContext.clientSessionId,
|
|
learningMode: "interactive",
|
|
network: requestContext.network,
|
|
projectId: requestContext.projectId,
|
|
projectKey: requestContext.projectKey,
|
|
sessionId,
|
|
traceId: requestContext.traceId,
|
|
});
|
|
|
|
return { binding, requestContext, created };
|
|
}
|
|
|
|
count(): number {
|
|
return this.abortControllers.size;
|
|
}
|
|
|
|
createClientSessionId() {
|
|
return `agent-${randomUUID().slice(0, 12)}`;
|
|
}
|
|
|
|
registerAbortController(clientSessionId: string, controller: AbortController) {
|
|
this.abortControllers.set(clientSessionId, controller);
|
|
}
|
|
|
|
finalizeRequest(clientSessionId: string) {
|
|
this.abortControllers.delete(clientSessionId);
|
|
}
|
|
|
|
async abort(context: {
|
|
clientSessionId?: string;
|
|
sessionId?: string;
|
|
}): Promise<SessionBinding | null> {
|
|
const clientSessionId = context.clientSessionId?.trim();
|
|
const sessionId = context.sessionId?.trim();
|
|
if (!clientSessionId || !sessionId) {
|
|
return null;
|
|
}
|
|
|
|
try { await this.runtime.cancelPendingUserInputs?.(sessionId); }
|
|
finally { await this.abortActiveRuntime(clientSessionId, sessionId); }
|
|
return {
|
|
clientSessionId,
|
|
sessionId,
|
|
startedAt: Date.now(),
|
|
};
|
|
}
|
|
|
|
async deleteSession(context: {
|
|
clientSessionId: string;
|
|
sessionId: string;
|
|
actorKey: string;
|
|
projectKey: string;
|
|
}) {
|
|
const clientSessionId = context.clientSessionId.trim();
|
|
const sessionId = context.sessionId.trim();
|
|
const controller = this.abortControllers.get(clientSessionId);
|
|
if (controller) {
|
|
this.abortControllers.delete(clientSessionId);
|
|
controller.abort();
|
|
}
|
|
try { await this.runtime.removeSession(sessionId, context); }
|
|
finally { removeRuntimeSessionContext(sessionId); }
|
|
}
|
|
|
|
private buildRequestContext(context: {
|
|
actorKey?: string;
|
|
sessionId?: string;
|
|
network?: string;
|
|
projectId?: string;
|
|
traceId?: string;
|
|
userId?: string;
|
|
projectKey?: string;
|
|
}): ChatRequestContext {
|
|
const sessionId = context.sessionId?.trim();
|
|
return {
|
|
clientSessionId: sessionId || this.createClientSessionId(),
|
|
actorKey: context.actorKey ?? toActorKey(`local:${context.userId ?? "local-user"}`),
|
|
network: context.network,
|
|
projectId: context.projectId,
|
|
projectKey: context.projectKey ?? toProjectKey(`local:${context.projectId ?? "local-project"}`),
|
|
traceId: context.traceId?.trim() || `trace-${randomUUID().slice(0, 12)}`,
|
|
userId: context.userId?.trim(),
|
|
};
|
|
}
|
|
|
|
private async abortActiveRuntime(clientSessionId: string, sessionId?: string) {
|
|
const controller = this.abortControllers.get(clientSessionId);
|
|
if (controller) {
|
|
this.abortControllers.delete(clientSessionId);
|
|
controller.abort();
|
|
}
|
|
|
|
if (!sessionId) {
|
|
return;
|
|
}
|
|
await this.runtime.abortSession(sessionId).catch((error) => {
|
|
logger.warn(
|
|
{ clientSessionId, sessionId, err: error },
|
|
"failed to abort active runtime session",
|
|
);
|
|
});
|
|
await this.runtime.waitForSessionIdle(sessionId).catch((error) => {
|
|
logger.warn(
|
|
{ clientSessionId, sessionId, err: error },
|
|
"failed while waiting for active runtime session to become idle",
|
|
);
|
|
});
|
|
}
|
|
}
|