merge: integrate langgraph execution runtime

# Conflicts:
#	agent-run-gateway.test.mjs
#	capabilities.mjs
#	package.json
#	server.mjs
This commit is contained in:
john
2026-07-25 00:09:15 +08:00
88 changed files with 14319 additions and 90 deletions
@@ -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 () => {
+15 -2
View File
@@ -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,