fix(v02): repair persisted final response fallback (#840)
Co-authored-by: Codex Agent <codex@hwlab.local>
This commit is contained in:
@@ -5,7 +5,6 @@ import { createServer } from "node:http";
|
||||
import { test } from "bun:test";
|
||||
|
||||
import { createAccessController } from "./access-control.ts";
|
||||
import { createCodeAgentTraceStore } from "./code-agent-trace-store.ts";
|
||||
import { createCloudApiServer } from "./server.ts";
|
||||
|
||||
const INTERNAL_TOKEN = "test-internal-token";
|
||||
@@ -74,128 +73,6 @@ test("Postgres workspace update query preserves parameter numbering", async () =
|
||||
assert.equal(update.params[17], "user");
|
||||
});
|
||||
|
||||
test("workbench workspace is account-scoped, persistent, and permits independent Code Agent turns", async () => {
|
||||
const server = createCloudApiServer({
|
||||
env: {
|
||||
HWLAB_ACCESS_CONTROL_REQUIRED: "1",
|
||||
HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
|
||||
HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass"
|
||||
},
|
||||
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 adminCookie = adminLogin.cookie;
|
||||
const alice = await postJson(port, "/v1/admin/users", { username: "alice-ws", password: "alice-pass" }, adminCookie);
|
||||
const bob = await postJson(port, "/v1/admin/users", { username: "bob-ws", password: "bob-pass" }, adminCookie);
|
||||
assert.equal(alice.status, 201);
|
||||
assert.equal(bob.status, 201);
|
||||
|
||||
const aliceLogin = await postJson(port, "/auth/login", { username: "alice-ws", password: "alice-pass" });
|
||||
const bobLogin = await postJson(port, "/auth/login", { username: "bob-ws", password: "bob-pass" });
|
||||
const workspace = await getJson(port, "/v1/workbench/workspace?projectId=prj_device_pod_workbench", aliceLogin.cookie);
|
||||
assert.equal(workspace.status, 200);
|
||||
assert.equal(workspace.body.contractVersion, "workbench-workspace-v1");
|
||||
assert.match(workspace.body.workspace.workspaceId, /^wsp_/u);
|
||||
assert.equal(workspace.body.workspace.revision, 1);
|
||||
assert.equal(workspace.body.workspace.selectedConversationId, null);
|
||||
|
||||
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: "running",
|
||||
lastTraceId: "trc_issue655_previous",
|
||||
messages: [{ role: "agent", text: "persist me", status: "running", traceId: "trc_issue655_active" }]
|
||||
}, aliceLogin.cookie);
|
||||
assert.equal(conversation.status, 200);
|
||||
assert.equal(conversation.body.conversation.conversationId, "cnv_issue655_shared");
|
||||
|
||||
const update = await patchJson(port, `/v1/workbench/workspace/${workspace.body.workspace.workspaceId}`, {
|
||||
projectId: "prj_device_pod_workbench",
|
||||
expectedRevision: 1,
|
||||
selectedConversationId: "cnv_issue655_shared",
|
||||
selectedAgentSessionId: "ses_issue655_shared",
|
||||
activeTraceId: "trc_issue655_active",
|
||||
providerProfile: "deepseek",
|
||||
sessionStatus: "running",
|
||||
threadId: "thread-issue-655",
|
||||
messages: [{ role: "agent", status: "running", traceId: "trc_issue655_active" }],
|
||||
updatedByClient: "test-suite"
|
||||
}, aliceLogin.cookie);
|
||||
assert.equal(update.status, 200);
|
||||
assert.equal(update.body.workspace.revision, 2);
|
||||
assert.equal(update.body.workspace.selectedConversationId, "cnv_issue655_shared");
|
||||
assert.equal(update.body.workspace.selectedConversation.conversationId, "cnv_issue655_shared");
|
||||
assert.equal(update.body.workspace.activeTraceId, "trc_issue655_active");
|
||||
assert.equal(update.body.workspace.secretMaterialStored, false);
|
||||
|
||||
const stale = await patchJson(port, `/v1/workbench/workspace/${workspace.body.workspace.workspaceId}`, {
|
||||
expectedRevision: 1,
|
||||
selectedConversationId: "cnv_issue655_shared",
|
||||
updatedByClient: "test-suite"
|
||||
}, aliceLogin.cookie);
|
||||
assert.equal(stale.status, 409);
|
||||
assert.equal(stale.body.error.code, "workspace_revision_conflict");
|
||||
assert.equal(stale.body.workspace.revision, 2);
|
||||
|
||||
const bobRead = await getJson(port, `/v1/workbench/workspace/${workspace.body.workspace.workspaceId}/events?afterRevision=0`, bobLogin.cookie);
|
||||
assert.equal(bobRead.status, 404);
|
||||
assert.equal(bobRead.body.error.code, "workbench_workspace_not_found");
|
||||
|
||||
const aliceRelogin = await postJson(port, "/auth/login", { username: "alice-ws", password: "alice-pass" });
|
||||
const restored = await getJson(port, "/v1/workbench/workspace?projectId=prj_device_pod_workbench", aliceRelogin.cookie);
|
||||
assert.equal(restored.status, 200);
|
||||
assert.equal(restored.body.workspace.workspaceId, workspace.body.workspace.workspaceId);
|
||||
assert.equal(restored.body.workspace.selectedConversationId, "cnv_issue655_shared");
|
||||
assert.equal(restored.body.workspace.revision, 2);
|
||||
|
||||
const events = await getJson(port, `/v1/workbench/workspace/${workspace.body.workspace.workspaceId}/events?afterRevision=1`, aliceRelogin.cookie);
|
||||
assert.equal(events.status, 200);
|
||||
assert.equal(events.body.status, "changed");
|
||||
assert.equal(events.body.latestRevision, 2);
|
||||
|
||||
const parallel = await postJson(port, "/v1/agent/chat", {
|
||||
message: "should run beside active workspace turn",
|
||||
traceId: "trc_issue717_parallel",
|
||||
conversationId: "cnv_issue717_parallel",
|
||||
workspaceId: workspace.body.workspace.workspaceId,
|
||||
expectedWorkspaceRevision: 2,
|
||||
shortConnection: true
|
||||
}, aliceRelogin.cookie, { prefer: "respond-async", "x-trace-id": "trc_issue717_parallel" });
|
||||
assert.equal(parallel.status, 202);
|
||||
assert.equal(parallel.body.status, "running");
|
||||
assert.equal(parallel.body.traceId, "trc_issue717_parallel");
|
||||
assert.equal(parallel.body.conversationId, "cnv_issue717_parallel");
|
||||
assert.equal(parallel.body.sessionId, null);
|
||||
assert.equal(parallel.body.workspaceId, workspace.body.workspace.workspaceId);
|
||||
assert.ok(parallel.body.workspaceRevision > 2);
|
||||
|
||||
const afterParallel = await getJson(port, "/v1/workbench/workspace?projectId=prj_device_pod_workbench", aliceRelogin.cookie);
|
||||
assert.equal(afterParallel.status, 200);
|
||||
assert.equal(afterParallel.body.workspace.activeTraceId, "trc_issue717_parallel");
|
||||
assert.equal(afterParallel.body.workspace.selectedConversationId, "cnv_issue717_parallel");
|
||||
assert.equal(afterParallel.body.workspace.selectedAgentSessionId, null);
|
||||
assert.equal(afterParallel.body.workspace.workspace.selectedAgentSessionId, undefined);
|
||||
assert.equal(afterParallel.body.workspace.workspace.previousActiveTraceId, "trc_issue655_active");
|
||||
assert.equal(afterParallel.body.workspace.workspace.workspaceRevisionConflict, false);
|
||||
|
||||
const reset = await postJson(port, `/v1/workbench/workspace/${workspace.body.workspace.workspaceId}/reset`, {
|
||||
confirm: true,
|
||||
updatedByClient: "test-suite"
|
||||
}, aliceRelogin.cookie);
|
||||
assert.equal(reset.status, 200);
|
||||
assert.equal(reset.body.reset, true);
|
||||
assert.equal(reset.body.workspace.activeTraceId, null);
|
||||
assert.equal(reset.body.workspace.selectedConversationId, null);
|
||||
} finally {
|
||||
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
|
||||
}
|
||||
});
|
||||
|
||||
test("cloud api /auth/oidc/login returns 302 to Keycloak when issuer and client are configured", async () => {
|
||||
const server = createCloudApiServer({
|
||||
env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1", HWLAB_KEYCLOAK_ISSUER: OIDC_ISSUER, HWLAB_KEYCLOAK_CLIENT_ID: OIDC_CLIENT_ID, HWLAB_PUBLIC_ENDPOINT },
|
||||
@@ -481,147 +358,6 @@ 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 releases orphan cancel-blocked active trace on new turn", async () => {
|
||||
const traceStore = createCodeAgentTraceStore();
|
||||
traceStore.append("trc_issue664_orphan_active", {
|
||||
type: "cancel",
|
||||
stage: "cancel",
|
||||
status: "blocked",
|
||||
label: "cancel:not_cancelable",
|
||||
message: "Cancel request did not include a bound Codex stdio sessionId.",
|
||||
errorCode: "cancel_session_missing",
|
||||
waitingFor: "session-binding"
|
||||
});
|
||||
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:9",
|
||||
HWLAB_CODE_AGENT_AGENTRUN_ALLOW_NON_K3S_URL: "1",
|
||||
HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE: "deepseek"
|
||||
},
|
||||
traceStore,
|
||||
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-orphan", password: "alice-pass" }, adminLogin.cookie);
|
||||
assert.equal(alice.status, 201);
|
||||
const aliceLogin = await postJson(port, "/auth/login", { username: "alice-ws-orphan", password: "alice-pass" });
|
||||
const workspace = await getJson(port, "/v1/workbench/workspace?projectId=prj_device_pod_workbench", aliceLogin.cookie);
|
||||
assert.equal(workspace.status, 200);
|
||||
|
||||
const update = await patchJson(port, `/v1/workbench/workspace/${workspace.body.workspace.workspaceId}`, {
|
||||
expectedRevision: 1,
|
||||
activeTraceId: "trc_issue664_orphan_active",
|
||||
providerProfile: "deepseek",
|
||||
sessionStatus: "running",
|
||||
updatedByClient: "test-suite"
|
||||
}, aliceLogin.cookie);
|
||||
assert.equal(update.status, 200);
|
||||
assert.equal(update.body.workspace.activeTraceId, "trc_issue664_orphan_active");
|
||||
|
||||
const next = await postJson(port, "/v1/agent/chat", {
|
||||
message: "new turn should replace orphan active trace",
|
||||
traceId: "trc_issue664_after_orphan",
|
||||
conversationId: "cnv_issue664_after_orphan",
|
||||
workspaceId: workspace.body.workspace.workspaceId,
|
||||
expectedWorkspaceRevision: 2,
|
||||
shortConnection: true
|
||||
}, aliceLogin.cookie, { prefer: "respond-async", "x-trace-id": "trc_issue664_after_orphan" });
|
||||
assert.equal(next.status, 202);
|
||||
assert.equal(next.body.status, "running");
|
||||
assert.equal(next.body.traceId, "trc_issue664_after_orphan");
|
||||
|
||||
const restored = await getJson(port, "/v1/workbench/workspace?projectId=prj_device_pod_workbench", aliceLogin.cookie);
|
||||
assert.equal(restored.body.workspace.activeTraceId, "trc_issue664_after_orphan");
|
||||
assert.equal(restored.body.workspace.workspace.sessionStatus, "running");
|
||||
} finally {
|
||||
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
|
||||
}
|
||||
});
|
||||
|
||||
test("workbench workspace permits continuation when stale active trace has idle active conversation", async () => {
|
||||
const workspaceDir = await mkdtemp("/tmp/hwlab-stale-active-idle-");
|
||||
const server = createCloudApiServer({
|
||||
@@ -1124,186 +860,6 @@ test("manual Code Agent session select restores running active trace", async ()
|
||||
}
|
||||
});
|
||||
|
||||
test("workbench workspace clears stale continuation after AgentRun thread resume failure", async () => {
|
||||
const staleThreadId = "019e0000-0000-7000-8000-000000000195";
|
||||
const agentRunCalls = [];
|
||||
const createRunInputs = [];
|
||||
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_thread_resume_failed/events") {
|
||||
return send({ items: [
|
||||
{ id: "evt_thread_resume_failed", runId: "run_workspace_thread_resume_failed", seq: 1, type: "terminal_status", payload: { commandId: "cmd_workspace_thread_resume_failed", terminalStatus: "failed" }, createdAt: "2026-06-01T00:00:01.000Z" }
|
||||
] });
|
||||
}
|
||||
if (request.method === "GET" && url.pathname === "/api/v1/runs/run_workspace_thread_resume_failed/commands/cmd_workspace_thread_resume_failed/result") {
|
||||
return send({
|
||||
runId: "run_workspace_thread_resume_failed",
|
||||
commandId: "cmd_workspace_thread_resume_failed",
|
||||
status: "failed",
|
||||
runStatus: "claimed",
|
||||
commandState: "failed",
|
||||
terminalStatus: "failed",
|
||||
completed: false,
|
||||
failureKind: "thread-resume-failed",
|
||||
failureMessage: `codex app-server thread/resume failed for existing thread: no rollout found for thread id ${staleThreadId}`,
|
||||
lastSeq: 1,
|
||||
sessionRef: { sessionId: "ses_issue195_stale", conversationId: "cnv_issue195_stale", threadId: staleThreadId }
|
||||
});
|
||||
}
|
||||
if (request.method === "POST" && url.pathname === "/api/v1/runs") {
|
||||
const body = await requestJson(request);
|
||||
createRunInputs.push(body);
|
||||
return send({ id: "run_workspace_after_stale", status: "pending", backendProfile: "deepseek", sessionRef: { sessionId: body.sessionRef?.sessionId } });
|
||||
}
|
||||
if (request.method === "POST" && url.pathname === "/api/v1/runs/run_workspace_after_stale/commands") {
|
||||
return send({ id: "cmd_workspace_after_stale", runId: "run_workspace_after_stale", state: "pending", type: "turn", seq: 1 });
|
||||
}
|
||||
if (request.method === "POST" && url.pathname === "/api/v1/runs/run_workspace_after_stale/runner-jobs") {
|
||||
return send({
|
||||
action: "create-kubernetes-job",
|
||||
runId: "run_workspace_after_stale",
|
||||
commandId: "cmd_workspace_after_stale",
|
||||
attemptId: "attempt_workspace_after_stale",
|
||||
runnerId: "runner_workspace_after_stale",
|
||||
namespace: "agentrun-v01",
|
||||
jobName: "agentrun-v01-runner-workspace-after-stale"
|
||||
});
|
||||
}
|
||||
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-thread-resume", password: "alice-pass" }, adminLogin.cookie);
|
||||
assert.equal(alice.status, 201);
|
||||
const aliceLogin = await postJson(port, "/auth/login", { username: "alice-ws-thread-resume", 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_issue195_stale", {
|
||||
projectId: "prj_device_pod_workbench",
|
||||
sessionId: "ses_issue195_stale",
|
||||
threadId: staleThreadId,
|
||||
sessionStatus: "active",
|
||||
lastTraceId: "trc_issue195_thread_resume_failed",
|
||||
messages: [{ role: "agent", text: "stale continuation still running", status: "running", traceId: "trc_issue195_thread_resume_failed" }]
|
||||
}, aliceLogin.cookie);
|
||||
assert.equal(conversation.status, 200);
|
||||
|
||||
agentSessions.set("trc_issue195_thread_resume_failed", {
|
||||
id: "ses_issue195_stale",
|
||||
ownerUserId: alice.body.user.id,
|
||||
conversationId: "cnv_issue195_stale",
|
||||
threadId: staleThreadId,
|
||||
status: "active",
|
||||
session: {
|
||||
messageId: "msg_issue195_thread_resume_failed",
|
||||
agentRun: {
|
||||
runId: "run_workspace_thread_resume_failed",
|
||||
commandId: "cmd_workspace_thread_resume_failed",
|
||||
backendProfile: "deepseek",
|
||||
managerUrl: `http://127.0.0.1:${agentRunPort}`,
|
||||
lastSeq: 0,
|
||||
sessionId: "ses_issue195_stale",
|
||||
conversationId: "cnv_issue195_stale",
|
||||
threadId: staleThreadId
|
||||
}
|
||||
},
|
||||
updatedAt: "2026-06-01T00:00:00.000Z"
|
||||
});
|
||||
|
||||
const update = await patchJson(port, `/v1/workbench/workspace/${workspace.body.workspace.workspaceId}`, {
|
||||
expectedRevision: 1,
|
||||
selectedConversationId: "cnv_issue195_stale",
|
||||
selectedAgentSessionId: "ses_issue195_stale",
|
||||
activeTraceId: "trc_issue195_thread_resume_failed",
|
||||
providerProfile: "deepseek",
|
||||
sessionStatus: "running",
|
||||
updatedByClient: "test-suite"
|
||||
}, aliceLogin.cookie);
|
||||
assert.equal(update.status, 200);
|
||||
assert.equal(update.body.workspace.selectedConversationId, "cnv_issue195_stale");
|
||||
assert.equal(update.body.workspace.activeTraceId, "trc_issue195_thread_resume_failed");
|
||||
|
||||
const restored = await getJson(port, "/v1/workbench/workspace?projectId=prj_device_pod_workbench", aliceLogin.cookie);
|
||||
assert.equal(restored.status, 200);
|
||||
assert.equal(restored.body.workspace.activeTraceId, null);
|
||||
assert.equal(restored.body.workspace.selectedConversationId, null);
|
||||
assert.equal(restored.body.workspace.selectedAgentSessionId, null);
|
||||
assert.equal(restored.body.workspace.selectedConversation, null);
|
||||
assert.equal(restored.body.workspace.workspace.sessionStatus, "failed");
|
||||
assert.equal(restored.body.workspace.workspace.lastTraceId, "trc_issue195_thread_resume_failed");
|
||||
assert.equal(restored.body.workspace.workspace.staleContinuationCleared, true);
|
||||
assert.equal(restored.body.workspace.workspace.staleContinuationReason, "thread-resume-failed");
|
||||
assert.equal(restored.body.workspace.workspace.staleConversationId, "cnv_issue195_stale");
|
||||
assert.equal(restored.body.workspace.workspace.staleAgentSessionId, "ses_issue195_stale");
|
||||
assert.equal(restored.body.workspace.workspace.staleThreadId, staleThreadId);
|
||||
assert.equal(restored.body.workspace.workspace.recoveryAction, "new-session-on-next-turn");
|
||||
|
||||
const failedConversation = await getJson(port, "/v1/agent/conversations/cnv_issue195_stale", aliceLogin.cookie);
|
||||
assert.equal(failedConversation.status, 200);
|
||||
assert.equal(failedConversation.body.conversation.status, "failed");
|
||||
assert.equal(failedConversation.body.conversation.threadId, staleThreadId);
|
||||
|
||||
const next = await postJson(port, "/v1/agent/chat", {
|
||||
message: "new turn after stale continuation was cleared",
|
||||
traceId: "trc_issue195_after_stale",
|
||||
workspaceId: workspace.body.workspace.workspaceId,
|
||||
expectedWorkspaceRevision: restored.body.workspace.revision,
|
||||
shortConnection: true
|
||||
}, aliceLogin.cookie, { prefer: "respond-async", "x-trace-id": "trc_issue195_after_stale" });
|
||||
assert.equal(next.status, 202);
|
||||
assert.equal(next.body.status, "running");
|
||||
assert.equal(next.body.traceId, "trc_issue195_after_stale");
|
||||
await waitForCondition(() => createRunInputs.length === 1);
|
||||
assert.equal(createRunInputs.length, 1);
|
||||
assert.notEqual(createRunInputs[0].sessionRef?.threadId, staleThreadId);
|
||||
assert.equal(createRunInputs[0].sessionRef?.threadId, undefined);
|
||||
assert.equal(createRunInputs[0].sessionRef?.conversationId, undefined);
|
||||
|
||||
const afterNext = await getJson(port, "/v1/workbench/workspace?projectId=prj_device_pod_workbench", aliceLogin.cookie);
|
||||
assert.equal(afterNext.body.workspace.activeTraceId, "trc_issue195_after_stale");
|
||||
assert.ok(agentRunCalls.some((call) => call.path === "/api/v1/runs/run_workspace_thread_resume_failed/commands/cmd_workspace_thread_resume_failed/result"));
|
||||
} 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("workbench workspace status repairs terminal selected conversation after active trace was cleared", async () => {
|
||||
const agentRunCalls = [];
|
||||
const agentSessions = new Map();
|
||||
@@ -1795,6 +1351,96 @@ test("cloud api exposes terminal conversation status when stored session status
|
||||
}
|
||||
});
|
||||
|
||||
test("cloud api repairs persisted final response fallback from terminal result", async () => {
|
||||
const codeAgentChatResults = new Map();
|
||||
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:9",
|
||||
HWLAB_CODE_AGENT_AGENTRUN_ALLOW_NON_K3S_URL: "1"
|
||||
},
|
||||
codeAgentChatResults,
|
||||
now: () => "2026-06-04T09:45: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 aliceCreate = await postJson(port, "/v1/admin/users", { username: "alice-issue834-fallback", password: "alice-pass" }, adminLogin.cookie);
|
||||
assert.equal(aliceCreate.status, 201);
|
||||
const aliceLogin = await postJson(port, "/auth/login", { username: "alice-issue834-fallback", password: "alice-pass" });
|
||||
const fallbackText = "Code Agent 仍在处理,可以继续 steer 或等待 trace 完成。";
|
||||
const finalText = "**直接答:能,但只到一半。**\n\n功能已经过了,剩余风险另开 issue 跟踪。";
|
||||
codeAgentChatResults.set("trc_issue834_final_response", {
|
||||
status: "completed",
|
||||
traceId: "trc_issue834_final_response",
|
||||
conversationId: "cnv_issue834_fallback",
|
||||
sessionId: "ses_issue834_fallback",
|
||||
threadId: "thread-issue-834-fallback",
|
||||
ownerUserId: aliceCreate.body.user.id,
|
||||
reply: { role: "assistant", content: finalText, messageId: "msg_issue834_real_final" },
|
||||
agentRun: { terminalStatus: "completed" },
|
||||
session: {
|
||||
sessionId: "ses_issue834_fallback",
|
||||
conversationId: "cnv_issue834_fallback",
|
||||
threadId: "thread-issue-834-fallback",
|
||||
status: "idle"
|
||||
}
|
||||
});
|
||||
|
||||
const stored = await putJson(port, "/v1/agent/conversations/cnv_issue834_fallback", {
|
||||
projectId: "prj_device_pod_workbench",
|
||||
sessionId: "ses_issue834_fallback",
|
||||
threadId: "thread-issue-834-fallback",
|
||||
sessionStatus: "completed",
|
||||
lastTraceId: "trc_issue834_final_response",
|
||||
messages: [
|
||||
{ id: "msg_issue834_question", role: "user", title: "用户", text: "功能是过了还是没过?", status: "sent", conversationId: "cnv_issue834_fallback", sessionId: "ses_issue834_fallback", threadId: "thread-issue-834-fallback" },
|
||||
{ id: "msg_issue834_fallback", role: "agent", title: "Agent", text: fallbackText, status: "completed", traceId: "trc_issue834_final_response", conversationId: "cnv_issue834_fallback", sessionId: "ses_issue834_fallback", threadId: "thread-issue-834-fallback" }
|
||||
]
|
||||
}, aliceLogin.cookie);
|
||||
assert.equal(stored.status, 200);
|
||||
assert.equal(stored.body.conversation.messages[1].text, fallbackText);
|
||||
assert.equal(stored.body.conversation.status, "completed");
|
||||
|
||||
const list = await getJson(port, "/v1/agent/conversations?projectId=prj_device_pod_workbench", aliceLogin.cookie);
|
||||
assert.equal(list.status, 200);
|
||||
const listed = list.body.conversations.find((conversation) => conversation.conversationId === "cnv_issue834_fallback");
|
||||
assert.ok(listed, "expected fallback conversation to be listed");
|
||||
assert.equal(listed.status, "idle");
|
||||
assert.equal(listed.session.status, "idle");
|
||||
assert.equal(listed.lastTraceId, "trc_issue834_final_response");
|
||||
assert.equal(listed.messages[1].text, finalText);
|
||||
assert.equal(listed.messages[1].status, "idle");
|
||||
|
||||
const direct = await getJson(port, "/v1/agent/conversations/cnv_issue834_fallback", aliceLogin.cookie);
|
||||
assert.equal(direct.status, 200);
|
||||
assert.equal(direct.body.conversation.status, "idle");
|
||||
assert.equal(direct.body.conversation.lastTraceId, "trc_issue834_final_response");
|
||||
assert.equal(direct.body.conversation.messages[1].text, finalText);
|
||||
assert.equal(direct.body.conversation.messages[1].status, "idle");
|
||||
|
||||
const workspace = await getJson(port, "/v1/workbench/workspace?projectId=prj_device_pod_workbench", aliceLogin.cookie);
|
||||
assert.equal(workspace.status, 200);
|
||||
const selected = await postJson(port, `/v1/workbench/workspace/${workspace.body.workspace.workspaceId}/select-conversation`, {
|
||||
projectId: "prj_device_pod_workbench",
|
||||
conversationId: "cnv_issue834_fallback",
|
||||
sessionId: "ses_issue834_fallback",
|
||||
updatedByClient: "test-suite"
|
||||
}, aliceLogin.cookie);
|
||||
assert.equal(selected.status, 200);
|
||||
assert.equal(selected.body.workspace.selectedConversation.messages[1].text, finalText);
|
||||
} finally {
|
||||
await new Promise((resolve, reject) => {
|
||||
server.close((error) => (error ? reject(error) : resolve()));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test("access controller restores AgentRun mapping by Code Agent traceId", async () => {
|
||||
const accessController = createAccessController({ now: () => "2026-06-01T00:00:00.000Z" });
|
||||
await accessController.recordAgentSessionOwner({
|
||||
|
||||
@@ -924,7 +924,11 @@ class AccessController {
|
||||
includeArchived: false,
|
||||
now: this.now()
|
||||
}) ?? [];
|
||||
const conversations = conversationsFromAgentSessions(sessions);
|
||||
const conversations = await this.repairVisibleConversationsIfNeeded(
|
||||
auth.actor,
|
||||
conversationsFromAgentSessions(sessions),
|
||||
projectId
|
||||
);
|
||||
return sendJson(response, 200, {
|
||||
ok: true,
|
||||
status: "succeeded",
|
||||
@@ -941,6 +945,8 @@ class AccessController {
|
||||
await this.ensureBootstrap();
|
||||
const auth = await this.authenticate(request, { required: true });
|
||||
if (!auth.ok) return sendJson(response, auth.status, auth);
|
||||
const url = new URL(request.url || "/", "http://localhost");
|
||||
const projectId = textOr(url.searchParams.get("projectId"), "");
|
||||
if (!safeConversationIdLocal(conversationId)) {
|
||||
return sendJson(response, 400, errorPayload("invalid_conversation_id", "conversationId must start with cnv_", 400));
|
||||
}
|
||||
@@ -953,8 +959,9 @@ class AccessController {
|
||||
includeArchived: true,
|
||||
now: this.now()
|
||||
}) ?? [];
|
||||
const conversation = conversationsFromAgentSessions(sessions).find((item) => item.conversationId === conversationId) ?? null;
|
||||
let conversation = conversationsFromAgentSessions(sessions).find((item) => item.conversationId === conversationId) ?? null;
|
||||
if (!conversation) return sendJson(response, 404, errorPayload("agent_conversation_not_found", "Agent conversation is not visible to the current actor", 404));
|
||||
conversation = await this.repairVisibleConversationIfNeeded(auth.actor, conversation, projectId) ?? conversation;
|
||||
return sendJson(response, 200, { ok: true, status: "found", contractVersion: "agent-conversations-v1", actor: publicActor(auth.actor), conversation });
|
||||
}
|
||||
|
||||
@@ -1209,24 +1216,9 @@ class AccessController {
|
||||
const repairTraceId = activeTraceId ? "" : await this.terminalWorkbenchRepairTraceId(workspace, actor, workspaceJson);
|
||||
const traceId = activeTraceId || repairTraceId;
|
||||
if (!traceId) return workspace;
|
||||
const options = {
|
||||
env: codeAgentEnv,
|
||||
fetchImpl: this.fetchImpl,
|
||||
accessController: this,
|
||||
codeAgentChatResults: this.codeAgentChatResults,
|
||||
traceStore: this.traceStore ?? defaultCodeAgentTraceStore
|
||||
};
|
||||
|
||||
try {
|
||||
const cached = this.codeAgentChatResults?.get?.(traceId) ?? null;
|
||||
const persisted = cached ? null : await loadPersistedAgentRunResult(traceId, options);
|
||||
const current = cached ?? persisted ?? null;
|
||||
if (!current || !canActorReadWorkspaceTrace(current, actor)) return workspace;
|
||||
const synced = current?.agentRun?.runId && current.status === "running"
|
||||
? await syncAgentRunChatResult({ traceId, currentResult: current, options, traceStore: options.traceStore })
|
||||
: { result: current };
|
||||
const result = synced.result ?? current;
|
||||
if (!isTerminalCodeAgentResult(result)) return workspace;
|
||||
const result = await this.terminalCodeAgentResultForActor(traceId, actor);
|
||||
if (!result) return workspace;
|
||||
|
||||
const now = this.now();
|
||||
const staleContinuation = isThreadResumeFailedResult(result);
|
||||
@@ -1301,7 +1293,7 @@ class AccessController {
|
||||
try {
|
||||
const traceId = safeTraceIdLocal(workspaceJson?.lastTraceId);
|
||||
if (!traceId || !safeConversationIdLocal(workspace?.selectedConversationId)) return "";
|
||||
const conversation = await this.visibleConversationForActor(actor, workspace.selectedConversationId, workspace.projectId);
|
||||
const conversation = await this.visibleConversationForActor(actor, workspace.selectedConversationId, workspace.projectId, { skipTerminalRepair: true });
|
||||
return conversationNeedsTerminalRepair(conversation, traceId) ? traceId : "";
|
||||
} catch {
|
||||
return "";
|
||||
@@ -1320,7 +1312,7 @@ class AccessController {
|
||||
|
||||
async syncTerminalWorkbenchConversation({ workspace, actor, result, activeTraceId, selectedConversationId, selectedAgentSessionId, now }) {
|
||||
if (!safeConversationIdLocal(selectedConversationId) || !safeAgentSessionId(selectedAgentSessionId)) return null;
|
||||
const existing = await this.visibleConversationForActor(actor, selectedConversationId, workspace.projectId);
|
||||
const existing = await this.visibleConversationForActor(actor, selectedConversationId, workspace.projectId, { skipTerminalRepair: true });
|
||||
const sessionStatus = terminalWorkbenchSessionStatus(result);
|
||||
const threadId = textOr(result.threadId ?? result.session?.threadId ?? result.sessionReuse?.threadId ?? result.providerTrace?.threadId, existing?.threadId ?? null);
|
||||
const messages = mergeTerminalConversationMessages(existing?.messages, result, {
|
||||
@@ -1358,6 +1350,95 @@ class AccessController {
|
||||
});
|
||||
}
|
||||
|
||||
async terminalCodeAgentResultForActor(traceId, actor) {
|
||||
const safeTraceId = safeTraceIdLocal(traceId);
|
||||
const codeAgentEnv = this.codeAgentEnv ?? this.env;
|
||||
if (!safeTraceId || !codeAgentAgentRunAdapterEnabled(codeAgentEnv)) return null;
|
||||
const options = {
|
||||
env: codeAgentEnv,
|
||||
fetchImpl: this.fetchImpl,
|
||||
accessController: this,
|
||||
codeAgentChatResults: this.codeAgentChatResults,
|
||||
traceStore: this.traceStore ?? defaultCodeAgentTraceStore
|
||||
};
|
||||
try {
|
||||
const cached = this.codeAgentChatResults?.get?.(safeTraceId) ?? null;
|
||||
const persisted = cached ? null : await loadPersistedAgentRunResult(safeTraceId, options);
|
||||
const current = cached ?? persisted ?? null;
|
||||
if (!current || !canActorReadWorkspaceTrace(current, actor)) return null;
|
||||
const synced = current?.agentRun?.runId && current.status === "running"
|
||||
? await syncAgentRunChatResult({ traceId: safeTraceId, currentResult: current, options, traceStore: options.traceStore })
|
||||
: { result: current };
|
||||
const result = synced.result ?? current;
|
||||
return isTerminalCodeAgentResult(result) ? result : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async repairVisibleConversationsIfNeeded(actor, conversations = [], projectId = "") {
|
||||
if (!Array.isArray(conversations) || conversations.length === 0) return [];
|
||||
const repaired = [];
|
||||
for (const conversation of conversations) {
|
||||
repaired.push(await this.repairVisibleConversationIfNeeded(actor, conversation, projectId) ?? conversation);
|
||||
}
|
||||
return repaired;
|
||||
}
|
||||
|
||||
async repairVisibleConversationIfNeeded(actor, conversation, projectId = "") {
|
||||
if (textOr(conversation?.status, "").toLowerCase() === "archived") return conversation;
|
||||
const traceId = terminalConversationFallbackRepairTraceId(conversation);
|
||||
if (!traceId) return conversation;
|
||||
const result = await this.terminalCodeAgentResultForActor(traceId, actor);
|
||||
if (!result) return conversation;
|
||||
const sessionId = safeAgentSessionId(
|
||||
result.sessionId
|
||||
?? result.session?.sessionId
|
||||
?? result.sessionReuse?.sessionId
|
||||
?? conversation.sessionId
|
||||
?? conversation.session?.sessionId
|
||||
);
|
||||
const conversationId = safeConversationIdLocal(result.conversationId) ? result.conversationId : conversation.conversationId;
|
||||
if (!safeConversationIdLocal(conversationId) || !sessionId) return conversation;
|
||||
const now = this.now();
|
||||
const threadId = textOr(result.threadId ?? result.session?.threadId ?? result.sessionReuse?.threadId ?? result.providerTrace?.threadId, conversation.threadId ?? null);
|
||||
const messages = mergeTerminalConversationMessages(conversation.messages, result, {
|
||||
traceId,
|
||||
conversationId,
|
||||
sessionId,
|
||||
threadId,
|
||||
now,
|
||||
existingMessageCount: conversation.messageCount ?? conversation.snapshot?.messageCount ?? null,
|
||||
firstUserMessagePreview: conversation.firstUserMessagePreview ?? conversation.snapshot?.firstUserMessagePreview ?? null
|
||||
});
|
||||
const mergedMessages = Array.isArray(messages) ? messages : messages?.messages ?? [];
|
||||
const mergedCount = Array.isArray(messages) ? null : messages?.messageCount ?? null;
|
||||
const mergedPreview = Array.isArray(messages) ? null : messages?.firstUserMessagePreview ?? null;
|
||||
const sessionStatus = terminalWorkbenchSessionStatus(result);
|
||||
const repaired = await this.recordAgentSessionOwner({
|
||||
ownerUserId: actor.id,
|
||||
sessionId,
|
||||
projectId: textOr(conversation.projectId ?? projectId, "prj_device_pod_workbench"),
|
||||
agentId: conversation.agentId ?? "hwlab-code-agent",
|
||||
status: sessionStatus,
|
||||
conversationId,
|
||||
threadId,
|
||||
traceId,
|
||||
session: normalizeConversationSnapshot({
|
||||
...(conversation.snapshot ?? {}),
|
||||
source: "conversation-terminal-repair",
|
||||
sessionStatus,
|
||||
updatedAt: now,
|
||||
lastTraceId: traceId,
|
||||
messages: mergedMessages,
|
||||
messageCount: mergedCount,
|
||||
firstUserMessagePreview: mergedPreview
|
||||
}, actor),
|
||||
now
|
||||
});
|
||||
return conversationsFromAgentSessions(repaired ? [repaired] : []).find((item) => item.conversationId === conversationId) ?? conversation;
|
||||
}
|
||||
|
||||
async visibleConversationForActor(actor, conversationId, projectId = "", options = {}) {
|
||||
if (!safeConversationIdLocal(conversationId)) return null;
|
||||
const sessions = await this.store.listAgentSessionsForUser?.({
|
||||
@@ -1370,7 +1451,9 @@ class AccessController {
|
||||
includeArchived: options.includeArchived === true,
|
||||
now: this.now()
|
||||
}) ?? [];
|
||||
return conversationsFromAgentSessions(sessions).find((item) => item.conversationId === conversationId) ?? null;
|
||||
const conversation = conversationsFromAgentSessions(sessions).find((item) => item.conversationId === conversationId) ?? null;
|
||||
if (!conversation || options.skipTerminalRepair === true) return conversation;
|
||||
return await this.repairVisibleConversationIfNeeded(actor, conversation, projectId) ?? conversation;
|
||||
}
|
||||
|
||||
async nextVisibleConversationForActor(actor, projectId = "", excludedConversationId = "") {
|
||||
@@ -2460,7 +2543,36 @@ function conversationNeedsTerminalRepair(conversation, traceId) {
|
||||
const traceMessage = messages.find((message) => message?.traceId === traceId && message?.role === "agent") ?? null;
|
||||
if (!traceMessage) return false;
|
||||
const messageStatus = textOr(traceMessage.status, "").toLowerCase();
|
||||
return ["", "running", "busy", "pending", "active"].includes(messageStatus);
|
||||
return ["", "running", "busy", "pending", "active"].includes(messageStatus)
|
||||
|| isLegacyFinalResponseFallbackText(traceMessage.text);
|
||||
}
|
||||
function terminalConversationRepairTraceId(conversation) {
|
||||
if (!conversation) return "";
|
||||
const lastTraceId = safeTraceIdLocal(conversation.lastTraceId ?? conversation.session?.lastTraceId ?? conversation.snapshot?.lastTraceId);
|
||||
if (lastTraceId && conversationNeedsTerminalRepair(conversation, lastTraceId)) return lastTraceId;
|
||||
const messages = Array.isArray(conversation.messages) ? conversation.messages : [];
|
||||
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
||||
const message = messages[index];
|
||||
const traceId = safeTraceIdLocal(message?.traceId);
|
||||
if (message?.role === "agent" && traceId && conversationNeedsTerminalRepair(conversation, traceId)) return traceId;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
function terminalConversationFallbackRepairTraceId(conversation) {
|
||||
if (!conversation) return "";
|
||||
const messages = Array.isArray(conversation.messages) ? conversation.messages : [];
|
||||
const lastTraceId = safeTraceIdLocal(conversation.lastTraceId ?? conversation.session?.lastTraceId ?? conversation.snapshot?.lastTraceId);
|
||||
const lastTraceMessage = messages.find((message) => message?.traceId === lastTraceId && message?.role === "agent") ?? null;
|
||||
if (lastTraceId && isLegacyFinalResponseFallbackText(lastTraceMessage?.text)) return lastTraceId;
|
||||
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
||||
const message = messages[index];
|
||||
const traceId = safeTraceIdLocal(message?.traceId);
|
||||
if (message?.role === "agent" && traceId && isLegacyFinalResponseFallbackText(message.text)) return traceId;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
function isLegacyFinalResponseFallbackText(value) {
|
||||
return textOr(value, "").trim() === "Code Agent 仍在处理,可以继续 steer 或等待 trace 完成。";
|
||||
}
|
||||
function mergeTerminalConversationMessages(existingMessages = [], result = {}, context = {}) {
|
||||
const messages = Array.isArray(existingMessages) ? existingMessages.map(redactConversationMessage).filter(Boolean) : [];
|
||||
|
||||
Reference in New Issue
Block a user