Apps SDK state management flaws

There is a bit of a conundrum here. I’m trying to manage my apps state lifecycle across multiple instances and refreshes.

  • toolOutput is whatever the last MCP response was for this App instance (but sometimes it isn’t, after a refresh its the original toolOutput that made the instance)

  • widgetState is something you set, it allows you to persist changes in its view across refreshes and instances.

In my app, currently I have it so we load from widgetState but whenever toolOutput changes we overwrite widgetState to persist it (a new MCP response came in because the user interacted or the initial response that created the instance came in). So whenever widgetState changes, the user sees that in the app instance basically.

User asks ChatGPT to draft a study:

  1. widgetState does not exist

  2. toolOutput delivers the study

  3. we set widgetState

User asks ChatGPT to edit the study:

  1. we immediately close the old one instance

  2. ChatGPT opens a new instance of our App

  3. widgetState exists so we render that while we wait for toolOutput

  4. toolOutput comes back with edit and then we update widgetSate

User deletes a step in the study with the button:

  1. we optimistically update widgetState

  2. new toolOutput arrives

  3. we update widgetState

Everything is correct until here. But then the user refreshes the window.

  1. widgetState exists and we load

  2. toolOutput exists too BUT it contains the original toolOutput from the edit (this is a quirk of how ChatGPT apps work)

  3. we overwrite widgetState with the old toolOutput

User loses their delete on refresh (the step is actually deleted but in ChatGPT it looks like it isn’t)

Because of the fact that ChatGPT changes the definition of toolOutput on refresh, you have no way of knowing if toolOutput is newer or older without building in something like timestamping (which comes with its own issues) into your MCP output to handle this.

Anyone else found a more elegant solution?

Timestamps can cause collisions (unlikely, but very possible). You’re probably going to need to add some sort of incrementing version number in your tool response. Then check it.

const toolOutputVersion = window.openai.toolOutput?.version ?? -1
const widgetStateVersion = window.openai.widgetState?.version ?? -1

if (toolOutputVersion > widgetStateVersion) {
  window.openai.setWidgetState({
    …window.openai.toolOutput,
  });
}

This is the classic synchronization problem. The usual (and simplest) solution is going to be versioning.

I hope that helps.