Files
memind/agent-run-deliverable-check.mjs

209 lines
6.4 KiB
JavaScript

export const RECOVERABLE_FINISH_ERROR_CODES = new Set([
'AGENT_RUN_TIMEOUT',
'SESSION_REPLY_TIMEOUT',
'SESSION_REPLY_INCOMPLETE',
'AGENT_RUN_STALE_RECOVERY',
]);
export const SESSION_FINISHED_STALE_GRACE_MS = 90 * 1000;
export const WORKSPACE_DELIVERABLE_LOOKBACK_MS = 60 * 1000;
export function isRecoverableFinishError(error) {
const code = String(error?.code ?? '').trim();
return RECOVERABLE_FINISH_ERROR_CODES.has(code);
}
export function hasRecoverableSessionDeliverables(summary) {
const pageCount = Number(summary?.pageCount ?? 0);
const publicationCount = Number(summary?.publicationCount ?? 0);
return pageCount > 0 || publicationCount > 0;
}
function mapDeliverableRows(rows) {
const pages = (rows ?? []).map((row) => ({
pageId: String(row.page_id ?? '').trim(),
title: String(row.title ?? '').trim(),
publicationId: row.publication_id ? String(row.publication_id) : null,
publicationStatus: row.publication_status ? String(row.publication_status) : null,
publicUrl: row.public_url ? String(row.public_url) : null,
workspaceRelativePath: row.workspace_relative_path ? String(row.workspace_relative_path) : null,
})).filter((row) => row.pageId);
const publicationCount = pages.filter((row) => row.publicationId).length;
return {
pageCount: pages.length,
publicationCount,
pages,
};
}
export function mergeDeliverableSummaries(...summaries) {
const pagesById = new Map();
for (const summary of summaries) {
for (const page of summary?.pages ?? []) {
if (!page?.pageId) continue;
pagesById.set(page.pageId, page);
}
}
const pages = [...pagesById.values()];
return {
pageCount: pages.length,
publicationCount: pages.filter((row) => row.publicationId).length,
pages,
};
}
export async function detectSessionDeliverables(pool, userId, sessionId, { sinceMs = null } = {}) {
if (!pool?.query || !userId || !sessionId) {
return { pageCount: 0, publicationCount: 0, pages: [] };
}
const sinceClause = sinceMs != null ? 'AND p.updated_at >= ?' : '';
const params = sinceMs != null ? [userId, sessionId, sinceMs] : [userId, sessionId];
const [rows] = await pool.query(
`SELECT p.id AS page_id,
p.title,
p.workspace_relative_path,
pr.id AS publication_id,
pr.status AS publication_status,
pr.public_url
FROM h5_page_records p
LEFT JOIN h5_publish_records pr
ON pr.page_id = p.id
AND pr.user_id = p.user_id
AND pr.status = 'online'
WHERE p.user_id = ?
AND p.source_session_id = ?
${sinceClause}
AND p.status <> 'deleted'
ORDER BY p.created_at ASC`,
params,
);
return mapDeliverableRows(rows);
}
export async function detectWorkspaceSyncedDeliverables(
pool,
userId,
{ sinceMs = null, onlyRelativePaths = null } = {},
) {
if (!pool?.query || !userId) {
return { pageCount: 0, publicationCount: 0, pages: [] };
}
const normalizedPaths = onlyRelativePaths == null
? null
: [...new Set(onlyRelativePaths.map((item) => String(item ?? '').trim()).filter(Boolean))];
if (normalizedPaths?.length === 0) {
return { pageCount: 0, publicationCount: 0, pages: [] };
}
const sinceClause = sinceMs != null ? 'AND p.updated_at >= ?' : '';
const pathClause = normalizedPaths == null
? ''
: `AND p.workspace_relative_path IN (${normalizedPaths.map(() => '?').join(', ')})`;
const params = [userId];
if (sinceMs != null) params.push(sinceMs);
if (normalizedPaths != null) params.push(...normalizedPaths);
const [rows] = await pool.query(
`SELECT p.id AS page_id,
p.title,
p.workspace_relative_path,
pr.id AS publication_id,
pr.status AS publication_status,
pr.public_url
FROM h5_page_records p
JOIN h5_page_versions pv ON pv.id = p.current_version_id
LEFT JOIN h5_publish_records pr
ON pr.page_id = p.id
AND pr.user_id = p.user_id
AND pr.status = 'online'
WHERE p.user_id = ?
AND p.status <> 'deleted'
${sinceClause}
${pathClause}
AND (
p.workspace_relative_path LIKE 'public/%.html'
OR JSON_UNQUOTE(JSON_EXTRACT(pv.source_snapshot_json, '$.relative_path')) LIKE 'public/%.html'
)
AND (
JSON_EXTRACT(pv.source_snapshot_json, '$.auto_synced') = true
OR JSON_UNQUOTE(JSON_EXTRACT(pv.source_snapshot_json, '$.content_mode')) = 'static_html'
)
ORDER BY p.updated_at DESC`,
params,
);
return mapDeliverableRows(rows);
}
export async function prepareAndDetectSessionDeliverables({
pool,
userId,
sessionId,
runStartedAtMs = null,
prepareDeliverables = null,
workspaceRelativePaths = null,
} = {}) {
let prepared = null;
if (typeof prepareDeliverables === 'function') {
prepared = await prepareDeliverables({ userId, sessionId }).catch(() => null);
}
const sinceMs = runStartedAtMs == null
? null
: Math.max(0, Number(runStartedAtMs) - WORKSPACE_DELIVERABLE_LOOKBACK_MS);
const bySession = await detectSessionDeliverables(pool, userId, sessionId, { sinceMs });
const byWorkspace = await detectWorkspaceSyncedDeliverables(pool, userId, {
sinceMs,
onlyRelativePaths: workspaceRelativePaths ?? prepared?.pageDataRelativePaths ?? null,
});
return mergeDeliverableSummaries(bySession, byWorkspace);
}
export async function tryRecoverRunFromDeliverables({
pool,
userId,
sessionId,
error = null,
requireRecoverableError = false,
prepareDeliverables = null,
runStartedAtMs = null,
} = {}) {
if (requireRecoverableError && !isRecoverableFinishError(error)) {
return null;
}
const deliverables = await prepareAndDetectSessionDeliverables({
pool,
userId,
sessionId,
runStartedAtMs,
prepareDeliverables,
});
if (!hasRecoverableSessionDeliverables(deliverables)) {
return null;
}
return {
deliverables,
originalError: error
? {
code: error?.code ?? null,
message: error instanceof Error ? error.message : String(error ?? ''),
}
: null,
};
}
export async function tryRecoverRunFromSessionDeliverables({
pool,
userId,
sessionId,
error,
prepareDeliverables = null,
runStartedAtMs = null,
} = {}) {
return tryRecoverRunFromDeliverables({
pool,
userId,
sessionId,
error,
requireRecoverableError: true,
prepareDeliverables,
runStartedAtMs,
});
}