merge: integrate langgraph execution runtime
# Conflicts: # agent-run-gateway.test.mjs # capabilities.mjs # package.json # server.mjs
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import crypto from 'node:crypto';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { createAgentRunGateway } from '../agent-run-gateway.mjs';
|
||||
import { isDirectChatSessionId } from '../direct-chat-service.mjs';
|
||||
import { scanWorkspaceFilesForProhibitedBrowserStorage } from '../mindspace-browser-storage-policy.mjs';
|
||||
import { evaluatePageDataHtmlContent } from '../mindspace-page-data-finish-guard.mjs';
|
||||
import { normalizeWorkspaceRelativePath } from '../mindspace-pages.mjs';
|
||||
@@ -8,6 +10,12 @@ import { resolveMindSpaceUserPublishDir } from '../mindspace-runtime-config.mjs'
|
||||
import { policyAllowsAction } from '../page-access-policy.mjs';
|
||||
import { detectPageDataDatasetUsageFromHtml } from '../page-data-html-detect.mjs';
|
||||
import { readPageAccessPolicy } from '../page-data-policy-store.mjs';
|
||||
import {
|
||||
cancelSessionActiveRequest,
|
||||
quiesceSessionStdioExtensions,
|
||||
} from '../session-runtime-lifecycle.mjs';
|
||||
import { createOrchestratorAdminConfigService } from '../services/orchestrator/admin-config.mjs';
|
||||
import { createWorkflowShadowObserver } from '../services/orchestrator/shadow-observer.mjs';
|
||||
import { createTkmindProxy } from '../tkmind-proxy.mjs';
|
||||
import { createToolGateway } from '../tool-gateway.mjs';
|
||||
|
||||
@@ -17,6 +25,70 @@ function isEnabledFlag(value, fallback = '') {
|
||||
);
|
||||
}
|
||||
|
||||
export function startPortalAgentRunRecoveryLoop(
|
||||
agentRunGateway,
|
||||
{
|
||||
env = process.env,
|
||||
logger = console,
|
||||
setIntervalFn = setInterval,
|
||||
} = {},
|
||||
) {
|
||||
if (
|
||||
typeof agentRunGateway?.dispatchQueuedRuns !== 'function' ||
|
||||
typeof agentRunGateway?.recoverStaleRunningRuns !== 'function'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const configuredInterval = Number(
|
||||
env.MEMIND_AGENT_RUN_RECOVERY_INTERVAL_MS ?? 30_000,
|
||||
);
|
||||
const intervalMs = Number.isFinite(configuredInterval)
|
||||
? Math.max(5_000, configuredInterval)
|
||||
: 30_000;
|
||||
const configuredHeartbeatStaleMs = Number(
|
||||
env.MEMIND_AGENT_RUN_HEARTBEAT_STALE_MS ?? 90_000,
|
||||
);
|
||||
const heartbeatStaleMs =
|
||||
Number.isFinite(configuredHeartbeatStaleMs)
|
||||
? Math.max(intervalMs * 2, configuredHeartbeatStaleMs)
|
||||
: Math.max(intervalMs * 2, 90_000);
|
||||
let sweepActive = false;
|
||||
const sweep = async () => {
|
||||
if (sweepActive) return;
|
||||
sweepActive = true;
|
||||
try {
|
||||
const heartbeatRecovery =
|
||||
await agentRunGateway.recoverStaleRunningRuns({
|
||||
staleMs: heartbeatStaleMs,
|
||||
limit: 20,
|
||||
dryRun: false,
|
||||
reason: 'worker_heartbeat_stale_recovery',
|
||||
});
|
||||
const result =
|
||||
await agentRunGateway.dispatchQueuedRuns();
|
||||
const recovered =
|
||||
Number(heartbeatRecovery?.recovered ?? 0) +
|
||||
Number(result?.staleRecovery?.recovered ?? 0);
|
||||
if (recovered > 0) {
|
||||
logger.warn(
|
||||
`[AgentRun] recovered ${recovered} stale run(s)`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(
|
||||
'[AgentRun] background recovery sweep failed:',
|
||||
error instanceof Error ? error.message : error,
|
||||
);
|
||||
} finally {
|
||||
sweepActive = false;
|
||||
}
|
||||
};
|
||||
void sweep();
|
||||
const timer = setIntervalFn(sweep, intervalMs);
|
||||
timer?.unref?.();
|
||||
return timer;
|
||||
}
|
||||
|
||||
export function createPortalRunDeliverablesValidator({
|
||||
h5Root,
|
||||
resolveMindSpaceUserPublishDirFn =
|
||||
@@ -154,8 +226,14 @@ export function bootstrapPortalGatewayServices({
|
||||
createTkmindProxyFn = createTkmindProxy,
|
||||
createToolGatewayFn = createToolGateway,
|
||||
createAgentRunGatewayFn = createAgentRunGateway,
|
||||
createOrchestratorAdminConfigServiceFn =
|
||||
createOrchestratorAdminConfigService,
|
||||
createWorkflowShadowObserverFn =
|
||||
createWorkflowShadowObserver,
|
||||
createRunDeliverablesValidatorFn =
|
||||
createPortalRunDeliverablesValidator,
|
||||
startAgentRunRecoveryLoopFn =
|
||||
startPortalAgentRunRecoveryLoop,
|
||||
readAssetFileFn = fs.promises.readFile,
|
||||
} = {}) {
|
||||
if (
|
||||
@@ -207,6 +285,32 @@ export function bootstrapPortalGatewayServices({
|
||||
});
|
||||
const validateRunDeliverables =
|
||||
createRunDeliverablesValidatorFn({ h5Root });
|
||||
const workflowShadowObserver =
|
||||
createWorkflowShadowObserverFn({
|
||||
configService:
|
||||
createOrchestratorAdminConfigServiceFn(pool),
|
||||
logger: console,
|
||||
});
|
||||
let runtimeRoot;
|
||||
try {
|
||||
runtimeRoot = fs.realpathSync(h5Root);
|
||||
} catch {
|
||||
runtimeRoot = path.resolve(h5Root);
|
||||
}
|
||||
const workerIdentity = {
|
||||
workerId:
|
||||
String(
|
||||
env.MEMIND_AGENT_RUN_WORKER_ID ?? '',
|
||||
).trim() || `${process.pid}:${crypto.randomUUID()}`,
|
||||
runtimeRoot,
|
||||
buildId:
|
||||
String(
|
||||
env.MEMIND_RUNTIME_BUILD_ID ??
|
||||
env.MEMIND_RELEASE_ID ??
|
||||
env.GIT_COMMIT ??
|
||||
'dev',
|
||||
).trim() || 'dev',
|
||||
};
|
||||
const agentRunGateway = createAgentRunGatewayFn({
|
||||
pool,
|
||||
userAuth,
|
||||
@@ -218,6 +322,7 @@ export function bootstrapPortalGatewayServices({
|
||||
chatIntentRouter,
|
||||
sessionSnapshotService,
|
||||
conversationMemoryService,
|
||||
observeWorkflowRun: workflowShadowObserver,
|
||||
observePersonalMemoryOnSuccess: async ({
|
||||
userId,
|
||||
sessionId,
|
||||
@@ -242,6 +347,37 @@ export function bootstrapPortalGatewayServices({
|
||||
isSessionExternallyBusy: ({ sessionId }) =>
|
||||
isSessionPageDeliveryActive(sessionId),
|
||||
validateRunDeliverables,
|
||||
quiesceSessionOnTerminal: async ({
|
||||
sessionId,
|
||||
requestId,
|
||||
status,
|
||||
}) => {
|
||||
if (isDirectChatSessionId(sessionId)) {
|
||||
return { removed: [], skipped: true };
|
||||
}
|
||||
const target =
|
||||
await tkmindProxy.resolveTarget(sessionId);
|
||||
const sessionApiFetch = (pathname, init) =>
|
||||
tkmindProxy.apiFetchTo(
|
||||
target,
|
||||
pathname,
|
||||
init,
|
||||
);
|
||||
const cancellation =
|
||||
status === 'failed'
|
||||
? await cancelSessionActiveRequest(
|
||||
sessionApiFetch,
|
||||
sessionId,
|
||||
requestId,
|
||||
)
|
||||
: { cancelled: false, skipped: true };
|
||||
const quiesced =
|
||||
await quiesceSessionStdioExtensions(
|
||||
sessionApiFetch,
|
||||
sessionId,
|
||||
);
|
||||
return { ...quiesced, cancellation };
|
||||
},
|
||||
autoDispatch: isEnabledFlag(
|
||||
env.MEMIND_AGENT_RUN_AUTODISPATCH,
|
||||
'1',
|
||||
@@ -253,12 +389,19 @@ export function bootstrapPortalGatewayServices({
|
||||
env.MEMIND_AGENT_RUN_TIMEOUT_MS ??
|
||||
15 * 60 * 1000,
|
||||
),
|
||||
workerIdentity,
|
||||
});
|
||||
const agentRunRecoveryTimer =
|
||||
startAgentRunRecoveryLoopFn(
|
||||
agentRunGateway,
|
||||
{ env, logger: console },
|
||||
);
|
||||
|
||||
return {
|
||||
tkmindProxy,
|
||||
toolGateway,
|
||||
agentRunGateway,
|
||||
agentRunRecoveryTimer,
|
||||
validateRunDeliverables,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,6 +3,9 @@ import {
|
||||
resolveAnalyticsOwnerSegment,
|
||||
sendMindSpaceAnalyticsEvent,
|
||||
} from '../mindspace-analytics.mjs';
|
||||
import {
|
||||
sendMindSpaceRybbitEvent,
|
||||
} from '../mindspace-rybbit.mjs';
|
||||
import { createPageEditSessionService } from '../mindspace-page-edit-session.mjs';
|
||||
import { resolveMindSpaceRuntimeConfig } from '../mindspace-runtime-config.mjs';
|
||||
import { createNotificationDispatcher } from '../notification-dispatcher.mjs';
|
||||
@@ -26,6 +29,7 @@ export async function bootstrapPortalIntegrationServices({
|
||||
systemDisclosurePolicyService,
|
||||
sessionSnapshotService,
|
||||
mindSpaceAnalyticsConfig,
|
||||
mindSpaceRybbitConfig,
|
||||
subscriptionService,
|
||||
apiTarget,
|
||||
apiSecret,
|
||||
@@ -41,6 +45,8 @@ export async function bootstrapPortalIntegrationServices({
|
||||
resolveAnalyticsOwnerLabel,
|
||||
sendMindSpaceAnalyticsEventFn =
|
||||
sendMindSpaceAnalyticsEvent,
|
||||
sendMindSpaceRybbitEventFn =
|
||||
sendMindSpaceRybbitEvent,
|
||||
createNotificationDispatcherFn =
|
||||
createNotificationDispatcher,
|
||||
startScheduleReminderWorkerFn =
|
||||
@@ -135,20 +141,19 @@ export async function bootstrapPortalIntegrationServices({
|
||||
artifacts = [],
|
||||
}) => {
|
||||
for (const artifact of artifacts) {
|
||||
void sendMindSpaceAnalyticsEventFn({
|
||||
config: mindSpaceAnalyticsConfig,
|
||||
const pageOwner =
|
||||
(await userAuth
|
||||
?.getUserById(userId)
|
||||
.catch(() => null)) ?? {};
|
||||
const analyticsPayload = {
|
||||
eventName: 'page_generated',
|
||||
ownerId: userId,
|
||||
ownerSegment:
|
||||
resolveAnalyticsOwnerSegmentFn(
|
||||
(await userAuth
|
||||
?.getUserById(userId)
|
||||
.catch(() => null)) ?? {},
|
||||
pageOwner,
|
||||
),
|
||||
ownerLabel: resolveAnalyticsOwnerLabelFn(
|
||||
(await userAuth
|
||||
?.getUserById(userId)
|
||||
.catch(() => null)) ?? {},
|
||||
pageOwner,
|
||||
),
|
||||
pageId: artifact.relativePath,
|
||||
publicationId: sessionId,
|
||||
@@ -158,6 +163,14 @@ export async function bootstrapPortalIntegrationServices({
|
||||
artifact.url ||
|
||||
artifact.relativePath ||
|
||||
'/',
|
||||
};
|
||||
void sendMindSpaceAnalyticsEventFn({
|
||||
config: mindSpaceAnalyticsConfig,
|
||||
...analyticsPayload,
|
||||
});
|
||||
void sendMindSpaceRybbitEventFn({
|
||||
config: mindSpaceRybbitConfig,
|
||||
...analyticsPayload,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -112,6 +112,7 @@ function createSetup(overrides = {}) {
|
||||
},
|
||||
sessionSnapshotService,
|
||||
mindSpaceAnalyticsConfig: { enabled: true },
|
||||
mindSpaceRybbitConfig: { enabled: true },
|
||||
subscriptionService,
|
||||
apiTarget: 'http://api',
|
||||
apiSecret: 'secret',
|
||||
@@ -151,6 +152,10 @@ function createSetup(overrides = {}) {
|
||||
calls.push(['analytics', event]);
|
||||
return Promise.resolve();
|
||||
},
|
||||
sendMindSpaceRybbitEventFn(event) {
|
||||
calls.push(['rybbit', event]);
|
||||
return Promise.resolve();
|
||||
},
|
||||
createNotificationDispatcherFn(receivedOptions) {
|
||||
calls.push(['notification-dispatcher']);
|
||||
notificationOptions = receivedOptions;
|
||||
@@ -333,7 +338,7 @@ test('preserves generated-page analytics projection', async () => {
|
||||
assert.equal(
|
||||
setup.calls.filter(([name]) => name === 'get-user')
|
||||
.length,
|
||||
2,
|
||||
1,
|
||||
);
|
||||
const analyticsCall = setup.calls.find(
|
||||
([name]) => name === 'analytics',
|
||||
@@ -350,6 +355,13 @@ test('preserves generated-page analytics projection', async () => {
|
||||
channel: 'wechat_mp',
|
||||
url: 'https://example/page',
|
||||
});
|
||||
const rybbitCall = setup.calls.find(
|
||||
([name]) => name === 'rybbit',
|
||||
);
|
||||
assert.deepEqual(rybbitCall[1], {
|
||||
...analyticsCall[1],
|
||||
config: { enabled: true },
|
||||
});
|
||||
});
|
||||
|
||||
test('preserves notification, recharge, reminder, and page-edit wiring', async () => {
|
||||
|
||||
@@ -11,6 +11,9 @@ import {
|
||||
resolveAnalyticsOwnerSegment,
|
||||
sendMindSpaceAnalyticsEvent,
|
||||
} from '../mindspace-analytics.mjs';
|
||||
import {
|
||||
sendMindSpaceRybbitEvent,
|
||||
} from '../mindspace-rybbit.mjs';
|
||||
import {
|
||||
markPageDeliveryContractReady,
|
||||
preparePageDeliveryContract,
|
||||
@@ -58,6 +61,7 @@ export function attachPortalSessionRoutes(
|
||||
getMindSpaceAssets = () => null,
|
||||
getMemoryV2 = () => null,
|
||||
getMindSpaceAnalyticsConfig = () => null,
|
||||
getMindSpaceRybbitConfig = () => null,
|
||||
isWorkspaceMaintenanceEnabled = () => false,
|
||||
ownsAgentSession,
|
||||
unregisterAgentSessionForUser,
|
||||
@@ -86,6 +90,8 @@ export function attachPortalSessionRoutes(
|
||||
materializePublicHtmlWritesFromSessionEvent,
|
||||
sendMindSpaceAnalyticsEventFn =
|
||||
sendMindSpaceAnalyticsEvent,
|
||||
sendMindSpaceRybbitEventFn =
|
||||
sendMindSpaceRybbitEvent,
|
||||
resolveAnalyticsOwnerSegmentFn =
|
||||
resolveAnalyticsOwnerSegment,
|
||||
resolveAnalyticsOwnerLabelFn =
|
||||
@@ -419,8 +425,7 @@ export function attachPortalSessionRoutes(
|
||||
continue;
|
||||
}
|
||||
generationAnalyticsEvents.add(relativePath);
|
||||
void sendMindSpaceAnalyticsEventFn({
|
||||
config: getMindSpaceAnalyticsConfig(),
|
||||
const analyticsPayload = {
|
||||
eventName: 'page_generated',
|
||||
ownerId: req.currentUser.id,
|
||||
ownerSegment:
|
||||
@@ -433,6 +438,14 @@ export function attachPortalSessionRoutes(
|
||||
channel: 'h5',
|
||||
url:
|
||||
`/${publishRootDir}/${req.currentUser.id}/${relativePath}`,
|
||||
};
|
||||
void sendMindSpaceAnalyticsEventFn({
|
||||
config: getMindSpaceAnalyticsConfig(),
|
||||
...analyticsPayload,
|
||||
});
|
||||
void sendMindSpaceRybbitEventFn({
|
||||
config: getMindSpaceRybbitConfig(),
|
||||
...analyticsPayload,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -8,12 +8,16 @@ import {
|
||||
resolveAnalyticsOwnerLabel,
|
||||
resolveAnalyticsOwnerSegment,
|
||||
} from '../mindspace-analytics.mjs';
|
||||
import {
|
||||
injectMindSpaceRybbit,
|
||||
} from '../mindspace-rybbit.mjs';
|
||||
import {
|
||||
resolveMindSpacePublicRequest,
|
||||
} from '../mindspace-public-route.mjs';
|
||||
import {
|
||||
buildPublishedHtmlViewContext,
|
||||
parseMindSpacePublishFilePath,
|
||||
resolveMindSpacePageDataContext,
|
||||
} from '../mindspace-public-page-context.mjs';
|
||||
import {
|
||||
preparePublicHtmlAssetDelivery,
|
||||
@@ -71,6 +75,7 @@ export function createPortalWorkspacePublicationDelivery({
|
||||
h5Root,
|
||||
internalAgentSecret,
|
||||
analyticsConfig,
|
||||
rybbitConfig,
|
||||
getAuthPool = () => null,
|
||||
getUserAuth = () => null,
|
||||
getMindSpacePages = () => null,
|
||||
@@ -90,6 +95,8 @@ export function createPortalWorkspacePublicationDelivery({
|
||||
resolveClosestHtmlRelativePath,
|
||||
ensureWorkspaceThumbnail =
|
||||
ensureWorkspaceHtmlThumbnail,
|
||||
resolvePageDataContext =
|
||||
resolveMindSpacePageDataContext,
|
||||
getDeliveryContract =
|
||||
getPageDeliveryContract,
|
||||
ensureThumbnail = ensureThumbnailPng,
|
||||
@@ -271,26 +278,20 @@ export function createPortalWorkspacePublicationDelivery({
|
||||
.catch(() => null)
|
||||
: null;
|
||||
let pageDataContext = null;
|
||||
const mindSpacePages =
|
||||
getMindSpacePages();
|
||||
const mindSpacePages = getMindSpacePages();
|
||||
if (
|
||||
mindSpacePages &&
|
||||
parsedPublishPath?.userId &&
|
||||
parsedPublishPath.relativePath
|
||||
) {
|
||||
const page = await mindSpacePages
|
||||
.findPageByRelativePath(
|
||||
parsedPublishPath.userId,
|
||||
parsedPublishPath.relativePath,
|
||||
)
|
||||
.catch(() => null);
|
||||
if (page?.id) {
|
||||
pageDataContext = {
|
||||
pageId: page.id,
|
||||
accessMode:
|
||||
page.publicationAccessMode ?? null,
|
||||
};
|
||||
}
|
||||
pageDataContext =
|
||||
await resolvePageDataContext({
|
||||
pool: getAuthPool(),
|
||||
pageService: mindSpacePages,
|
||||
userId: parsedPublishPath.userId,
|
||||
relativePath:
|
||||
parsedPublishPath.relativePath,
|
||||
logger,
|
||||
});
|
||||
}
|
||||
html = injectMindSpaceAnalytics(html, {
|
||||
ownerId:
|
||||
@@ -310,6 +311,24 @@ export function createPortalWorkspacePublicationDelivery({
|
||||
'',
|
||||
config: analyticsConfig,
|
||||
});
|
||||
html = injectMindSpaceRybbit(html, {
|
||||
ownerId:
|
||||
parsedPublishPath?.userId ?? '',
|
||||
ownerSegment:
|
||||
resolveAnalyticsOwnerSegment(
|
||||
pageOwner ?? {},
|
||||
),
|
||||
ownerLabel: resolveAnalyticsOwnerLabel(
|
||||
pageOwner ?? {},
|
||||
),
|
||||
pageId:
|
||||
pageDataContext?.pageId ?? '',
|
||||
publicationId:
|
||||
pageDataContext?.publicationId ??
|
||||
pageDataContext?.publication_id ??
|
||||
'',
|
||||
config: rybbitConfig,
|
||||
});
|
||||
const decorated =
|
||||
decorateMindSpacePublishedHtml({
|
||||
html,
|
||||
|
||||
Reference in New Issue
Block a user