fix: release completed agentrun workspace turns

This commit is contained in:
Codex
2026-06-02 00:49:48 +08:00
parent 21cf7efe52
commit 90f030e8f7
2 changed files with 177 additions and 1 deletions
+153
View File
@@ -171,6 +171,159 @@ test("workbench workspace is account-scoped, persistent, and gates active agent
}
});
test("workbench workspace permits a new turn after AgentRun active trace reaches terminal status", async () => {
const agentRunCalls = [];
const agentSessions = new Map();
const agentRunServer = createServer(async (request, response) => {
const url = new URL(request.url || "/", "http://127.0.0.1");
agentRunCalls.push({ method: request.method, path: url.pathname, search: url.search });
const send = (data) => {
response.writeHead(200, { "content-type": "application/json" });
response.end(`${JSON.stringify({ ok: true, data })}\n`);
};
if (request.method === "GET" && url.pathname === "/api/v1/runs/run_workspace_done/events") {
return send({ items: [
{ id: "evt_done", runId: "run_workspace_done", seq: 1, type: "terminal_status", payload: { commandId: "cmd_workspace_done", terminalStatus: "completed" }, createdAt: "2026-06-01T00:00:01.000Z" }
] });
}
if (request.method === "GET" && url.pathname === "/api/v1/runs/run_workspace_done/commands/cmd_workspace_done/result") {
return send({
runId: "run_workspace_done",
commandId: "cmd_workspace_done",
status: "completed",
runStatus: "completed",
commandState: "completed",
terminalStatus: "completed",
completed: true,
reply: "previous turn completed",
lastSeq: 1,
sessionRef: { sessionId: "ses_issue655_shared", conversationId: "cnv_issue655_shared", threadId: "thread-issue-655" }
});
}
if (request.method === "POST" && url.pathname === "/api/v1/runs") {
return send({ id: "run_workspace_next", status: "pending", backendProfile: "deepseek", sessionRef: { sessionId: "ses_issue655_shared", conversationId: "cnv_issue655_shared" } });
}
if (request.method === "POST" && url.pathname === "/api/v1/runs/run_workspace_next/commands") {
return send({ id: "cmd_workspace_next", runId: "run_workspace_next", state: "pending", type: "turn", seq: 1 });
}
if (request.method === "POST" && url.pathname === "/api/v1/runs/run_workspace_next/runner-jobs") {
return send({
action: "create-kubernetes-job",
runId: "run_workspace_next",
commandId: "cmd_workspace_next",
attemptId: "attempt_workspace_next",
runnerId: "runner_workspace_next",
namespace: "agentrun-v01",
jobName: "agentrun-v01-runner-workspace-next"
});
}
response.writeHead(404, { "content-type": "application/json" });
response.end(`${JSON.stringify({ ok: false, message: `unexpected ${request.method} ${url.pathname}` })}\n`);
});
await new Promise((resolve) => agentRunServer.listen(0, "127.0.0.1", resolve));
const agentRunPort = agentRunServer.address().port;
const accessController = createAccessController({
env: {
HWLAB_ACCESS_CONTROL_REQUIRED: "1",
HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass"
},
now: () => "2026-06-01T00:00:00.000Z"
});
accessController.getAgentSessionByTraceId = async (traceId) => agentSessions.get(traceId) ?? null;
const server = createCloudApiServer({
env: {
HWLAB_ACCESS_CONTROL_REQUIRED: "1",
HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass",
HWLAB_CODE_AGENT_ADAPTER: "agentrun-v01",
AGENTRUN_MGR_URL: `http://127.0.0.1:${agentRunPort}`,
HWLAB_CODE_AGENT_AGENTRUN_ALLOW_NON_K3S_URL: "1",
HWLAB_CODE_AGENT_AGENTRUN_SOURCE_COMMIT: "0123456789abcdef0123456789abcdef01234567",
HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE: "deepseek"
},
accessController,
now: () => "2026-06-01T00:00:00.000Z"
});
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 alice = await postJson(port, "/v1/admin/users", { username: "alice-ws-terminal", password: "alice-pass" }, adminLogin.cookie);
assert.equal(alice.status, 201);
const aliceLogin = await postJson(port, "/auth/login", { username: "alice-ws-terminal", password: "alice-pass" });
const workspace = await getJson(port, "/v1/workbench/workspace?projectId=prj_device_pod_workbench", aliceLogin.cookie);
assert.equal(workspace.status, 200);
const conversation = await putJson(port, "/v1/agent/conversations/cnv_issue655_shared", {
projectId: "prj_device_pod_workbench",
sessionId: "ses_issue655_shared",
threadId: "thread-issue-655",
sessionStatus: "active",
lastTraceId: "trc_issue655_previous",
messages: [{ role: "agent", text: "previous completed turn", traceId: "trc_issue655_previous" }]
}, aliceLogin.cookie);
assert.equal(conversation.status, 200);
const previousPayload = {
messageId: "msg_workspace_done",
agentRun: {
runId: "run_workspace_done",
commandId: "cmd_workspace_done",
backendProfile: "deepseek",
managerUrl: `http://127.0.0.1:${agentRunPort}`,
lastSeq: 0,
sessionId: "ses_issue655_shared",
conversationId: "cnv_issue655_shared",
threadId: "thread-issue-655"
}
};
agentSessions.set("trc_issue655_active_done", {
id: "ses_issue655_shared",
ownerUserId: alice.body.user.id,
conversationId: "cnv_issue655_shared",
threadId: "thread-issue-655",
status: "active",
session: previousPayload,
updatedAt: "2026-06-01T00:00:00.000Z"
});
const update = await patchJson(port, `/v1/workbench/workspace/${workspace.body.workspace.workspaceId}`, {
expectedRevision: 1,
selectedConversationId: "cnv_issue655_shared",
selectedAgentSessionId: "ses_issue655_shared",
activeTraceId: "trc_issue655_active_done",
providerProfile: "deepseek",
sessionStatus: "running",
updatedByClient: "test-suite"
}, aliceLogin.cookie);
assert.equal(update.status, 200);
assert.equal(update.body.workspace.activeTraceId, "trc_issue655_active_done");
const next = await postJson(port, "/v1/agent/chat", {
message: "new turn after completed active trace",
traceId: "trc_issue655_after_done",
conversationId: "cnv_issue655_shared",
sessionId: "ses_issue655_shared",
workspaceId: workspace.body.workspace.workspaceId,
expectedWorkspaceRevision: 2,
shortConnection: true
}, aliceLogin.cookie, { prefer: "respond-async", "x-trace-id": "trc_issue655_after_done" });
assert.equal(next.status, 202);
assert.equal(next.body.status, "running");
assert.equal(next.body.traceId, "trc_issue655_after_done");
assert.ok(agentRunCalls.some((call) => call.path === "/api/v1/runs/run_workspace_done/commands/cmd_workspace_done/result"));
const restored = await getJson(port, "/v1/workbench/workspace?projectId=prj_device_pod_workbench", aliceLogin.cookie);
assert.equal(restored.body.workspace.activeTraceId, "trc_issue655_after_done");
assert.equal(restored.body.workspace.workspace.sessionStatus, "running");
} finally {
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
await new Promise((resolve, reject) => agentRunServer.close((error) => (error ? reject(error) : resolve())));
}
});
test("cloud api access control grants visible device pods and requires device-pod executor", async () => {
let directGatewayDispatches = 0;
const gatewayRegistry = {
+24 -1
View File
@@ -850,7 +850,7 @@ async function claimWorkbenchWorkspaceTurn({ params = {}, options = {}, traceId,
return { blocked: true };
}
const activeTraceId = safeTraceId(workspace.activeTraceId);
const activeResult = activeTraceId ? options.codeAgentChatResults?.get(activeTraceId) ?? null : null;
const activeResult = activeTraceId ? await resolveWorkbenchActiveTraceResult(activeTraceId, options) : null;
if (activeTraceId && (!activeResult || activeResult.status === "running")) {
sendJson(response, 409, {
ok: false,
@@ -890,6 +890,29 @@ async function claimWorkbenchWorkspaceTurn({ params = {}, options = {}, traceId,
return { workspace: updated ?? workspace };
}
async function resolveWorkbenchActiveTraceResult(traceId, options = {}) {
if (!safeTraceId(traceId)) return null;
const cached = options.codeAgentChatResults?.get?.(traceId) ?? null;
if (cached?.agentRun?.runId && cached.status === "running") {
try {
const synced = await syncAgentRunChatResult({ traceId, currentResult: cached, options, traceStore: options.traceStore ?? defaultCodeAgentTraceStore });
return synced.result ?? cached;
} catch {
return cached;
}
}
if (cached) return cached;
if (!codeAgentAgentRunAdapterEnabled(options.env ?? process.env)) return null;
try {
const persisted = await loadPersistedAgentRunResult(traceId, options);
if (!persisted?.agentRun?.runId) return persisted;
const synced = await syncAgentRunChatResult({ traceId, currentResult: persisted, options, traceStore: options.traceStore ?? defaultCodeAgentTraceStore });
return synced.result ?? persisted;
} catch {
return null;
}
}
function safeWorkspaceId(value) {
const text = textValue(value);
return /^wsp_[A-Za-z0-9_.:-]+$/u.test(text) ? text : "";