fix: ignore stale workbench active trace
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtemp } from "node:fs/promises";
|
||||
import { createServer } from "node:http";
|
||||
import { test } from "bun:test";
|
||||
|
||||
@@ -324,6 +325,82 @@ test("workbench workspace permits a new turn after AgentRun active trace reaches
|
||||
}
|
||||
});
|
||||
|
||||
test("workbench workspace ignores stale active trace when selected conversation already failed", async () => {
|
||||
const workspaceDir = await mkdtemp("/tmp/hwlab-stale-active-");
|
||||
const server = createCloudApiServer({
|
||||
env: {
|
||||
PATH: process.env.PATH,
|
||||
HWLAB_ACCESS_CONTROL_REQUIRED: "1",
|
||||
HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
|
||||
HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass",
|
||||
HWLAB_CODE_AGENT_PROVIDER: "codex-stdio",
|
||||
HWLAB_CODE_AGENT_CODEX_WORKSPACE: workspaceDir,
|
||||
HWLAB_CODE_AGENT_CODEX_SANDBOX: "danger-full-access",
|
||||
OPENAI_API_KEY: "test-openai-key-material"
|
||||
},
|
||||
codexStdioManager: {
|
||||
describe: () => ({ available: true, ready: true, workspace: workspaceDir, sandbox: "danger-full-access" }),
|
||||
probe: async () => ({ available: true, ready: true, workspace: workspaceDir, sandbox: "danger-full-access" }),
|
||||
chat: async (params = {}) => ({
|
||||
status: "completed",
|
||||
traceId: params.traceId,
|
||||
conversationId: params.conversationId,
|
||||
sessionId: params.sessionId,
|
||||
reply: { role: "assistant", content: "ok" },
|
||||
runnerTrace: { traceId: params.traceId, status: "completed", events: [], eventCount: 0 },
|
||||
session: { sessionId: params.sessionId, conversationId: params.conversationId, status: "idle" }
|
||||
}),
|
||||
cancel() {},
|
||||
reapIdle() {}
|
||||
}
|
||||
});
|
||||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||
|
||||
try {
|
||||
const { port } = server.address();
|
||||
const adminLogin = await postJson(port, "/auth/login", { username: "admin", password: "admin-pass" });
|
||||
const workspace = await getJson(port, "/v1/workbench/workspace?projectId=prj_device_pod_workbench", adminLogin.cookie);
|
||||
const conversation = await putJson(port, "/v1/agent/conversations/cnv_stale_active_failed", {
|
||||
projectId: "prj_device_pod_workbench",
|
||||
sessionId: "ses_stale_active_failed",
|
||||
threadId: "thread-stale-active",
|
||||
status: "failed",
|
||||
sessionStatus: "failed",
|
||||
lastTraceId: "trc_stale_active_missing",
|
||||
messages: [{ role: "agent", text: "failed", status: "failed", traceId: "trc_stale_active_missing" }]
|
||||
}, adminLogin.cookie);
|
||||
assert.equal(conversation.status, 200);
|
||||
|
||||
const update = await patchJson(port, `/v1/workbench/workspace/${workspace.body.workspace.workspaceId}`, {
|
||||
expectedRevision: 1,
|
||||
selectedConversationId: "cnv_stale_active_failed",
|
||||
selectedAgentSessionId: "ses_stale_active_failed",
|
||||
activeTraceId: "trc_stale_active_missing",
|
||||
sessionStatus: "running",
|
||||
updatedByClient: "test-suite"
|
||||
}, adminLogin.cookie);
|
||||
assert.equal(update.status, 200);
|
||||
|
||||
const next = await postJson(port, "/v1/agent/chat", {
|
||||
message: "new turn after stale active trace",
|
||||
traceId: "trc_stale_active_next",
|
||||
conversationId: "cnv_stale_active_failed",
|
||||
sessionId: "ses_stale_active_failed",
|
||||
workspaceId: workspace.body.workspace.workspaceId,
|
||||
expectedWorkspaceRevision: 2,
|
||||
shortConnection: true
|
||||
}, adminLogin.cookie, { prefer: "respond-async", "x-trace-id": "trc_stale_active_next" });
|
||||
|
||||
assert.equal(next.status, 202);
|
||||
assert.equal(next.body.traceId, "trc_stale_active_next");
|
||||
|
||||
const restored = await getJson(port, "/v1/workbench/workspace?projectId=prj_device_pod_workbench", adminLogin.cookie);
|
||||
assert.equal(restored.body.workspace.activeTraceId, "trc_stale_active_next");
|
||||
} finally {
|
||||
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
|
||||
}
|
||||
});
|
||||
|
||||
test("workbench workspace status clears completed AgentRun active trace on read", async () => {
|
||||
const agentRunCalls = [];
|
||||
const agentSessions = new Map();
|
||||
|
||||
@@ -902,7 +902,17 @@ async function claimWorkbenchWorkspaceTurn({ params = {}, options = {}, traceId,
|
||||
}
|
||||
const activeTraceId = safeTraceId(workspace.activeTraceId);
|
||||
const activeResult = activeTraceId ? await resolveWorkbenchActiveTraceResult(activeTraceId, options) : null;
|
||||
if (activeTraceId && (!activeResult || activeResult.status === "running")) {
|
||||
const selectedConversationId = safeConversationId(workspace.selectedConversationId);
|
||||
const shouldInspectSelectedConversation = Boolean(
|
||||
activeTraceId && !activeResult && selectedConversationId && options.accessController?.visibleConversationForActor
|
||||
);
|
||||
const selectedConversation = shouldInspectSelectedConversation
|
||||
? await options.accessController.visibleConversationForActor(options.actor, selectedConversationId, workspace.projectId)
|
||||
: null;
|
||||
const activeTraceBlocksTurn = Boolean(
|
||||
activeTraceId && (activeResult?.status === "running" || (!activeResult && isActiveWorkbenchConversation(selectedConversation)))
|
||||
);
|
||||
if (activeTraceBlocksTurn) {
|
||||
sendJson(response, 409, {
|
||||
ok: false,
|
||||
status: "workspace_agent_busy",
|
||||
@@ -941,6 +951,15 @@ async function claimWorkbenchWorkspaceTurn({ params = {}, options = {}, traceId,
|
||||
return { workspace: updated ?? workspace };
|
||||
}
|
||||
|
||||
function isActiveWorkbenchConversation(conversation = null) {
|
||||
if (!conversation) return false;
|
||||
const values = [conversation.status, conversation.session?.status, conversation.snapshot?.sessionStatus];
|
||||
for (const message of Array.isArray(conversation.messages) ? conversation.messages.slice(-5) : []) {
|
||||
values.push(message?.status, message?.runnerTrace?.sessionStatus, message?.sessionStatus);
|
||||
}
|
||||
return values.some((value) => ["running", "busy", "pending", "active", "queued"].includes(textValue(value).toLowerCase()));
|
||||
}
|
||||
|
||||
async function resolveWorkbenchActiveTraceResult(traceId, options = {}) {
|
||||
if (!safeTraceId(traceId)) return null;
|
||||
const cached = options.codeAgentChatResults?.get?.(traceId) ?? null;
|
||||
|
||||
Reference in New Issue
Block a user