fix(agent-run): align cursor executor path with 103 production hotfixes
Recover requiredExecutor fallback, direct cursor launch in tool gateway, and user-facing brand sanitization in source so the next portal release can replace manual bundled edits on 103. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+163
-14
@@ -37,6 +37,13 @@ import {
|
||||
recordMemoryV2ProductEvent,
|
||||
} from './memory-v2-product-events.mjs';
|
||||
import { buildMemoryRecallPreviews } from './memory-v2-user-feedback.mjs';
|
||||
import { pickTkmindLoadingTip } from './tkmind-loading-tips.mjs';
|
||||
import {
|
||||
resolveExecutorDisplayLabel,
|
||||
sanitizeUserFacingBrandText,
|
||||
} from './executor-display-label.mjs';
|
||||
import { applyCursorFirstAgentExecution } from './cursor-page-routing.mjs';
|
||||
import { cursorDeepseekFallbackEnabled } from './cursor-agent-launch.mjs';
|
||||
|
||||
const DEFAULT_RUN_RETRY_DELAYS_MS = [1500, 5000, 15000];
|
||||
const TERMINAL_STATUSES = new Set(['succeeded', 'failed']);
|
||||
@@ -65,6 +72,30 @@ function safeJsonParse(value, fallback = null) {
|
||||
}
|
||||
}
|
||||
|
||||
export function agentRunUserMessageFingerprint(userMessage) {
|
||||
const metadata = userMessage?.metadata ?? {};
|
||||
const displayText = String(metadata.displayText ?? '').trim();
|
||||
if (displayText) {
|
||||
return displayText.replace(/\s+/g, ' ').toLowerCase();
|
||||
}
|
||||
const content = userMessage?.content;
|
||||
if (typeof content === 'string') {
|
||||
const text = content.trim();
|
||||
return text ? text.replace(/\s+/g, ' ').toLowerCase() : '';
|
||||
}
|
||||
if (Array.isArray(content)) {
|
||||
const text = content
|
||||
.filter((item) => item?.type === 'text')
|
||||
.map((item) => String(item.text ?? '').trim())
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
.trim();
|
||||
return text ? text.replace(/\s+/g, ' ').toLowerCase() : '';
|
||||
}
|
||||
const fallback = String(userMessage?.text ?? userMessage?.value ?? '').trim();
|
||||
return fallback ? fallback.replace(/\s+/g, ' ').toLowerCase() : '';
|
||||
}
|
||||
|
||||
function parseDbJsonColumn(value, fallback = null) {
|
||||
if (value == null || value === '') return fallback;
|
||||
if (typeof value === 'object') return value;
|
||||
@@ -337,8 +368,8 @@ function summarizeText(value, limit = TOOL_GATEWAY_SUMMARY_LIMIT) {
|
||||
}
|
||||
|
||||
export function buildCodeRunCompletionReply(result) {
|
||||
const executor = String(result?.executor ?? 'code executor').trim() || 'code executor';
|
||||
const output = summarizeText(result?.stdout, 2400).trim();
|
||||
const executor = resolveExecutorDisplayLabel(result?.executor, result?.executorLabel);
|
||||
const output = sanitizeUserFacingBrandText(summarizeText(result?.stdout, 2400).trim());
|
||||
return [
|
||||
`已由 ${executor} 完成执行,并通过平台文件验收。`,
|
||||
output ? `\n${output}` : '',
|
||||
@@ -464,8 +495,10 @@ function normalizeSessionMessageCount(value) {
|
||||
export function resolveRequiredCodeExecutor(userMessage) {
|
||||
const metadata = userMessage?.metadata;
|
||||
const runMetadata = metadata?.[RUN_METADATA_KEY] ?? metadata?.agentRun ?? {};
|
||||
const executor = String(runMetadata?.executor ?? '').trim().toLowerCase();
|
||||
return ['aider', 'openhands'].includes(executor) ? executor : null;
|
||||
const executor = String(
|
||||
runMetadata?.executor ?? runMetadata?.requiredExecutor ?? '',
|
||||
).trim().toLowerCase();
|
||||
return ['aider', 'openhands', 'cursor'].includes(executor) ? executor : null;
|
||||
}
|
||||
|
||||
export function assertRequiredCodeExecutorAvailable(requiredExecutor, toolGatewayStatus) {
|
||||
@@ -509,6 +542,49 @@ function getRunOptionsFromMessage(userMessage) {
|
||||
};
|
||||
}
|
||||
|
||||
function resolveEffectiveToolMode(runOptions) {
|
||||
return runOptions?.pageDataAiderWorkflow ? 'chat' : (runOptions?.toolMode ?? 'chat');
|
||||
}
|
||||
|
||||
function restoreCursorFallbackUserMessage(userMessage) {
|
||||
const message = (userMessage && typeof userMessage === 'object' && !Array.isArray(userMessage))
|
||||
? { ...userMessage }
|
||||
: { value: userMessage };
|
||||
const metadata = (message.metadata && typeof message.metadata === 'object' && !Array.isArray(message.metadata))
|
||||
? { ...message.metadata }
|
||||
: {};
|
||||
const runMetadata = (metadata[RUN_METADATA_KEY] && typeof metadata[RUN_METADATA_KEY] === 'object' && !Array.isArray(metadata[RUN_METADATA_KEY]))
|
||||
? { ...metadata[RUN_METADATA_KEY] }
|
||||
: {};
|
||||
const wasCursorPageRewrite = runMetadata.pageCursorDefault === true;
|
||||
if (String(runMetadata.executor ?? '').trim().toLowerCase() === 'cursor') {
|
||||
delete runMetadata.executor;
|
||||
}
|
||||
delete runMetadata.pageCursorDefault;
|
||||
delete runMetadata.cursorAgentDefault;
|
||||
delete runMetadata.cursorTaskKind;
|
||||
delete runMetadata.suggestedDelivery;
|
||||
runMetadata.toolMode = 'chat';
|
||||
if (runMetadata.taskType === 'h5_chat_code_task') delete runMetadata.taskType;
|
||||
metadata[RUN_METADATA_KEY] = runMetadata;
|
||||
|
||||
if (wasCursorPageRewrite) {
|
||||
const displayText = String(metadata.displayText ?? '').trim();
|
||||
if (displayText) {
|
||||
const content = Array.isArray(message.content) ? [...message.content] : [];
|
||||
const textIndex = content.findIndex((item) => item?.type === 'text');
|
||||
if (textIndex >= 0) {
|
||||
content[textIndex] = { ...content[textIndex], text: displayText };
|
||||
} else {
|
||||
content.unshift({ type: 'text', text: displayText });
|
||||
}
|
||||
return { ...message, metadata, content };
|
||||
}
|
||||
}
|
||||
|
||||
return { ...message, metadata };
|
||||
}
|
||||
|
||||
export async function collectAiderReviewFiles(cwd, sinceMs = 0) {
|
||||
if (!cwd) return [];
|
||||
const root = path.resolve(String(cwd));
|
||||
@@ -1024,6 +1100,26 @@ export function createAgentRunGateway({
|
||||
return rows[0] ?? null;
|
||||
}
|
||||
|
||||
async function findActiveDuplicateRun(userId, userMessage) {
|
||||
const fingerprint = agentRunUserMessageFingerprint(userMessage);
|
||||
if (!fingerprint) return null;
|
||||
const [rows] = await pool.query(
|
||||
`SELECT id, user_message_json, status
|
||||
FROM h5_agent_runs
|
||||
WHERE user_id = ? AND status NOT IN ('succeeded', 'failed')
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 20`,
|
||||
[userId],
|
||||
);
|
||||
for (const row of rows) {
|
||||
const parsed = parseDbJsonColumn(row.user_message_json, {});
|
||||
if (agentRunUserMessageFingerprint(parsed) === fingerprint) {
|
||||
return row;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function createRun(userId, {
|
||||
sessionId = null,
|
||||
requestId,
|
||||
@@ -1087,6 +1183,15 @@ export function createAgentRunGateway({
|
||||
}
|
||||
}
|
||||
|
||||
const duplicateActive = await findActiveDuplicateRun(userId, userMessage);
|
||||
if (duplicateActive) {
|
||||
const conflict = new Error('相同内容已有任务正在处理,请稍候');
|
||||
conflict.code = 'SESSION_RUN_CONFLICT';
|
||||
conflict.status = 409;
|
||||
conflict.existingRunId = duplicateActive.id;
|
||||
throw conflict;
|
||||
}
|
||||
|
||||
const runId = crypto.randomUUID();
|
||||
const createdAt = nowMs();
|
||||
const runMessage = withRunMetadata(userMessage, {
|
||||
@@ -1213,9 +1318,11 @@ export function createAgentRunGateway({
|
||||
|
||||
function startRunHeartbeat(runId, { attempt }) {
|
||||
let stopped = false;
|
||||
let heartbeatCount = 0;
|
||||
const writeHeartbeat = async () => {
|
||||
if (stopped) return;
|
||||
try {
|
||||
heartbeatCount += 1;
|
||||
await appendEvent(runId, 'worker_heartbeat', {
|
||||
attempt,
|
||||
pid: process.pid,
|
||||
@@ -1224,6 +1331,14 @@ export function createAgentRunGateway({
|
||||
runtimeRoot: worker.runtimeRoot,
|
||||
buildId: worker.buildId,
|
||||
});
|
||||
const tip = pickTkmindLoadingTip({
|
||||
seed: `${runId}:${heartbeatCount}:${Date.now()}`,
|
||||
});
|
||||
await appendEvent(runId, 'loading_tip', {
|
||||
text: tip.text,
|
||||
category: tip.category,
|
||||
source: 'tkmind_executor',
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('[AgentRun] worker heartbeat failed:', err instanceof Error ? err.message : err);
|
||||
}
|
||||
@@ -1381,15 +1496,9 @@ export function createAgentRunGateway({
|
||||
|
||||
async function executeRun(row, runId) {
|
||||
let userMessage = safeJsonParse(row.user_message_json, {});
|
||||
const runOptions = getRunOptionsFromMessage(userMessage);
|
||||
let runOptions = getRunOptionsFromMessage(userMessage);
|
||||
const toolGatewayStatus = toolGateway?.getStatus ? toolGateway.getStatus() : null;
|
||||
assertRequiredCodeExecutorAvailable(
|
||||
runOptions.requiredExecutor ?? runOptions.reviewExecutor,
|
||||
toolGatewayStatus,
|
||||
);
|
||||
const effectiveToolMode = runOptions.pageDataAiderWorkflow
|
||||
? 'chat'
|
||||
: runOptions.toolMode;
|
||||
let effectiveToolMode = resolveEffectiveToolMode(runOptions);
|
||||
let disclosureDecision = null;
|
||||
try {
|
||||
disclosureDecision = systemDisclosurePolicyService?.evaluate?.({
|
||||
@@ -1446,6 +1555,40 @@ export function createAgentRunGateway({
|
||||
}
|
||||
const routing = await resolveRunRouting(row, userMessage, runOptions);
|
||||
const routingDecision = resolveLegacyRouteFromClassification(routing) ?? routing?.route ?? null;
|
||||
const cursorFirstApplied = applyCursorFirstAgentExecution(userMessage, runOptions, {
|
||||
routingDecision,
|
||||
env: process.env,
|
||||
});
|
||||
userMessage = cursorFirstApplied.userMessage;
|
||||
runOptions = cursorFirstApplied.runOptions;
|
||||
effectiveToolMode = resolveEffectiveToolMode(runOptions);
|
||||
const cursorFirstAgent = cursorFirstApplied.cursorFirst === true;
|
||||
const fallbackCursorExecutorToDeepseek = async (err) => {
|
||||
const canFallback = cursorDeepseekFallbackEnabled(process.env)
|
||||
&& (cursorFirstAgent || runOptions.requiredExecutor === 'cursor');
|
||||
if (!canFallback) return false;
|
||||
await appendEvent(runId, 'cursor_executor_fallback_to_deepseek', {
|
||||
code: err?.code ?? null,
|
||||
message: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
userMessage = restoreCursorFallbackUserMessage(userMessage);
|
||||
runOptions = {
|
||||
...runOptions,
|
||||
toolMode: 'chat',
|
||||
requiredExecutor: null,
|
||||
taskType: null,
|
||||
};
|
||||
effectiveToolMode = resolveEffectiveToolMode(runOptions);
|
||||
return true;
|
||||
};
|
||||
try {
|
||||
assertRequiredCodeExecutorAvailable(
|
||||
runOptions.requiredExecutor ?? runOptions.reviewExecutor,
|
||||
toolGatewayStatus,
|
||||
);
|
||||
} catch (err) {
|
||||
if (!(await fallbackCursorExecutorToDeepseek(err))) throw err;
|
||||
}
|
||||
let agentMemoryContext = null;
|
||||
if (routingDecision === CHAT_INTENT_ROUTE.AGENT && chatIntentRouter?.resolveAgentMemoryContext) {
|
||||
const displayText = userMessage?.metadata?.displayText
|
||||
@@ -1540,8 +1683,9 @@ export function createAgentRunGateway({
|
||||
}
|
||||
}
|
||||
const preferDirectChat =
|
||||
routingDecision === CHAT_INTENT_ROUTE.DIRECT_CHAT ||
|
||||
(isDirectChatSessionId(row.agent_session_id ?? null) && !runOptions.forceDeepReasoning);
|
||||
!cursorFirstAgent &&
|
||||
(routingDecision === CHAT_INTENT_ROUTE.DIRECT_CHAT ||
|
||||
(isDirectChatSessionId(row.agent_session_id ?? null) && !runOptions.forceDeepReasoning));
|
||||
const directChatInput = {
|
||||
sessionId: row.agent_session_id ?? null,
|
||||
toolMode: runOptions.toolMode,
|
||||
@@ -1602,11 +1746,13 @@ export function createAgentRunGateway({
|
||||
const workingDir = userAuth?.resolveWorkingDir
|
||||
? await userAuth.resolveWorkingDir(row.user_id)
|
||||
: undefined;
|
||||
try {
|
||||
await invalidatePortalDirectChatSnapshot(row.agent_session_id ?? null);
|
||||
await appendEvent(runId, 'tool_gateway_dispatch', {
|
||||
protocol: toolGatewayStatus.protocol ?? 'agent-run-v1',
|
||||
taskType: runOptions.taskType,
|
||||
workingDir: workingDir ?? null,
|
||||
executor: runOptions.requiredExecutor ?? null,
|
||||
});
|
||||
const result = await toolGateway.executeJob({
|
||||
runId,
|
||||
@@ -1685,6 +1831,9 @@ export function createAgentRunGateway({
|
||||
return { sessionId: delivery.sessionId, routing };
|
||||
}
|
||||
return { sessionId: row.agent_session_id ?? null, routing };
|
||||
} catch (err) {
|
||||
if (!(await fallbackCursorExecutorToDeepseek(err))) throw err;
|
||||
}
|
||||
}
|
||||
|
||||
let sessionId = resolveGatewayAgentSessionId({
|
||||
|
||||
Reference in New Issue
Block a user