Plugin Run Keep-Alive
A plugin doing long work in its own page (a deck build, a narrated video) declares it with `runs.start`, beats with `runs.heartbeat` and ends it with `runs.finish`. While a run is live, opening another plugin no longer destroys that plugin's screen, and if the screen dies anyway the user gets one inbox card saying what stopped, why, and how to reopen the plugin.
What it is
Omniscio normally keeps only the last plugin you opened mounted in the background. Before this, a
run living in a plugin's page died the moment you opened a second plugin, popped the first one out
or quit, and nothing told you. The runs bridge methods let a plugin say "I'm working", so the
host keeps that screen alive and never lets the work stop silently. It needs no manifest
permission: a run can only keep the calling plugin's own screen alive.
Where to find it
There is nothing to switch on in Omniscio. A plugin calls the three methods from its own page through the bridge object the host hands it. What a user sees is the result: a busy plugin keeps working while they look at another one, and if a run is cut short, a card titled "<Plugin> stopped before it finished" appears in the inbox with an Open the plugin button.
How it behaves
if (typeof AgentMC.runs?.start === 'function') {
const res = await AgentMC.runs.start({
runId: 'motion-q3', // your id, 1-200 chars; starting it again just refreshes it
label: "Motion for 'Q3 pitch'", // shown to the user if the run dies, 200 chars max
inboxItemIds: ['motion-q3'] // optional: your own inbox.setItems / sidebar rows to clear then
})
// res: { ok: true, heartbeatEveryMs: 30000, staleAfterMs: 180000, maxRunMs: 7200000 }
// or { ok: false, reason: 'too-many-runs' | 'no-screen' }
}
await AgentMC.runs.heartbeat('motion-q3', { label: "Motion for 'Q3 pitch': slide 6 of 18" })
await AgentMC.runs.finish('motion-q3') // on success, failure AND cancel
- The screen stays alive while any run is live, up to 3 plugin screens in total. Past that cap the oldest screen that is not the one on view is let go.
- A run never stops silently. If the screen closes, crashes or reloads, Omniscio quits, or no heartbeat arrives for 3 minutes (plus a 60-second check, so a laptop waking from sleep is not a stop), the host removes the rows you named and posts one card per plugin with your latest label, the reason and an Open button. The card is the app's own, so it arrives even when agent alerts are switched off.
- There are hard limits you cannot change. One run is kept for at most 2 hours (
maxRunMs), however faithfully it beats, and starting it again does not restart that clock. One plugin may have 5 live runs across all its windows; a sixthstartanswerstoo-many-runs. A beat sooner than 5 seconds after the last one is ignored, so beat about every 30 seconds. The limits are fixed in the host:startrefuses any field that tries to set them. A run stopped for running too long gets the same card, with its own reason. - The card is built by the host. It names the plugin by its manifest name and shows your label as one quoted phrase on a single line, so a label cannot pose as the card's own words. Only rows in your own plugin's lists are ever removed; naming another plugin's row does nothing.
- Popping a busy plugin out is refused with a plain message, because the pop-out would replace the screen the run lives in.
- Beat at least every 30 seconds. A hidden screen's timers can slow to once a minute, and the
3-minute window allows for that.
heartbeatanswers{ ok: false, reason: 'unknown-run' }when the host no longer knows the run, so callstartagain. - On a phone the methods succeed silently and return nothing, because the phone's own browser
holds the page. Read
res?.okrather than assuming an answer.
For agents
The host side is runs-handler.ts (the bridge namespace) over plugin-run-registry.ts (runs keyed
by the calling screen), and the dashboard keeps screens through use-kept-plugin-screens.ts. The
card's key is plugin-run-stopped:<pluginId>. The rules are in
.claude/memory/contracts/plugin-run-keep-alive-contract.md.
Related
The rest of what a plugin can call is on the Plugin Bridge Capabilities page and its second part,
which also covers the inbox rows (inbox.setItems) and cards (inbox.postAlert) a run names.
Last verified 2026-10-14