Editor text not being saved in notebook model

I’m using jupyterlab components via the npm packages to build a custom interface. The cells are rendering correctly and notebook actions work as expected, but code can’t be executed because cell.model.sharedModel.getSource() is always an empty string. Relevant code from CodeCell:

(function (CodeCell) {
    /**
     * Execute a cell given a client session.
     */
    async function execute(cell, sessionContext, metadata) {
        var _a;
        const model = cell.model;
        const code = model.sharedModel.getSource();
        if (!code.trim() || !((_a = sessionContext.session) === null || _a === void 0 ? void 0 : _a.kernel)) {
            model.sharedModel.transact(() => {
                model.clearExecution();
            });
            return;
        }

This is the code I’m using to instantiate the notebook (based on the code in /examples/notebook)

  const languages = new EditorLanguageRegistry();
  const factoryService = new CodeMirrorEditorFactory({ languages });
  const mimeTypeService = new CodeMirrorMimeTypeService(languages);
  const rendermime = new RenderMimeRegistry({ initialFactories });
  const editorFactory = factoryService.newDocumentEditor;
  const contentFactory = new Notebook.ContentFactory({ editorFactory });

  const context = {
    session: {
      kernel: new BackgroundKernel(),
    },
  } as ISessionContext;

  const nb = new Notebook({
    rendermime,
    mimeTypeService,
    contentFactory,
  });

  nb.model = new NotebookModel({
    defaultCell: 'code',
  });

  const panel = new Panel();
  panel.id = 'main';
  panel.addWidget(nb);
  Widget.attach(panel, container);

I’m using a custom ISessionContext because I’m loading the UI in a Chrome extension and the network calls to the notebook server need to be made from the background script. BackgroundKernel implements IKernelConnection and simply proxies the requests to the extension’s background script. I can post the full code for this, but it’s quite long so leaving out for now.

I realise this is an unusual setup so expect it’s something I’m doing wrong, but can’t figure out what’s preventing the shared model’s source being updated with editor state. Any suggestions or pointers very much appreciated.

FWIW, this was working ~2 weeks ago, but stopped after an update. The version that worked had Notebook.defaultContentFactory, which was removed at somepoint.