|
|
|
@@ -5,12 +5,6 @@
|
|
|
|
|
import { createHash, createPublicKey, randomBytes, randomUUID, verify as verifySignature } from "node:crypto";
|
|
|
|
|
|
|
|
|
|
import { CLOUD_API_SERVICE_ID } from "../audit/index.mjs";
|
|
|
|
|
import {
|
|
|
|
|
codeAgentAgentRunAdapterEnabled,
|
|
|
|
|
loadPersistedAgentRunResult,
|
|
|
|
|
syncAgentRunChatResult
|
|
|
|
|
} from "./code-agent-agentrun-adapter.ts";
|
|
|
|
|
import { defaultCodeAgentTraceStore } from "./code-agent-trace-store.ts";
|
|
|
|
|
import {
|
|
|
|
|
HWLAB_TOOL_IDS,
|
|
|
|
|
createOpenFgaAuthorizer,
|
|
|
|
@@ -1051,12 +1045,7 @@ class AccessController {
|
|
|
|
|
includeArchived: false,
|
|
|
|
|
now: this.now()
|
|
|
|
|
}) ?? [];
|
|
|
|
|
let conversations = await this.repairVisibleConversationsIfNeeded(
|
|
|
|
|
auth.actor,
|
|
|
|
|
conversationsFromAgentSessions(sessions),
|
|
|
|
|
projectId,
|
|
|
|
|
{ deadlineMs: 1500 }
|
|
|
|
|
);
|
|
|
|
|
let conversations = conversationsFromAgentSessions(sessions);
|
|
|
|
|
conversations = includeConversation(conversations, includeConversationId ? await this.visibleConversationForActor(auth.actor, includeConversationId, projectId) : null);
|
|
|
|
|
return sendJson(response, 200, {
|
|
|
|
|
ok: true,
|
|
|
|
@@ -1088,9 +1077,8 @@ class AccessController {
|
|
|
|
|
includeArchived: true,
|
|
|
|
|
now: this.now()
|
|
|
|
|
}) ?? [];
|
|
|
|
|
let conversation = conversationsFromAgentSessions(sessions).find((item) => item.conversationId === conversationId) ?? null;
|
|
|
|
|
const 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,7 +1197,7 @@ class AccessController {
|
|
|
|
|
}
|
|
|
|
|
const selectedConversationId = textOr(body.selectedConversationId ?? body.conversationId, current.selectedConversationId ?? "");
|
|
|
|
|
if (selectedConversationId) {
|
|
|
|
|
const visible = await this.visibleConversationForActor(auth.actor, selectedConversationId, body.projectId ?? current.projectId, { skipTerminalRepair: true });
|
|
|
|
|
const visible = await this.visibleConversationForActor(auth.actor, selectedConversationId, body.projectId ?? current.projectId);
|
|
|
|
|
if (!visible) return sendJson(response, 403, errorPayload("workspace_conversation_forbidden", "Selected conversation is not visible to the current actor", 403));
|
|
|
|
|
}
|
|
|
|
|
const workspace = await this.store.updateWorkspace?.({
|
|
|
|
@@ -1241,7 +1229,7 @@ class AccessController {
|
|
|
|
|
const body = await jsonBody(request);
|
|
|
|
|
const conversationId = textOr(body.conversationId, "") || `cnv_${randomUUID()}`;
|
|
|
|
|
if (!safeConversationIdLocal(conversationId)) return sendJson(response, 400, errorPayload("invalid_conversation_id", "conversationId must start with cnv_", 400));
|
|
|
|
|
const existing = await this.visibleConversationForActor(auth.actor, conversationId, body.projectId ?? current.projectId, { skipTerminalRepair: true });
|
|
|
|
|
const existing = await this.visibleConversationForActor(auth.actor, conversationId, body.projectId ?? current.projectId);
|
|
|
|
|
if (!existing && body.create !== true) return sendJson(response, 404, errorPayload("agent_conversation_not_found", "Agent conversation is not visible to the current actor", 404));
|
|
|
|
|
const sessionId = safeAgentSessionId(body.sessionId ?? existing?.sessionId) || `ses_${conversationId.slice(4)}`;
|
|
|
|
|
if (!existing) {
|
|
|
|
@@ -1329,251 +1317,13 @@ class AccessController {
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async syncTerminalWorkbenchWorkspace(workspace, actor) {
|
|
|
|
|
const activeTraceId = safeTraceIdLocal(workspace?.activeTraceId);
|
|
|
|
|
const codeAgentEnv = this.codeAgentEnv ?? this.env;
|
|
|
|
|
if (!workspace || !codeAgentAgentRunAdapterEnabled(codeAgentEnv)) return workspace;
|
|
|
|
|
const workspaceJson = normalizeObject(workspace.workspace);
|
|
|
|
|
const repairTraceId = activeTraceId ? "" : await this.terminalWorkbenchRepairTraceId(workspace, actor, workspaceJson);
|
|
|
|
|
const traceId = activeTraceId || repairTraceId;
|
|
|
|
|
if (!traceId) return workspace;
|
|
|
|
|
try {
|
|
|
|
|
const result = await this.terminalCodeAgentResultForActor(traceId, actor);
|
|
|
|
|
if (!result) return workspace;
|
|
|
|
|
|
|
|
|
|
const now = this.now();
|
|
|
|
|
const staleContinuation = isThreadResumeFailedResult(result);
|
|
|
|
|
const resultConversationId = safeConversationIdLocal(result.conversationId) ? result.conversationId : workspace.selectedConversationId;
|
|
|
|
|
const resultAgentSessionId = safeAgentSessionId(result.sessionId ?? result.session?.sessionId ?? result.sessionReuse?.sessionId) || workspace.selectedAgentSessionId;
|
|
|
|
|
const currentSelectedConversationId = safeConversationIdLocal(workspace.selectedConversationId) ? workspace.selectedConversationId : null;
|
|
|
|
|
const currentSelectedAgentSessionId = safeAgentSessionId(workspace.selectedAgentSessionId) || null;
|
|
|
|
|
const resultMatchesCurrentSelection = Boolean(
|
|
|
|
|
resultConversationId &&
|
|
|
|
|
(!currentSelectedConversationId || currentSelectedConversationId === resultConversationId) &&
|
|
|
|
|
(!currentSelectedAgentSessionId || currentSelectedAgentSessionId === resultAgentSessionId)
|
|
|
|
|
);
|
|
|
|
|
const preserveCurrentSelection = Boolean(currentSelectedConversationId && !resultMatchesCurrentSelection);
|
|
|
|
|
const selectedConversationId = staleContinuation
|
|
|
|
|
? (preserveCurrentSelection ? currentSelectedConversationId : null)
|
|
|
|
|
: (preserveCurrentSelection ? currentSelectedConversationId : resultConversationId);
|
|
|
|
|
const selectedAgentSessionId = staleContinuation
|
|
|
|
|
? (preserveCurrentSelection ? currentSelectedAgentSessionId : null)
|
|
|
|
|
: (preserveCurrentSelection ? currentSelectedAgentSessionId : resultAgentSessionId);
|
|
|
|
|
const nextSessionStatus = preserveCurrentSelection ? workspaceJson.sessionStatus : terminalWorkbenchSessionStatus(result);
|
|
|
|
|
await this.syncTerminalWorkbenchConversation({
|
|
|
|
|
workspace,
|
|
|
|
|
actor,
|
|
|
|
|
result,
|
|
|
|
|
activeTraceId: traceId,
|
|
|
|
|
selectedConversationId: resultConversationId,
|
|
|
|
|
selectedAgentSessionId: resultAgentSessionId,
|
|
|
|
|
now
|
|
|
|
|
});
|
|
|
|
|
const staleThreadId = threadResumeFailureThreadId(result);
|
|
|
|
|
const updated = await this.store.updateWorkspace?.({
|
|
|
|
|
workspaceId: workspace.id,
|
|
|
|
|
ownerUserId: workspace.ownerUserId,
|
|
|
|
|
actorRole: actor?.role ?? "user",
|
|
|
|
|
selectedConversationId,
|
|
|
|
|
selectedAgentSessionId,
|
|
|
|
|
activeTraceId: null,
|
|
|
|
|
providerProfile: workspace.providerProfile,
|
|
|
|
|
patch: {
|
|
|
|
|
...workspaceJson,
|
|
|
|
|
selectedConversationId,
|
|
|
|
|
selectedAgentSessionId,
|
|
|
|
|
activeTraceId: null,
|
|
|
|
|
sessionStatus: nextSessionStatus,
|
|
|
|
|
lastTraceId: traceId,
|
|
|
|
|
syncedTraceStatus: result.status ?? result.agentRun?.terminalStatus ?? null,
|
|
|
|
|
...(staleContinuation ? {
|
|
|
|
|
staleContinuationCleared: true,
|
|
|
|
|
staleContinuationReason: "thread-resume-failed",
|
|
|
|
|
staleConversationId: resultConversationId,
|
|
|
|
|
staleAgentSessionId: resultAgentSessionId,
|
|
|
|
|
staleThreadId,
|
|
|
|
|
recoveryAction: "new-session-on-next-turn"
|
|
|
|
|
} : {}),
|
|
|
|
|
updatedAt: now,
|
|
|
|
|
source: "workbench-status-sync",
|
|
|
|
|
valuesRedacted: true,
|
|
|
|
|
secretMaterialStored: false
|
|
|
|
|
},
|
|
|
|
|
updatedBySessionId: workspace.updatedBySessionId,
|
|
|
|
|
updatedByClient: "workbench-status-sync",
|
|
|
|
|
now
|
|
|
|
|
});
|
|
|
|
|
return updated ?? workspace;
|
|
|
|
|
} catch {
|
|
|
|
|
return workspace;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async terminalWorkbenchRepairTraceId(workspace, actor, workspaceJson) {
|
|
|
|
|
try {
|
|
|
|
|
const traceId = safeTraceIdLocal(workspaceJson?.lastTraceId);
|
|
|
|
|
if (!traceId || !safeConversationIdLocal(workspace?.selectedConversationId)) return "";
|
|
|
|
|
const conversation = await this.visibleConversationForActor(actor, workspace.selectedConversationId, workspace.projectId, { skipTerminalRepair: true });
|
|
|
|
|
return conversationNeedsTerminalRepair(conversation, traceId) ? traceId : "";
|
|
|
|
|
} catch {
|
|
|
|
|
return "";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async publicWorkbenchWorkspace(workspace, actor) {
|
|
|
|
|
const conversation = workspace?.selectedConversationId
|
|
|
|
|
? await this.visibleConversationForActor(actor, workspace.selectedConversationId, workspace.projectId, { skipTerminalRepair: true })
|
|
|
|
|
? await this.visibleConversationForActor(actor, workspace.selectedConversationId, workspace.projectId)
|
|
|
|
|
: null;
|
|
|
|
|
return publicWorkbenchWorkspace(workspace, { conversation });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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, { skipTerminalRepair: true });
|
|
|
|
|
const sessionStatus = terminalWorkbenchSessionStatus(result);
|
|
|
|
|
const threadId = textOr(result.threadId ?? result.session?.threadId ?? result.sessionReuse?.threadId ?? result.providerTrace?.threadId, existing?.threadId ?? null);
|
|
|
|
|
const workspaceJson = normalizeObject(workspace?.workspace);
|
|
|
|
|
const workspaceMessages = workspace?.selectedConversationId === selectedConversationId && workspace?.selectedAgentSessionId === selectedAgentSessionId && Array.isArray(workspaceJson.messages)
|
|
|
|
|
? workspaceJson.messages
|
|
|
|
|
: [];
|
|
|
|
|
const existingMessages = Array.isArray(existing?.messages) && existing.messages.length > 0 ? existing.messages : workspaceMessages;
|
|
|
|
|
const messages = mergeTerminalConversationMessages(existingMessages, result, {
|
|
|
|
|
traceId: activeTraceId,
|
|
|
|
|
conversationId: selectedConversationId,
|
|
|
|
|
sessionId: selectedAgentSessionId,
|
|
|
|
|
threadId,
|
|
|
|
|
now,
|
|
|
|
|
existingMessageCount: existing?.messageCount ?? existing?.snapshot?.messageCount ?? (workspaceMessages.length || null),
|
|
|
|
|
firstUserMessagePreview: existing?.firstUserMessagePreview ?? existing?.snapshot?.firstUserMessagePreview ?? firstUserPreviewFromMessages(workspaceMessages) ?? 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;
|
|
|
|
|
return await this.recordAgentSessionOwner({
|
|
|
|
|
ownerUserId: actor.id,
|
|
|
|
|
sessionId: selectedAgentSessionId,
|
|
|
|
|
projectId: workspace.projectId,
|
|
|
|
|
agentId: existing?.agentId ?? "hwlab-code-agent",
|
|
|
|
|
status: sessionStatus,
|
|
|
|
|
conversationId: selectedConversationId,
|
|
|
|
|
threadId,
|
|
|
|
|
traceId: activeTraceId,
|
|
|
|
|
session: normalizeConversationSnapshot({
|
|
|
|
|
...(existing?.snapshot ?? {}),
|
|
|
|
|
source: "workbench-status-sync",
|
|
|
|
|
sessionStatus,
|
|
|
|
|
updatedAt: now,
|
|
|
|
|
lastTraceId: activeTraceId,
|
|
|
|
|
messages: mergedMessages,
|
|
|
|
|
messageCount: mergedCount,
|
|
|
|
|
firstUserMessagePreview: mergedPreview
|
|
|
|
|
}, actor),
|
|
|
|
|
now
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
? 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 = "", options = {}) {
|
|
|
|
|
if (!Array.isArray(conversations) || conversations.length === 0) return [];
|
|
|
|
|
const deadlineMs = positiveInteger(options.deadlineMs, 0);
|
|
|
|
|
const deadline = deadlineMs > 0 ? Date.now() + deadlineMs : 0;
|
|
|
|
|
const repaired = [];
|
|
|
|
|
for (const conversation of conversations) {
|
|
|
|
|
if (!terminalConversationRepairTraceId(conversation)) {
|
|
|
|
|
repaired.push(conversation);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
const remainingMs = deadline > 0 ? deadline - Date.now() : 0;
|
|
|
|
|
if (deadline > 0 && remainingMs <= 0) {
|
|
|
|
|
repaired.push(conversation);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
const repair = this.repairVisibleConversationIfNeeded(actor, conversation, projectId);
|
|
|
|
|
const next = deadline > 0 ? await resolveBeforeDeadline(repair, remainingMs) : await repair;
|
|
|
|
|
repaired.push(next ?? conversation);
|
|
|
|
|
}
|
|
|
|
|
return repaired;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async repairVisibleConversationIfNeeded(actor, conversation, projectId = "") {
|
|
|
|
|
if (textOr(conversation?.status, "").toLowerCase() === "archived") return conversation;
|
|
|
|
|
const traceId = terminalConversationRepairTraceId(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_hwpod_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?.({
|
|
|
|
@@ -1586,9 +1336,7 @@ class AccessController {
|
|
|
|
|
includeArchived: options.includeArchived === true,
|
|
|
|
|
now: this.now()
|
|
|
|
|
}) ?? [];
|
|
|
|
|
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;
|
|
|
|
|
return conversationsFromAgentSessions(sessions).find((item) => item.conversationId === conversationId) ?? null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async nextVisibleConversationForActor(actor, projectId = "", excludedConversationId = "") {
|
|
|
|
@@ -2451,130 +2199,6 @@ function numberOrNull(value) {
|
|
|
|
|
const parsed = Number(value);
|
|
|
|
|
return Number.isFinite(parsed) ? parsed : null;
|
|
|
|
|
}
|
|
|
|
|
function isTerminalCodeAgentResult(result = {}) {
|
|
|
|
|
const status = textOr(result.status, "").toLowerCase();
|
|
|
|
|
const terminalStatus = textOr(result.agentRun?.terminalStatus, "").toLowerCase();
|
|
|
|
|
return [status, terminalStatus].some((value) => ["completed", "failed", "blocked", "canceled", "cancelled", "timeout", "error"].includes(value));
|
|
|
|
|
}
|
|
|
|
|
function isThreadResumeFailedResult(result = {}) {
|
|
|
|
|
const values = [
|
|
|
|
|
result.error?.code,
|
|
|
|
|
result.error?.category,
|
|
|
|
|
result.blocker?.code,
|
|
|
|
|
result.blocker?.category,
|
|
|
|
|
result.providerTrace?.failureKind,
|
|
|
|
|
result.agentRun?.providerTrace?.failureKind,
|
|
|
|
|
result.agentRun?.failureKind
|
|
|
|
|
].map((value) => textOr(value, "").toLowerCase().replace(/_/gu, "-"));
|
|
|
|
|
return values.some((value) => value === "thread-resume-failed" || value === "session-store-evicted")
|
|
|
|
|
|| /session stor(?:e|age).*evicted|pvc-backed session|no rollout found for thread id|thread\/resume failed/iu.test(String(result.error?.message ?? result.blocker?.message ?? result.providerTrace?.failureMessage ?? result.agentRun?.providerTrace?.failureMessage ?? ""));
|
|
|
|
|
}
|
|
|
|
|
function threadResumeFailureThreadId(result = {}) {
|
|
|
|
|
return boundedText(textOr(
|
|
|
|
|
result.providerTrace?.threadId
|
|
|
|
|
?? result.agentRun?.providerTrace?.threadId
|
|
|
|
|
?? result.session?.threadId
|
|
|
|
|
?? result.sessionReuse?.threadId
|
|
|
|
|
?? result.threadId,
|
|
|
|
|
""
|
|
|
|
|
), 240) || null;
|
|
|
|
|
}
|
|
|
|
|
function terminalWorkbenchSessionStatus(result = {}) {
|
|
|
|
|
if (isThreadResumeFailedResult(result)) return "thread-resume-failed";
|
|
|
|
|
const sessionStatus = textOr(result.session?.status ?? result.sessionSummary?.status ?? result.sessionLifecycleStatus ?? result.runnerTrace?.sessionStatus, "").toLowerCase();
|
|
|
|
|
if (sessionStatus && sessionStatus !== "running" && sessionStatus !== "busy" && sessionStatus !== "pending") return sessionStatus === "cancelled" ? "canceled" : sessionStatus;
|
|
|
|
|
const status = textOr(result.status ?? result.agentRun?.terminalStatus, "").toLowerCase();
|
|
|
|
|
if (status === "completed") return "idle";
|
|
|
|
|
if (status === "cancelled") return "canceled";
|
|
|
|
|
return status || "idle";
|
|
|
|
|
}
|
|
|
|
|
function canActorReadWorkspaceTrace(result = {}, actor = null) {
|
|
|
|
|
const ownerUserId = textOr(result.ownerUserId, "");
|
|
|
|
|
if (!ownerUserId || actor?.role === "admin") return true;
|
|
|
|
|
return ownerUserId === actor?.id;
|
|
|
|
|
}
|
|
|
|
|
function conversationNeedsTerminalRepair(conversation, traceId) {
|
|
|
|
|
if (!conversation || !safeTraceIdLocal(traceId)) return false;
|
|
|
|
|
const status = textOr(conversation.status, "").toLowerCase();
|
|
|
|
|
if (["running", "busy", "pending", "active"].includes(status)) return true;
|
|
|
|
|
const messages = Array.isArray(conversation.messages) ? conversation.messages : [];
|
|
|
|
|
const traceMessage = messages.find((message) => message?.traceId === traceId && message?.role === "agent") ?? null;
|
|
|
|
|
const latestAgent = latestAgentConversationMessage(messages);
|
|
|
|
|
if (!traceMessage) return Boolean(latestAgent && traceId === safeTraceIdLocal(conversation.lastTraceId ?? conversation.session?.lastTraceId ?? conversation.snapshot?.lastTraceId));
|
|
|
|
|
if (latestAgent?.traceId && latestAgent.traceId !== traceId && traceId === safeTraceIdLocal(conversation.lastTraceId ?? conversation.session?.lastTraceId ?? conversation.snapshot?.lastTraceId)) return true;
|
|
|
|
|
if (markdownTextLooksFlattened(traceMessage.text ?? traceMessage.content ?? traceMessage.message) && traceId === safeTraceIdLocal(conversation.lastTraceId ?? conversation.session?.lastTraceId ?? conversation.snapshot?.lastTraceId)) return true;
|
|
|
|
|
const messageStatus = textOr(traceMessage.status, "").toLowerCase();
|
|
|
|
|
return ["", "running", "busy", "pending", "active"].includes(messageStatus);
|
|
|
|
|
}
|
|
|
|
|
function markdownTextLooksFlattened(value) {
|
|
|
|
|
const text = conversationTextRaw(value).replace(/\r\n?/gu, "\n").trim();
|
|
|
|
|
if (!text || text.includes("\n")) return false;
|
|
|
|
|
return /\|\s*:?-{3,}:?\s*\|/u.test(text) || /```[\s\S]+```/u.test(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 mergeTerminalConversationMessages(existingMessages = [], result = {}, context = {}) {
|
|
|
|
|
const baseMessages = dedupeConversationMessages(existingMessages);
|
|
|
|
|
const traceId = safeTraceIdLocal(result.traceId ?? context.traceId);
|
|
|
|
|
const terminalStatus = terminalWorkbenchSessionStatus(result);
|
|
|
|
|
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) ?? (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) => {
|
|
|
|
|
if (traceId && message.traceId === traceId && message.role === "agent") {
|
|
|
|
|
return redactConversationMessage({
|
|
|
|
|
...message,
|
|
|
|
|
text: assistantText || message.text,
|
|
|
|
|
status: terminalStatus,
|
|
|
|
|
conversationId: context.conversationId ?? message.conversationId,
|
|
|
|
|
sessionId: context.sessionId ?? message.sessionId,
|
|
|
|
|
threadId: context.threadId ?? message.threadId,
|
|
|
|
|
runnerTrace: runnerTrace ?? message.runnerTrace,
|
|
|
|
|
updatedAt: now
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return message;
|
|
|
|
|
}).filter(Boolean);
|
|
|
|
|
let addedAgentMessage = false;
|
|
|
|
|
if (traceId && !next.some((message) => message.traceId === traceId && message.role === "agent")) {
|
|
|
|
|
const messageId = result.reply?.messageId ?? result.messageId ?? `msg_${traceId.slice(4)}_agent`;
|
|
|
|
|
next.push(redactConversationMessage({
|
|
|
|
|
id: messageId,
|
|
|
|
|
messageId,
|
|
|
|
|
role: "agent",
|
|
|
|
|
title: "Code Agent result",
|
|
|
|
|
text: assistantText,
|
|
|
|
|
status: terminalStatus,
|
|
|
|
|
traceId,
|
|
|
|
|
turnId: traceId,
|
|
|
|
|
conversationId: context.conversationId,
|
|
|
|
|
sessionId: context.sessionId,
|
|
|
|
|
threadId: context.threadId,
|
|
|
|
|
runnerTrace,
|
|
|
|
|
createdAt: result.reply?.createdAt ?? result.createdAt ?? now,
|
|
|
|
|
updatedAt: now
|
|
|
|
|
}));
|
|
|
|
|
addedAgentMessage = true;
|
|
|
|
|
}
|
|
|
|
|
const finalMessages = dedupeConversationMessages(next).slice(-50);
|
|
|
|
|
return {
|
|
|
|
|
messages: finalMessages,
|
|
|
|
|
messageCount: finalMessages.length,
|
|
|
|
|
firstUserMessagePreview: firstUserPreviewFromMessages(finalMessages) ?? firstUserPreview
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
function ensureTerminalUserMessage(messages = [], context = {}) {
|
|
|
|
|
if (messages.some((message) => message?.role === "user")) return messages;
|
|
|
|
|
const userText = conversationText(context.firstUserPreview, 12000);
|
|
|
|
@@ -2598,25 +2222,6 @@ function ensureTerminalUserMessage(messages = [], context = {}) {
|
|
|
|
|
});
|
|
|
|
|
return userMessage ? [userMessage, ...messages] : messages;
|
|
|
|
|
}
|
|
|
|
|
function terminalRunnerTraceForConversationMessage(result = {}, context = {}) {
|
|
|
|
|
const source = result.runnerTrace && typeof result.runnerTrace === "object" ? result.runnerTrace : null;
|
|
|
|
|
if (!source) return null;
|
|
|
|
|
const events = Array.isArray(source.events) ? source.events : [];
|
|
|
|
|
const lastEvent = source.lastEvent && typeof source.lastEvent === "object" ? source.lastEvent : events.at(-1);
|
|
|
|
|
return pruneEmpty({
|
|
|
|
|
traceId: safeTraceIdLocal(source.traceId ?? result.traceId ?? context.traceId) || undefined,
|
|
|
|
|
status: textOr(source.status ?? result.status ?? result.agentRun?.terminalStatus ?? context.terminalStatus, ""),
|
|
|
|
|
sessionId: safeAgentSessionId(source.sessionId ?? result.sessionId ?? result.session?.sessionId ?? result.sessionReuse?.sessionId ?? context.sessionId) || undefined,
|
|
|
|
|
threadId: textOr(source.threadId ?? result.threadId ?? result.session?.threadId ?? result.sessionReuse?.threadId ?? context.threadId, ""),
|
|
|
|
|
eventCount: numberOrNull(source.eventCount ?? result.traceSummary?.sourceEventCount ?? result.traceSummary?.eventCount) ?? events.length,
|
|
|
|
|
eventsCompacted: source.eventsCompacted === true || events.length === 0,
|
|
|
|
|
fullTraceLoaded: false,
|
|
|
|
|
lastEvent,
|
|
|
|
|
traceStatus: textOr(result.traceStatus, ""),
|
|
|
|
|
finalResponse: redactTraceFinalResponse(result.finalResponse),
|
|
|
|
|
traceSummary: redactTraceSummary(result.traceSummary)
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
function redactConversationMessage(message) {
|
|
|
|
|
if (!message || typeof message !== "object") return null;
|
|
|
|
|
const runnerTrace = message.runnerTrace && typeof message.runnerTrace === "object" ? message.runnerTrace : null;
|
|
|
|
@@ -2978,19 +2583,6 @@ function nonNegativeResultInteger(value) {
|
|
|
|
|
const parsed = Number.parseInt(String(value ?? ""), 10);
|
|
|
|
|
return Number.isInteger(parsed) && parsed >= 0 ? parsed : 0;
|
|
|
|
|
}
|
|
|
|
|
function positiveInteger(value, fallback = 0) { const number = Number(value); return Number.isFinite(number) && number > 0 ? Math.floor(number) : fallback; }
|
|
|
|
|
async function resolveBeforeDeadline(promise, timeoutMs) {
|
|
|
|
|
/** @type {ReturnType<typeof setTimeout> | null} */
|
|
|
|
|
let timer = null;
|
|
|
|
|
const boundedTimeoutMs = Math.max(1, positiveInteger(timeoutMs, 1));
|
|
|
|
|
const guarded = Promise.resolve(promise).catch(() => null);
|
|
|
|
|
const timeout = new Promise((resolve) => { timer = setTimeout(() => resolve(null), boundedTimeoutMs); });
|
|
|
|
|
try {
|
|
|
|
|
return await Promise.race([guarded, timeout]);
|
|
|
|
|
} finally {
|
|
|
|
|
if (timer) clearTimeout(timer);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
function accessWriteErrorPayload(result = {}) {
|
|
|
|
|
const error = result.error ?? {};
|
|
|
|
|
const code = textOr(error.code, "openfga_write_failed");
|
|
|
|
|