feat(page-data): add validation suggestions, repair path, and thinking fixes
Memind CI / Test, build, and release guards (push) Failing after 3m18s
Memind CI / Test, build, and release guards (push) Failing after 3m18s
Map Page Data failure codes to Chinese remediation hints, trigger one-shot goosed repair for remediable cases, and recover poisoned thinking sessions. Route local DeepSeek through the no-think proxy via host.docker.internal so tool rounds no longer hit reasoning_content 400; document gate and case-study scenarios for event registration repair. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+142
-48
@@ -26,6 +26,12 @@ import {
|
||||
isPageDataIntent,
|
||||
isPageGenerationIntent,
|
||||
} from './chat-skills.mjs';
|
||||
import {
|
||||
buildPageDataValidationRepairPrompt,
|
||||
formatPageDataValidationUserMessage,
|
||||
isPageDataSuggestionRemediable,
|
||||
resolvePageDataValidationSuggestions,
|
||||
} from './page-data-validation-suggestions.mjs';
|
||||
|
||||
const DEFAULT_RUN_RETRY_DELAYS_MS = [1500, 5000, 15000];
|
||||
const TERMINAL_STATUSES = new Set(['succeeded', 'failed']);
|
||||
@@ -192,6 +198,31 @@ function appendFreshSessionContext(userMessage, conversation) {
|
||||
return { ...message, content };
|
||||
}
|
||||
|
||||
const FRESH_SESSION_RECOVERY_CODES = new Set([
|
||||
'SESSION_TOOL_HISTORY_FRESH_SESSION_REQUIRED',
|
||||
'SESSION_REASONING_CONTENT_POISONED',
|
||||
]);
|
||||
|
||||
async function resolveConversationForFreshSessionRecovery({
|
||||
err,
|
||||
previousSessionId,
|
||||
userId,
|
||||
tkmindProxy,
|
||||
sessionSnapshotService,
|
||||
}) {
|
||||
if (err?.code === 'SESSION_TOOL_HISTORY_FRESH_SESSION_REQUIRED') {
|
||||
return Array.isArray(err?.repairedConversation) ? err.repairedConversation : [];
|
||||
}
|
||||
if (typeof tkmindProxy?.fetchSessionConversationForUser === 'function') {
|
||||
const conversation = await tkmindProxy.fetchSessionConversationForUser(
|
||||
userId,
|
||||
previousSessionId,
|
||||
).catch(() => []);
|
||||
if (conversation.length > 0) return conversation;
|
||||
}
|
||||
return loadSnapshotMessages(sessionSnapshotService, previousSessionId);
|
||||
}
|
||||
|
||||
function resolveRequiredImageGeneration(row, routing) {
|
||||
const message = parseDbJsonColumn(row?.user_message_json, {}) ?? {};
|
||||
const metadata = message.metadata ?? {};
|
||||
@@ -516,7 +547,6 @@ export function createAgentRunGateway({
|
||||
observeWorkflowValidation = null,
|
||||
isSessionExternallyBusy = null,
|
||||
validateRunDeliverables = null,
|
||||
cancelSessionOnRetry = null,
|
||||
quiesceSessionOnTerminal = null,
|
||||
retryDelaysMs = DEFAULT_RUN_RETRY_DELAYS_MS,
|
||||
autoDispatch = envFlag(process.env.MEMIND_AGENT_RUN_AUTODISPATCH, true),
|
||||
@@ -544,6 +574,10 @@ export function createAgentRunGateway({
|
||||
process.env.MEMIND_ORCHESTRATOR_PAGE_DATA_VALIDATION_GATE_ENABLED,
|
||||
false,
|
||||
),
|
||||
enablePageDataSuggestionRepair = envFlag(
|
||||
process.env.MEMIND_PAGE_DATA_SUGGESTION_REPAIR_ENABLED,
|
||||
true,
|
||||
),
|
||||
workerIdentity = null,
|
||||
}) {
|
||||
const worker = normalizeAgentRunWorkerIdentity(workerIdentity ?? {});
|
||||
@@ -551,6 +585,7 @@ export function createAgentRunGateway({
|
||||
const inFlight = new Set();
|
||||
const queuedDispatches = [];
|
||||
const queuedDispatchSet = new Set();
|
||||
const suggestionRepairTriggered = new Set();
|
||||
|
||||
function enqueueRun(runId) {
|
||||
if (!runId || inFlight.has(runId) || queuedDispatchSet.has(runId)) return false;
|
||||
@@ -614,35 +649,6 @@ export function createAgentRunGateway({
|
||||
}
|
||||
}
|
||||
|
||||
async function cancelRetrySession(runId, {
|
||||
userId,
|
||||
sessionId,
|
||||
requestId = null,
|
||||
}) {
|
||||
if (!sessionId || typeof cancelSessionOnRetry !== 'function') return;
|
||||
try {
|
||||
const result = await cancelSessionOnRetry({
|
||||
runId,
|
||||
userId,
|
||||
sessionId,
|
||||
requestId,
|
||||
});
|
||||
await appendEvent(runId, 'session_request_cancelled_for_retry', {
|
||||
sessionId,
|
||||
cancellation: result ?? null,
|
||||
});
|
||||
} catch (err) {
|
||||
await appendEvent(runId, 'session_retry_cancellation_failed', {
|
||||
sessionId,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
}).catch(() => {});
|
||||
console.warn(
|
||||
`[AgentRun] failed to cancel session ${sessionId} before retry:`,
|
||||
err instanceof Error ? err.message : err,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function appendExecutionPlanEvent(input, source) {
|
||||
const executionPlan = source?.executionPlan;
|
||||
if (!executionPlan?.dryRun) return;
|
||||
@@ -787,6 +793,71 @@ export function createAgentRunGateway({
|
||||
return validationDispatcher.dispatch(input);
|
||||
}
|
||||
|
||||
async function maybeTriggerPageDataSuggestionRepair({
|
||||
userId,
|
||||
sessionId,
|
||||
runId,
|
||||
code = null,
|
||||
message = '',
|
||||
} = {}) {
|
||||
if (!enablePageDataSuggestionRepair) {
|
||||
return { triggered: false, reason: 'disabled' };
|
||||
}
|
||||
if (!isPageDataSuggestionRemediable(code)) {
|
||||
return { triggered: false, reason: 'not_remediable' };
|
||||
}
|
||||
const normalizedSessionId = String(sessionId ?? '').trim();
|
||||
if (!normalizedSessionId || isDirectChatSessionId(normalizedSessionId)) {
|
||||
return { triggered: false, reason: 'no_session' };
|
||||
}
|
||||
if (!tkmindProxy?.submitSessionReplyForUser) {
|
||||
return { triggered: false, reason: 'no_proxy' };
|
||||
}
|
||||
if (suggestionRepairTriggered.has(runId)) {
|
||||
return { triggered: false, reason: 'already_triggered' };
|
||||
}
|
||||
suggestionRepairTriggered.add(runId);
|
||||
const prompt = buildPageDataValidationRepairPrompt({ code, message });
|
||||
const requestId = crypto.randomUUID();
|
||||
const userMessage = {
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: prompt }],
|
||||
metadata: {
|
||||
displayText: '请按系统纠正建议修复 Page Data 交付',
|
||||
userVisible: false,
|
||||
agentVisible: true,
|
||||
memindRun: {
|
||||
pageDataSuggestionRepair: true,
|
||||
sourceRunId: runId,
|
||||
},
|
||||
},
|
||||
};
|
||||
try {
|
||||
await tkmindProxy.submitSessionReplyForUser(
|
||||
userId,
|
||||
normalizedSessionId,
|
||||
requestId,
|
||||
userMessage,
|
||||
);
|
||||
await appendEvent(runId, 'page_data_suggestion_repair_triggered', {
|
||||
code: String(code ?? '').slice(0, 128),
|
||||
requestId,
|
||||
sessionId: normalizedSessionId,
|
||||
});
|
||||
return { triggered: true, requestId };
|
||||
} catch (err) {
|
||||
await appendEvent(runId, 'page_data_suggestion_repair_failed', {
|
||||
code: String(code ?? '').slice(0, 128),
|
||||
message: String(err instanceof Error ? err.message : err).slice(0, 500),
|
||||
}).catch(() => {});
|
||||
return {
|
||||
triggered: false,
|
||||
reason: 'submit_failed',
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function enforcePageDataValidationGate(input) {
|
||||
const startedAt = nowMs();
|
||||
let result = null;
|
||||
@@ -832,20 +903,26 @@ export function createAgentRunGateway({
|
||||
const failure = resolved instanceof Error
|
||||
? resolved
|
||||
: new Error(String(resolved ?? 'Page Data Orchestrator validation failed'));
|
||||
const suggestions = resolvePageDataValidationSuggestions([code]);
|
||||
const userMessage = formatPageDataValidationUserMessage({
|
||||
code,
|
||||
message: failure.message,
|
||||
});
|
||||
failure.message = userMessage;
|
||||
await appendEvent(input.runId, 'workflow_validation_gate_failed', {
|
||||
code,
|
||||
upstreamCode: gateError
|
||||
? null
|
||||
: String(error?.code ?? 'WORKFLOW_VALIDATION_OBSERVER_FAILED').slice(0, 128),
|
||||
message: String(
|
||||
failure.message,
|
||||
).slice(0, 1000),
|
||||
message: String(userMessage).slice(0, 1000),
|
||||
suggestions,
|
||||
verdict: result?.validation?.verdict ?? null,
|
||||
latencyMs: Math.max(0, nowMs() - startedAt),
|
||||
}).catch(() => {});
|
||||
failure.code = code;
|
||||
failure.retryable = false;
|
||||
failure.validationObservationAttempted = true;
|
||||
failure.suggestions = suggestions;
|
||||
throw failure;
|
||||
}
|
||||
}
|
||||
@@ -1593,15 +1670,16 @@ export function createAgentRunGateway({
|
||||
} catch (err) {
|
||||
if (
|
||||
!replacedPoisonedSession
|
||||
&& (
|
||||
err?.code === 'SESSION_TOOL_HISTORY_FRESH_SESSION_REQUIRED'
|
||||
|| err?.code === 'SESSION_REASONING_CONTENT_POISONED'
|
||||
)
|
||||
&& FRESH_SESSION_RECOVERY_CODES.has(String(err?.code ?? '').trim())
|
||||
) {
|
||||
const previousSessionId = sessionId;
|
||||
const repairedConversation = Array.isArray(err?.repairedConversation)
|
||||
? err.repairedConversation
|
||||
: [];
|
||||
const conversationForContext = await resolveConversationForFreshSessionRecovery({
|
||||
err,
|
||||
previousSessionId,
|
||||
userId: row.user_id,
|
||||
tkmindProxy,
|
||||
sessionSnapshotService,
|
||||
});
|
||||
const replacement = await tkmindProxy.startSessionForUser(row.user_id);
|
||||
sessionId = replacement.id;
|
||||
replacedPoisonedSession = true;
|
||||
@@ -1613,19 +1691,19 @@ export function createAgentRunGateway({
|
||||
conversationMemoryService,
|
||||
sessionId,
|
||||
userId: row.user_id,
|
||||
messages: repairedConversation,
|
||||
messages: conversationForContext,
|
||||
});
|
||||
await appendEvent(runId, 'poisoned_session_replaced', {
|
||||
previousSessionId,
|
||||
sessionId,
|
||||
reason: err?.code ?? null,
|
||||
repairedMessageCount: repairedConversation.length,
|
||||
reason: err.code,
|
||||
repairedMessageCount: conversationForContext.length,
|
||||
persistedMessageCount: persisted.saved,
|
||||
});
|
||||
await appendRunSnapshot(runId);
|
||||
await invalidatePortalDirectChatSnapshot(sessionId);
|
||||
submitMessage = ensureGooseUserMessageMetadata(
|
||||
appendFreshSessionContext(userMessage, repairedConversation),
|
||||
appendFreshSessionContext(userMessage, conversationForContext),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
@@ -1872,17 +1950,33 @@ export function createAgentRunGateway({
|
||||
await finalizeSuccessfulRun(runId, row, recoverySessionId);
|
||||
return;
|
||||
}
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
const rawMessage = err instanceof Error ? err.message : String(err);
|
||||
const alreadyEnriched = (Array.isArray(err?.suggestions) && err.suggestions.length > 0)
|
||||
|| /建议(/.test(rawMessage);
|
||||
const pageDataFailure = !alreadyEnriched && (
|
||||
isPageDataWorkflowRequested(
|
||||
parseDbJsonColumn(row.user_message_json, {}) ?? {},
|
||||
getRunOptionsFromMessage(parseDbJsonColumn(row.user_message_json, {}) ?? {}).taskType,
|
||||
) || Boolean(resolvePageDataValidationSuggestions([err?.code]).length)
|
||||
);
|
||||
const message = pageDataFailure
|
||||
? formatPageDataValidationUserMessage({
|
||||
code: err?.code,
|
||||
message: rawMessage,
|
||||
})
|
||||
: rawMessage;
|
||||
const timedOut = err?.code === 'AGENT_RUN_TIMEOUT';
|
||||
if (timedOut) {
|
||||
await appendEvent(runId, 'timeout', { timeoutMs: runTimeoutMs });
|
||||
}
|
||||
const retryable = !timedOut && err?.retryable !== false && nextAttempt < retryDelaysMs.length;
|
||||
if (retryable) {
|
||||
await cancelRetrySession(runId, {
|
||||
if (!retryable && isPageDataSuggestionRemediable(err?.code)) {
|
||||
await maybeTriggerPageDataSuggestionRepair({
|
||||
userId: row.user_id,
|
||||
sessionId: recoverySessionId,
|
||||
requestId: row.request_id ?? null,
|
||||
runId,
|
||||
code: err?.code,
|
||||
message: rawMessage,
|
||||
});
|
||||
}
|
||||
await markRun(runId, retryable ? 'retryable' : 'failed', {
|
||||
|
||||
Reference in New Issue
Block a user