fix: unify workbench conversation message persistence (#1281)

This commit is contained in:
Lyon
2026-06-15 21:28:31 +08:00
committed by GitHub
parent 0cbe2bc89a
commit ce7c2658cf
4 changed files with 129 additions and 25 deletions
+38 -10
View File
@@ -2190,7 +2190,7 @@ function agentSessionContainsTraceId(session, traceId) {
return [snapshot.messages, snapshot.chatMessages].some((messages) => Array.isArray(messages) && messages.some((message) => safeTraceIdLocal(message?.traceId) === id));
}
function normalizeFinalResponseConversationMessages(messages, finalResponse) {
const finalText = textOr(finalResponse?.text, "");
const finalText = conversationText(finalResponse?.text ?? finalResponse?.content ?? finalResponse?.message, 12000);
const traceId = safeTraceIdLocal(finalResponse?.traceId);
if (!Array.isArray(messages) || !finalText) return messages;
return messages.map((message) => {
@@ -2238,11 +2238,13 @@ function dedupeConversationMessages(messagesSource = []) {
return merged.slice(-50);
}
function mergeConversationMessage(existing = {}, next = {}) {
const nextText = conversationText(next.text ?? next.content ?? next.message, 12000);
const existingText = conversationText(existing.text ?? existing.content ?? existing.message, 12000);
return pruneEmpty({
...existing,
...next,
title: textOr(next.title, existing.title),
text: textOr(next.text, existing.text),
text: nextText || existingText,
runnerTrace: next.runnerTrace ?? existing.runnerTrace,
updatedAt: textOr(next.updatedAt, existing.updatedAt),
createdAt: textOr(next.createdAt, existing.createdAt)
@@ -2386,7 +2388,7 @@ function firstUserPreviewFromMessages(messagesSource) {
for (const message of messagesSource) {
if (!message || typeof message !== "object") continue;
if (String(message.role ?? "").toLowerCase() !== "user") continue;
const text = textOr(message.text ?? message.title ?? message.content, "");
const text = conversationText(message.text ?? message.content ?? message.message ?? message.title, 240);
if (text) return boundedText(text, 240);
}
return null;
@@ -2465,9 +2467,9 @@ function mergeTerminalConversationMessages(existingMessages = [], result = {}, c
const baseMessages = dedupeConversationMessages(existingMessages);
const traceId = safeTraceIdLocal(result.traceId ?? context.traceId);
const terminalStatus = terminalWorkbenchSessionStatus(result);
const assistantText = textOr(result.reply?.content ?? result.assistantText ?? result.message?.content ?? result.error?.message ?? result.userMessage, "");
const assistantText = firstConversationText([result.reply?.content, result.assistantText, result.message?.content, result.error?.message, result.userMessage], 12000);
const now = textOr(context.now, new Date().toISOString());
const firstUserPreview = firstUserPreviewFromMessages(baseMessages) ?? textOr(context.firstUserMessagePreview, null);
const firstUserPreview = firstUserPreviewFromMessages(baseMessages) ?? (conversationText(context.firstUserMessagePreview, 240) || null);
const messages = dedupeConversationMessages(ensureTerminalUserMessage(baseMessages, { ...context, traceId, now, firstUserPreview }));
const runnerTrace = terminalRunnerTraceForConversationMessage(result, { ...context, traceId, terminalStatus });
const next = messages.map((message) => {
@@ -2512,7 +2514,7 @@ function mergeTerminalConversationMessages(existingMessages = [], result = {}, c
}
function ensureTerminalUserMessage(messages = [], context = {}) {
if (messages.some((message) => message?.role === "user")) return messages;
const userText = textOr(context.firstUserPreview, "");
const userText = conversationText(context.firstUserPreview, 12000);
if (!userText) return messages;
const traceId = safeTraceIdLocal(context.traceId);
const userMessage = redactConversationMessage({
@@ -2552,11 +2554,12 @@ function terminalRunnerTraceForConversationMessage(result = {}, context = {}) {
function redactConversationMessage(message) {
if (!message || typeof message !== "object") return null;
const runnerTrace = message.runnerTrace && typeof message.runnerTrace === "object" ? message.runnerTrace : null;
const text = conversationText(message.text ?? message.content ?? message.message, 12000);
return pruneEmpty({
id: textOr(message.id, ""),
role: textOr(message.role, ""),
title: textOr(message.title, ""),
text: boundedText(message.text, 12000),
text,
status: textOr(message.status, ""),
traceId: textOr(message.traceId, ""),
conversationId: textOr(message.conversationId, ""),
@@ -2588,8 +2591,8 @@ function redactTraceFinalResponse(value) {
role: textOr(response.role, ""),
status: textOr(response.status, ""),
messageId: textOr(response.messageId, ""),
text: boundedText(response.text ?? response.content ?? response.message, 12000),
textPreview: boundedText(response.textPreview, 1200),
text: conversationText(response.text ?? response.content ?? response.message, 12000),
textPreview: conversationText(response.textPreview, 1200),
textChars: numberOrNull(response.textChars)
});
}
@@ -2677,7 +2680,7 @@ function publicAgentConversation(session) {
session: pruneEmpty({ sessionId: session.id, threadId: session.threadId, status }),
messages: displayMessages,
messageCount: displayMessages.length,
firstUserMessagePreview: textOr(snapshot.firstUserMessagePreview, null),
firstUserMessagePreview: conversationText(snapshot.firstUserMessagePreview, 240) || null,
snapshot: pruneEmpty({
sessionStatus: snapshot.sessionStatus,
source: snapshot.source,
@@ -2724,6 +2727,31 @@ function boundedText(value, maxBytes) {
const buffer = Buffer.from(text, "utf8");
return buffer.length > maxBytes ? buffer.subarray(0, maxBytes).toString("utf8") : text;
}
function firstConversationText(values = [], maxBytes = 12000) {
for (const value of values) {
const text = conversationText(value, maxBytes);
if (text) return text;
}
return "";
}
function conversationText(value, maxBytes = 12000) {
const extracted = conversationTextRaw(value).replace(/\s+/gu, " ").trim();
if (!extracted || extracted === "[object Object]") return "";
return boundedText(extracted, maxBytes);
}
function conversationTextRaw(value) {
if (value === undefined || value === null) return "";
if (typeof value === "string") return value;
if (typeof value === "number" || typeof value === "boolean") return String(value);
if (Array.isArray(value)) return value.map(conversationTextRaw).filter(Boolean).join("\n");
if (typeof value === "object") {
for (const key of ["text", "content", "message", "prompt", "value", "summary", "preview", "title"]) {
const text = conversationTextRaw(value[key]);
if (text) return text;
}
}
return "";
}
function pruneEmpty(value) { return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined && item !== "" && item !== null)); }
function safeReturnTo(value) {
const text = textOr(value, "/workbench").trim() || "/workbench";
+24 -4
View File
@@ -924,7 +924,7 @@ function agentRunPromptTextFromMessages(messages = [], traceId = null) {
}
function agentRunPromptMetadataFromText(value, traceId = null, source = "hwlab-code-agent-request", options = {}) {
const promptText = String(value ?? "").trim();
const promptText = promptTextValue(value);
if (!promptText) return null;
return agentRunPromptMetadataFromParts({
traceId,
@@ -991,11 +991,31 @@ function agentRunPromptId(traceId = null, digest = null) {
}
function clippedPromptText(value, limit) {
const promptText = String(value ?? "").trim();
const promptText = promptTextValue(value);
if (!promptText) return undefined;
return promptText.length > limit ? `${promptText.slice(0, limit)}...` : promptText;
}
function promptTextValue(value) {
const extracted = promptTextRaw(value).replace(/\s+/gu, " ").trim();
if (!extracted || extracted === "[object Object]") return "";
return extracted;
}
function promptTextRaw(value) {
if (value === undefined || value === null) return "";
if (typeof value === "string") return value;
if (typeof value === "number" || typeof value === "boolean") return String(value);
if (Array.isArray(value)) return value.map(promptTextRaw).filter(Boolean).join("\n");
if (typeof value === "object") {
for (const key of ["text", "content", "message", "prompt", "value", "summary", "preview", "title"]) {
const text = promptTextRaw(value[key]);
if (text) return text;
}
}
return "";
}
function sha256Text(value) {
return createHash("sha256").update(String(value)).digest("hex");
}
@@ -1009,7 +1029,7 @@ function adapterError(code, message, details = {}) {
}
function buildAgentRunCommandInput({ params, traceId, backendProfile, sessionId }) {
const prompt = String(params.message ?? params.prompt ?? "").trim();
const prompt = promptTextValue(params.message ?? params.prompt ?? "");
const promptEvidence = agentRunPromptMetadataFromText(prompt, traceId, "agentrun-command-payload", { fullText: true });
const threadId = safeOpaqueId(params.threadId);
return {
@@ -1036,7 +1056,7 @@ function buildAgentRunCommandInput({ params, traceId, backendProfile, sessionId
}
function buildAgentRunSteerCommandInput({ params, traceId, steerTraceId, mapped }) {
const prompt = String(params.message ?? params.prompt ?? params.text ?? "").trim();
const prompt = promptTextValue(params.message ?? params.prompt ?? params.text ?? "");
const promptEvidence = agentRunPromptMetadataFromText(prompt, steerTraceId, "agentrun-steer-command-payload", { fullText: true });
return {
type: "steer",
+21 -1
View File
@@ -2037,11 +2037,31 @@ function codeAgentConversationMessagesEvidence(payload = {}, params = {}, traceI
}
function boundedConversationMessageText(value, limit = 12000) {
const text = textValue(value);
const text = conversationText(value);
if (!text) return "";
return text.length > limit ? text.slice(0, limit) : text;
}
function conversationText(value) {
const extracted = conversationTextRaw(value).replace(/\s+/gu, " ").trim();
if (!extracted || extracted === "[object Object]") return "";
return extracted;
}
function conversationTextRaw(value) {
if (value === undefined || value === null) return "";
if (typeof value === "string") return value;
if (typeof value === "number" || typeof value === "boolean") return String(value);
if (Array.isArray(value)) return value.map(conversationTextRaw).filter(Boolean).join("\n");
if (typeof value === "object") {
for (const key of ["text", "content", "message", "prompt", "value", "summary", "preview", "title"]) {
const text = conversationTextRaw(value[key]);
if (text) return text;
}
}
return "";
}
function codeAgentMessageTraceSuffix(traceId) {
const text = safeTraceId(traceId) || `local_${Date.now().toString(36)}`;
return text.replace(/^trc_/u, "").replace(/[^A-Za-z0-9_.:-]/gu, "_").slice(0, 48) || "trace";
+46 -10
View File
@@ -2262,12 +2262,12 @@ async function agentSend(context: any) {
if (!isHttpSuccess(accepted)) {
return responsePayload("client.agent.send", accepted, context, { route: route("POST", "/v1/agent/chat"), traceId, conversationId, continuation, replay: replay.summary });
}
await saveAcceptedAgentWorkspaceState(context, accepted.body, { traceId, conversationId, sessionId, threadId, providerProfile: requestBody.providerProfile });
await saveAcceptedAgentWorkspaceState(context, accepted.body, { traceId, conversationId, sessionId, threadId, providerProfile: requestBody.providerProfile, message });
if (parsed.wait !== true) {
return responsePayload("client.agent.send", accepted, context, { route: route("POST", "/v1/agent/chat"), traceId, conversationId, continuation, replay: replay.summary, workspace: workspaceSummaryFromSession(await loadStoredState({ parsed, env: context.env, cwd: context.cwd ?? process.cwd() })), waited: false, waitPolicy: agentSendWaitPolicy(traceId) });
}
const result = await pollAgentResult(context, traceId, accepted.body);
const completedWorkspace = await saveCompletedAgentWorkspaceState(context, result.response?.body, { traceId, conversationId, sessionId, threadId, providerProfile: requestBody.providerProfile });
const completedWorkspace = await saveCompletedAgentWorkspaceState(context, result.response?.body, { traceId, conversationId, sessionId, threadId, providerProfile: requestBody.providerProfile, message });
return ok("client.agent.send", {
status: result.final ? "succeeded" : "timeout",
route: route("POST", "/v1/agent/chat"),
@@ -3351,18 +3351,40 @@ function agentSessionSummary(session: any, fallback: any = {}) {
}
function compactAgentMessagesForWorkspace(resultBody: any, fallback: any) {
return [{
const traceId = text(resultBody?.traceId) || text(fallback.traceId);
const conversationId = text(resultBody?.conversationId) || text(fallback.conversationId);
const sessionId = text(resultBody?.sessionId) || text(fallback.sessionId);
const threadId = text(resultBody?.threadId ?? resultBody?.session?.threadId ?? resultBody?.providerTrace?.threadId ?? fallback.threadId);
const now = new Date().toISOString();
const userText = messageTextValue(fallback.message ?? resultBody?.userMessage ?? resultBody?.prompt);
const messages = [];
if (userText) {
messages.push({
id: traceId ? `msg_${traceId.slice(4)}_user` : makeId("msg_user"),
role: "user",
title: "用户",
text: userText,
status: "sent",
traceId,
conversationId,
sessionId,
threadId,
updatedAt: now
});
}
messages.push({
id: makeId("msg"),
role: "agent",
title: "Code Agent result",
text: assistantText(resultBody) || text(resultBody?.error?.message) || text(resultBody?.status),
text: messageTextValue(assistantText(resultBody) || resultBody?.error?.message || resultBody?.status),
status: text(resultBody?.status) || "unknown",
traceId: text(resultBody?.traceId) || text(fallback.traceId),
conversationId: text(resultBody?.conversationId) || text(fallback.conversationId),
sessionId: text(resultBody?.sessionId) || text(fallback.sessionId),
threadId: text(resultBody?.threadId ?? resultBody?.session?.threadId ?? resultBody?.providerTrace?.threadId ?? fallback.threadId),
updatedAt: new Date().toISOString()
}];
traceId,
conversationId,
sessionId,
threadId,
updatedAt: now
});
return messages;
}
function compactProbe(method: string, pathName: string, response: any) {
@@ -4183,6 +4205,20 @@ function cliError(code: string, message: string, details: Record<string, unknown
function route(method: string, pathName: string) { return { method, path: pathName }; }
function isHttpSuccess(response: any) { return response.status >= 200 && response.status < 300; }
function text(value: unknown) { return String(value ?? "").trim(); }
function messageTextValue(value: unknown): string {
if (value === undefined || value === null) return "";
if (typeof value === "string") return value.trim();
if (typeof value === "number" || typeof value === "boolean") return String(value).trim();
if (Array.isArray(value)) return value.map(messageTextValue).filter(Boolean).join("\n").trim();
if (typeof value === "object") {
const record = value as Record<string, unknown>;
for (const key of ["text", "content", "message", "prompt", "value", "summary", "preview", "title"]) {
const extracted = messageTextValue(record[key]);
if (extracted) return extracted;
}
}
return "";
}
function textOrNull(value: unknown) { const result = text(value); return result || null; }
function requiredText(value: unknown, field: string) { const result = text(value); if (!result) throw cliError("missing_required_value", `${field} is required`, { field }); return result; }
function numberOption(value: unknown) { const parsed = Number.parseInt(String(value ?? ""), 10); return Number.isFinite(parsed) ? parsed : undefined; }