fix: converge workbench session authority
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user