fix: harden long conversation continuity
Memind CI / Test, build, and release guards (push) Successful in 1m28s
Memind CI / Test, build, and release guards (push) Successful in 1m28s
This commit is contained in:
+150
-18
@@ -154,6 +154,14 @@ function pageDataFailureCheckId(code) {
|
||||
}
|
||||
}
|
||||
|
||||
function hasPageMutationToolEvidence(toolEvidence) {
|
||||
if (!toolEvidence || !Array.isArray(toolEvidence.calls)) return null;
|
||||
return toolEvidence.calls.some((name) =>
|
||||
/(?:^|[-_:])(edit|write|publish|workspace|page[-_]?data)(?:[-_:]|$)/i.test(
|
||||
String(name ?? ''),
|
||||
));
|
||||
}
|
||||
|
||||
function buildFreshSessionContext(conversation, { maxMessages = 16, maxChars = 12_000 } = {}) {
|
||||
const visible = [];
|
||||
for (const message of Array.isArray(conversation) ? conversation : []) {
|
||||
@@ -219,6 +227,7 @@ const FRESH_SESSION_RECOVERY_CODES = new Set([
|
||||
'SESSION_TOOL_HISTORY_FRESH_SESSION_REQUIRED',
|
||||
'SESSION_REASONING_CONTENT_POISONED',
|
||||
'SESSION_VISUAL_CONTEXT_UNSUPPORTED',
|
||||
'SESSION_EMPTY_FINISH',
|
||||
]);
|
||||
|
||||
async function resolveConversationForFreshSessionRecovery({
|
||||
@@ -231,14 +240,20 @@ async function resolveConversationForFreshSessionRecovery({
|
||||
if (err?.code === 'SESSION_TOOL_HISTORY_FRESH_SESSION_REQUIRED') {
|
||||
return Array.isArray(err?.repairedConversation) ? err.repairedConversation : [];
|
||||
}
|
||||
let gooseConversation = [];
|
||||
if (typeof tkmindProxy?.fetchSessionConversationForUser === 'function') {
|
||||
const conversation = await tkmindProxy.fetchSessionConversationForUser(
|
||||
gooseConversation = await tkmindProxy.fetchSessionConversationForUser(
|
||||
userId,
|
||||
previousSessionId,
|
||||
).catch(() => []);
|
||||
if (conversation.length > 0) return conversation;
|
||||
}
|
||||
return loadSnapshotMessages(sessionSnapshotService, previousSessionId);
|
||||
const snapshotConversation = await loadSnapshotMessages(
|
||||
sessionSnapshotService,
|
||||
previousSessionId,
|
||||
);
|
||||
return snapshotConversation.length > gooseConversation.length
|
||||
? snapshotConversation
|
||||
: gooseConversation;
|
||||
}
|
||||
|
||||
function resolveRequiredImageGeneration(row, routing) {
|
||||
@@ -597,6 +612,14 @@ export function createAgentRunGateway({
|
||||
process.env.MEMIND_PAGE_DATA_SUGGESTION_REPAIR_ENABLED,
|
||||
true,
|
||||
),
|
||||
sessionCompactMessageCount = positiveInteger(
|
||||
process.env.MEMIND_AGENT_SESSION_COMPACT_MESSAGE_COUNT,
|
||||
180,
|
||||
),
|
||||
sessionCompactCharCount = positiveInteger(
|
||||
process.env.MEMIND_AGENT_SESSION_COMPACT_CHAR_COUNT,
|
||||
120_000,
|
||||
),
|
||||
workerIdentity = null,
|
||||
}) {
|
||||
const worker = normalizeAgentRunWorkerIdentity(workerIdentity ?? {});
|
||||
@@ -829,7 +852,10 @@ export function createAgentRunGateway({
|
||||
if (!normalizedSessionId || isDirectChatSessionId(normalizedSessionId)) {
|
||||
return { triggered: false, reason: 'no_session' };
|
||||
}
|
||||
if (!tkmindProxy?.submitSessionReplyForUser) {
|
||||
if (
|
||||
!tkmindProxy?.submitSessionReplyForUser
|
||||
&& !tkmindProxy?.submitSessionReplyAndAwaitFinishForUser
|
||||
) {
|
||||
return { triggered: false, reason: 'no_proxy' };
|
||||
}
|
||||
if (suggestionRepairTriggered.has(runId)) {
|
||||
@@ -852,17 +878,32 @@ export function createAgentRunGateway({
|
||||
},
|
||||
};
|
||||
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,
|
||||
});
|
||||
if (typeof tkmindProxy.submitSessionReplyAndAwaitFinishForUser === 'function') {
|
||||
const finish = await tkmindProxy.submitSessionReplyAndAwaitFinishForUser(
|
||||
userId,
|
||||
normalizedSessionId,
|
||||
requestId,
|
||||
userMessage,
|
||||
{ timeoutMs: Math.min(runTimeoutMs, 120_000) },
|
||||
);
|
||||
await appendEvent(runId, 'page_data_suggestion_repair_completed', {
|
||||
requestId,
|
||||
sessionId: normalizedSessionId,
|
||||
toolCalls: finish?.toolEvidence?.calls ?? [],
|
||||
});
|
||||
} else {
|
||||
await tkmindProxy.submitSessionReplyForUser(
|
||||
userId,
|
||||
normalizedSessionId,
|
||||
requestId,
|
||||
userMessage,
|
||||
);
|
||||
}
|
||||
return { triggered: true, requestId };
|
||||
} catch (err) {
|
||||
await appendEvent(runId, 'page_data_suggestion_repair_failed', {
|
||||
@@ -1648,6 +1689,81 @@ export function createAgentRunGateway({
|
||||
saved: transcriptPersisted.saved,
|
||||
});
|
||||
}
|
||||
if (typeof tkmindProxy.compactSessionConversationForUser === 'function') {
|
||||
try {
|
||||
const compaction = await tkmindProxy.compactSessionConversationForUser(
|
||||
row.user_id,
|
||||
sessionId,
|
||||
{
|
||||
messageThreshold: sessionCompactMessageCount,
|
||||
charThreshold: sessionCompactCharCount,
|
||||
},
|
||||
);
|
||||
if (compaction?.compacted) {
|
||||
userMessage = appendFreshSessionContext(
|
||||
userMessage,
|
||||
compaction.conversation,
|
||||
);
|
||||
await appendEvent(runId, 'session_context_compacted', {
|
||||
sessionId,
|
||||
messageCount: compaction.messageCount ?? null,
|
||||
charCount: compaction.charCount ?? null,
|
||||
clientMessageCount: runOptions.sessionMessageCount,
|
||||
retainedContextMessages: 16,
|
||||
retainedContextChars: 12_000,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
if (
|
||||
err?.code === 'SESSION_CONTEXT_FRESH_SESSION_REQUIRED'
|
||||
&& typeof tkmindProxy.startSessionForUser === 'function'
|
||||
) {
|
||||
const previousSessionId = sessionId;
|
||||
const conversationForContext = Array.isArray(err?.conversation)
|
||||
? err.conversation
|
||||
: await resolveConversationForFreshSessionRecovery({
|
||||
err,
|
||||
previousSessionId,
|
||||
userId: row.user_id,
|
||||
tkmindProxy,
|
||||
sessionSnapshotService,
|
||||
});
|
||||
const replacement = await tkmindProxy.startSessionForUser(row.user_id);
|
||||
sessionId = replacement.id;
|
||||
await pool.query(
|
||||
`UPDATE h5_agent_runs SET agent_session_id = ?, updated_at = ? WHERE id = ?`,
|
||||
[sessionId, nowMs(), runId],
|
||||
);
|
||||
const persisted = await persistSessionTranscriptMessages({
|
||||
conversationMemoryService,
|
||||
sessionId,
|
||||
userId: row.user_id,
|
||||
messages: conversationForContext,
|
||||
});
|
||||
userMessage = appendFreshSessionContext(
|
||||
userMessage,
|
||||
conversationForContext,
|
||||
);
|
||||
await appendEvent(runId, 'session_context_rotated', {
|
||||
previousSessionId,
|
||||
sessionId,
|
||||
reason: err.code,
|
||||
messageCount: err?.messageCount ?? conversationForContext.length,
|
||||
charCount: err?.charCount ?? null,
|
||||
persistedMessageCount: persisted.saved,
|
||||
retainedContextMessages: 16,
|
||||
retainedContextChars: 12_000,
|
||||
});
|
||||
await appendRunSnapshot(runId);
|
||||
} else {
|
||||
await appendEvent(runId, 'session_context_compaction_failed', {
|
||||
sessionId,
|
||||
code: String(err?.code ?? '').slice(0, 128),
|
||||
message: String(err instanceof Error ? err.message : err).slice(0, 500),
|
||||
}).catch(() => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
await invalidatePortalDirectChatSnapshot(sessionId);
|
||||
let toolEvidence = null;
|
||||
const awaitSessionFinish = envFlag(process.env.MEMIND_AGENT_RUN_AWAIT_SESSION_FINISH, true)
|
||||
@@ -1833,6 +1949,18 @@ export function createAgentRunGateway({
|
||||
pageGenerationIntent
|
||||
&& !isGenericPageGenerationRequest(runDisplayText)
|
||||
));
|
||||
if (requiresPageDeliverable && hasPageMutationToolEvidence(toolEvidence) === false) {
|
||||
const error = new Error(
|
||||
pageDataIntent
|
||||
? 'Page Data 任务未执行页面修改工具,不能用历史页面标记成功'
|
||||
: '页面任务未执行写入或发布工具,不能用历史页面标记成功',
|
||||
);
|
||||
error.code = pageDataIntent
|
||||
? 'PAGE_DATA_DELIVERABLE_MISSING'
|
||||
: 'PUBLIC_PAGE_DELIVERABLE_MISSING';
|
||||
error.retryable = false;
|
||||
throw error;
|
||||
}
|
||||
let deliverables = null;
|
||||
if (requiresPageDeliverable || typeof validateRunDeliverables === 'function') {
|
||||
const latest = await getRunById(runId);
|
||||
@@ -2277,14 +2405,18 @@ export function createAgentRunGateway({
|
||||
new Error(`agent run stale after ${ageMs}ms`),
|
||||
{ code: 'AGENT_RUN_STALE_RECOVERY' },
|
||||
);
|
||||
const deliverableRecovered = await recoverRunFromDeliverables({
|
||||
runId: row.id,
|
||||
userId: row.user_id,
|
||||
sessionId: row.agent_session_id ?? null,
|
||||
err: staleError,
|
||||
runStartedAtMs: row.started_at ?? null,
|
||||
requireRecoverableError: false,
|
||||
});
|
||||
// A synchronized historical page can receive fresh database timestamps
|
||||
// without this run having executed any model or file tool. Only recover
|
||||
// a stale run as successful after its own session Finish was observed.
|
||||
const deliverableRecovered = sessionFinishedAt != null
|
||||
&& await recoverRunFromDeliverables({
|
||||
runId: row.id,
|
||||
userId: row.user_id,
|
||||
sessionId: row.agent_session_id ?? null,
|
||||
err: staleError,
|
||||
runStartedAtMs: row.started_at ?? null,
|
||||
requireRecoverableError: false,
|
||||
});
|
||||
if (deliverableRecovered) {
|
||||
const marked = await markRun(row.id, 'succeeded', {
|
||||
agent_session_id: row.agent_session_id ?? null,
|
||||
|
||||
Reference in New Issue
Block a user