feat(page-data): add validation suggestions, repair path, and thinking fixes
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:
john
2026-07-25 23:01:55 +08:00
parent 6b6f9ed01c
commit 8ccf2db05c
28 changed files with 1652 additions and 154 deletions
+142 -48
View File
@@ -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', {