fix: converge workbench session authority

This commit is contained in:
lyon
2026-06-18 15:24:16 +08:00
parent dc4cac04a6
commit 08b2e3077a
42 changed files with 797 additions and 8096 deletions
-219
View File
@@ -99,8 +99,6 @@ export async function handleCodeAgentChatHttp(request, response, options) {
};
const manualSession = perf ? await perf.measure("manual_session", () => prepareManualCodeAgentSession({ params: chatParams, options, traceId, response })) : await prepareManualCodeAgentSession({ params: chatParams, options, traceId, response });
if (manualSession?.blocked) return;
const workspaceClaim = perf ? await perf.measure("workspace_claim", () => claimWorkbenchWorkspaceTurn({ params: chatParams, options, traceId, response })) : await claimWorkbenchWorkspaceTurn({ params: chatParams, options, traceId, response });
if (workspaceClaim?.blocked) return;
const billingPreflight = perf ? await perf.measure("billing_preflight", () => preflightCodeAgentBilling({ params: chatParams, options, traceId, response })) : await preflightCodeAgentBilling({ params: chatParams, options, traceId, response });
if (billingPreflight?.blocked) return;
const nativeSessionChatParams = stripSyntheticConversationContext({ ...chatParams, userBillingReservation: billingPreflight?.reservation ?? null }, traceId, options);
@@ -121,9 +119,6 @@ export async function handleCodeAgentChatHttp(request, response, options) {
controlSemantics: "submit-and-poll",
traceId,
...lifecycle,
workspaceId: workspaceClaim?.workspace?.id ?? null,
workspaceRevision: workspaceClaim?.workspace?.revision ?? null,
conversationId: safeConversationId(nativeSessionChatParams.conversationId) || null,
sessionId: safeSessionId(nativeSessionChatParams.sessionId) || null,
traceUrl,
resultUrl: `/v1/agent/chat/result/${encodeURIComponent(traceId)}`,
@@ -227,13 +222,11 @@ async function createManualCodeAgentSession(request, response, options) {
secretMaterialStored: false
}
});
const workspace = await updateManualSessionWorkspace({ params, options, session, providerProfile, source: "code-agent-manual-session-create" });
sendJson(response, 201, {
ok: true,
status: "created",
contractVersion: "code-agent-manual-session-v1",
session: publicManualAgentSession(session),
workspace,
nextCommands: manualSessionNextCommands(session),
valuesRedacted: true,
secretMaterialStored: false
@@ -271,14 +264,11 @@ async function selectManualCodeAgentSession(request, response, options, sessionI
sendJson(response, 409, manualSessionErrorPayload({ code: "session_not_usable", message: `Code Agent session ${sessionId} is ${session.status}; create a new session before continuing.`, sessionId, session }));
return;
}
const providerProfile = textValue(body.value.providerProfile) || session.session?.providerProfile || DEFAULT_CODE_AGENT_PROVIDER_PROFILE;
const workspace = await updateManualSessionWorkspace({ params: body.value, options, session, providerProfile, source: "code-agent-manual-session-select" });
sendJson(response, 200, {
ok: true,
status: "selected",
contractVersion: "code-agent-manual-session-v1",
session: publicManualAgentSession(session),
workspace,
nextCommands: manualSessionNextCommands(session),
valuesRedacted: true,
secretMaterialStored: false
@@ -347,86 +337,13 @@ async function getVisibleManualAgentSession(options, sessionId) {
return null;
}
async function updateManualSessionWorkspace({ params = {}, options = {}, session, providerProfile, source }) {
const store = options.accessController?.store;
if (!store || !options.actor?.id || !session) return null;
const workspaceId = safeWorkspaceId(params.workspaceId);
const projectId = textValue(params.projectId) || session.projectId || DEFAULT_CODE_AGENT_PROJECT_ID;
const current = workspaceId
? await store.getWorkspaceForUser?.({ workspaceId, ownerUserId: options.actor.id, actorRole: options.actor.role })
: await store.getOrCreateDefaultWorkspace?.({ ownerUserId: options.actor.id, projectId });
if (!current) return null;
const selectedActiveTraceId = manualSessionSelectableActiveTraceId(session);
const selectedLastTraceId = safeTraceId(session.lastTraceId) || selectedActiveTraceId || null;
const updated = await store.updateWorkspace?.({
workspaceId: current.id,
ownerUserId: options.actor.id,
actorRole: options.actor.role,
projectId: current.projectId ?? projectId,
name: current.name,
status: current.status,
selectedConversationId: session.conversationId,
selectedAgentSessionId: session.id,
activeTraceId: selectedActiveTraceId,
providerProfile,
patch: {
...(current.workspace && typeof current.workspace === "object" ? current.workspace : {}),
selectedConversationId: session.conversationId,
selectedAgentSessionId: session.id,
activeTraceId: selectedActiveTraceId,
...(selectedLastTraceId ? { lastTraceId: selectedLastTraceId } : {}),
sessionStatus: session.status,
threadId: safeOpaqueId(session.threadId) || null,
providerProfile,
source,
updatedAt: new Date().toISOString(),
valuesRedacted: true,
secretMaterialStored: false
},
updatedBySessionId: options.authSession?.id ?? null,
updatedByClient: textValue(params.updatedByClient ?? params.client) || source,
now: new Date().toISOString()
});
if (typeof options.accessController.publicWorkbenchWorkspace === "function") {
return await options.accessController.publicWorkbenchWorkspace(updated ?? current, options.actor);
}
return updated ?? current;
}
function manualSessionSelectableActiveTraceId(session = {}) {
if (!manualSessionHasRunningTraceStatus(session)) return null;
return safeTraceId(
session.currentTraceId
?? session.session?.currentTraceId
?? session.session?.traceId
?? session.session?.runnerTrace?.traceId
?? session.session?.agentRun?.traceId
) || safeTraceId(session.lastTraceId) || null;
}
function manualSessionHasRunningTraceStatus(session = {}) {
return [
session.status,
session.session?.sessionStatus,
session.session?.lifecycleStatus,
session.session?.runnerTrace?.sessionStatus,
session.session?.runnerTrace?.status,
session.session?.agentRun?.runStatus,
session.session?.agentRun?.commandState,
session.session?.agentRun?.terminalStatus
].map((value) => String(value ?? "").trim().toLowerCase().replace(/_/gu, "-"))
.some((value) => ["running", "busy", "pending", "creating"].includes(value));
}
function publicManualAgentSession(session) {
if (!session || typeof session !== "object") return null;
return {
sessionId: session.id,
projectId: session.projectId ?? null,
agentId: session.agentId ?? "hwlab-code-agent",
status: session.status ?? null,
usable: manualSessionUsable(session.status),
conversationId: safeConversationId(session.conversationId) || null,
threadId: safeOpaqueId(session.threadId) || null,
lastTraceId: safeTraceId(session.lastTraceId) || null,
providerProfile: textValue(session.session?.providerProfile) || null,
@@ -1916,7 +1833,6 @@ export async function handleCodeAgentSteerHttp(request, response, options) {
return;
}
if (!currentResult?.agentRun?.runId || !currentResult?.agentRun?.commandId) {
await clearStaleWorkbenchSteerTrace({ params, options, traceId, reason: "steer-trace-not-found" });
sendJson(response, 404, {
ok: false,
status: "not_found",
@@ -1961,7 +1877,6 @@ export async function handleCodeAgentSteerHttp(request, response, options) {
status: "failed",
traceId,
route: "/v1/agent/chat/steer",
conversationId: safeConversationId(currentResult.conversationId ?? params.conversationId) || null,
sessionId: safeSessionId(currentResult.sessionId ?? params.sessionId) || null,
threadId: safeOpaqueId(currentResult.threadId ?? params.threadId) || null,
agentRun: currentResult.agentRun,
@@ -2025,135 +1940,6 @@ function codeAgentOwnerStatusForResult(result = {}) {
return result?.status ?? "active";
}
async function claimWorkbenchWorkspaceTurn({ params = {}, options = {}, traceId, response } = {}) {
const workspaceId = safeWorkspaceId(params.workspaceId);
if (!workspaceId || !options.accessController?.store || !options.actor) return null;
const store = options.accessController.store;
const workspace = await store.getWorkspaceForUser?.({ workspaceId, ownerUserId: options.actor.id, actorRole: options.actor.role });
if (!workspace) {
sendJson(response, 404, {
ok: false,
status: "failed",
error: { code: "workbench_workspace_not_found", message: "Workbench workspace is not visible to the current actor" },
workspaceId
});
return { blocked: true };
}
const requestedProjectId = textValue(params.projectId);
const workspaceProjectId = textValue(workspace.projectId);
if (requestedProjectId && workspaceProjectId && requestedProjectId !== workspaceProjectId) {
sendJson(response, 409, {
ok: false,
status: "failed",
error: { code: "workspace_project_mismatch", message: "Workbench workspace project does not match the requested project" },
projectId: requestedProjectId,
workspaceProjectId,
workspaceId
});
return { blocked: true };
}
const expectedRevision = Number.parseInt(String(params.expectedWorkspaceRevision ?? params.workspaceRevision ?? ""), 10);
const workspaceRevisionConflict = Number.isInteger(expectedRevision) && expectedRevision > 0 && workspace.revision !== expectedRevision;
const requestedConversationId = safeConversationId(params.conversationId) || null;
const requestedSessionId = safeSessionId(params.sessionId) || null;
const nextSelectedConversationId = requestedConversationId || workspace.selectedConversationId;
const nextSelectedAgentSessionId = requestedSessionId || (requestedConversationId && requestedConversationId !== workspace.selectedConversationId ? null : workspace.selectedAgentSessionId);
const activeTraceId = safeTraceId(workspace.activeTraceId);
const activeTraceSnapshot = activeTraceId ? (options.traceStore ?? defaultCodeAgentTraceStore).snapshot(activeTraceId) : null;
const activeTraceOrphaned = Boolean(activeTraceId && isWorkbenchOrphanActiveTrace(activeTraceSnapshot));
if (activeTraceOrphaned) {
await store.updateWorkspace?.({
workspaceId,
ownerUserId: options.actor.id,
actorRole: options.actor.role,
selectedConversationId: workspace.selectedConversationId,
selectedAgentSessionId: workspace.selectedAgentSessionId,
activeTraceId: null,
providerProfile: workspace.providerProfile,
patch: {
...(workspace.workspace && typeof workspace.workspace === "object" ? workspace.workspace : {}),
activeTraceId: null,
staleActiveTraceId: activeTraceId,
staleActiveTraceReason: "orphan-cancel-session-missing",
sessionStatus: "failed",
updatedAt: new Date().toISOString(),
source: "code-agent-submit-active-trace-repair",
valuesRedacted: true,
secretMaterialStored: false
},
updatedBySessionId: options.authSession?.id ?? null,
updatedByClient: "code-agent-submit-active-trace-repair",
now: new Date().toISOString()
});
}
const updated = await store.updateWorkspace?.({
workspaceId,
ownerUserId: options.actor.id,
actorRole: options.actor.role,
selectedConversationId: nextSelectedConversationId,
selectedAgentSessionId: nextSelectedAgentSessionId,
activeTraceId: traceId,
providerProfile: textValue(params.providerProfile) || workspace.providerProfile,
patch: {
selectedConversationId: nextSelectedConversationId,
selectedAgentSessionId: nextSelectedAgentSessionId,
previousActiveTraceId: activeTraceId || null,
workspaceRevisionConflict,
expectedWorkspaceRevision: Number.isInteger(expectedRevision) && expectedRevision > 0 ? expectedRevision : null,
activeTraceId: traceId,
providerProfile: textValue(params.providerProfile) || workspace.providerProfile,
sessionStatus: "running",
updatedAt: new Date().toISOString(),
source: "code-agent-submit",
valuesRedacted: true,
secretMaterialStored: false
},
updatedBySessionId: options.authSession?.id ?? null,
updatedByClient: textValue(params.updatedByClient ?? params.client) || "code-agent-chat",
now: new Date().toISOString()
});
return { workspace: updated ?? workspace };
}
async function clearStaleWorkbenchSteerTrace({ params = {}, options = {}, traceId, reason }) {
const workspaceId = safeWorkspaceId(params.workspaceId);
if (!workspaceId || !safeTraceId(traceId) || !options.accessController?.store || !options.actor) return null;
const store = options.accessController.store;
const workspace = await store.getWorkspaceForUser?.({ workspaceId, ownerUserId: options.actor.id, actorRole: options.actor.role });
if (!workspace || safeTraceId(workspace.activeTraceId) !== traceId) return null;
return await store.updateWorkspace?.({
workspaceId,
ownerUserId: options.actor.id,
actorRole: options.actor.role,
selectedConversationId: workspace.selectedConversationId,
selectedAgentSessionId: workspace.selectedAgentSessionId,
activeTraceId: null,
providerProfile: workspace.providerProfile,
patch: {
...(workspace.workspace && typeof workspace.workspace === "object" ? workspace.workspace : {}),
activeTraceId: null,
staleActiveTraceId: traceId,
staleActiveTraceReason: reason,
sessionStatus: "failed",
updatedAt: new Date().toISOString(),
source: "code-agent-steer-active-trace-repair",
valuesRedacted: true,
secretMaterialStored: false
},
updatedBySessionId: options.authSession?.id ?? null,
updatedByClient: "code-agent-steer-active-trace-repair",
now: new Date().toISOString()
});
}
function isWorkbenchOrphanActiveTrace(snapshot) {
if (!snapshot || typeof snapshot !== "object") return false;
const last = snapshot.lastEvent && typeof snapshot.lastEvent === "object" ? snapshot.lastEvent : null;
if (!last) return false;
if (last.errorCode !== "cancel_session_missing" && last.label !== "cancel:not_cancelable") return false;
return !snapshot.sessionId && !snapshot.runId && !snapshot.commandId && !snapshot.sessionStatus && !snapshot.turn;
}
async function resolveWorkbenchActiveTraceResult(traceId, options = {}) {
if (!safeTraceId(traceId)) return null;
const cached = options.codeAgentChatResults?.get?.(traceId) ?? null;
@@ -2177,11 +1963,6 @@ async function resolveWorkbenchActiveTraceResult(traceId, options = {}) {
}
}
function safeWorkspaceId(value) {
const text = textValue(value);
return /^wsp_[A-Za-z0-9_.:-]+$/u.test(text) ? text : "";
}
function textValue(value) {
return String(value ?? "").trim();
}