fix: unify workbench conversation message persistence (#1281)
This commit is contained in:
@@ -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";
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user