Skip to content

feat(server,web,mobile): let plugins add context to the turns you send - #16063

Open
saphid wants to merge 91 commits into
pingdotgg:mainfrom
saphid:stack/20-context-transforms
Open

saphid wants to merge 91 commits into
pingdotgg:mainfrom
saphid:stack/20-context-transforms

Conversation

@saphid

@saphid saphid commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #16062 (and #15010). Review only the top commit: 98939ce.

Rebased 2026-10-10 onto #15010's current head (1fe8efd69a, on main 57b3780). Run context enrichment and its tests import the ID allocator, notification and provider failure helpers from @t3tools/provider-core, where main moved them, and type the allocator as IdAllocatorV2["Service"]. The thread stream and work-log presentation tests keep main's new compact-snapshot and live-thought cases beside the plugin context cases. On mobile, the expanded-row helper no longer splits the doc comment of formatItemFullDetail. Main moved the host process references into a HostProcess module (#17641), so the context enrichment test provides the process arguments through HostProcess.Arguments. Main now shows a T3 Code notice's own detail when a mobile activity row is expanded (#17723). The mobile expanded-row helper keeps that case beside the plugin context case: file reads first, then notices, then plugin context, then the generic detail. GPT-6.1 Sol (high) reviewed this port: SHIP. Captures below were taken at the revisions they name. At this head (98939ce7e9) these pass: focused tests (34 files, 716 tests), typecheck (@t3tools/mobile, t3, @t3tools/web, @t3tools/client-runtime, @t3tools/contracts), lint and fmt on the changed files, knip, lint:mobile.

iOS: this PR's mobile surfaces were also captured on an iPhone 17 Pro simulator (iOS 26.5), at the top-of-stack head 5ec8b17, which contains this PR: light-context-expanded. These are after-only captures; the before/after comparison below is on Android.

Problem

A plugin cannot give the agent context. Teams keep conventions, codenames, ticket notes and runbooks in places the agent cannot see, and the only way to supply them today is to paste them into every message. Plugins can already react to finished runs, offer tools and show statuses, but none of that reaches the prompt of the turn the user is sending.

Why this qualifies

This is the proposal route in CONTRIBUTING, and no maintainer has agreed to it yet. It needs two approvals:

  • The OV2 owner must approve how enrichment is recorded in orchestration: the plugin_context dynamic_tool item convention (including one overflow record keyed plugins), recording before any plugin code runs under the thread's command lock, the wire projection keeping those items' output, the interrupt closing a running record, and the rule that only turns the history pager counts are enriched. Nothing in the pager, wire schema or history selection changes.
  • The plugin-system approval on #6837, like the plugin PRs below it.

It stacks on the plugin notifications PR for ordering, and depends on the plugin host and event delivery PRs lower in the stack (the reserved t3.* handler names and PluginCatalog.invoke). If the answer is no, we close it; nothing above it in the stack depends on it except the approvals PR, which touches the same plugin child guard. Previous PR in this stack: feat(server,web,mobile): let plugins send notifications (#16062).

Fix

  • Author side (pluginTransforms.ts): capability transforms; manifest transforms: { enrich: { timeoutSeconds? } } (1–10 s, default 5), which needs the capability and "proposedApi": true or the plugin is refused at add. The plugin registers t3.transform.enrich, the one host-called name besides t3.tool.*; it receives { environmentId, projectId, threadId, runId, cwd, message: { text ≤ 16,000, truncated } } and answers { context: [{ title ≤ 100, text ≤ 6,000 }] } (≤ 4 entries, ≤ 8 KiB) or null.
  • When: in the provider-turn start effect, after the run is confirmed starting and before the provider session opens. Only for turns the history pager counts (isThreadHistoryUserTurn, the pager's own predicate, imported), minus / commands: steering (including steering that restarts a run), notifications and every agent/server wake are never enriched.
  • Recording: one guarded batch commits a running record per called plugin before any plugin code runs, under the thread's command lock (so Stop either sees and closes the records, or commits first and nothing is recorded or called); plugin calls run concurrently outside the lock; a second guarded batch saves each outcome before the provider starts. At most 4 plugins are called, in catalogue order; one more record says how many were not. At most 8 KiB of context is kept per run. A retried or replayed start reuses saved records and never calls a plugin twice.
  • Fail open: deadline, error, crash, oversize, malformed answer, disable mid-call or past the run's budget → a failed record with a reason (≤ 300 chars); the turn goes on without it.
  • Provider text: <plugin-context plugin="id" title="…">…</plugin-context> blocks ahead of the typed text, composed once in ProviderTurnStartService. The saved user message is unchanged.
  • Clients: the records are existing dynamic_tool items, so every client already decodes them. A shared pluginContextInspection (client-runtime) drives the web/desktop inspector and the mobile expanded detail: "From <plugin (id)>" and the context, or "Not added" and the reason, instead of the input JSON. The plugin details capability list now explains transforms: it reads your messages and can add context to them before they reach the agent (before this PR the server refused the capability, so the list showed it by name only).
  • Docs: docs/user/plugins.md gains a "Context" section. No internals doc.

Size: 35 files, +2744 / −67; 1,878 of the added lines are tests and fixtures.

Evidence

Environment: macOS arm64; this PR on top of the plugin notifications PR.

How to exercise it: isolated vp run dev; add the test fixture plugin (apps/server/src/plugins/testFixtures/contextPlugin, copied to a temp directory), approve and enable it; on a Claude thread send What is the project codename? Reply with only the codename.

Environment for the captures: isolated vp run dev with fresh state at the parent and at this PR (an earlier revision with an identical patch); the environment is labelled "Proof server". Web in headless Chromium; the built desktop app (vp run build:desktop, isolated profile) rendering the same threads; the Android app on an emulator; one vp run dev --share pass from a fresh browser over the non-loopback HTTPS origin. Turns used Claude Opus 5.5. The plugin is the layer's context test fixture, with its deadline raised to 10 s so Stop can be pressed by hand; for the overflow case, five copies with distinct ids.

Observed

Before (parent) After (this PR)
Add the plugin "this server does not support transforms." review explains transforms
"What is the project codename?" no row; the agent finds no codename "Added context from Context fixture" → "From Context fixture (test.context)", the context, reply PERIWINKLE-42; the bubble keeps the typed text
After light dark
[fail]: not added, with the reason; the reply still arrives
[wait] + Stop: "The run was interrupted before the plugin answered.", no spinner
Five plugins: four rows + "Context from 1 more plugin not added" ("At most 4 plugins add context to one run; 1 more was not called.")
Desktop (built app)
Android
  • Disable → the next turn has no row and the agent cannot find the codename; re-enable → the row returns: video,
  • Videos: send → row → expand → reply, wait + Stop, desktop, Android, before.
  • Remote (--share): the enriched row renders and expands, and disable/re-enable behaves the same over the remote origin: video
  • The prompt Claude received (read from its session transcript) begins with <plugin-context plugin="test.context" title="Project codename">The project codename is PERIWINKLE-42.</plugin-context> followed by the typed text.

Checks at this head (6bc32c0b3d), re-run 2026-10-05 (CI=true vp test run …, all exit 0):

  • server: src/plugins plus RunContextEnrichment, ProviderTurnStartService, runtimeLayer, WireProjection, ThreadStream: 26 files, 368 tests pass. They cover record-before-call and reuse on every retry with zero re-invocations; a cut-off call recorded as not added; fail-open mixes; the cap with 1,000 eligible plugins (records and batches constant); the run budget in UTF-8 bytes; Stop holding the real thread lock between read and commit (no records, no call); stale runs writing nothing; eligibility equal to the pager predicate for every intent × actor; steering restarts and wakes not enriched; context sent verbatim to the provider and reused on a session-open retry; the real orchestrator closing a running record on interrupt; the wire keeping these items' output; the snapshot staying well inside its byte budget with maximal answers. With real plugin processes: eligibility (consent, capability-only, disabled), manifest refusals, the input the plugin sees, fail-open answers, the declared deadline (TestClock), disable mid-call, a stale generation refused, five enabled plugins → four answers in catalogue order plus one overflow record and exactly four blocks in the provider text, and disable → no record and plain provider text → re-enable → enriched again at the new generation.
  • contracts pluginTransforms, plugin, pluginCatalog (18); client-runtime work-log/presentation, state/pluginContributions (105); web MessagesTimeline.test.tsx (74; mounted timeline, row clicked, inspector text read: context and reason shown, input not shown); mobile threadActivity.test.ts (79; expanded detail for completed, failed and interrupted records).
  • Recorded on an earlier revision with an identical patch: on the parent, the new server and contracts suites do not load (new modules); the wire, interrupt, inspector (web, mobile, client-runtime) tests fail on their assertions. Mutations: removing the 4-plugin cap fails the overflow test; ignoring the enabled state fails the disable/re-enable test.
  • vp run --filter typecheck for contracts, t3, client-runtime, web and mobile; vp lint --report-unused-disable-directives and vp fmt --check on the touched files (26 warnings, all on unchanged lines, the same 26 at the parent); vp run knip:check; vp run lint:mobile; web build; vp run build:desktop; node scripts/release-smoke.ts. All pass.

Surfaces

  • Entry points: the turn the user sends (composer, queued turn). Steering, / commands and server-started turns are deliberately not enriched. The way out is disabling or removing the plugin in Settings > Plugins; it takes effect from the next turn.
  • Clients: web and desktop (V2ItemInspector and the timeline's fallback body), mobile (expanded row detail and copy text, iOS and Android). All clients already render the rows as integration tool rows.
  • Providers: the start service composes the text once; every adapter reads it. Claude: receives the blocks (live-proved during development); its adapter-buffered wakes are not enriched. Codex: receives them on turn/start; background-command wakes and native restart resumes are not enriched. Cursor: receives them; no continuations. Grok (ACP): receives them; post-settle continuations send no prompt and are not enriched. OpenCode (v1 and 2): receives them; OpenCode 2 continuation wakes are not enriched. Antigravity: receives them; no continuations. Pi: receives them through its prompt payload; no continuations. Only Claude was run live; the others are by code reading plus the server-side gate tests.
  • Contracts: additive: one schema module, one optional manifest field, one forward-compatible catalogue field. Old client + new server: rows render as generic tool rows (no context shown). New client + old server: no rows; a plugin declaring transforms is refused at add.
  • Reverse states: disable/remove stops enrichment from the next turn and fails an in-flight call; re-enable resumes it under a new generation; Stop closes a running record.
  • Connection modes: server-side; records travel inside the existing snapshot and stream budget, unchanged across local, remote/relay and tunnel.
  • Docs: new "Context" section in docs/user/plugins.md.

Not verified

  • iOS was not run. The remote pass used --share; relay/T3 Connect was not exercised. Android was captured after the change only: before this PR no plugin can declare transforms, so there is no row to show.
  • The desktop captures render the threads from the isolated dev server added as an environment in the built app; the built app's own bundled server ran no provider turn (it had no provider login in the isolated profile).
  • Live provider proof: Claude only (the captures above). The other six providers are by code reading plus the server-side gate tests.
  • A slow plugin delays the turn: enrichment runs before the provider session opens, bounded by the 10 s plugin deadline.
  • Plugin context text is not escaped inside its block (plugins are consented, trusted code); a plugin could close the tag.
  • Context is not replayed into a context handoff after a provider switch. The overflow record counts plugins not called but does not name them.

Claude Opus 5.5 (build), GPT-6.1 Sol (review) and GPT-6 Astra (captures) via T3 Code
🤖 Generated with Claude Code

@github-actions github-actions Bot added the vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. label Oct 5, 2026
@macroscopeapp

macroscopeapp Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Macroscope has since reviewed this pull request. An earlier review was skipped by a cost limit; a review has now completed, so that notice no longer applies.

@github-actions github-actions Bot added the size:XXL 1,000+ changed lines (additions + deletions). label Oct 5, 2026
@macroscopeapp

macroscopeapp Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This is a large, cross-cutting plugin platform that adds production child processes, persistence, RPC/MCP surfaces, client workflows, and plugin-controlled context before provider turns. It also enables new capability defaults and broadens a static-analysis exception, so the change requires human review.

You can add or adjust custom eligibility rules. Learn more.

@saphid
saphid force-pushed the stack/20-context-transforms branch 10 times, most recently from d76e27a to 9517b07 Compare October 6, 2026 16:03
@saphid

saphid commented Oct 6, 2026

Copy link
Copy Markdown
Contributor Author

Review requested

Date (UTC) Reviewer Where
2026-10-06 Julius Discord DM

Logged so this PR shows when a maintainer was asked to review it.

@saphid
saphid force-pushed the stack/20-context-transforms branch 9 times, most recently from a2ee53e to 29b615f Compare October 10, 2026 05:57
saphid and others added 5 commits October 10, 2026 17:21
Preview becomes the second panel on the side-panel registry that Diff
started. Each definition now also carries the panel's title, icon, launcher
letter, client support and unavailable copy, so the tabs, the empty
launcher and the add menu read one ordered list instead of three
hand-kept ones. Labels, letters, order and copy are unchanged.

Panel props are inferred from each lazily loaded body, and the caller is a
closed union, so another panel's props, unknown ids and widened ids do not
compile. ChatView lends the rendered panel a small host (thread, right
panel visibility, composer draft target, workspace mutation id and the
annotation send) instead of drilling the same props into each body; the
annotation send keeps the per-render closure it had before, and PreviewView
still drops a pick that settles after a thread switch.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ChatView built a new PanelHost on every render, so every usePanelHost
consumer re-rendered even when no host field changed. Memoize it on its
fields and send annotations through onSendRef so the sender stays stable.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Plain server threads reuse one ChatView, so the memoized panel host's
sender could resolve to the next thread's composer when a pick settled
after a switch. The latest sender now carries its thread key, and each
host forwards only to a sender for its own thread.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ChatView replaced the panel host's annotation sender while rendering. If
React threw that render away, an in-flight preview pick could still call
its onSend, for example one that edits a queued message instead of sending
a turn. Update the sender in a layout effect so only committed renders
lend it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
github-actions Bot and others added 29 commits October 11, 2026 00:21
When an environment had plugin views placed outside the side panel, the
filtered list was rebuilt on every chat render, so the launchers and both
right-panel tab strips got a new array each time. The filtered list is now
memoized on the session's views.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…roup

Typing every WebSocket handler against the instrumented group runs past the
type checker's instantiation limit once the plugin view RPCs join main's
WebSocket methods, and the checker then silently widens the server layer's
requirements to `any`. RpcServer finds a handler by its tag alone, so the
handlers are typed against the plain group while the server still runs the
instrumented one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A view asset such as `..board.js` sits inside the plugin directory, but the
containment check treated any `..` prefix as leaving it, so the whole
installation's views failed to load. Only a whole `..` segment now counts,
matching the entry check in the manifest loader.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A view's files were read before the directory was digested, so a file changed
for the read and restored before the digest served bytes that were never
approved. Each read file's hash must now match the hash the digest pass took of
it, and a file that several views share must read the same bytes every time. A
view file that fails to read also runs the digest, so an approved plugin whose
view was edited into an invalid file is disabled instead of keeping its stale
approval.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed updates

Adds `plugins.npm.list/add/stageUpdate/applyUpdate/discardUpdate`, gated on the
`pluginNpm` environment capability. Install and update download one exact
version, require the registry's sha512 integrity to match, check the whole
archive in memory before writing it, refuse install scripts and unbundled
dependencies, and hand the unpacked directory to the catalogue, which still
requires consent to its digest before anything runs. Applying an update
consents to the staged digest and swaps the files in one catalogue step;
interrupted swaps are finished or rolled back at startup. Listing needs
orchestration:read; installing and updating need access:write.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The integrity check compares the tarball with the sha512 the same registry
publishes. Over plain http a network attacker can replace both, so the
check authenticated nothing. Registries must now use https, except a
loopback registry for local testing.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…https rule

Only the registry typed into add was checked. Metadata requests followed
redirects anywhere, so one plain http hop let a network attacker supply
both the integrity and the tarball, and an update of an installation saved
from a plain http registry skipped the check entirely. Metadata requests
now follow each redirect only to https or a loopback registry, and every
metadata lookup refuses a saved registry that is not one. Tarball
downloads still follow redirects: their integrity comes from that
metadata.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Three header cases were misread. A GNU header's `ustar  ` magic passed the
POSIX check, so its access time became a path prefix and files landed under
the wrong names. A directory entry's size is space to reserve, not data, so
a nonzero one shifted every later header. A pax global header that sets
`path` or `size` was skipped, so later entries kept names and sizes their
writer did not mean. Only the full POSIX magic and version now read a
prefix, directories carry no data, and a global path or size is refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A staged update's summary was built by its own copy of the manifest
summarizer, which left out tools, settings, and actions, so an
administrator reviewing an update could not see changes to them before
applying it. The npm installer now uses the catalogue's summarizer, so the
review shows exactly what the catalogue will list once the update is applied.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The inflate cap allowed 1.5 KiB of header and padding per file, but directories
and extended headers are entries of their own, so an archive inside the file,
byte, and entry limits could still be refused as too large. The cap now budgets
every entry and one tar record of end padding.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The pax reader dropped each record's last declared byte as its newline without
checking it, so a malformed record such as `20 path=package/fooX` renamed the
next file instead of being refused. A record whose last byte is not a newline
now makes the archive unsafe.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PluginNpm now reaches the plugin catalogue through its module namespace and
builds each PluginCatalogError where the failure happens instead of through
a forwarding helper. A failed file step keeps the file system error as the
storage error's cause, and every Node builtin import exemption in the npm
install modules says why it is needed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A pax size waiting for the next file was applied to a GNU long-name record
in between, so the reader misread the name and refused a valid archive.
Extended headers now always use their declared size.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A pax path may carry a NUL, which passed the path checks and then failed the
staging write after earlier files were already written. Such an archive is
now refused before anything is staged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nabled again

When enabling the plugin again failed after the consent to the new files was
saved, applying the update reported a failure although the new version was
installed, and a retry found no update to apply. It now returns the applied
version and the installation as it is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Applying an update whose files match the installed ones could lose the
package: after a restart between the two moves, recovery read the existing
consent as a finished swap and deleted the only copy. Such an update is now
discarded before anything moves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Plugins could only be added, approved, enabled or removed by sending raw
RPCs from an administrative connection. Settings now has a Plugins page on
web and desktop: add a directory, review its files and digest before
approving, enable, disable, resume, check files again and remove. Event
delivery is shown beside the process state, and retrying or stopped delivery
offers Resume. Controls need access:write from the current session read and
a live catalogue; a standard pairing sees the list read-only.

Mobile shows the same catalogue, states and details read-only, with a
notice to manage plugins from an administrative web or desktop connection:
the mobile app always pairs with standard scopes, so it has no access:write.

The client-runtime catalogue subscription and the shared presentation model
keep web and mobile on the same states and copy. The five per-feature plugin
pages are rewritten into one guide, docs/user/plugins.md.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…lugins

Deselecting every environment on the Plugins screen said to update T3 Code,
even when every connected server supports plugins. It now asks to select an
environment, as the usage screens do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Plugins could declare settings and secrets, but the only way to save them
was a raw plugins.settings.update request. Each installed plugin that
declares settings now gets a form under Settings > Integrations on web and
desktop. Secrets are write-only (the form shows only whether one is saved,
with Clear), and values are checked against the field before they are sent.

Saving needs access:write from the current session read. A standard pairing
sees the values read-only, and every save path (submit, reset or clear,
choices, switches) refuses at dispatch, not only through disabled controls.

Mobile lists each plugin's saved values read-only on the environment's
settings screen (secrets only as saved or not set), and says to edit them
from an administrative web or desktop connection: the mobile app always
pairs with standard scopes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Adds Install from npm next to Add plugin on web and desktop, a review of
the downloaded package (name, version, registry, sha512 integrity, scripts
policy) before approval, Discard for an unapproved download, and an Updates
section that downloads, reviews and applies a new version bound to the
digest the user acknowledged. Only servers that report the pluginNpm
capability get the entry or any npm request, and every step needs
administrative access, as on the server. The removal confirmation says
what happens to the files for each origin, and that saved settings and
storage are deleted.

Mobile shows where a plugin came from, read-only: the npm package on its
row, and Package and Integrity in its details. Installing and updating stay
on web and desktop, because the mobile app pairs with standard scopes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…the list

A staged npm download of the installed version still needs to be applied or
discarded, but the plugin row hid it because only a new version counted. The
row now says a download is ready to review whenever one is staged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s allow

Plugin details and the consent review now list a plugin's declared
contributions (actions with targets and placements, tools, settings,
events, and view titles once enabled) from its manifest summary, without
starting it, and give each capability a plain meaning. A plugin the
environment's action limit left out says so in its details. A downloaded
npm update lists what the new version contributes before it is applied.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A plugin with the `status` capability (proposed API) can set and clear
short statuses on threads. They reach clients through the existing thread
status channel and show beside provider statuses (such as a Pi extension's)
in the web and desktop thread header and the mobile status row, attributed
to the plugin by name.

Plugin statuses use their own capacity pool (3 sources per thread, 32
threads, 64 items), so provider statuses keep exactly the room they had.
Each plugin process is limited to 16 statuses and 10 updates at once, then
2 per second, and everything it set is cleared when that process stops.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The status store drops a new status when the thread already shows its most
plugin sources or items. status.set still reported success, kept the key
against the plugin's 16-status budget, and left the thread's source open
with nothing shown. It now checks what the store admitted, frees the slot
and returns an error.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A plugin with the `notifications` capability (proposed API) can send short
notifications. Web and desktop show them as toasts and mobile as a banner,
with an "Open thread" action when the notification names a thread.

The server keeps the newest 20 for 2 minutes in memory and sends that whole
set on subscribe and after every change, so a client that reconnects shows
what it missed exactly once and closes what was withdrawn meanwhile. A
plugin's notifications are withdrawn when its process stops. Each process
may send 5 at once, then one every 5 seconds. Subscribing needs
orchestration:read.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The banner disappears after a few seconds, so a screen reader user heard
it only if focus happened to land on it. It is now a live region for
TalkBack and announced on iOS when it appears.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ions

PluginNotifications reaches the plugin supervisor through its module
namespace and builds each PluginHostCallError where the call fails, without
a forwarding helper or a shared stopped error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A plugin with the `transforms` capability and a declared `transforms.enrich`
can now answer `t3.transform.enrich` with short titled context. Before the
provider session opens, the turn-start effect records one `plugin_context`
item per plugin (at most 4, plus one record counting any plugins not
called), calls the plugins outside the thread lock, saves their answers or
why none was added, and sends the provider the kept context ahead of the
user's text. Only turns the history pager counts are enriched; steering,
`/` commands and wakes are not. A retried start reuses the saved records and
never calls a plugin twice; Stop closes a running record. Failures fail open.

The records are existing `dynamic_tool` items, so every client decodes them.
The web/desktop inspector and the mobile expanded detail show "From <plugin>"
with the context, or "Not added" with the reason, instead of the input. The
capability's consent line now says the plugin reads your messages.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@saphid
saphid force-pushed the stack/20-context-transforms branch from aced8e8 to 98939ce Compare October 10, 2026 13:24

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant