Omniscio documentation
Browse all documentation
  1. Getting Started19
  2. Sessions & Agents135
  3. Inbox & Notifications68
  4. Projects & Tasks98
  5. Automation & Scheduling76
  6. Knowledge & Memory28
  7. AI Features71
  8. Integrations112
  9. Plugins & Marketplace41
  10. Cloud & Teams52
  11. Settings & Customization76
  12. Account & Billing28
  13. Troubleshooting80
  14. CLI & API Reference28
  15. Legal & Policies5
  16. Uncategorised17

Plugin Bridge Capabilities (part 3)

Part 3 of the plugin bridge: the session capabilities — spawning and driving a Claude session, reading the user's granted past session history, and watching a session live or through an ambient status feed.

What it is

This is part 3 of the Plugin Bridge Capabilities page. Part 1 covers the permission model and the host-primitive capabilities; part 2 covers the data, file and output capabilities. This part covers everything a plugin can do with a session: start and drive one, read the user's granted history, and observe activity as it happens.

Where to find it

There is no Omniscio menu that opens this surface — a plugin reaches it through the sandboxed bridge object it is handed. What a user meets directly is the consent: each capability below names the permission it takes, and the resulting grants are managed in Settings → Plugins.

How it behaves

ctx.sessions.* — start and drive a Claude session (permission: sessions)

Lets a plugin spawn a Claude Code session and talk to it — the "go do this work and tell me when it's done" capability (e.g. Test Tracker running a suite and reading back the failures). Every session it starts spends the user's own Claude quota or API credit, so this is the one namespace where a loop bug costs real money.

One optional argument decides the blast radius:

  • No scope → the plugin's own sandbox. The session runs in the plugin's private virtual project (__plugin_<id>__), which is not a directory in any repository of the user's. This is the original behaviour and rides on the sessions permission alone.

  • With a scope → a real repository the user granted. { projectId, worktree } puts the session in an actual working copy with full tool access — it writes files and runs commands there. Three things must hold at once, and any one missing refuses the spawn:

    1. the sessions permission, declared in manifest.json;
    2. the workspace.exec tier in that same manifest — a session that writes and runs cannot be authorized by a grant whose consent copy told the user "it cannot change or delete anything, and it cannot run anything";
    3. a live per-project workspace grant for that exact project — the same grant ctx.workspace uses, revocable from Settings → Plugins → "Folders this plugin can reach".
  • The grant is re-checked on every later call, not just at spawn. sendMessage, getMessages, getStatus and stop each re-read it, so revoking a project stops a plugin driving a session that is already running.

  • worktree: null means the project's main checkout; a named worktree means that working copy. The host resolves the directory from the grant itself and never re-derives it from what the caller asked for, so a path outside the granted set cannot be reached.

  • Ownership is enforced per call. A plugin reaches only the sessions it started; another plugin's session id — or one of the user's own — is refused.

  • A background session is read by your plugin, not a person. A sandbox session the user did not open (userInitiated unset) is hidden from the inbox, so Omniscio gives it none of its person-facing instructions or standing reminders and sends it no check-ins: the last assistant message is the agent's own answer (hidden-plugin-session-contract.md). The stored text can still open with the app's activity lines (▸ …) and a [[OMNISCIO_FINAL]] line — strip those before showing it.

  • One size cap for every message. create's prompt, launchBuild's prompt and each sendMessage accept up to 200,000 characters (PLUGIN_SESSION_MESSAGE_MAX); a longer one is refused with a bridge argument error.

const { sessionId } = await ctx.sessions.create({
  prompt: 'run the test suite and summarise the failures',
  scope: { projectId, worktree: null } // omit `scope` entirely for the plugin's own sandbox
})

await ctx.sessions.sendMessage(sessionId, 'now fix the first failure')
const status = await ctx.sessions.getStatus(sessionId)
await ctx.sessions.stop(sessionId)

Saying an AI started the session — worker backend only. ctx.sessions.create takes an optional startedBy: 'me' | 'agent' beside userInitiated: true. 'agent' keeps the session visible but makes it screened by the Agent Firewall; 'me' or leaving it out changes nothing; anything else is refused. The answer carries startedBy: 'agent' when Omniscio honoured it, so a plugin on an older Omniscio (which ignores the key) can notice the field is missing and refuse to forward untrusted text:

const created = await ctx.sessions.create({ prompt, userInitiated: true, startedBy: 'agent' })
if (created.startedBy !== 'agent') throw new Error('This Omniscio cannot screen AI-started sessions.')

Polling a transcript by cursor — worker backend only. On the worker leg, ctx.sessions.getMessages(id, { after, limit }) lets a plugin ask "what's new since I last looked" instead of re-reading the whole conversation:

const first = await ctx.sessions.getMessages(sessionId) // newest page; no cursor needed
let cursor = first.at(-1)?.cursor
const next = await ctx.sessions.getMessages(sessionId, { after: cursor, limit: 200 })
  • A message is { id, role, text, answer, timestamp, cursor } — the body field is text, not content. Each message carries the opaque cursor pointing at it, so a poller resumes by handing back the last one it saw.
  • Read answer for what the AI said. text is the stored transcript: Omniscio's activity lines (▸ Bash: …, ← result received, ▸ Extended thinking), the final-message marker and any summary card sit in it, and a plugin that parses those (Test Tracker does) keeps them. answer is the AI's own words with all of that removed — '' for a row that is only activity. On your own messages answer equals text.
  • The cursor survives an app restart — both halves of the position are persisted database values, unlike an in-memory high-water mark.
  • A cursor-less call returns the NEWEST page, not the start of the transcript, so a plugin's first tick sees the present rather than ancient history.
  • Partial (streaming) rows are included here, and only here. With no push primitive on the worker leg, a partial row is how a poller sees a turn in progress rather than a silent gap until it completes. The webview reads filter them; the divergence is deliberate.

The webview leg differs. AgentMC.sessions.create takes the same scope under the same three gates, but AgentMC.sessions.getMessages(id) is the older full-transcript read — no cursor, partials filtered, and its body field is content, next to the same answer — show answer, not content, when you display what the AI said. It also exposes rename, getCost, launchBuild and launchWithDraft, which the worker leg does not; the worker leg exposes onStatusChange / offStatusChange, which the webview does not.

The plugin data fence (storage/CLI side). The host's generic POST/PATCH/DELETE /plugins/:id/db/:collection write routes refuse a WHOLE collection (403 PLUGIN_COLLECTION_NOT_AGENT_WRITABLE, whatever key the caller holds) unless the manifest opts it in with agentWritable: true; cliReadOnlyColumns then narrows an opted-in collection. The key-value store (ctx.storage) is fenced the same way: PUT/DELETE /plugins/:id/kv/:key answer 403 PLUGIN_KV_NOT_AGENT_WRITABLE unless the manifest sets storage.kv.agentWritable: true, while GET /plugins/:id/kv lists every key with its value to any caller, so a plugin keeps no secret there (ctx.secrets is the encrypted store). A setting may carry agentWritable: true too: a CLI PATCH /plugins/:id/settings applies at once only when every named setting has it, otherwise it becomes the person's approval card, and a password setting is never writable from the CLI. Older hosts strip these keys but still require storage.collections. Rules: plugin-command-trust-contract.md.

fanout is a separate permission. Batch-spawning across arbitrary projects is gated on sessions.launchAny rather than sessions, because it fires paid sessions into projects the user never picked.


sessionHistory — read the user's past Omniscio work (permission: sessions.readHistory)

Lets a plugin read the user's past session/project conversation history — the "build something from my existing work" capability (e.g. Decks turning your sessions into a deck). Because that history can contain anything the user ever typed, it is governed by a strict scoped + audited + persisted privacy model, which the host enforces:

  • Default-deny, user-scoped. A plugin reads nothing until the user grants it. Call requestAccess to pop a picker; the user chooses which projects/sessions to hand over. A project grant transitively covers that project's (non-hidden) sessions.
  • Text-only. getMessages returns a { id, role, content, timestamp }[] projection with content reduced to plain text and system rows dropped — raw tool-use / tool-result blocks (which can carry shell output, file contents, secrets) are never handed over. An AI message's content is its answer alone: no activity lines, so no command the AI ran.
  • Audited + revocable. Every read is appended to a host-owned audit table the plugin cannot touch, and grants persist and are revocable from Settings → Plugins → "Session history access", which also shows the read log.
// 1) ask the user to grant scope (opens the picker; resolves with what they granted)
await AgentMC.sessionHistory.requestAccess({ kinds: ['session', 'project'] }) // kinds optional

// 2) enumerate what you're allowed to see
const projects = await AgentMC.sessionHistory.listProjects() // granted projects
const sessions = await AgentMC.sessionHistory.listSessions() // granted sessions (silent lanes filtered out)

// 3) read a granted session's transcript (throws if not granted)
const messages = await AgentMC.sessionHistory.getMessages({ sessionId })

Watching a session live — host.sessionOutput and sessions.observeStatus

Two separate, deliberately narrow ways to see activity as it happens. They are not interchangeable, and neither one hands over a transcript.

host.sessionOutput — live output from a session your plugin OWNS (no extra permission)

A plugin panel that launched a session (via ctx.sessions.create) can watch that session's output as it streams, instead of polling getMessages:

const unsubscribe = AgentMC.events.on('host.sessionOutput', (data) => {
  // data = { sessionId, text, source, messageId?, streaming?, timestamp? }
  appendToMyView(data.text)
})
// later
unsubscribe()

AgentMC.events.on(channel, cb) returns an unsubscribe function — call it when your view unmounts. (Subscriptions are also released automatically when the panel reloads or closes.)

Webview panels only, today. Delivery is scoped to the plugin's panel webContents. A v2 worker backend does not receive this channel — its ctx.events namespace exposes emit only. A headless plugin that wants to follow a session should page the durable ctx.sessions.getMessages(id, { after }) cursor instead, which is the reliable path either way.

What the host guarantees:

  • Owner-only. Delivery is scoped to the plugin whose ownership tag is on that session. A plugin never receives output from a session it did not launch, and this rides the per-plugin dispatch, never the all-plugins broadcast.
  • Revocable. For a scope-spawned session the project's workspace grant is re-checked before each delivery, so revoking that project in Settings stops the stream.
  • Trimmed. You get the text and its timing. You do not get attachments (which carry the user's real on-disk filenames), the free-form metadata record, sidechain ids, or the comment/tool counts. It can be widened on request; it will not be narrowed back.
  • Batched, not per-fragment. Output is gathered over a short window and delivered as one event with the text concatenated, so a busy session does not become an event storm.
  • Not a correctness channel. Treat it as a progress hint. If your plugin misses an event it must still be correct from the durable ctx.sessions.getMessages(id, { after }) cursor read.

sessions.observeStatus — the ambient status feed for an overlay (standard tier)

An overlay plugin (a companion, a status pet, an activity HUD) can react to every session starting, finishing, or needing attention. Declare it in your manifest:

{ "permissions": ["sessions.observeStatus"] }
  • It carries status only — never message text. The consent copy says exactly that.
  • Unlike host.sessionOutput it is not scoped to your own sessions: reacting to the user's whole workspace is the point of an ambient overlay. That breadth is why it takes its own declared permission rather than riding the broad sessions one, which now also gates spawning agents.
  • An overlay whose plugin does not declare it receives nothing (the host logs the refusal once).
  • Do not confuse it with ctx.sessions.onStatusChange, which is a per-session subscription for a session you own and rides the plain sessions permission.

For agents

The source paths for the whole bridge are on part 1: src/main/ipc/bridge-method-schemas.ts for every method and its argument shapes, and src/shared/plugin-permissions.ts for the permission each namespace requires. The session capabilities below are backed by the session service on the main side and the plugin's own dispatch, never by a renderer path.

Related

Last verified 2026-10-17