mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-09 09:54:40 +00:00
28c188513e87bd035e06acd2c75b41b9cac70964
76
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
28c188513e |
feat: plugin API for agent options, metadata, composer and New workspace (#8744)
* feat(protocol): option controls, options patch, and provider.controls.list
Add AgentOptionControl descriptors, snapshot options/controls, an options patch on
agent.config.apply, provider.controls.list RPC, and server_info.features.providerOptionControls.
featureValues stays accepted on the wire (COMPAT).
* feat(server): fold featureValues into providerOptions and describe composer controls
Providers emit option controls over top-level providerOptions keys; the daemon stores one
options record per agent, merges config.json defaults as effective values, and applies patches
live via AgentSession.applyOptions. Legacy featureValues (wire, stored records, profiles,
schedules, hooks, MCP) fold into providerOptions; snapshots still carry features built from
controls. Snapshot options expose only keys that have a control.
* fix(server): address Phase 1 audit findings on option controls
- Removing auto_accept (OpenCode, ACP) turns live approval off; ACP selects return to their session default.
- agent.create.before hook featureValues edits override options the create already set.
- Claude commits fast_mode only after the runtime accepts it.
- Codex snapshots show a saved Fast preference as the tier the model resolves it to (AgentSession.displayOptions).
- Profiles are folded into options in the daemon config store; the config API still answers with featureValues.
- Snapshot options and provider.controls.list carry all effective options.
- Test: Claude fast_mode survives a restart through stored providerOptions.
* fix(server): keep snapshot options as configured, restore OMP baseline, cover options through public interfaces
- Codex: drop displayOptions; a select choice may carry aliases so a saved Fast preference selects the model's Fast tier from options + controls alone.
- OMP: removing fast_mode restores the runtime's starting setting.
- ACP/Cursor removal tests run through real session initialization.
- agent.config.apply of a non-control option reaches the provider and shows in the next snapshot (daemon e2e).
- Daemon config store persists legacy-featureValues profiles as options on the next profile save.
* fix(server): read a pre-v0.10 Codex fast_mode record as service_tier "fast" and store it that way
A record holding `fast_mode: true` without `service_tier` ran Fast but the speed control showed
Normal, and picking Normal then wrote "default". The Codex session now normalizes the old boolean
at construction and reports the new shape through its existing options_changed event.
* fix(server): show Fast for a stored Codex fast_mode: true without rewriting the record
Reverts the storage migration from a987b4a43. The Codex speed control now takes its default from
the session's current options with the same rule the runtime uses (an explicit service_tier wins;
fast_mode: true applies only while service_tier is absent). This holds on create, resume, live
apply and for config.json defaults, and stored options stay as set.
* chore(website): regenerate the config schema for profile options
* refactor(server): providers report option differences once, after create, resume and apply
Providers used to write their own reports into the agent's options while a session was being
created, resumed or updated, which needed a growing set of reconciliation, baseline and
suppression rules to stay in step with the daemon.
The contract is now transactional. A provider opens or updates with the options it is given and
emits nothing while doing so; if the result differs it says so once, afterwards, as
`options_changed`. Live apply hands the provider only the keys the patch sets and the daemon stores
them onto the agent's current options when the provider accepts, so the last write wins. A change
the provider makes on its own is stored and forwarded, not undone. Removing an option drops the key
and sends nothing live. Controls carry no state; `options` is the only option state.
A legacy `agent.create.before` hook's `featureValues` edits are applied as a patch over the
returned options, so its deletions and replacements hold. The unreleased `aliases` field on option
choices is gone; a pre-v0.10 Codex Fast preference is reported once as the model's tier id.
* refactor(server): providers return what apply and open ended with instead of reporting it afterwards
applyOptions returns the options the session ended up with and the session
exposes its open difference as openOptionsPatch, so the manager stores the
patch and the provider's answer in one step. Removal is an ordinary patch:
the provider receives the default the key falls back to, or null, and returns
what it kept. Controls no longer take a default from a stored legacy feature
value.
* fix(server): apply an old app's profile featureValues over the profile's stored options
The unchanged app's profile form only knows the controls, so a name-only
edit dropped options it never shows, such as sandbox_mode. Its
featureValues now apply as a patch over the options already stored for the
profile (null removes a key), and a write with no featureValues keeps them.
* fix(server): ACP reports the selects it moved during open or apply as that call's result
A config_option_update that arrived before the write response was emitted
as an event on top of the result the manager stores. Notifications during
open or apply now fold into the open patch or apply result, and a rejected
apply reports what the agent moved since nothing is stored. Live options
are validated with the creation schema. Tests drive a stdio ACP agent.
* fix(server): providers validate live option patches and Claude reports Fast dropped by a model switch
Live patches go through the schema used at creation. A Claude model switch
that cannot keep Fast reports fast_mode as removed so the stored options
match the session.
* fix(server): Claude reports Fast as off, not removed, when a model switch drops it
A removed key lets a config.json default of fast_mode: true fill back in
while Fast is off at runtime.
* fix(server): fold an old app's profile featureValues by the provider and options the write carries
A write with options folds featureValues over those options. A write
without them patches the stored options, unless the profile moved to
another provider, which starts from no options.
* docs(server): a rejected apply reports what the provider moved after the call ends
* test(server): a patch key is resolved against nested configured defaults
* test(server): a setting a plugin changes on its own is forwarded as options_changed
* test(server): assert the forwarded plugin setting without nesting callbacks
* test(server): removal of auto_accept returns ACP to asking; Claude keeps opening restrictions across a patch
* fix(server): Codex returns the tier it resolved and drops a speed the new model lacks
A live service_tier: fast is stored as the catalog's Fast tier id instead of
a value the control cannot show, and a model switch to a model without the
stored speed reports Normal so choosing a supporting model again does not
bring the paid tier back.
* fix(server): Claude applies only the keys a patch sets over its current options
A Fast patch kept a copy of the options taken before the runtime answered, and
assigning it afterwards dropped a patch that landed meanwhile.
* fix(server): a plugin that moves settings and then refuses an apply reports what moved
* refactor(server): the legacy featureValues folds take one named-input object
* fix(server): a live patch is rejected for an option the session only reads at start, and opening reports every option it ended up with
A stored option has to describe what runs. OpenCode, OMP and ACP now reject a live change
to a key they apply only when the agent opens; Claude applies such a change from the next
turn and rejects it while a turn is running. Opening reports every select or setting the
session came up with, including ones nobody set, so a native default (Cursor opening with
Fast on) is stored as option state like any other.
* fix(server): Claude rejects a live change to an option it reads when the query starts
Replacing a started query to apply the option would end its background shells and
tasks. Once the query exists only Fast can change; before the first prompt every option
still applies, because it is read when the query is built.
* fix(server): a live options patch is rejected when the session cannot apply it
A session with no applyOptions (Pi) now rejects a patch instead of storing a value that
fails on reopen. Codex applies only service_tier, plan_mode and fast_mode live, since the
approval policy and sandbox are fixed when the thread starts. Claude also rejects a launch
option change while its first query is starting, after the options have been copied.
* fix(server): plugins reject keys they do not report as settings, and Codex opening reports the speed it runs
A plugin session applied the settings it knew and silently stored the rest; it now rejects
a patch that names a key the plugin does not report. Codex opening on a model without the
saved speed ran Normal while the stored options still said priority; opening now reports
the tier it runs, as a later model switch does.
* refactor(server): option helpers take named inputs, control types come from their schemas, and a plugin accepts launch options it opened with
A whole profile is applied by the unchanged app, so a launch-only plugin option whose value
has not changed must not fail the apply. applyProviderOptionsPatch and featuresFromControls
take one object, and the protocol control types are inferred from the wire schemas.
* fix(server): ACP reports a select that appears after startup with its native value
A model switch can introduce a select (Cursor Fast) that the session did not have at open.
Its value is now stored like any other native selection instead of being skipped because
the previous snapshot did not name it.
* fix(server): a loaded agent's options stay what its session opened with when a provider default is edited
effectiveOptions was recomputed from the current config.json defaults on every state emission, so editing a default changed the snapshot of an agent whose session never received it. It is now computed when the session opens and moves only through an apply or an options_changed. Editing a default affects agents the next time they open.
* fix(server): a live apply shows the merged defaults the provider was given in the options snapshot
The snapshot took the raw patch, so a nested patch over configured defaults dropped the defaults the provider had accepted. The snapshot now takes what the provider was sent plus what it reports back; stored options still keep the raw patch.
* fix(server): an option a session drops when opening is not brought back by a configured default
The opening snapshot merged defaults over the options after the session's openOptionsPatch had removed a key. Defaults now merge into what the session was given and the patch applies after.
* fix(server): Codex keeps the speed it opened with when the legacy Fast key is removed
Opening moves a legacy fast_mode into service_tier, so removing fast_mode afterwards changed the runtime speed to Normal while the stored options still said Fast. Removing the alias now changes no speed.
* fix(server): opening snapshots keep the defaults the session launched with, and config-change pushes carry legacy profile values
An opening resolved defaults once for the session and again at registration; an edit in between showed options the session never received. Create, resume and reload now resolve once, before the provider opens, and registration starts from that. The daemon_config_changed push skipped the featureValues projection that config replies apply, so the unchanged app lost a profile's legacy values when any setting changed.
* fix(server): every opening path hands registration the options it launched with
Registration took the launch options as optional and reread the defaults when a path did not supply them, as provider-session import did. It is now required, so create, resume, reload and import each resolve once before opening. The option helpers take named inputs.
* fix(server): a saved legacy Codex Fast preference wins over a configured service_tier default
The defaults merge put a configured service_tier next to the agent's own legacy fast_mode, and Codex reads service_tier first, so the agent opened Normal. The agent's own alias now replaces the default of the key it stands for.
* fix(server): Codex keeps a Fast chosen by alias as the catalog's tier id
A live fast_mode or service_tier "fast" was stored as the alias while the apply reported the catalog id, so a later switch to a model with a different Fast id ran Fast under an option that said otherwise. The session now stores the id it accepted and drops the folded fast_mode.
* fix(server): the legacy Codex Fast alias folds into one speed on apply and in configured defaults
A patch with both speed keys left the alias in the stored options, so a reload started Fast again after the speed was removed; the apply now reports the folded alias. A configured fast_mode default now supplies the service_tier default, so removing an agent's own speed falls back to the configured Fast like opening does.
* fix(server): an OMP replacement runtime starts with the Fast the session has now
A restart (for example an approval-mode change) restored Fast from the options the session opened with, turning back on a Fast the user had switched off. The session now keeps the Fast it applied or the provider changed, which is what a replacement reads.
* refactor(server): Codex's legacy fast_mode converts to service_tier where old data enters
Stored records, config.json defaults, profiles, older clients' requests, MCP settings and
plugin hooks go through convertLegacyOptions as they come in. Past that point the defaults
merge, the manager and the Codex session never see fast_mode for Codex, so the alias branch
in the defaults merge and Codex's fast_mode handling are deleted.
* fix(server): a legacy hook's replaced speed and an apply request's options convert before they are compared or applied
A hook that swaps the canonical speed it was sent for fast_mode is diffed in canonical form, so
its replacement is not read as a deletion. An apply request carrying featureValues and options
sends one options patch with options winning, so a superseded legacy value is never applied.
* refactor(server): the agent manager converts Codex's legacy fast_mode for the adapter behind the provider id
The rename is adapter-specific, so it lives where the provider id resolves to its adapter:
AgentManager converts an agent's own options on create, resume, import, reload, apply and
draft controls, following derivedFromProviderId, so a custom provider extending codex keeps
working with a saved or requested fast_mode. Storage, MCP, schedules, hub and profiles pass
options through. The featureValues-to-options fold stays provider-agnostic at the legacy edges.
* fix(server): a rejected OpenCode auto_accept leaves the session asking for permission
applyOptions committed the new flag before approving pending requests, so a failed reply left
the session auto-approving while the stored option stayed off. The flag is restored when the
apply throws.
* fix(server): legacy Fast reaches draft discovery and saved profiles, and overlapping or replaced options keep the state they committed
Draft controls and commands convert the config they hand to the provider, so inspecting a
legacy Fast preference no longer fails Codex's strict options. A profile naming codex converts
its saved fast_mode for the app's controls. OpenCode serializes auto_accept applies so a failed
one restores exactly what it changed, and a replacement OMP runtime starts from the Fast the
session opened with.
* fix(server): an OMP replacement runtime that refuses the session's Fast publishes what it accepted
The approval-mode restart installed the new runtime's state without comparing its Fast result to the session's, so a refusal never reached options_changed, the remembered value, or storage. The replacement now goes through the same comparison as refreshState.
* fix(server): OMP turn completion publishes a Fast change, and Claude opens with Fast off on a model that has none
OMP installed runtime state in several places and only one compared Fast; every state read from the runtime now goes through one install step. Claude kept a Fast default on a model without Fast, which made unrelated options reject until the user cleared it; the session now opens with Fast off and reports it in openOptionsPatch, as the model-switch path already does.
* fix(server): a malformed Codex fast_mode is rejected, and profile options are renamed for clients by the agent manager's conversion
convertLegacyOptions read any truthy fast_mode as Fast, so the string "false" ran priority. It now accepts only true, false or null. The profile store no longer renames keys: a literal codex profile was renamed in storage while an extends-codex profile was not. The config API now converts each profile's options with the manager's adapter-aware conversion, so codex and my-codex profiles read the same.
* fix(server): a profile is stored in the current option keys, converted for the adapter its provider extends
Profiles in config.json are old data entering, so the store converts them (stored value and incoming patch) with the same conversion the agent manager uses. Storage and clients now share one vocabulary: resetting a legacy Codex profile's speed removes what was shown, and a malformed fast_mode rejects the write instead of breaking later config reads. The config API goes back to copying the field shape only. The extends chain is followed by one function over either the registry's definitions or config.json's providers.
* fix(server): profile options convert against the provider graph a write commits
A write that edited a provider and a profile together folded the profile against a shallow merge that dropped the provider's extends, so its legacy Fast was not converted. The fold now sees the same deep-merged providers the write commits.
* fix(server): draft speed shows the tier the first turn will run with
Draft discovery reported the options as requested, so a Fast configured as a legacy fast_mode or the fast alias read as Normal while creating sent the model's own Fast tier. The draft now reports the options the scratch session opened with, as registration does.
* fix(server): ACP option applies run one at a time
Two overlapping applies shared one transaction guard, so when the first was refused after the agent had moved a select, its report was dropped as 'still in a transaction' and the second saw no difference. Applies are serialized, as OpenCode's are, so each reconciles against the state the previous one left.
* fix(server): an ACP removal reports the choice the select lands on, and the Codex plan tests enable plan mode
A removal asks the select for its empty choice and compared the result against that
same empty choice, so applyOptions returned nothing and the manager stored the removal
as null while the session ran the Default choice. The difference is now computed against
what was asked (null), so the options hold the value the session runs with.
The Codex plan-mode end-to-end tests called the removed session.setFeature through an
optional call and silently skipped plan mode; they apply the option and fail loudly if
the method is gone. The MCP parity wrapper forwards listControls instead of the removed
listFeatures.
* fix(server): plugin option applies run one at a time, and OpenCode reports the Auto Accept legacy full-access turns on
Plugin sessions let two applyOptions overlap. The second finished and re-recorded the
settings the plugin held, which included a change the first had already made and not yet
reported; when the first then failed, nothing was left to report and the stored options
kept the old value while the plugin ran the new one. Applies now queue on the session,
the way the ACP adapter's do, so each reconciles against what the one before left.
OpenCode normalizes the legacy full-access mode to build with Auto Accept on, but the
session never said so, so the options the manager keeps did not hold Auto Accept and a
reopen turned it off. The session now reports it through openOptionsPatch on create and
resume.
* refactor(server): openCodeOpenOptionsPatch takes one named-fields object
* feat(app): the client and app work on options and controls
The agent snapshot's `options` are the state and `controls` only draw them.
Running and closed agents draw from the snapshot; drafts and profile forms ask
`provider.controls.list.request` with their `providerOptions`, so aliases such
as a legacy Fast profile's `service_tier: "fast"` are resolved once, by the
daemon, and the draft keeps Fast highlighted. A draft adds only the user's
edits and each provider's last-used control values; applying a profile seeds
its non-control options (e.g. `sandbox_mode`) into the draft, and a profile
form saves them back instead of pruning to the controls it can draw.
A daemon without `server_info.features.providerOptionControls` still sends
`features` and `featureValues`. `LegacyProviderOptions` in the client package
is the one place that translates snapshots, draft listings, create and apply
requests, and daemon-config profiles, so the app never branches on it.
Persisted preferences and tab layouts rename `featureValues` through tagged
COMPAT preprocessors.
The features/controls bridge moves from the server into the protocol package
so the client can reuse it.
* fix(app): drafts and profile forms send every option layer to the daemon
A cold create against a daemon without providerOptionControls routed control
options (Claude fast_mode, Codex service_tier) as providerOptions, which that
daemon does not read for them. The client now lists the provider's features
before the first create or workspace create that carries options, so callers
never prime a cache.
Draft options moved into a plain model. Applying a profile replaces provider
and options in one step, so the form's provider change that follows no longer
clears a cross-provider profile's non-control options; choosing a provider by
hand still starts over. The model sends its complete options (seed, last-used
values once the provider says it still draws them, edits) to the daemon and
the controls draw only the answer, so a remembered legacy fast is drawn as
what it runs as. A toggle shows over the last answer until its own arrives.
The profile form does the same: its edits go through resolution too.
* fix(client): a legacy create routes its options from the controls of its own config
Which options a legacy daemon exposes as features depends on the model, mode
and thinking level, not only the provider: a Haiku snapshot with no features
says nothing about Opus. The provider-keyed memory of control keys sent an
Opus create's fast_mode as providerOptions, which that daemon does not read.
A create that carries options now lists the controls for its own draft
config and splits on that answer. The memory, and the snapshot and listing
hooks that fed it, are gone.
* fix(app): a closed agent shows its stored options, and a provider switch asks about its own options
A closed agent's snapshot is projected from its stored record, which kept
controls and provider options but dropped them from the payload, so the
app drew no speed or other controls for it. The stored projection now
carries them, with the legacy features derived from the same source.
The draft read the form's provider and the model's options separately, so
for the render after a manual provider change it asked the new provider
about the previous provider's options. The request is now built from one
snapshot of the model and is absent until the model has observed the new
provider.
* fix: persisted options keep their full shape, and every legacy snapshot gains controls
Draft setups and form preferences validated option values as booleans,
strings or null, but the options they now receive come from agents and
profiles and can be objects, arrays or numbers. One such value failed
validation and wiped the whole saved layout or the saved preferences. Both
schemas now accept any JSON value.
The legacy-daemon translation skipped the snapshots carried by
agent_created, agent_resumed and timeline responses, so a created agent or
a timeline refresh left the SDK handle without options and controls.
The profile journey asserted the create request's options field; the wire
field is providerOptions.
* fix(client): a legacy profile with both featureValues and options keeps both
A legacy daemon keeps an unknown `options` field on a profile it stored
(profiles are passthrough), for example after a downgrade, while its own app
keeps editing `featureValues`. Reading such a profile took `options` and
dropped `featureValues`, so a Fast setting there was lost and the next save
replaced it. Both are merged on read with an explicit `options` key winning,
the same rule the daemon applies when it folds `featureValues`.
The draft form input of draftControlsRequest is a named type.
* fix(app): a draft stops sending a toggle the model in use does not offer
A toggle made on one model stayed in the draft's edits after the form moved
to a model whose controls do not include it, and was sent with the create.
On a legacy daemon that key is not a feature for the new model, so it went
out as a native provider option the daemon rejects. The draft now sends an
edit only while the latest answer draws a control for it, and sends it again
when a control for it returns.
* fix(server): a closed agent's options include the provider defaults, like a live one's
The stored snapshot projected only the options the agent owns, so an agent
that ran with a config.json default (for example Claude fast_mode: true)
showed the option as missing, and its legacy Fast control as off, until it
was resumed. The projection takes the same resolver the live agent uses
(the provider's defaults under the agent's own options).
* fix(app): a draft keeps sending an option no control has drawn
The previous change stopped sending any edit the latest answer drew no control
for, which also dropped options set without a control (a plugin setting a
token budget on the draft). Only an edit of a key an earlier answer for this
draft drew as a control stops applying when a later answer draws none; every
other edit always applies.
* test(server): the MCP test agent manager resolves effective options
* fix(app): a profile stops saving a toggle the model in use does not offer
Switching a profile from Opus to Haiku kept the Fast edit, so the saved
profile carried fast_mode that no control draws and an old daemon's
provider rejects at create. The profile editor now drops a key an earlier
answer drew as a control and the latest one does not, the rule the draft
already follows; options no control ever drew are never touched.
* fix(client): resetting an option on an old daemon writes the default it can take
setAgentOptions({ service_tier: null }) sent a feature write with value
null, which the old Codex setter rejects, leaving the paid Fast tier on.
A toggle now resets to off and a choice to its default; an option with
no control to name a default fails with the host-update message before
anything is written.
* fix(client): applying a config with a reset option on an old daemon writes the default it can take
applyAgentConfig({ options: { service_tier: null } }) reached an old
daemon as featureValues with null, which its Codex setter rejects. The
config now takes the same translation as setAgentOptions: a toggle goes
off, a choice goes to its default, and an option with no named default
fails with the host-update message before anything is sent.
* feat(plugin): provider plugins declare option controls and receive option changes
A provider plugin reports `controls` and the `providerOptions` it runs in
`session.config`, and receives running-session changes as
`session.configure` `changes.providerOptions` (null returns a key to its
default), under the same apply-or-throw contract as built-in providers.
A control may declare a `defaultValue`; a value a plugin reports at that
default for a key nobody set stays unset instead of being pinned to the agent.
A plugin that still reports `settings` keeps working: the daemon reads each
setting as a control plus an option and sends changes in both shapes
(COMPAT(plugin-provider-settings)).
The app resolves a control's icon as a Lucide name when it is not one of the
built-in kebab-case names, so a plugin's icon shows live and in drafts.
Public docs and the provider-direct example use only `providerOptions` and
controls; the migration doc gains the rows.
* fix(server): a plugin provider's reported options are stored like any provider's
A control's `defaultValue` only says what the control shows for a key with no
value. Opening and applying store every option the session reports, defaults
and values nobody set included, as built-in providers do; the special case that
left a value at the default unset is removed.
* fix(server): a plugin decides which options it applies live
A plugin reporting `controls` and `providerOptions` receives every key a live
change touches, with or without a control, and applies them or rejects the
whole request. The daemon's live-key guard now runs only for a plugin still
reporting `settings`, which cannot refuse a key.
* fix(server): a rewound plugin session reopens with its options as they stand
The reopen merged the plugin's reported options over the launch record, so a
key the plugin reset by leaving it out came back at its launch value. The
session now keeps the committed options, removals included. The provider-direct
example also refuses a combined request before changing anything.
* fix(server): a plugin that stops reporting an option has reset it
The adapter only visited keys present in a new report, so a key the plugin
dropped to return to its default kept its stored value and its rewind value.
A previously reported key that disappears is now reported as removed.
* fix(server): an apply reports the options the plugin reset along the way
Applying and unsolicited reports now share one conversion from the plugin's
report to a patch, so a key the plugin stops reporting is a removal on both.
* fix(server): an apply never reads a requested option as one the plugin dropped
A plugin may run an option it never reports. Only a key it reported before can be
reported as removed, and a rewind reopens with the accepted request plus what the
plugin reported back.
* test(server): an apply to a plugin that reports controls sends its changes as provider options
* feat(plugins): agents and workspaces carry plugin metadata
Plugins had nowhere to keep their own data on an agent or workspace, so
Phase 5 and 6 surfaces would each have invented a side store. Both now
hold an opaque Record<string, JsonValue> that Paseo stores, snapshots and
hands to hooks without reading a key.
Set at creation or changed with agent.metadata.update.request /
workspace.metadata.update.request (top-level keys replace, null removes).
Persisted on the agent and workspace records, so it survives a restart.
Reaches agent.create.before (which may add to it), agent.open.before
(read-only), agent.create.after and workspace.create.before/after.
Gated once on server_info.features.entityMetadata; the client throws
"Update the host" instead of sending to a host without it.
* fix(plugins): an agent.open.before result may omit metadata
The result schema required metadata, so a plugin written before metadata
existed that returns its own object instead of spreading the request
failed validation and the session open failed, even for agents with no
metadata. An omitted metadata now means unchanged and is filled back in
from the request so later plugins in the chain still see it; a present
value that differs is still rejected.
The restart test now sets metadata through the PaseoApi handles and
checks that agent.open.before sees the patched value after the restart.
* fix(plugins): omitted metadata in a hook result means unchanged
An agent.create.before hook that returned its own { config, env } dropped
metadata that an earlier hook in the chain had added, because only the
caller's original metadata was restored at the end. The omitted field is
now carried forward at each hook handoff, as agent.open.before already
does.
The agent.open.before result type also required metadata, so an older
TypeScript plugin that returned its own object no longer compiled even
though the daemon accepted it. The result type now makes it optional.
* fix(plugins): omitted metadata in a workspace.create.before result means unchanged
Same handoff as agent.create.before: a legacy hook that returned only
{ source, ... } dropped the caller's metadata and anything an earlier
hook added, and workspace provisioning then defaulted it to {}. The
omitted field is carried forward at each handoff; an explicit {} still
replaces.
* test(server): fixtures for agents and workspaces carry metadata
Agent snapshots and workspace records now always carry metadata, so the
hand-built fixtures and expectations in three existing suites name it.
* docs(plugins): the hook result comment names metadata as the field that stays
* fix(server): reopening a closed agent keeps metadata saved just before
The loader reads the agent record, then queues a resume. A setMetadata
queued ahead of that resume saves a newer record, and the open hooks saw
it, but registration took metadata from the loader's older read and wrote
it back over the saved patch. Registration now uses the metadata read
inside the lifecycle lane.
* docs(server): name the metadata gap when a handle-less agent is recreated on load
* docs(server): the handle-less recreate gap spans the agent.create.before hook
* docs(plugins): an agent.open.before hook starts writes to its own agent without awaiting
An agent.open.before hook that awaits setMetadata (or any write) on the
agent being opened deadlocks: the write joins the agent's lifecycle lane
behind the open, and the open waits on the hook until it times out. The
hooks reference now says so and shows the supported shape, a write
started without awaiting, which runs right after the open.
Deferred, not in this stack: reject such a write immediately instead of
letting the hook time out. The host knows which plugin hook it is
awaiting for the agent and which channel the write arrives on, so the
rejection can be keyed on "this plugin is inside an open hook for this
agent" without cross-process tagging, and an unrelated client's write
would still wait as it does today.
* feat(plugins): composer items and controls in all three composers
A plugin registers a component with addComposerItem and reads and changes the
composer through useComposer(): provider, model, mode, thinking, options,
prompt, attachments and agent metadata. The agent, draft and New workspace
composers provide the same ComposerInput and ComposerAgent halves, so a
command reaches the right model without the plugin knowing which composer
it sits in.
Toggle, Select and Action are drawn by one ComposerControl renderer that
the thinking select and provider option controls now use too. Controls sit in
the toolbar when there is room and become rows in the existing overflow sheet
otherwise, so on a phone they appear in the overflow. Plugin items count in
the density presence so the composer condenses to make room. Model and mode
stay specialized but share the trigger, density and overflow.
Draft metadata is kept locally, sent at creation and reset when the prompt is
sent. PluginAgentSnapshot.metadata, deferred from the metadata phase, lands
here. The unused provider badge is removed.
vitest resolves @floating-ui/react-native to its ESM build: the CJS entry
requires the real react-native, whose Flow sources do not parse.
* fix(plugins): composer commands run on current state, report failures, and reach the overflow sheet
Draft commands read the state captured at render, so a plugin's
`await setProvider(); await setModel()` acted on a stale provider. The draft
form and options are now plain stores that commands read live, and
`setProvider`/`setModel` reject an unavailable provider or a model the loaded
catalog does not list. Draft `options` include the plugin's pending edits.
Shared composer controls own the pending state and show a rejected command as
an error toast, like the built-in controls. Callback payloads are objects
(`{ value }`, `onPress(event)`) so they can grow without breaking plugins.
The overflow sheet's content portals to the app root and lost the composer's
agent, input, toast and query contexts, so plugin items drew nothing in it.
`useComposerSheetBridge` carries them across.
* test(plugins): composer commands tested on the models, overflow sheet in Playwright
The draft composer's command rules (unavailable provider, unlisted model,
edits belonging to the provider the form has now) moved from the React hooks
into the form store and the draft options model. They take the host's catalog
as a plain input, so the command tests run on the real models with no module
mocks and no renderer.
The overflow-sheet test faked the portal it was meant to verify. It is
replaced by a Playwright spec that installs the composer-controls example and
checks its rows in the narrow composer's Features sheet; it fails when the
sheet's context bridge is removed.
* test(plugins): lock draft option ownership without a subscription, name the overflow-sheet steps
The draft option test subscribed the options model to the form, which in the
app happens in a render effect, so it passed without the model observing the
form's provider itself. A case with no subscription now fails when that
observation is removed.
The overflow-sheet spec reads as steps through a helper.
* fix(plugins): composer option resets and model checks match the documented commands
A draft kept `null` as an option value, so `setOptions({ key: null })` sent the null to the
provider and creation failed. A null edit now removes the key from every draft layer, as the patch
does on a running agent. A running agent's `setModel` accepted ids its catalog does not list; it
now rejects them the way a draft does.
The composer controls imported the toast host through the toast context, which pulled the host's
animation module into every module that loads plugins. They use the API-only context instead.
The migration guide's registration table gets the `addComposerItem` row its test requires.
* fix(plugins): draft metadata survives a remount, plugin commands follow the composer lock
Draft metadata was component state while the prompt lived in the draft store, so a composer that
remounted across the compact breakpoint kept its text and lost the plugin's choices. It now lives
in the draft store beside the prompt, is cleared when the draft is sent, and is not cleared when
the prompt is emptied. It is not persisted across app restarts.
While the New workspace composer is creating the agent, the built-in controls are disabled but
plugin controls and commands were not. They read the composer's own lock: Paseo controls render
disabled, and setPrompt, insertText and the agent commands refuse.
The options and metadata a plugin reads are frozen copies, through the same helper the timeline
projection uses. The composer control test mounted components in JSDOM, which the testing rules
ban; it now runs in the browser project.
* fix(plugins): a kept command honors the composer lock, and useAgent metadata is frozen
A plugin action that awaits an RPC holds the composer it was created with. The lock was captured
at that render, so a submit that started meanwhile did not stop its insertText. Commands now ask
the composer whether it is locked when they run.
The agent snapshot a plugin gets from useAgent froze only the outer metadata record. It now uses
the same deep frozen copy as the composer.
* fix(plugins): attachment commands honor the composer lock
The creation request captures the attachments when it starts, so an attachment a plugin added or
removed afterwards was lost or left in the request. They refuse like the prompt commands.
* fix(plugins): a built-in select in the compact sheet keeps its action name
The shared control named every icon-only select by its value ("Speed: Fast"),
including in the compact sheet, where the row already shows the value. The sheet
row is named by its action again ("Select speed"), as before the shared renderer.
* test(app): composer item test installation carries the subscribe main added to InstalledPlugin
Main's plugin streaming (#8527) made subscribe a required field of InstalledPlugin; the
composer item model test builds its own installation and predates it.
* feat(plugins): New workspace items set the branch and workspace metadata
addNewWorkspaceItem mounts a plugin field on the New workspace screen, and
useNewWorkspace() exposes host, project, isolation, base ref, branch and
metadata with commands that follow the composer item rules: they act on the
screen's state when they run, resolve once applied, reject while the screen
is locked, and take JSON payloads. Host and project are read-only because a
command that switched them would unmount the item that called it.
The screen's pickable state (base selection, manual isolation, branch,
metadata) moves into one plain store so pickers and plugin commands write to
the same place; the screen reports derived facts to it on each render. The
branch is sent as source.branchName only for a branch-off worktree, and
metadata on workspace.create.
Items render through ComposerControl with a new "field" surface (badge on
desktop, full-width field when compact). Combobox ships here: Paseo owns the
loading and error states, keeps previous results, and drops superseded
searches.
* fix(plugins): New workspace commands work from mount effects, and branch and base ref match what creation uses
A command called from a New workspace item's mount effect rejected because the
setup model only received its inputs and the pickers' handlers from a passive
effect on the screen, which runs after a child's. The model now takes its
inputs at construction, picks the base and isolation itself, and is refreshed
in a layout effect; the screen supplies the side effects of a pick (the PR
attachment, the remembered isolation) as listeners.
The branch a plugin sets was dropped when no base was resolved, such as on a
detached HEAD, because it was sent only for an explicit branch-off. It now
applies whenever the worktree cuts a new branch, decided in one place that
mirrors the daemon.
baseRef.name was the display-shortened branch name, so setting back what was
read based the worktree on the local branch instead of the origin one. It is
now the exact ref creation uses, and host and baseRef are frozen like project
and metadata.
* fix(plugins): New workspace commands have the same effects as a pick, even from a mount effect
The screen attached the pick side effects (remembering the isolation, syncing the picker's PR
attachment) as listeners in its own useEffect, which runs after a child item's mount effect. A
mount-time setIsolation("worktree") switched the screen but was not saved, and setBaseRef skipped
the attachment sync. The effects are now injected when the setup model is built, so a command and
a pick run the same code from the first render. Closing the picker stays in the picker's handler.
* fix(plugins): a Combobox shows a failed refresh instead of the old results
The query keeps its cached data when a reopened list fails to refresh, so the old items stayed
selectable and the error never showed. A failed search now clears the list so its error shows.
The migration guide maps addNewWorkspaceItem, which its registration-coverage test requires, and
the superseded-search test waits on a later search instead of sleeping.
* fix(plugins): a branch and metadata chosen for one project are not sent for another
The setup model outlives the project and host it was chosen for, so an issue picked for project A
was sent on workspace.create after the user switched to project B. The model now drops the branch
and metadata when the project changes (project ids are host-scoped, so a host switch counts). A
project that is still resolving is not a switch. The base selection is left alone: the built-in
picker keeps a branch pick across a project switch, and that behavior is unchanged.
* test(plugins): the project-switch spec expects the other project's metadata to be empty
* fix(plugins): a host switch drops the branch and metadata even when the project id repeats
A remote repository keeps the same project id on every host, so comparing the project id alone
missed a switch from host A to host B for the same repository. The target is now the host and the
project id together.
* fix(server): a branch the caller named is not renamed from the first agent context
The daemon marked every new branch-off worktree for automatic naming, recording the chosen branch
as the placeholder, so the first agent's prompt could rename a branch a plugin or client had
named through branchName. Only a branch the daemon derived from the worktree slug is a
placeholder now.
* fix(plugins): New workspace items remount when the project or host changes
An item stayed mounted across a project switch, and its Combobox kept the previous project's
results (same query key, cached data) selectable until the next search returned, so an issue from
the old project could set the branch and metadata on the new one. A Combobox cannot detect that
its inline search now means another project, so the item host keys the items by host and project
id. Items and their search results start fresh for the new target, and a mount effect sets up
again after the model clears the branch and metadata.
* docs(plugins): a layout-effect command on mount also races the reset on a project switch
* fix(plugins): a New workspace item's commands stop working once its project or host is left
Every item received the same command functions, which act on the current screen state. A lookup
that finished after the user switched projects could still call setBranch or setMetadata and
overwrite the new project's choices; the reset on a switch only clears what was already written.
The setup model now hands out commands per target with commandsFor({ serverId, projectId }).
They reject with a "moved to another project or host" error when the screen is on another target,
like the lock rejects, so the promise only resolves when the change took effect.
* feat(plugins): reach a running agent's composer by reference
useComposers(selector) in any plugin component and composers(selector) on every command context return the composer of a running agent, named by id or by focus, whether or not its tab is open.
A running agent now has one store-backed composer model apart from any mounted tab. The mounted composer plugs its cursor-aware insert in while mounted; otherwise insertText and setPrompt write the agent's saved prompt. model and thinking resolve through the provider catalog with the picker's own function, so a read equals what the picker shows.
The host reads one ComposerModel from context and no longer knows whether it is in an agent, a draft or New workspace. The daemon rejects an unlisted model or thinking option for a running agent, replacing the app-side guard.
* fix(plugins): composers by reference follow the focused agent and reach retained composers
- `{ focused: true }` is the agent Command Center hands to agent items, published by its
registration through plugins/focused-agent.ts and empty outside a workspace. The composer source
reads that module instead of the remembered workspace selection, the navigation store and the
layout store, which also removes the require cycles those imports added.
- A composer the app retains in a workspace the user left has no live input. `insertText` and
`setPrompt` now store the text with `replaceDraftText`, which asks the mounted input to show it;
before, the text reached the store but the retained input never displayed it.
- `model` and `thinking` read exactly what the picker resolves before the catalog loads (no
models), then re-resolve when it lands, instead of reading null.
- The daemon validates a thinking option against the model the agent runs: the runtime model when
the provider lists it, else the configured one.
- The composer sources subscribe to query-cache updates only. A query another component creates
during its render is announced as `added`, and a subscriber that updates then made React warn
about updating a component while rendering a different one. A catalog read no longer starts its
load synchronously either.
* test(plugins): a Command Center agent item reaches the composer through the real client state
The command-path test now reads workspace and agent through
createPluginClientStateSource over seeded session stores, so an empty
lookup, which makes the Command Center return before calling the item,
would fail it. Also records at the focused-agent publisher that it
publishes in an effect.
* fix(plugins): a running agent's composer validates and refreshes against the agent's own catalog
setAgentModel and setAgentThinkingOption checked the provider's global catalog,
while the picker reads the catalog of the agent's directory, so a model only
the project lists was rejected. The validation input now carries the directory.
The composer read the catalog once with no observer, so reconnect repair, which
refetches only active queries, never refreshed it and a failed load never
retried. While anything watches, the composer now observes the catalog query.
The shared composer map kept every agent's composer for the session. It is now
dropped when nothing watches it and no composer is mounted.
* fix(plugins): useComposers holds the composer it names, and a composer lives as long as its agent
PluginComposersSource.subscribe watched the stores directly and never the
composer model, so a component using useComposers did not keep the model's
catalog observer alive and reconnect repair skipped it. subscribe now takes the
selector and holds the model it names, following focus.
Dropping a composer when it went idle left a command a plugin had saved calling
a dead model after the tab reopened. A composer is now dropped when its agent
leaves the session and nothing watches it or has one mounted.
* fix(plugins): a composer refreshes a stale catalog on resumed demand, shares its store watches, and is released with its last holder
Watching again, or reading once, after the catalog was invalidated or failed
while nothing watched now starts one deduplicated refresh; the observer
refetches on mount. Reconnect repair only refetches watched queries, so an
unwatched catalog stayed stale.
Each subscribe installed its own session and draft store callbacks, so N
watchers received N squared notifications, including for other agents' drafts.
One pair of subscriptions now serves all listeners, and the draft callback
ignores drafts of other agents.
A composer whose agent was removed while it was held was only released on the
next session change. The last holder letting go now checks too, and an archived
agent's idle composer is releasable.
* test(server): live model and thinking switches use values the provider lists
The daemon rejects a model or thinking option its provider does not list, so the live-preferences
e2e can no longer switch to a made-up id. A provider that lists nothing to switch to skips the case.
* test(server): the test providers list two models with thinking options, and the live switches run
The fake providers listed one model and no thinking options, so once the daemon validated
selections against the catalog the live model and thinking switches had nothing valid to switch to
and skipped. Every fake provider now lists two models with thinking options, and the switches run
for all three providers without depending on installed binaries.
* fix(plugins): a retained composer that is not showing takes plugin writes through the draft
On native a retained composer keeps its input mounted under display none, so the ref alone said
the input was live and a plugin insertText replaced the stale selection instead of appending to
the draft. The input is live only while its retained panel is showing.
* test(plugins): the retained input liveness test runs in real Chromium
* fix(plugins): a model or thinking change, an unreported option key, and a draft model reach the provider unchecked
Paseo no longer checks these in front of the provider. A running agent's model or thinking
change no longer waits for the catalog or checks it, a plugin provider is sent every option
key and refuses what it cannot apply, and a draft setModel keeps the id as the model picker
does.
* fix(plugins): a plugin reporting settings is sent every key of an options patch
* Run option, model, thinking and mode changes in the agent's lifecycle lane
The agent manager owns all sequencing for one agent: applyOptions, setModel,
setThinkingOption and setMode now run in the same lane as open, close, archive,
labels and metadata, so a provider session never sees two of them at once.
Provider-side serialization (the applying chains in the ACP, plugin and OpenCode
sessions, and Claude's apply-over-current-options handling) is deleted.
* Reopen a handleless agent inside its lifecycle lane
A stored agent with no resumable session is now reopened by
AgentManager.createAgentFromRecord, which rereads the record inside the agent's
lane before creating the first session. A metadata or label write queued ahead
is in the record it reads; one queued behind applies to the live agent. The
loader no longer passes a record it read outside the lane. createAgent itself
also runs in the lane, like resume.
* Return the draft's options from AgentClient.listControls
listControls now answers { controls, options } like AgentManager.listDraftControls:
the options a session opened from the draft would run with, with config
defaults merged in and anything the provider resolves natively for the ones
nobody set. ACP fills unset feature selects from the probe session's
currentValue, so a fresh Cursor draft shows the Fast selection the agent opens
with instead of no selection. The legacy features response and the MCP draft
listing already project from the manager's result, so they follow.
* Send a profile's options on every write, empty included
The profile form always sends options. The daemon reads a write without them as
one from an app that predates options and folds featureValues over the stored
ones; a write that carries them, empty included, replaces them. A profile left
with no options is stored without the key, so removing the last one persists.
* Read a remembered Codex fast_mode as its service tier
The create-agent preferences convert the legacy key on read, keyed on the
built-in codex provider id, so the Codex draft shows the Fast selection the user
last used. Tagged COMPAT(featureValues) with the other legacy-option shims.
* Document one option change at a time per agent
The agent manager runs option, model, thinking and mode changes in the agent's
lifecycle lane; providers do no sequencing. The hook deadlock warning now covers
agent.create.before and option writes.
* Flatten the profile fold's base selection and drop an unused import
* Carry the New workspace context through portal hosts and the draft's metadata through retry
* Convert legacy options when resolving a stored agent's effective options
* Keep stored-record reads tolerant of a legacy option the conversion rejects
* Keep plugin attachment items separate from the draft's own
* Hand plugin command inputs to the host as its own copies
|
||
|
|
56c051503b |
fix(server): keep promptless Codex agents usable after reopening (#8393)
* fix(server): report a Codex thread only once Codex can resume it Codex writes a new thread's rollout at its first turn, but the adapter starts a thread when the agent is created and reported its ID at once. AgentManager stored that ID as the persistence handle, so a Codex agent that was reopened before its first prompt (idle runtime release, daemon restart, plugin tool reload) failed every prompt with "no rollout found". The adapter now reports a thread's ID, as runtime info and as a handle, only once it is resumable: after Codex announces the thread's first turn, or when the thread was resumed or forked. Until then a reopened agent starts a new session, as an unprompted Claude agent does. Document that a persistence handle must be resumable. * test(server): reopen promptless Codex agents without model requests Exercise the real app-server before any first turn, so publishing an unsaved persistence handle fails at the actual resume operation. Co-authored-by: Jeong-Uk,Kim <dnrtoki@gmail.com> --------- Co-authored-by: Jeong-Uk,Kim <dnrtoki@gmail.com> Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> |
||
|
|
74613e4bc3 |
feat(opencode): reuse running OpenCode v2 services (#7435)
* feat(opencode): connect v2 client to an existing server * feat(providers): reuse running agent servers Keep shared server sessions isolated and retain native tools while refreshes reuse the running service. * fix(providers): recognize formatted bridge manifests Managed plugin ownership must survive harmless JSON formatting changes. * test(providers): distinguish isolated session bindings Use separate session identities to verify that dedicated helpers keep their environments isolated. --------- Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com> |
||
|
|
91154ba4e8 |
fix(opencode): advertise agent-scoped bridge tool schemas (#7383)
Co-authored-by: gszep <contact@gszep.com> Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> |
||
|
|
aec01ea605 |
feat(claude): offer Mythos 5.1 when Claude Code advertises it (#6403)
* feat(claude): add Mythos 5.1 with its model capabilities * feat(claude): gate Mythos on advertised model identity * fix(claude): retain capabilities for explicitly configured Mythos * test(usage): load SQLite before timed store tests The first Windows OMP store case exceeded its five-second budget while subsequent equivalent cases completed under a second. Load the builtin during test collection so its cold initialization does not consume the first test deadline. * test(workspaces): publish complete service environment fixtures Waiting for file existence races with writeFileSync creating the file before writing its contents. Rename completed JSON snapshots into place so the existing readiness check cannot expose an empty file. |
||
|
|
8ddeb79c8d |
Claude agents stay idle while background helpers work, and send or Stop no longer kills them (#6295)
* fix(claude): open autonomous turns only when Claude starts a turn Paseo guessed from frame shape whether the main session had started work on its own, and only a result could close the turn that guess opened. A background subagent's task frames, its own background jobs' task_notification and rate limit events all arrive with no parent tool use id, so they looked like main-session work and opened a turn no result would ever close. The agent then reported "running" while its main session sat idle, and the app's message queue never drained. Claude brackets every main-session turn itself: system/init opens it and exactly one result closes it, whether a prompt, a wake-up after background work, a slash command or an API error started it. Background subagents never emit either on the main stream. An autonomous turn now opens only on that init, and only when no foreground turn is open. The frame heuristic is removed. - Paseo launches the CLI with CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS=1, added as an overlay in buildSdkEnv so the SDK and the usage probe share one env object. An "idle" session state closes an autonomous turn as a safety net, never a foreground one. "running" opens nothing: Claude Code 2.1.280 stays "running" for as long as any background subagent is alive. State frames never reach the timeline. - An init clears a leftover pendingInterruptAbort. A new turn has begun, so the interrupted one is over, and a stale flag would swallow the new turn's result. Tests that opened autonomous turns from bare frames now send the init Claude sends. New coverage: helper frames never open a turn while the main session is idle, a wake-up turn opens on init and closes on its result, a folded steer keeps one turn while a turn Claude starts after the result gets its own, an idle closes a turn no result closed, session state never opens a turn, and the env flag reaches the SDK launch. docs/agent-lifecycle.md records that only system/init and result bracket a main-session turn, since the code cannot tell a reader why frame shape fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(claude): interrupt Claude only while its main turn runs, and spare helpers Every Stop, and every send that replaces a running turn (paseo send, MCP send_agent_prompt, voice, the app's queue drain, Send now), ends in query.interrupt(). Without the SDK's perTaskStopAffordance declaration that call kills every running background subagent, even when the main session is idle and there is nothing of ours to stop. - Declare perTaskStopAffordance, so an interrupt aborts only the main turn, like Esc in Claude Code. Claude stops a helper itself with its TaskStop tool. - Track whether a main-session turn is in flight from Claude's own brackets: set by system/init and by a pushed message's command_lifecycle "started" (/compact works for seconds before its init), cleared by the result or an idle session state. - Call query.interrupt() and set pendingInterruptAbort only when that turn is in flight, or the cancelled turn's own message has already been started. Otherwise withdraw our not-yet-started messages with cancel_async_message instead: an idle interrupt reaches only helpers, and with no result to follow it the flag would linger. - Track the foreground prompt with the queued steers until Claude starts it, so Stop withdraws a message still waiting in the queue. A turn stopped while the Claude process is still starting never sends it. - Accept command_lifecycle "cancelled" as terminal, so a cancelled steer no longer waits to be withdrawn or keeps denying permission cards. Interrupt tests now wait for the pump to read the init before stopping, since that is when Paseo learns a turn is in flight. New coverage: a stop before Claude starts the message withdraws it without interrupting, a stop while only helpers run never reaches Claude, a stop mid-turn interrupts once and drops the aborted result, a new turn clears a flag no result will consume, the SDK option is passed, and a cancelled steer is not withdrawn again. docs/providers.md described the old unconditional interrupt; it now states when the Claude adapter interrupts and why it declares the affordance. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(claude): keep a helper spawned in the background out of a turn's cancel When a turn is canceled, Paseo terminalizes the subagents that were running in its foreground and skips the backgrounded ones, which outlive it. It only learned a child was backgrounded from task_updated's patch.is_backgrounded, which Claude sends when a foreground task moves to the background later. A child spawned in the background carries the flag on task_started instead, and a resumed subagent always does, so the track marked those helpers "canceled" while they kept working. Read is_backgrounded from task_started as well. docs/agent-lifecycle.md said task_updated was the only signal; it now names both. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(app): drain queued messages as steers The composer queue drains when an agent goes idle. It sent each message with no activeTurnBehavior, which the daemon treats as "interrupt": if the agent had just started a turn of its own (a Claude wake-up after background work finished), the drained message stopped that turn and replaced it. Send drained messages with activeTurnBehavior "steer". A drain that races such a turn folds into it; with no turn running the daemon starts a normal one, as before. "steer" is an existing protocol value, so older daemons need nothing new. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(claude): cover background helpers against real Claude Every other test in this file tells Claude "Do not use a background task", so a message sent or a Stop pressed while a backgrounded subagent runs was never exercised. Four real-Claude scenarios cover it: - "a background subagent keeps running after a message is sent while it works": one helper, the main turn ends, the parent is not reported running, a message in that window is answered, and the helper is never read as canceled and completes. - "a helper's own nested background job does not phantom-run the parent or kill the helper": a helper starts its own background Bash job, then keeps working in the foreground. The parent stays idle across the nested job's completion (sampled for 30 s around a 5 s job), a message sent in that window is answered, and the helper reports CHILD_DONE. - "the app's default steer survives a backgrounded helper while the main session works": the app's default send behavior (steer) does not kill a helper when it interrupts the main session's own turn. - "Stop cancels the main session's own turn but spares its background helper": the same for Stop. The wake-up turn Claude starts after a helper finishes lands a moment after the helper reads completed (0.1 to 5 s in recorded traces), and the daemon announces it as a pushed "running" agent_update, not as a turn_started stream event. The checks poll for it and read the final idle once the wake-up settles. waitForFinish can return a stale "idle" right after a send, and the push stream can miss a message's final text when it resolves, so the tests read fetchAgentTimeline like the rest of the file. Helper status comes from agent.provider_subagents.list, since agent_stream carries no provider_subagent events. The scenarios need the previous commits together. Run against the frame heuristic this branch removes, the nested-job test fails: the parent flips to running once the nested job finishes and stays there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(claude): interrupt a stopped prompt Claude has already taken Stop before Claude announces a prompt started withdraws it with cancel_async_message instead of interrupting. Claude can dequeue a message for its next turn before announcing it, and withdrawing it is then a no-op that reports cancelled: false, so the stopped prompt kept running. Interrupt whenever a message could not be withdrawn. Co-authored-by: Isak Persson <isak.a.persson@gmail.com> * fix(claude): withdraw a stopped prompt only once When the withdrawal of a stopped prompt failed, the interrupt path withdrew a second time, which could cancel a prompt sent while the first withdrawal was in flight. Withdraw once and interrupt on failure. The real-Claude steer scenario now waits for the background helper to exist before steering, so it proves the helper survives the steer. Co-authored-by: Isak Persson <isak.a.persson@gmail.com> --------- Co-authored-by: Isak Persson <isak.a.persson@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> |
||
|
|
9760292650 |
fix(providers): launch plugin CLIs on Windows and tolerate Muse startup (#6151)
* fix(providers): launch plugin CLIs on Windows and tolerate Muse startup Plugin providers bypassed the core Windows launcher and the plugin host exposed an empty server SDK module. Share the launcher through the SDK and bind it in the host. Give Muse initialization its own deadline because native startup traverses session history before answering MSP. * fix(providers): terminate Windows launcher process trees A command-script launcher leaves the CLI behind cmd.exe. Expose forced process-tree termination in the server SDK, use it for Muse and SDK ACP teardown, and bound Muse closing so inherited streams cannot leave callers waiting forever. * test(muse): keep platform setup outside the teardown assertion * refactor(plugin): expose process capabilities without quoting helpers * fix(providers): close process trees on uncorrelated protocol errors |
||
|
|
73d25317a4 |
fix(usage): derive window names and unify pin selection (#5903)
* fix(usage): derive window names and unify pin selection Quota slots do not identify a period. Derive numeric windows and IDs from reported duration and quota scope; keep unknown lengths neutral. Resolve source defaults and explicit pins through one model, with a persisted empty selection distinct from defaults. * fix(plugins): declare the duration helper host requirement * fix(usage): verify persisted selections at the storage boundary |
||
|
|
05e94ab52e |
Keep usage logins visible and explain unavailable quota (#5876)
* fix(usage): keep existing logins visible and explain unavailable quota Credential discovery must preserve accounts whose login exists even when it has expired. Keep vendor failures on the discovered account and isolate discovery failures without inventing provider cards. * test(usage): align credential route tests with account discovery |
||
|
|
7b0cace991 |
Restore Antigravity with Full access and session warnings (#5795)
* Revert "Remove the unreleased Antigravity provider plugin (#5781)"
This reverts commit
|
||
|
|
9d9d9af993 |
Remove the unreleased Antigravity provider plugin (#5781)
* refactor(providers): remove the unreleased Antigravity plugin Remove the provider at the owner’s request because headless command approvals are unsupported. Preserve Muse, shared plugin infrastructure, and earlier editor and terminal integrations. * test(plugins): keep builtin startup coverage generic |
||
|
|
29c198f955 |
fix(providers): apply configured options to every agent provider (#5780)
* fix(providers): apply configured options at session startup Treat provider options as one opaque record, with provider-owned validation and per-agent overrides over configured defaults. Keep agent overrides separate from defaults so configuration changes apply on the next launch or resume. * test(providers): verify provider-owned validation through Hub * fix(providers): preserve the complete wrapped session interface |
||
|
|
4417d7c47a |
fix(pi): give Pi's built-in MCP the agent's MCP servers (#5762)
Pi 0.99 ships MCP as a built-in extension. Paseo only recognized pi-mcp-adapter's /mcp command, so a Pi agent without the adapter started without Paseo's MCP servers. Pi 0.99 also rejects the adapter's --mcp-config flag, so the servers are now registered with pi.registerMcpServer from Paseo's per-session extension. Sessions that load pi-mcp-adapter keep the --mcp-config path. Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> Co-authored-by: ArietidsZ <69024461+ArietidsZ@users.noreply.github.com> |
||
|
|
8332f4d1ff |
feat(muse): add Muse Code as a built-in provider plugin (#5719)
* test(muse): record real host protocol fixtures * feat(muse): add built-in MSP provider Co-authored-by: millerben95 <millerben95@users.noreply.github.com> * fix(muse): expose provider settings in the app The daemon registered host settings without a client contribution, leaving sandbox, network, and workspace trust unreachable. Share the definition with a settings screen and expose it through Command Center even when installed plugins are disabled. * docs(muse): publish setup and provider support * test: correct runtime fixtures and restart readiness * fix(muse): hide reminder housekeeping from the timeline * fix(muse): use the official Muse Code mark Source: https://muse.ai/images/favicon/app-squiggle.svg Preserve the official silhouette path verbatim. Remove its gradient definition and use currentColor, scaling the original 100-unit coordinates into the 24-unit viewBox. Black silhouette renders match pixel-for-pixel. * fix(muse): adapt to current plugin APIs Use the screen API introduced with sidebar plugin items. Usage reports now come from source account discovery; remove the retired agent usage-reference request and headline marker, and exercise discovery through the existing provider fixtures. --------- Co-authored-by: millerben95 <millerben95@users.noreply.github.com> |
||
|
|
7b7685678d |
feat(antigravity): add Antigravity provider plugin (#5714)
* test(antigravity): record agy stream-json fixtures * feat(antigravity): add Antigravity provider plugin * fix(antigravity): address Phase 1 review findings * test(antigravity): record parity discovery blockers * feat(antigravity): support image prompts and daemon e2e * docs(antigravity): document provider setup and limitations * test(antigravity): respect Windows interrupt semantics * docs(plugins): avoid duplicate provider override guidance * fix(antigravity): report fresh denials and correct mode metadata Native denial results accumulate within a driver process. Track reported occurrences until respawn so later text-only turns do not repeat notices, while new denials of the same action remain visible. * fix(antigravity): use the official Antigravity mark |
||
|
|
b0c9de0c90 |
feat(plugins): first-class plugin providers — status, launch resolution, config overrides (#5707)
* feat(plugins): support provider status, launch resolution, and config overrides * fix(plugins): defer override warnings until provider startup settles |
||
|
|
3a9304607c |
feat: sidebar header and footer plugin items, Usage in the sidebar footer (#5685)
* refactor(usage): remove agent-to-usage wiring
* feat(plugins): screens, sidebar header and footer items
Plugins register screens with addScreen and contribute component-based
sidebar items with addSidebarHeaderItem and addSidebarFooterItem. Items get
currentScreen, openScreen and openPopover, and render the SidebarRow and
SidebarButton kit. The footer shares the header's ordering model through the
new sidebarFooterItems preference, and Settings > Sidebar gains a Footer card.
addSurface, openSurface and addSidebarItem stay as aliases with their
settings keys and routes.
* refactor(plugins): share resolvePluginPlatform and keep header remembered-host key
Consolidate the five resolvePlatform copies into plugins/platform.ts.
Header sidebar items keep the <pluginId>/sidebar/<id> remembered-host key
so existing users keep their remembered host; footer items use
<pluginId>/sidebar-footer/<id>.
* feat(usage): global usage summary, pins and used/remaining display
- Device-local usage preferences (displayAs, pinned windows) in app settings.
- Sidebar footer usage summary: pinned windows in pin order, one per account;
opens the expanded view as a popover on desktop and a sheet on compact.
- Pin checkboxes on window rows and a used/remaining toggle shared by the
summary, popover, Usage screen and Settings usage section.
- Usage screen shows one host with a selector when several are connected.
- Extract HostSwitcher and the sidebar popover host for reuse.
* fix(usage): render each usage report as its own card
* feat(usage): row pinning, one title row for usage controls, even sidebar footer
- Window rows are the pin toggle (checkbox role, checked state); pinned rows
get a surface2 highlight, unpinned rows an interactionHighlight hover.
- Usage screen, Settings section, popover and sheet put the title left and
the host selector, used/remaining toggle and Refresh right (UsageControls).
PageLayout gains an actions slot; the menu sheet header a trailing slot.
- Sidebar footer rows share 28px height, spacing[1] gaps and one leading rail.
* fix(usage): keep row hover lighter than the pinned highlight
Hovering an unpinned window row shows the pinned surface2 at half opacity on
a separate layer, so it never reads stronger than a pinned row. Pinned rows
do not react to hover.
* fix(app): defer sidebar styles and preserve layout exports in timeline test
* fix(usage): open the Usage screen on desktop, drop the Usage footer icon, match git panel refresh
- Desktop presses on the usage summary open the Usage screen; only compact opens the sheet.
- The Usage footer icon is gone; with nothing pinned the summary renders a plain "Usage" row
in the footer row style. A stored `usage` footer preference is ignored.
- Refresh controls use the Changes panel's icon-only ToolbarButton with RotateCw.
* fix(sidebar): keep the footer icon row fixed; footer items are rows only
The footer's icon row (Hosts, Import session, Help and support, Settings) is back to main's
fixed implementation: not in Settings, not reorderable, and plugins can't add to it.
- Footer items are full-width rows between Add project and the icon row: the usage summary
and plugin `addSidebarFooterItem` rows. The Footer card in Settings lists only those rows.
- Stored `sidebarFooterItems` keys for hosts, import, help and usage are ignored.
- `SidebarButton` is removed from the plugin UI kit; a footer item renders `SidebarRow`.
* refactor(usage): call the sidebar footer item Usage
One concept, one word: the footer item is the Usage item, with key `usage`.
- Settings > Sidebar > Footer lists it as "Usage"; its preference key is `usage`.
- `usage/summary-item.tsx` becomes `usage/sidebar-item.tsx` (`UsageSidebarItem`); both states
use testID `sidebar-usage`.
- `usage/summary.ts` becomes `usage/pinned.ts` (`resolvePinnedUsage`, `PinnedUsageWindow`):
it resolves the pinned windows the item shows.
* feat(plugins): screen params, several rows per sidebar item, SidebarSeparator
openScreen takes { screenId, params }; params live in the screen route's
query so reload, history and links keep them. currentScreen carries the
params. A sidebar item may render a fragment or array of rows; a row press
anchors the popover to that row. SidebarSeparator joins the plugin UI kit.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(plugins): any screen param key; trailing presses anchor the row's popover
Screen params go in the query under a param. prefix, so keys like serverId
no longer collide with the route segments Expo Router merges into search
params. A press anywhere inside a SidebarRow, trailing content included,
makes that row the popover anchor.
Co-Authored-By: Claude <noreply@anthropic.com>
* feat(plugins): plugins title their screens
addScreen takes { id, title, Component }. title is a string or a function
of the screen's params; the screen header shows it and follows param
changes. A title function that throws or returns empty falls back to the
screen id. Legacy addSidebarItem screens keep their item's title and icon,
and the addSurface alias titles its screen with the id.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(usage): require discovery and remove unreleased agent resolver
* fix(plugins): restore legacy sidebar rows, share the item runtime, header hint presses its row
- Legacy addSidebarItem rows are their own group kind again: main's row, registered icon in
Settings, /sidebar/<id> route, host remembered per row.
- New-API items follow the host last used on any of the plugin's screens.
- /sidebar/<id> falls back to a same-id screen so pre-migration links resolve.
- params moves to PluginScreenProps; settings screens no longer receive it.
- Sidebar items, buttons and popovers share one runtime per installation, so no empty first frame.
- A header row's hint slot presses the row; labels wrap as on main.
- Skill and docs corrected.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(usage): one host resolver, sheet reuses the screen's controls, name the host
- The Usage row and screen default to the same host.
- The compact sheet carries the screen's controls, host selector included.
- The host selector shows with one host, and unavailable messages name the host.
- The row, sheet title and pinned label use the sidebar.footer.usage translation.
- Window rows read their percent and reset after the pin label.
- Drop the unread per-report cache writes and headline fixture fields.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(plugins): popovers, header buttons and composer pills own their runtime again
Only sidebar item rows use the installation's shared runtime. Popover content
disposes its runtime on unmount, so observations close with the popover.
* fix(plugins): a sidebar item releases its subscriptions when it unmounts
Each mounted sidebar item gets its own API scope over the host's client, created
during render so its first frame has content. The scope closes a microtask after
unmount (StrictMode's remount keeps it), when the item moves to another host, or
when the installation stops. Replaces the installation-wide shared runtime.
* refactor(plugins): one Paseo client per plugin; authors own their subscriptions
usePaseo() in every plugin surface, and client.paseo in setup, is the
installation's client, created with the installation in the registry. Authors
release their subscriptions in their own cleanup; plugin teardown disposes the
client and ends the rest. Nothing is created or disposed per mounted surface:
the per-surface runtimes, their boundaries, and the per-run Command Center and
slash command APIs are gone.
* refactor(plugins): one Paseo client per plugin; authors own their subscriptions
usePaseo() in every plugin surface, and client.paseo in setup, is the
installation's client, created with the installation in the registry. Authors
release their subscriptions in their own cleanup; plugin teardown disposes the
client and ends the rest. Nothing is created or disposed per mounted surface:
the per-surface runtimes, their boundaries, and the per-run Command Center and
slash command APIs are gone.
* fix(usage): show usage from released hosts
Normalize released-host usage responses at the client boundary so existing usage and pins remain available across app and daemon versions. Include account labels in legacy responses for older apps.
* fix(usage): keep the usage capability check out of the public SDK
* test(server): retry transient locks during runtime test cleanup
* test(usage): model hosts without either usage capability
---------
Co-authored-by: Claude <noreply@anthropic.com>
|
||
|
|
be2fb1cab9 |
fix(opencode): keep long v2 turns alive until execution completes (#5674)
The long-lived session wait hit Node’s headers timeout while OpenCode kept working. Reconcile execution events against active state and the durable log so transport observation failures do not fail healthy turns, including across event gaps and reconnects. |
||
|
|
792715e762 |
feat(usage): usage sources as built-in plugins, following the session's account (#5465)
* feat(server): run internal plugins in-process through the plugin runtime
* fix(server): harden internal plugin startup and boundary checks
* feat(usage): add plugin usage sources and Codex discovery
* fix(usage): share SDK helpers and bound source cache
* feat(usage): migrate usage fetchers to plugin sources
Serve the legacy usage RPC from discovered source reports so older apps retain the same host usage list while new clients use the source registry. Keep credential reads and normalization inside each source.
* refactor(usage): shape sources as fetch functions
Keep the plugin usage API limited to stable report and presentation helpers. Source-local parsers and timeout handling preserve credential and response parity while the cases move alongside each source.
* feat(usage): resolve reports from active session accounts
Resolve source inputs at fetch time so model and credential changes select the account each session spends. Keep the legacy discovered-account RPC for existing clients. Sync OpenCode model events outside foreground turns so usage follows model switches.
* fix usage source discovery and session report parity
* test adapter usage references through daemon RPC
* name usage reference contract across server
* refactor(usage): own usage RPCs and validate session auth
* feat(omp): resolve session usage from active OAuth credential
* fix(omp): match usage source input and valid credential fallback
* test(omp): use upstream credential database schema
* test(omp): verify live session usage references
* fix(usage): declare YAML parser and consolidate credential validation
* feat(app): show usage sources in the composer and a Usage screen
Replace the provider-matched usage in the context ring tooltip with a
composer pill driven by agent.get_usage_report, add a sidebar Usage
screen over usage.list_reports grouped by host, and move the host
settings Usage page to usage.list_reports. Hosts without
features.usageSources show an update message. Source icons come from
the report's catalog SVG. The app no longer calls provider.usage.list.
* fix(usage): declare the Paseo requirement on internal usage-source plugins
A manifest without requirements.paseo targets Paseo before 0.8, so the daemon
refused to start eight of the nine usage sources.
* fix(app): keep host connection statuses live under the React Compiler
The hook read a store version only as a memo trigger (void version); the
compiler dropped it, so pages that mounted before a host came online kept
showing it as connecting.
* fix(app): place the context ring slot before the usage pill
The ring reserves its slot before it has data. With the pill first, the empty
slot sat between the pill and the mic; now it falls in the flexible space.
* refactor(app): extract the settings page layout into PageLayout
Desktop puts the title in the page above a centered column; compact moves it
into a back header. Settings renders the same pixels through it.
* feat(app): lay out the Usage screen like a settings page
The title moves from the header bar into the page on desktop; compact gets the
settings back header, which returns home when the screen was opened by URL.
* refactor(app): drop the unused actions slot from PageLayout
* test(plugin): classify the usage source SDK entry
* fix(usage): preserve platform test contracts
* fix(settings): preserve page title test contract
* feat(usage): identify and cache reports by account ID
Keep report identity stable across input routes and token rotation, and make selective refresh operate on known account reports.
* feat(app): read usage reports through report IDs
Share report data between agent pills and host views while preserving the existing popover interaction for the next UI phase.
* fix(usage): keep account identity aligned with credentials
Read labels from local token claims and use one credential lookup for usage fetches and account identification. Bound cached profile identities across token rotation.
* fix(usage): isolate failure IDs and prune expired reports
Failure reports live outside the account-key namespace, and cache writes discard entries past the five-minute TTL.
* refactor(app): share the compact time-ago prose formatter
Move the tab tooltip's "3m" -> "3m ago" wording into utils/time so the
usage card can reuse it.
* feat(usage): show when each report was fetched
The usage card reads fetchedAt as "Updated 3m ago": a tooltip on the
card header where hover exists (web, desktop), printed in the card
footer on native and compact. Shared by the pill popover and the Usage
screen.
* feat(usage): open the pill popover from cache and refresh on demand
Opening the popover no longer forces a vendor fetch; it shows the cached
report. A Refresh control force-refreshes only this report
(usage.list_reports with its single ID and forceRefresh), shows
Refreshing... while it runs, keeps the previous report visible, and says
"Unable to refresh usage" if the request fails.
* feat(app): let ToolbarButton show a tooltip other than its label
An icon action can now say a status in its tooltip ("Updated 3m ago")
while keeping its label for accessibility.
* feat(usage): refresh a report from its card header
Refresh is an icon button at the right edge of every usage card header,
in the pill popover and on the Usage screen. It force-refreshes only that
report, writes the result to the report's cache and its host list, shows
a spinner and disables itself while pending, and prints "Unable to
refresh usage" in the card on failure. Its tooltip carries the
freshness; without hover the freshness stays in the card footer. The
status indicator sits just left of the button, and the popover's
separate refresh row is gone.
* fix(server): include internal plugin assets in built output
* feat(server): built-in plugins in /plugins (#5544)
* feat(server): load compiled built-in Codex plugin
* test(server): guard built-in plugin loading and installs
* test(server): verify built-in plugins from dist in CI
* refactor(server): migrate remaining usage sources to built-in plugins
* test(server): guard all packaged usage source registrations
* docs: describe usage validation across plugin hosts
* test(ci): include built-in plugins in routing contract
* test(server): import moved Codex usage source
* fix(server): retain ACP updates received with session creation
|
||
|
|
c081e0350d |
Support popular Pi extensions through per-extension adapters (#5309)
* refactor(server): give Pi extension handling a registry * feat(server): map Pi task extensions to Paseo task lists * feat(server): map Pi subagent extensions to the Subagents track * refactor(server): hydrate Pi child sessions asynchronously through the extension host Child session JSONL reads blocked the daemon during live events and replay. The extension host now prepares immediate upserts and bounded async timeline hydration; replay awaits it, and closed sessions discard it. Preserve claimed notification text so completion details remain visible in the parent conversation. * feat(server): present Pi question extensions as one question form Keep the extension questionnaire in one Paseo permission while Pi walks its RPC dialogs. The host defers an early dialog until the form is answered, then replies to each dialog using the collected choices. * fix(server): satisfy Pi extension start hook contract The server build checks the session interface more strictly than the fast typecheck. Return the optional permission explicitly so the adapter satisfies the published extension contract. * docs: document Pi extension adapters * chore(server): format Pi subagent captures These real RPC fixtures were added before the formatter ran over the captured JSON. Normalize whitespace so repository format checks pass without changing the captured data. * test(server): resolve Pi child fixtures as local paths on Windows File URL pathnames retain a leading slash before a Windows drive letter. Convert fixture URLs to filesystem paths and escape those paths when inserting them into captured JSON, so child timeline assertions exercise the real file on every CI platform. * refactor(server): remove Oh My Pi handling from the Pi provider * fix(server): keep Pi extension adapter failures out of core session handling * fix(server): bound Pi child-session hydration during history replay |
||
|
|
c906c2f4a4 |
fix(opencode): support v2 with automatic version selection (#5198)
* fix(opencode): support v2 while preserving v1 provider behavior * fix(opencode): target v2.0.7, keeping v1 support OpenCode 2.0.4 removed the /api/health endpoint and the plugin await-activation endpoint, and moved form reply/cancel under the session namespace. The v2 adapter was pinned to the 2.0.3 SDK, so any 2.0.4+ binary failed at helper-server startup with an opaque HTTP 404. - Pin @opencode/client to 2.0.7 and require v2 >= 2.0.4 at runtime selection, with a clear error for older binaries. - Use server.info for helper-server readiness. - Move form reply/cancel/list to session.form and send the permission decision field instead of reply. - Replace plugin.awaitActivation with a plugin.list readiness wait. A cold location registers its config-derived commands, skills, and providers asynchronously; an empty inventory means it is still warming. - Send session.command.name, which replaced command, and drop the removed SkillInfo.slash field. - Update the v2 test harness, unit tests, dependency lockfile, CI matrix, and provider docs to 2.0.7. * fix(opencode): target 2.0.10 and address v2 review findings * test(opencode): colocate v2 suites with their modules Greptile's review flagged that the v2 tests lived in top-level opencode-agent.*.test.ts files and imported the readiness helper, v2 client, harness, and API types from a distance. Move each v2 suite beside the module it describes and leave the top-level files to the v1 public path: - opencode/v2/agent.test.ts: session lifecycle, structured output, resume, reconciliation, interrupt/replacement, helper isolation. - opencode/v2/readiness.test.ts: location readiness deadline behavior. - opencode/v2/permissions.test.ts: question and approval routing. - opencode/v2/configuration.test.ts: permissionRules, feature flags, and helper-isolation decisions, exercised directly through the module. - opencode/v2/mapping.test.ts: model and mode normalization. - opencode/runtime-client.test.ts: runtime version selection. The v1 recovery, permission-action, and catalog-deadline suites stay in place. Test counts are preserved, with deeper per-module coverage for configuration. * test(opencode): split v2 local E2E by behavior and add harness helpers The re-review flagged the v2 scenarios in opencode-bridge.local.e2e.test.ts for inline event filtering, permission-kind branching, and polling in test bodies. Move stream, permission, and subagent coordination behind helpers in opencode/test-utils/v2-local-e2e-helpers.ts, and split the session scenarios into opencode-v2.local.e2e.test.ts so each file covers one behavior: - opencode-bridge.local.e2e.test.ts: process sharing, shell.env scoping, the native tool bridge, and runtime version selection. - opencode-v2.local.e2e.test.ts: session lifecycle, structured output, streaming, approvals, MCP preapproval, stop/replacement, rewind and subagents, commands, and skills/attachments. Each helper waits on or answers the provider directly, so the tests state domain intent and assert the collected result instead of polling or branching inline. The CI-selected "versioned runtime" case stays in the bridge file, and the live approvals and stop/replacement scenarios were re-run green through the new helpers. * fix(ci): preserve required server checks across OpenCode versions Run v1 and v2 integration coverage sequentially so branch protection keeps its existing static check names. * fix(agents): load startup history before registering sessions A failed history stream while adding a runtime notice must close the unregistered session instead of leaving an initializing agent behind. * fix(agents): limit startup history before buffering it Apply the existing timeline content limits to each incoming history event so raw cumulative tool output is not retained until registration completes. * fix(opencode): resolve Windows executables from platform packages Recent v1 npm releases place opencode.exe in the architecture dependency rather than the CLI bin directory. Resolve that dependency from the CLI package while retaining support for the older layout. * refactor(opencode): name public runtime option types * test(opencode): let the spec own both runtime versions Install each version in an isolated temporary directory and select it through provider configuration. Restore the CI workflow unchanged so version coverage is owned by the spec rather than workflow orchestration. * test(opencode): launch native Windows binaries in isolated specs Avoid npm command wrappers retaining locked executable files after helper shutdown during temporary installation cleanup. * fix(opencode): preserve the legacy path when adding v2 support Fall back to the existing client when version probing is inconclusive. Load v2 code and its bridge plugin only after positive v2 selection, keep v1 sessions undecorated, and restore original v1 discovery errors. * fix(opencode): retry inconclusive version selection A failed or unrecognized version probe previously cached the legacy fallback, pinning an upgraded binary to the v1 API until client recreation. Retry detection on the next operation while keeping the legacy path available. * fix(opencode): release legacy runtime after inconclusive probe Retryable version detection can clear the selected client after falling back to v1. Track whether the legacy client was selected so shutdown still releases its server without touching an unused v1 manager during a v2-only run. * test(opencode): assert full runtime notice history * fix(opencode): restore notice and retry behavior after rebase --------- Co-authored-by: Jonathan karras <6307688+karrots@users.noreply.github.com> Co-authored-by: Dhruv Singal <dhruvsingalabc@gmail.com> |
||
|
|
829cc5e173 |
Let Claude sessions pass structured launch arguments (#5206)
Expose Claude Agent SDK's documented extraArgs map through providerOptions so Claude-specific flags such as --chrome reach the CLI. Document boolean and valued flags, link the upstream contract, and validate through schema tests, the public session interface, and a real Claude launch. |
||
|
|
d0e1edca7f |
fix(server): show the right thinking options for each Cursor model (#4180)
* fix(server): probe Cursor thinking options per model Cursor ACP only reports thought_level for the current model, so the catalog was copying Haiku's Off/On onto Grok 4.6 until the next probe happened to land on Grok. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(server): drop inherited thinking options when an ACP catalog probe fails A rejected model switch was leaving the probe session's thinking list on the failed model, so Grok could still advertise Haiku's Off/On. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(server): probe per-model thinking when session/new has no thought_level Composer/Auto as the Cursor ACP default omits thought_level, so the catalog helper skipped every model switch and Grok kept an empty thinking list. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(server): discover model options without changing preferences Use the provider model catalog extension instead of probing model switches that persist user preferences. Keep provider-specific discovery in each shim and validate extension responses at the boundary. --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com> |
||
|
|
dd7671be20 |
Remember model selections across workspace creation (#4401)
* fix(providers): remember model selections across workspace creation Keep saved model intent independent of catalogue readiness and use the caller-owned host and directory from the first render. Share discovery through provider-owned keys while preserving execution context. Give each target one committed snapshot, publish actual transitions atomically, and share catalogue content across client directory associations. Regression coverage exercises successful repeated creation and provider lifecycle boundaries. * fix(providers): preserve shared cache bodies during eviction Admit directory references with their bodies, retain reference recency during eviction and recovery, and remove the unused persisted index. Refresh known workspace catalogues concurrently. Make repeated-creation picker states explicit and seed mode tests from the installed provider catalogue, preserving assertions across Codex versions. * fix(providers): bound discovery independently per provider Settings refresh repairs all remembered targets, which otherwise starts an unbounded number of discoveries. Admit work after catalogue-key deduplication and keep each provider’s budget independent so a stalled provider cannot block another. Preserve active accounting across configuration changes and settle obsolete queued requests during replacement and shutdown. |
||
|
|
53c9607479 |
Control Paseo tools per provider (#4277)
Add backend-only exact-provider controls for the Paseo tool catalog across native and MCP-backed agents, including persisted configuration, session snapshots, public documentation, and regression coverage.\n\nCo-authored-by: Matt Cowger <matt@cowger.us> |
||
|
|
95575ad05a |
fix(omp): honor configured timeout while waiting for ready (#4143)
* fix(omp): honor configured timeout while waiting for ready * fix(omp): increase ready timeout to 20 seconds * docs: keep OMP timeout policy in one place --------- Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com> |
||
|
|
ecec33265e |
feat(pi): add active-turn steering via native steer RPC (#3752)
* feat(pi): add active-turn steering via native steer RPC * fix(pi): clear queued steering before interrupt --------- Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> |
||
|
|
650f3ca7f6 |
Reuse OpenCode helpers across ordinary agent sessions (#4009)
* fix(opencode): reuse servers across agent sessions Paseo's always-present per-agent environment forced every OpenCode session onto a dedicated helper, multiplying cold starts and memory use. Route ordinary sessions through one shared helper while a daemon-owned session bridge preserves exact environment and caller-scoped tools; keep custom process environment and MCP configuration isolated. * fix(build): bundle the agent bridge from existing dependencies The helper package duplicated Zod and changed the fixed-output desktop dependency graph even though its tool wrapper was identity-only. Use the server's existing Zod runtime directly so packaged daemons can materialize the bridge without extra traced dependencies. * refactor(server): isolate provider runtime composition Keep provider-specific lifecycle and tool-catalog wiring behind an agent-owned runtime boundary so the generic daemon bootstrap remains provider-agnostic. The runtime now owns bridge startup, snapshot injection, and idempotent cleanup. * fix(desktop): prebundle the agent bridge plugin Packaged daemons run from an application archive where esbuild cannot spawn its binary. Produce the self-contained plugin during the server build and make compiled runtimes read that artifact; source-mode tests retain a gated compiler fallback. |
||
|
|
463415ae84 |
fix(providers): tolerate slow Pi and OMP RPC startup (#4008)
MCP and extension initialization can keep provider control-plane requests pending beyond 30 seconds even when the response will arrive. Use a configurable 60-second provider default and include the active RPC phase in timeout errors. |
||
|
|
71e375c2bc | fix(server): stop quota fetcher from writing provider credentials (#3442) | ||
|
|
bc02cfd8cd |
feat(opencode): steer active turns without interrupting (#3580)
* feat(opencode): add active-turn steering via native V2 prompt Add OpenCode support for active-turn steering (PR #3394) using the native V2 session prompt with delivery: steer. Implements steerActiveTurn, pending steer echo correlation for multiple steers, prompted event handling, and definitive vs ambiguous error handling. Updates docs and SDK to 1.18.18 for typed V2 access. * fix(opencode): use V1 prompt for steer so model sees it V2 prompt was admitted to a separate inbox not visible to the V1 processor, so "How old is bigbird?" was shown inline but never in the LLM context. Switch steer to session/prompt_async (V1) which queues while busy and is included in the next LLM call's history. Update tests to expect V1 and keep the timeline grouping. * fix(opencode): honor steering permission contract Admit the steer before denying permissions that block its delivery, matching the provider steering contract. Remove the abandoned V2 path and SDK upgrade because active-turn delivery uses the existing prompt endpoint. * chore(opencode): preserve SDK patch metadata --------- Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com> |
||
|
|
c6b04f302a |
fix(server): stop a steered prompt from deadlocking on a permission (#3585)
* fix(server): stop a steered prompt from deadlocking on a permission Claude parks its turn inside the blocking canUseTool callback while a plan approval or a tool prompt waits for an answer. The SDK stops to read its input stream there, so a message sent at the open card is queued and never read. The user must reject the card or interrupt the agent by hand, and then send the message a second time. Deny the pending permissions after the steer is admitted. The prompt is already in the SDK input queue when the permission callback returns, so Claude reads it in the same turn. Plan mode stays on and no turn restarts. One pass is not enough. A turn that made several tool calls opens its next request 10 to 300 milliseconds after the previous one is answered, so a single snapshot advances the batch by one and parks the turn again. The Claude adapter already tracks the steers the SDK has not read, so expose that as an optional AgentSession.hasUnreadSteer and answer each new request while that method reports the prompt as unread. The release stops the moment the prompt is read, so a permission the agent asks for after it acts on the message still reaches the user. Only the two entrypoints that carry a message from a person set the flag. A scheduled prompt, a child-finished notification, and an agent-to-agent prompt must not answer a card the user is looking at. Denial left no timeline row, so a denied plan disappeared from the transcript. Claude and Codex now push a plan_approval row with approved: false. For Codex the row comes from resolvePlanPermission, which is the path that dismissPendingPlanApprovals also uses. In the app, queue mode held the message until the turn ended, and a parked turn never ends. The composer now sends while a permission is pending, and the button label agrees with the submit path. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(server): keep a post-prompt permission out of the release The release loop answered the pending requests before it asked whether the agent had read the prompt. The agent can read the prompt during a poll interval and then open a permission request because of it. The next pass denied that request, so a decision that belongs to the user was made for them. The loop now asks first and answers second. The first pass stays unconditional, because a provider that does not report unread steers must still get one release. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(server): make steer permission clearing provider-owned Keep the daemon policy explicit while letting each provider queue the steer and release permissions that block it. This removes timing-based polling and preserves rejected plans as canonical timeline rows. * fix(server): preserve omitted steer options Only construct provider steer options when permission clearing is requested. System prompts otherwise keep the existing undefined options contract. --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com> |
||
|
|
f7e4606452 |
fix(server): discard unread Claude steers on interrupt
A steer pushed into the live SDK query input survived query.interrupt(): the SDK dequeued it afterwards and resumed the turn the user had just stopped. Codex clears pending turn input when it aborts a turn; Claude does not, so its adapter now cancels the SDK messages it queued before interrupting. Interrupt-discards is the deliberate ceiling for steer cancellation: Codex exposes no un-steer operation, so a cross-provider take-back cannot exist. A steer the model never read keeps its transcript row. |
||
|
|
2347242cdf |
fix(usage): read Cursor plan usage from cursor-agent auth.json (#3486)
Headless hosts only have ~/.config/cursor/auth.json from a CLI login, not the desktop state.vscdb token. Without that fallback Settings usage stayed unavailable on VPS-style machines. Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
42245d139a |
Add active-turn steering (#3394)
* feat(agents): add active-turn steering Let users choose whether an immediate prompt interrupts or joins the active turn. The daemon owns exact-turn admission, provider delivery, safe fallback, and canonical timeline identity while queueing remains client-side. * fix(agents): stabilize steering history and replay QA * fix(agents): preserve steering timeline order * fix(agents): reconcile resumed provider history * fix(agents): serialize steering with cancellation * test(agents): prove Claude multi-tool steering * test(agents): force explicit history hydration * fix(agents): flush output before steer admission |
||
|
|
20bccd31a3 |
Make provider catalog refresh deadlines configurable (#3322)
* fix(providers): make catalog refresh deadlines configurable Provider startup can legitimately exceed a fixed short timeout when catalog discovery has substantial initialization work. Give each provider refresh one configurable deadline covering availability and the complete catalog probe, and cancel named child operations before allowing retries so timed-out work cannot accumulate in the background. * fix(opencode): abort catalog server acquisition A provider refresh could expire while the helper server was still starting because acquisition only tracked the activity without observing its abort signal. Reject acquisition before transferring a server reference, while leaving the manager-owned startup reusable by the next caller. * test(file-observer): allow native burst recovery |
||
|
|
f24664b882 |
fix: terminalize Codex compaction on turn end (#3211)
* fix: terminalize Codex compaction on turn end * fix: ignore stale Codex compaction completions --------- Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com> |
||
|
|
f0eb7cea4b |
feat(providers): add native options and exact MCP grants (#3025)
* feat(providers): add native options and exact MCP grants Keep provider policy in provider-owned schemas so unattended Hub runs can grant only requested tools from injected MCP servers. Preserve the native resolved sandbox policy when a workflow override is present, including configured writable roots. * test(hub): verify real provider tool policy * test(app): avoid pinning transient restart status |
||
|
|
7dbeff8290 |
perf(app): reduce startup websocket work
Make provider catalogs compact and content-addressed so repeat launches can reuse the persisted snapshot instead of transferring it again. Remove eager global catalog loading and the stopped-agent defensive tail fetch, while keeping sequence-gap and reconnect recovery as the correctness paths. Add an opt-in production-ID Android profiling build for local startup traces. |
||
|
|
048b82f2a7 |
Preserve interactive ACP permission choices (#2762)
* fix(acp): preserve permission choices ACP question bridges encode answers as repeated permission options. Preserve each option through the UI and return the exact selected ID, while keeping auto-accept from choosing on the user's behalf. * fix(acp): validate selected permission behavior Reject inconsistent action IDs instead of allowing the action ID to override the response behavior. Move the misplaced browser spec so the current base CI contract can run. * fix(acp): preserve invalid permission requests Validate selected ACP actions before consuming the pending request so inconsistent client responses can be corrected and retried. * test(acp): verify real permission questions * fix(acp): reject empty permission action ids * test(e2e): scope Kimi config to ACP spec |
||
|
|
b0a5c09ace |
Add shared auto-accept controls to ACP sessions (#2752)
* feat(acp): auto-accept shared permission requests Some ACP agents expose permission requests without a native mode that can bypass them. Keep the opt-in at the shared adapter boundary so every ACP implementation gets the same per-session behavior. * test(acp): include the shared feature in provider expectations * fix(acp): honor unattended auto-accept intent * fix(agent): list client features without a model |
||
|
|
8021d2b6c9 |
Keep agent timelines stable from submission through resume (#2718)
* Reapply: Stop completed turns from appearing stuck (#2484)
* Reapply: Keep older chat history and image previews stable (#2490)
* Lock timeline layout stability with user-driven specs
Two reds reproduce the reported jank:
- new agent, first prompt: the row paints at top=100 then jumps to 144 through
authoritative hydration, because the draft tab and the agent panel each render
their own AgentStreamView for the same logical row.
- long loaded timeline, submit: scrollHeight collapses 10793 -> 9111 mid-turn,
because splitOrderedTail reclassifies measured rows into the virtualized segment
where the virtualizer falls back to estimateStreamItemHeight.
Also restores centered timeline rows: the pagination anchor wrapper introduced in
#2490 was a plain block div, which broke alignSelf: center.
* Measure the turn footer blink from when the footer appears
Bounding the window at the prompt row conflated two things: the footer arriving a
frame after the row, and the footer leaving the layout and coming back. Bounded from
the footer's first frame, the blink still reproduces, so the footer-ownership race is
real rather than a sampling artifact.
* Rename the shadowed prompt row binding in the frame recorder
* fix(desktop): allow concurrent worktree dev instances
Desktop terminals can inherit another instance's profile path and debugging flags. Scope the profile to the current dev root and select an available debugging port so instances do not share global runtime state.
* fix(desktop): expose development providers in dev builds
Desktop-managed daemons inherited no Paseo runtime mode, so dev builds constructed the production provider registry. Mark unpackaged internal launches as development while continuing to force packaged launches to production.
* fix(timeline): stabilize optimistic turn layout
Keep initial history loading dormant until user intent and preserve active-turn layout across delayed transport updates. Add painted-frame coverage for optimistic timeline transitions.
* fix(timeline): preserve optimistic prompts through hydration
Canonical replacement relied on a transport submission record that can settle before a provider echoes the user row. Keep presentation continuity owned by the unreconciled row while destructive history replacement remains governed by active transactions.
* fix(timeline): make submitted prompts canonical
Submission settlement previously depended on status updates that could arrive after the optimistic row. Persist every accepted prompt in the canonical timeline and keep the footer active until RPC and canonical acknowledgement converge.
* fix(timeline): stop older history pages repeating entries
Backward pagination counted source sequence rows, so page boundaries inside a merged projected item could return that item repeatedly. Count projected entries and advance from their display anchors instead, with server and browser regressions covering the contract.
* fix(timeline): resume agents from the latest history
Use a bounded tail probe on resume so long background runs appear atomically instead of replaying every missed page. Preserve unchanged views, reconcile local/live rows, and keep skipped history reachable through backward pagination.
* fix(timeline): reconcile turn state after missed updates
Turn completion events can be missed while an agent timeline is unsubscribed, leaving footer activity stale even after the directory reports idle. Reconcile the single-owned turn lifecycle from authoritative resume snapshots while preserving newer live evidence.
* test(timeline): cover running state across refresh
Protect the hydration boundary where directory state must restore consistent tab, composer, and footer activity. Expose the running indicator semantically so the regression checks user-visible state instead of spinner implementation details.
* fix(timeline): keep history loading geometry stable
Reserve a fixed history-start gutter regardless of pagination state so showing or hiding the loader cannot shift timeline content.
* fix(timeline): keep turn activity coherent
Derive footer, tab, and composer activity from one lifecycle and submission projection. Idle directory transitions now request an authoritative resume so missed terminal events converge without treating directory delivery order as turn authority.
* fix(tests): align checks with runtime boundaries
* fix(desktop): stop dev app surviving its terminal
Electron was detached from the desktop runner, allowing it to outlive a stopped workspace terminal with broken output pipes and retain the single-instance lock. Keep Electron owned by the runner and preserve explicit process-group cleanup for detached child trees.
* fix(timeline): preserve activity through turn handoff
Correlate lifecycle events and snapshots with daemon-owned turn identity, and keep optimistic submissions active until their canonical row arrives. This prevents stale updates and RPC timing from exposing a false idle state.
* refactor(timeline): remove submission timing state
* fix(timeline): restore prompt identity on resume
* fix(timeline): exclude attachments from canonical prompt text
Structured provider prompts can include hidden history attachments before the user's text. Persist only plain prompt blocks so authoritative user messages match what the composer displayed while provider input remains unchanged.
* refactor(agent): remove unused pending-run message identity
* fix(desktop): let Chromium allocate the debugging port
* refactor(agent): remove impossible finalized-turn tracking
* fix(timeline): load history after keyboard scrolling
Treat observed upward displacement as user intent so keyboard and scrollbar navigation can wake dormant pagination.
* test(server): preserve accepted turn stream order
The manager-owned accepted turn boundary intentionally precedes coalesced output. Assert that lifecycle event explicitly while retaining the timeline-before-terminal ordering contract.
* test(app): scope sidebar fixtures by identity
Shared E2E daemon state may contain unrelated projects. Keep stale-route coverage tied to its named missing project and drag only the two rows seeded by the reorder spec.
* fix(timeline): gate prompt revisions by client capability
* fix(timeline): finalize canonical prompt delivery
Treat provider identity as a single stored-row transition so legacy clients receive one deliverable canonical prompt. Release identity-pending rows before later foreground output or the current terminal while preserving stale-turn isolation.
* fix(server): preserve legacy prompt pagination
* fix(timeline): keep assistant block identity stable
Assign canonical block IDs during live promotion so terminal finalization cannot remount image-bearing rows.
* fix(timeline): apply covered revisions during resume append
Run the exact canonical revision pass before overlapping resume pages append newer rows, while leaving cursor advancement to the incremental path.
* fix(server): preserve queued agent transitions
Project each queued live snapshot before awaiting stored metadata so later lifecycle state cannot overwrite an earlier transition.
* fix(server): snapshot queued agent updates
Capture each live agent payload before it enters the per-agent queue so shared nested state cannot change while an earlier storage read is pending.
* docs(app): date agent turn compatibility cleanup
* refactor(timeline): mint submitted prompt identity at acceptance
Canonical prompt rows now keep one immutable wire identity while provider identity remains internal for rewind translation. This removes the unreleased same-sequence revision and legacy projection contract.
* refactor(app): remove prompt revision reconciliation
Submitted prompt rows are immutable on the wire, so resume and live reducers no longer need to apply already-covered same-sequence updates.
* test(app): cover submitted prompt rewind roundtrip
* docs(timeline): describe immutable submitted prompt identity
Provider echoes now enrich daemon-internal identity only; wire rows keep their acceptance-time identity and rewind performs the provider translation.
* test(server): validate mock rewind targets
Reject unknown provider message IDs so app rewind coverage exercises wire-to-provider identity translation.
* fix(server): validate rewind before interrupting
* fix(server): omit rewind identity for daemon prompts
* test(app): guard rewind eligibility and rejection
* fix(server): preserve prompt identity across hydration
* refactor(timeline): remove stale revision remnants
* Revert "fix(server): preserve prompt identity across hydration"
This reverts commit
|
||
|
|
f3fee73976 |
Stop OpenCode cancels from killing the next turn (#2662)
* fix(opencode): prevent late aborts from canceling replacement turns OpenCode aborts are session-scoped, so provider idle alone does not make a session safe to reuse. Keep abort retries owned by the stopping turn and require both abort settlement and provider idle before dispatching the next runner operation. * fix(opencode): retain the stop barrier through provider idle Keep the stopping operation authoritative until both its session-scoped abort and provider-idle evidence settle, including stale idle-session interrupts. Also keep the real drift fixture credential-allowlisted and wait for command output pipes to drain. * test(opencode): launch Windows fixture shims safely * fix(opencode): recover stop observation safely * fix(opencode): preserve autonomous wake through stop * fix(opencode): retain deferred wake completion * fix(opencode): serialize stop boundary replay * test(opencode): use shared real e2e lane * fix(opencode): own stops started during replay * fix(opencode): retry stop observation on demand * fix(opencode): follow replacement stop boundaries * fix(opencode): retain wakes during stop recovery * fix(opencode): hold replay turns behind boundary * fix(opencode): filter canceled reconnect output * fix(opencode): bind status evidence to stop * fix(opencode): acknowledge owned cancellation * fix(opencode): reconcile stale busy evidence * fix(opencode): bind replay terminals to wakes * refactor(opencode): own the stop boundary in one explicit state The run boundary had grown into a mutable flag bag plus a raw-event buffer that was replayed and re-classified after the fact. Each new interleaving needed another predicate, and the classification kept losing autonomous wakes. Restore stopping as a turn-state variant that owns exactly two things: the session-scoped abort it issued (with its single owned retry) and the canceled run's terminal. Runner operations wait on both; a failed abort fails closed. Provider status reconciliation now ends the stop through the same transition an SSE terminal takes. The canceled run's residue is suppressed only up to that terminal. Events published afterwards are a new run by construction and take the normal live path, so a wake no longer waits for abort settlement and cannot be dropped by a retention heuristic. * fix(opencode): emit cancellation before adopting the next run The acknowledgement ran off a promise combinator, so a wake adopted from the event right after the terminal could reach subscribers before the canceled turn's turn_canceled. Acknowledge synchronously from whichever proof lands first instead. * docs(opencode): attach the stop doc comment to its interface * fix(opencode): retry the stop in progress instead of opening a second Pressing Stop again after an abort failed built a detached boundary: its fresh abort could succeed while the canceled turn kept waiting for an acknowledgement, so the daemon sat out the full cancel timeout before force-canceling. A repeated Stop now replaces the abort owned by the stop already in progress, which is what carries the acknowledgement and what lifts the fail-closed gate. * fix(opencode): accumulate every abort a stop has issued A repeated Stop replaced the stop's abort settlement instead of adding to it, so a first request still in flight — the slow-teardown case the cancel cap exists for — no longer held the gate. The replacement could start while that request was live and get cancelled by it, which is the race this boundary exists to close. Reuse now waits for every abort the stop issued, while only the newest can hold the gate closed. * fix(opencode): scope abort settlement to the session, not the stop Abort is session-scoped, so its settlement has to be too. Tracking it on the stop meant a new stop boundary silently dropped an older request that was still in flight, and that request could still cancel whatever run started next. The session now accumulates every abort it has issued; the stop is left owning only the run it is stopping. * fix(opencode): recheck the abort gate after waiting on it A waiting prompt read the settlement once, so a Stop issued while it waited was invisible to it: the older abort would settle, the prompt would start, and the newer one could still cancel it. Drain until the settlement observed is the current one. |
||
|
|
5bd317205c | fix(omp): expose injected Paseo tools directly (#2418) | ||
|
|
6acc82e9d3 |
fix(pi): preserve discovered project prompts
Append Paseo instructions from the integration extension because Pi's append-system-prompt flag suppresses automatic APPEND_SYSTEM.md discovery. |
||
|
|
e1bda8e498 |
fix(app): keep submitted prompts in timeline order (#2259)
Canonical provider message IDs and optimistic client IDs occupy different namespaces. Preserve both identities for deterministic reconciliation while retaining a dated content fallback for older daemon timelines. |
||
|
|
d2308f4835 |
feat(omp): add native OMP provider (#2067)
Pi and OMP share only the JSONL child-process transport while retaining provider-owned launch, RPC, runtime, session, history, and permission behavior. Removes captured fixtures in favor of typed harnesses and real-provider coverage. Closes #2006 Closes #2060 |
||
|
|
ada0955334 | fix(server): preserve Pi MCP config during injection (#1990) | ||
|
|
ebaecace2b |
Keep Pi text-only sessions from bricking on images (#1960)
* fix(pi): avoid raw images for text-only models Pi text-only models reject image content, and Pi persists the rejected raw image in session history. Materialize those image prompts to local files and send path hints while preserving raw images for vision-capable models. * fix(pi): require explicit vision support Default unknown Pi model capabilities to the text-hint path so raw image forwarding only happens for models that declare image input. Also keep materialized image files in a private per-user temp directory with restrictive file modes. * fix(pi): harden materialized image temp dirs * test(server): accept per-user image attachment dirs * fix(server): use private temp dirs for materialized images * fix(server): refresh missing image temp cache * test(server): clean image temp dirs |
||
|
|
7cb0ccba60 |
Show Claude Ultra Code where supported (#1872)
* fix(providers): show Claude Ultra Code by capability Expose Ultra Code for xhigh-capable Claude models and keep first-party Claude model metadata in one manifest so future model updates do not need scattered feature lists. * fix(providers): preserve Claude fast mode aliases Normalize versioned Claude model IDs through the manifest before checking fast-mode support so dated Opus IDs keep the same feature surface as their base model. * fix(providers): keep Claude fast mode first-party Anchor Claude model normalization so custom settings models that merely contain first-party IDs do not inherit manifest-only fast mode. * fix(providers): preserve Claude runtime aliases Keep Fast Mode capability checks strict to manifest IDs while allowing provider-form Claude runtime model strings to resolve known model metadata. |