mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-17 14:52:36 +00:00
Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8e24677e1 | ||
|
|
1f3bb0239b | ||
|
|
f7e68e32d1 | ||
|
|
94fc4e5d49 | ||
|
|
7bcf167862 | ||
|
|
8598b6aab9 | ||
|
|
ae1cf642bb | ||
|
|
2ee0191ea1 | ||
|
|
f3917173a9 |
+68
-61
@@ -1,91 +1,98 @@
|
||||
# Changelog
|
||||
|
||||
## 0.8.0-beta.1 - 2026-09-08
|
||||
## 0.8.0 - 2026-09-10
|
||||
|
||||
This release considerably extends what plugins can do: complete providers, custom components in the chat, slash commands, settings screens, and agent lifecycle hooks.
|
||||
Paseo 0.8 adds plugin header buttons, custom providers, and richer chat components, alongside fixes for desktop updates and mobile keyboards.
|
||||
|
||||
The plugin API has a breaking change. Plugins now use separate client and server entries and runtime-owned SDK entry points, so plugins written for 0.7 must follow the [migration guide](https://paseo.sh/docs/plugins/v0.8/migration) before they load on 0.8.
|
||||
|
||||
It also fixes regressions across desktop updates, mobile, Import session, and agent lifecycle.
|
||||
|
||||
### Plugins
|
||||
|
||||
- Added provider contributions: a plugin registers a complete provider, directly or through the bundled ACP shim, with its own icon, session settings, permissions, and timeline rendering ([#4314](https://github.com/getpaseo/paseo/pull/4314))
|
||||
- Added live timeline items: a plugin renders custom components in the chat by transforming timeline items at render time ([e34aea2](https://github.com/getpaseo/paseo/commit/e34aea206e6ec7ac2d5fa9766db28fb221ae990c))
|
||||
- Added durable timeline rows: a daemon handler appends its own persisted rows to an agent's timeline ([e34aea2](https://github.com/getpaseo/paseo/commit/e34aea206e6ec7ac2d5fa9766db28fb221ae990c))
|
||||
- Added slash commands: a plugin registers commands in the composer that run on the client without a provider turn ([e34aea2](https://github.com/getpaseo/paseo/commit/e34aea206e6ec7ac2d5fa9766db28fb221ae990c))
|
||||
- Added settings screens: a plugin contributes screens under Settings → Plugins from shared settings components, with host-scoped persistence, validation, and Command Center navigation ([#4357](https://github.com/getpaseo/paseo/pull/4357))
|
||||
- Added lifecycle hooks: server callbacks observe agent creation, turns, permissions, and archive events, and can answer permissions or send follow-ups ([#4435](https://github.com/getpaseo/paseo/pull/4435))
|
||||
- Added agent creation transforms: before hooks rewrite a new agent's provider, model, launch environment, MCP servers, and workspace isolation ([#4435](https://github.com/getpaseo/paseo/pull/4435))
|
||||
- Added modal bodies: a plugin owns the whole sheet body with custom padding, ScrollView or FlatList scrolling, horizontal tabs, keyboard-aware inputs, and a clipboard copy action ([#4392](https://github.com/getpaseo/paseo/pull/4392), [#4431](https://github.com/getpaseo/paseo/pull/4431))
|
||||
- Added terminals to the SDK: create, list, write, send keys, capture, and kill terminals by workspace ID ([#4358](https://github.com/getpaseo/paseo/pull/4358))
|
||||
- Added `paseo.projects.subscribe()` for live project updates ([#3983](https://github.com/getpaseo/paseo/pull/3983) by [@omercnet](https://github.com/omercnet))
|
||||
- Added `agent.respondToPermission()` to answer pending permissions from a plugin ([#3985](https://github.com/getpaseo/paseo/pull/3985) by [@omercnet](https://github.com/omercnet))
|
||||
- Added version requirements: `requirements.paseo` in `paseo-plugin.json` declares the supported Paseo versions, incompatible installs, updates, and reloads are rejected before any code runs, and prerelease Paseo versions satisfy any range their stable core satisfies ([#4430](https://github.com/getpaseo/paseo/pull/4430), [#4452](https://github.com/getpaseo/paseo/pull/4452))
|
||||
- Changed plugins to separate `index.client.ts` and `index.server.ts` entries with `client/`, `server/`, and `shared/` directories; plugins written for 0.7 must follow the [migration guide](https://paseo.sh/docs/plugins/v0.8/migration) ([#4206](https://github.com/getpaseo/paseo/pull/4206))
|
||||
- Changed the plugin SDK into runtime-owned entry points: `@getpaseo/plugin` for shared code, `@getpaseo/plugin/client` for the app, and `@getpaseo/plugin/server` for the daemon, with crossed imports rejected at compile time ([#4347](https://github.com/getpaseo/paseo/pull/4347) by [@liujin0506](https://github.com/liujin0506), [@panrafal](https://github.com/panrafal))
|
||||
- Changed legacy plugin load failures to say the plugin was built for an older Paseo and link to the migration guide ([#4265](https://github.com/getpaseo/paseo/pull/4265))
|
||||
- Changed `paseo plugin ls` to report runtime state, source, installed commit, and load errors without contacting a remote ([#4265](https://github.com/getpaseo/paseo/pull/4265))
|
||||
- Fixed workspace plugin panels missing from the Explorer tab-rail menu ([#4446](https://github.com/getpaseo/paseo/pull/4446))
|
||||
- Added versioned plugin documentation for v0.7 and v0.8 with a migration guide ([#4245](https://github.com/getpaseo/paseo/pull/4245), [#4452](https://github.com/getpaseo/paseo/pull/4452))
|
||||
**Before upgrading:** desktop now requires macOS 13 or newer. Plugins written for 0.7 need the [0.8 migration guide](https://paseo.sh/docs/plugins/v0.8/migration), including separate client/server entries and the revised composer pill API.
|
||||
|
||||
### Added
|
||||
|
||||
- Added Import session to the sidebar footer, the History and sidebar empty states, and the Command Center ([#4216](https://github.com/getpaseo/paseo/pull/4216))
|
||||
- Added search, Load more, and a This workspace / Show all scope to the Import session sheet ([#4216](https://github.com/getpaseo/paseo/pull/4216))
|
||||
- Added customizable top-level sidebar items: reorder and hide New workspace, History, Search, Schedules, and plugin items from Appearance settings ([#4203](https://github.com/getpaseo/paseo/pull/4203))
|
||||
- Added provider notifications from Pi extensions and OpenCode as timeline items with info, warning, and error levels ([#3411](https://github.com/getpaseo/paseo/pull/3411) by [@trim21](https://github.com/trim21))
|
||||
- Added a fullscreen viewer for Mermaid diagrams on web and desktop ([#4107](https://github.com/getpaseo/paseo/pull/4107) by [@gengjiawen](https://github.com/gengjiawen))
|
||||
- Added Copy, Copy line, and Select all to the web diff context menu, with Cmd/Ctrl+A and Escape ([#4229](https://github.com/getpaseo/paseo/pull/4229))
|
||||
- Added answer forms for Codex questions asked while the agent continues working ([#4587](https://github.com/getpaseo/paseo/pull/4587))
|
||||
- Added Mark as unread to finished workspaces in the sidebar menu ([#3603](https://github.com/getpaseo/paseo/pull/3603) by [@edihasaj](https://github.com/edihasaj))
|
||||
- Added an in-app What's new sheet in the help menu, Settings → About, and desktop update callout ([#4576](https://github.com/getpaseo/paseo/pull/4576))
|
||||
- Added Import session to the sidebar footer, History, and Command Center ([#4216](https://github.com/getpaseo/paseo/pull/4216))
|
||||
- Added search, pagination, and workspace filtering to Import session ([#4216](https://github.com/getpaseo/paseo/pull/4216))
|
||||
- Added sidebar item reordering and visibility controls in Appearance settings ([#4203](https://github.com/getpaseo/paseo/pull/4203))
|
||||
- Added provider notifications from Pi extensions and OpenCode to the timeline ([#3411](https://github.com/getpaseo/paseo/pull/3411) by [@trim21](https://github.com/trim21))
|
||||
- Added a fullscreen Mermaid viewer on web and desktop ([#4107](https://github.com/getpaseo/paseo/pull/4107) by [@gengjiawen](https://github.com/gengjiawen))
|
||||
- Added Copy, Copy line, and Select all to the web diff context menu ([#4229](https://github.com/getpaseo/paseo/pull/4229))
|
||||
- Added Gajae Code to the ACP provider catalog ([#3471](https://github.com/getpaseo/paseo/pull/3471) by [@Yeachan-Heo](https://github.com/Yeachan-Heo))
|
||||
- Added per-provider Paseo tool policy in daemon config to disable all or selected Paseo tools for a provider ([#4277](https://github.com/getpaseo/paseo/pull/4277) by [@mcowger](https://github.com/mcowger))
|
||||
- Added explicit setup approval for fork pull request workspaces: setup, automatic terminals, and scripts wait for Run setup or `paseo workspace setup <id>` ([#4215](https://github.com/getpaseo/paseo/pull/4215))
|
||||
- Added Hub follow-ups to existing agents, archived workspace recovery, and provider catalog discovery through the daemon API ([#4354](https://github.com/getpaseo/paseo/pull/4354), [#4242](https://github.com/getpaseo/paseo/pull/4242))
|
||||
- Added per-provider controls for disabling all or selected Paseo tools ([#4277](https://github.com/getpaseo/paseo/pull/4277) by [@mcowger](https://github.com/mcowger))
|
||||
- Added explicit approval before running setup, automatic terminals, or scripts in fork pull request workspaces ([#4215](https://github.com/getpaseo/paseo/pull/4215))
|
||||
- Added Hub follow-ups to existing agents, including archived workspace recovery ([#4354](https://github.com/getpaseo/paseo/pull/4354))
|
||||
- Added provider catalog discovery to Hub executions ([#4242](https://github.com/getpaseo/paseo/pull/4242))
|
||||
|
||||
### Plugins
|
||||
|
||||
- Added custom providers with their own icons, settings, permissions, and timeline rendering ([#4314](https://github.com/getpaseo/paseo/pull/4314))
|
||||
- Added provider session recovery after plugin reload ([#4629](https://github.com/getpaseo/paseo/pull/4629) by [@mcowger](https://github.com/mcowger))
|
||||
- Added custom timeline components and persisted timeline rows ([e34aea2](https://github.com/getpaseo/paseo/commit/e34aea206e6ec7ac2d5fa9766db28fb221ae990c))
|
||||
- Added client slash commands that run without a provider turn ([e34aea2](https://github.com/getpaseo/paseo/commit/e34aea206e6ec7ac2d5fa9766db28fb221ae990c))
|
||||
- Added plugin settings screens under Settings → Plugins ([#4357](https://github.com/getpaseo/paseo/pull/4357))
|
||||
- Added lifecycle hooks for agent creation, turns, permissions, and archive events ([#4435](https://github.com/getpaseo/paseo/pull/4435))
|
||||
- Added transforms for new agents' provider, model, environment, MCP servers, and workspace isolation ([#4435](https://github.com/getpaseo/paseo/pull/4435))
|
||||
- Added header buttons and composer pills with actions, menus, custom popovers, and live updates ([#4577](https://github.com/getpaseo/paseo/pull/4577))
|
||||
- Added custom modal bodies with scrolling, keyboard-aware inputs, and clipboard actions ([#4392](https://github.com/getpaseo/paseo/pull/4392))
|
||||
- Added terminal creation, input, capture, and management to the SDK ([#4358](https://github.com/getpaseo/paseo/pull/4358))
|
||||
- Added live project subscriptions through `paseo.projects.subscribe()` ([#3983](https://github.com/getpaseo/paseo/pull/3983) by [@omercnet](https://github.com/omercnet))
|
||||
- Added permission responses through `agent.respondToPermission()` ([#3985](https://github.com/getpaseo/paseo/pull/3985) by [@omercnet](https://github.com/omercnet))
|
||||
- Added provider usage queries through `providers.listUsage()` ([#4062](https://github.com/getpaseo/paseo/pull/4062) by [@ivanbrykov](https://github.com/ivanbrykov))
|
||||
- Added supported Paseo version requirements to plugin manifests ([#4430](https://github.com/getpaseo/paseo/pull/4430))
|
||||
- Changed plugins to separate client and server entries with runtime-specific SDK imports ([#4347](https://github.com/getpaseo/paseo/pull/4347) by [@liujin0506](https://github.com/liujin0506), [@panrafal](https://github.com/panrafal))
|
||||
- Changed composer pill registration to use `button`, `registration.update()`, and `registration.remove()` ([#4577](https://github.com/getpaseo/paseo/pull/4577))
|
||||
- Changed `paseo plugin ls` to show runtime state, installed commit, and load errors without contacting a remote ([#4265](https://github.com/getpaseo/paseo/pull/4265))
|
||||
- Fixed workspace plugin panels missing from the Explorer menu ([#4446](https://github.com/getpaseo/paseo/pull/4446))
|
||||
|
||||
### Changed
|
||||
|
||||
- Changed the desktop app to require macOS 13 or newer; macOS 12 stops receiving desktop updates ([#4322](https://github.com/getpaseo/paseo/pull/4322))
|
||||
- Changed `--host` to a global CLI option: `paseo --host <target> <command>` ([#4238](https://github.com/getpaseo/paseo/pull/4238))
|
||||
- Changed the sidebar footer Home button to Import session; the Home route and tile remain ([#4216](https://github.com/getpaseo/paseo/pull/4216))
|
||||
- Changed the sidebar footer Home button to Import session ([#4216](https://github.com/getpaseo/paseo/pull/4216))
|
||||
- Changed `paseo hub init` and default `paseo hub deploy` to use organization triggers in `.paseo/triggers/` ([#4517](https://github.com/getpaseo/paseo/pull/4517))
|
||||
- Changed Codex Fast availability to the supported model list, including GPT-6 Astra ([#4640](https://github.com/getpaseo/paseo/pull/4640))
|
||||
- Changed CLI installs to resolve patched React 19.1.x releases ([#4100](https://github.com/getpaseo/paseo/pull/4100) by [@liujin0506](https://github.com/liujin0506))
|
||||
|
||||
### Improved
|
||||
|
||||
- Reconnected every host immediately on app foreground instead of waiting for reconnect backoff ([#4160](https://github.com/getpaseo/paseo/pull/4160))
|
||||
- Smoothed mobile keyboard transitions by moving the timeline and composer together instead of relaying out every frame ([#4275](https://github.com/getpaseo/paseo/pull/4275))
|
||||
- Reloaded only the providers whose configuration changed instead of restarting every provider and resetting every catalog ([#4332](https://github.com/getpaseo/paseo/pull/4332))
|
||||
- Shortened agent tab tooltips to one title line with the agent ID and last activity ([#4214](https://github.com/getpaseo/paseo/pull/4214))
|
||||
- Added macOS updater and ShipIt state to App Diagnostics ([#4322](https://github.com/getpaseo/paseo/pull/4322))
|
||||
- Reconnected every host immediately when returning to the app ([#4160](https://github.com/getpaseo/paseo/pull/4160))
|
||||
- Smoothed mobile keyboard transitions by moving the timeline and composer together ([#4275](https://github.com/getpaseo/paseo/pull/4275))
|
||||
- Reduced stalls when opening large diffs ([#4574](https://github.com/getpaseo/paseo/pull/4574))
|
||||
- Reloaded only providers whose configuration changed ([#4332](https://github.com/getpaseo/paseo/pull/4332))
|
||||
- Removed unsolicited timeline and catalog traffic from idle SDK connections ([#4470](https://github.com/getpaseo/paseo/pull/4470))
|
||||
- Added macOS updater diagnostics to App Diagnostics ([#4322](https://github.com/getpaseo/paseo/pull/4322))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed the mobile terminal keyboard closing and reopening when using toolbar keys or Enter ([#4469](https://github.com/getpaseo/paseo/pull/4469))
|
||||
- Fixed macOS desktop updates closing the app without installing the new version ([#4322](https://github.com/getpaseo/paseo/pull/4322))
|
||||
- Fixed the desktop updater reporting up to date while a new release had no updater manifests yet ([#4201](https://github.com/getpaseo/paseo/pull/4201))
|
||||
- Fixed the desktop updater reporting up to date before release manifests were available ([#4201](https://github.com/getpaseo/paseo/pull/4201))
|
||||
- Fixed `paseo restart` failing from the desktop-installed CLI on Linux `.deb`, RPM, and tarball installs ([#4207](https://github.com/getpaseo/paseo/pull/4207))
|
||||
- Fixed the packaged macOS Dock icon rendering oversized ([#4389](https://github.com/getpaseo/paseo/pull/4389))
|
||||
- Fixed cached conversations and the workspace list not painting when live updates arrived during cache reads ([#4436](https://github.com/getpaseo/paseo/pull/4436))
|
||||
- Fixed the mobile terminal keyboard closing and reopening when using toolbar keys or Enter ([#4469](https://github.com/getpaseo/paseo/pull/4469))
|
||||
- Fixed cached conversations and workspace lists failing to appear when live updates arrived during loading ([#4436](https://github.com/getpaseo/paseo/pull/4436))
|
||||
- Fixed queued messages not sending after a turn completed while the app was in the background ([#4160](https://github.com/getpaseo/paseo/pull/4160))
|
||||
- Fixed the New workspace form losing the selected model after creating a workspace ([#4401](https://github.com/getpaseo/paseo/pull/4401))
|
||||
- Fixed a crash with `Select a model` when the selected provider disappeared during workspace creation ([#4332](https://github.com/getpaseo/paseo/pull/4332))
|
||||
- Fixed Codex model rows staying on Loading forever in the New workspace model picker ([#4283](https://github.com/getpaseo/paseo/pull/4283) by [@colonelpanic8](https://github.com/colonelpanic8))
|
||||
- Fixed reloading a Codex agent failing with `already has an active writer` ([#4353](https://github.com/getpaseo/paseo/pull/4353))
|
||||
- Fixed Codex reloads and follow-ups after workspace archive failing with an active-writer error ([#4353](https://github.com/getpaseo/paseo/pull/4353))
|
||||
- Fixed repeated Claude compaction updates leaving extra Compacting rows spinning ([#4391](https://github.com/getpaseo/paseo/pull/4391) by [@tomgrin10](https://github.com/tomgrin10))
|
||||
- Fixed Pi agents stuck running after an extension-triggered turn ([#3849](https://github.com/getpaseo/paseo/pull/3849) by [@mjakl](https://github.com/mjakl))
|
||||
- Fixed nested provider subagents shown as direct children of the root agent and their notifications appearing in the root timeline ([#4321](https://github.com/getpaseo/paseo/pull/4321))
|
||||
- Fixed repeated provider tool-call IDs across autonomous turns producing duplicate timeline rows ([140b0bb](https://github.com/getpaseo/paseo/commit/140b0bb716205cf0e00c0ff5b6b3c20af6c79413))
|
||||
- Fixed the Import session spinner never stopping when one provider hung; the provider now shows an inline error with Retry ([#4216](https://github.com/getpaseo/paseo/pull/4216))
|
||||
- Fixed delayed workspace creation navigating away after you switched elsewhere ([#2986](https://github.com/getpaseo/paseo/pull/2986) by [@cleiter](https://github.com/cleiter))
|
||||
- Fixed New workspace losing the selected model after creating a workspace ([#4401](https://github.com/getpaseo/paseo/pull/4401))
|
||||
- Fixed a crash when the selected provider disappeared during workspace creation ([#4332](https://github.com/getpaseo/paseo/pull/4332))
|
||||
- Fixed Codex model rows staying on Loading in the model picker ([#4283](https://github.com/getpaseo/paseo/pull/4283) by [@colonelpanic8](https://github.com/colonelpanic8))
|
||||
- Fixed nested subagents appearing under the root agent instead of their actual parent ([#4321](https://github.com/getpaseo/paseo/pull/4321))
|
||||
- Fixed repeated tool-call IDs across autonomous turns producing duplicate timeline rows ([140b0bb](https://github.com/getpaseo/paseo/commit/140b0bb716205cf0e00c0ff5b6b3c20af6c79413))
|
||||
- Fixed Import session loading forever when one provider hung ([#4216](https://github.com/getpaseo/paseo/pull/4216))
|
||||
- Fixed Import session ignoring Claude Code `/rename` titles ([#4216](https://github.com/getpaseo/paseo/pull/4216))
|
||||
- Fixed importing a session into a directory with an active workspace creating a duplicate workspace ([#4216](https://github.com/getpaseo/paseo/pull/4216))
|
||||
- Fixed importing a session creating a duplicate workspace for an already-open directory ([#4216](https://github.com/getpaseo/paseo/pull/4216))
|
||||
- Fixed empty ACP sessions appearing in Import session ([#4335](https://github.com/getpaseo/paseo/pull/4335))
|
||||
- Fixed diff file headers lagging behind the document while scrolling on web and desktop ([#4240](https://github.com/getpaseo/paseo/pull/4240))
|
||||
- Fixed the Changes tree and its working diff going out of sync when switching between Committed and Uncommitted ([#4199](https://github.com/getpaseo/paseo/pull/4199))
|
||||
- Fixed Mermaid diagrams shrinking on every streaming re-render and the inline diagram box collapsing ([#4107](https://github.com/getpaseo/paseo/pull/4107) by [@gengjiawen](https://github.com/gengjiawen))
|
||||
- Fixed Mermaid source corrupted on reload when cached history overlapped the live message ([#4210](https://github.com/getpaseo/paseo/pull/4210))
|
||||
- Fixed restored terminal titles briefly showing Terminal before loading ([#4553](https://github.com/getpaseo/paseo/pull/4553))
|
||||
- Fixed diff file headers lagging behind while scrolling on web and desktop ([#4240](https://github.com/getpaseo/paseo/pull/4240))
|
||||
- Fixed Changes and its diff going out of sync when switching between Committed and Uncommitted ([#4199](https://github.com/getpaseo/paseo/pull/4199))
|
||||
- Fixed Mermaid diagrams shrinking during streaming or becoming corrupted on reload ([#4210](https://github.com/getpaseo/paseo/pull/4210))
|
||||
- Fixed Settings navigation retaining closed views in memory on desktop ([#4228](https://github.com/getpaseo/paseo/pull/4228))
|
||||
- Fixed the file watcher missing nested directory changes on Windows ([#4354](https://github.com/getpaseo/paseo/pull/4354), [#4358](https://github.com/getpaseo/paseo/pull/4358))
|
||||
- Fixed daemon Git commands honoring a repository's `core.fsmonitor`, which let a checkout run its own command ([#4208](https://github.com/getpaseo/paseo/pull/4208))
|
||||
- Fixed terminal activity reporting for OpenCode 2 sessions started from a terminal ([#4300](https://github.com/getpaseo/paseo/pull/4300) by [@mr-karan](https://github.com/mr-karan))
|
||||
- Changed the plugin SDK's React peer range to `~19.1.0` so CLI installs resolve a patched 19.1.x instead of the version dependency scanners flag ([#4100](https://github.com/getpaseo/paseo/pull/4100) by [@liujin0506](https://github.com/liujin0506))
|
||||
- Fixed the paseo.sh homepage offering the Mac download to every visitor before hydration and desktop builds to phones ([#4222](https://github.com/getpaseo/paseo/pull/4222))
|
||||
- Fixed the file watcher missing nested directory changes on Windows ([#4354](https://github.com/getpaseo/paseo/pull/4354))
|
||||
- Fixed daemon Git commands allowing a repository's `core.fsmonitor` to execute its own command ([#4208](https://github.com/getpaseo/paseo/pull/4208))
|
||||
- Fixed terminal activity reporting for OpenCode 2 sessions ([#4300](https://github.com/getpaseo/paseo/pull/4300) by [@mr-karan](https://github.com/mr-karan))
|
||||
- Fixed the website offering downloads for the wrong platform before loading ([#4222](https://github.com/getpaseo/paseo/pull/4222))
|
||||
|
||||
## 0.7.2 - 2026-09-02
|
||||
|
||||
|
||||
@@ -106,9 +106,14 @@ Repo dev commands use checkout-local state by default. In this checkout, `PASEO_
|
||||
|
||||
See [docs/development.md](docs/development.md) for full setup, build sync requirements, and debugging.
|
||||
|
||||
## Critical rules
|
||||
## Release branches
|
||||
|
||||
- **Before changing the plugin SDK, compiler, host module maps, scaffold, or examples, read [SDK import boundaries](docs/plugins.md#sdk-import-boundaries).** Classify the export by runtime first and preserve the enforced boundaries.
|
||||
When the user says "this goes to next", create or
|
||||
retarget the PR to `next` and preserve that destination through delivery. Follow
|
||||
[release branch discipline](docs/release.md#release-branch-discipline) for creating
|
||||
and updating `next`, integrating it after a release, and releasing a hotfix from a tag.
|
||||
|
||||
## Critical rules
|
||||
|
||||
- **NEVER restart the main Paseo daemon on port 6767 without permission** — it manages all running agents. If you're an agent, restarting it kills your own process.
|
||||
- **NEVER assume a timeout means the service needs restarting** — timeouts can be transient.
|
||||
|
||||
+31
-1
@@ -47,6 +47,36 @@ commit the prepared inputs locally and run the release command. Its branch and
|
||||
tag push is the one remote release batch and starts CI for the complete release
|
||||
commit.
|
||||
|
||||
## Release branch discipline
|
||||
|
||||
While you finalize a release on `main`, use a temporary `next` branch for work intended for the following
|
||||
release. This applies to both beta and stable releases.
|
||||
|
||||
- Create each new `next` from freshly fetched `origin/main`. Reuse it while active.
|
||||
- "This goes to next" means create the PR against `next` or retarget an existing
|
||||
PR, and keep that destination through delivery.
|
||||
- Keep `next` current by merging `origin/main` into it as release fixes land.
|
||||
Avoid rebasing this shared branch because agents and open PRs depend on its history.
|
||||
- After the release ships, bring `next` up to date and open a `next` → `main` PR.
|
||||
Pass CI and merge without squashing away the individual PR commits needed for
|
||||
the changelog. Retarget remaining PRs based on `next` to `main` and delete the integrated
|
||||
`next`. Create it fresh when needed again.
|
||||
|
||||
**Setup still needed:** CI, Docker, and Nix PR checks currently target only `main`,
|
||||
and GitHub permits only squash merges. Enable checks and required-check protection
|
||||
for `next`, CI on its pushes, and merge commits for the integration PR. Handle PR
|
||||
base changes (`edited` events) so retargeting runs checks against the new base;
|
||||
GitHub's default PR events do not cover this. Deployment triggers stay unchanged.
|
||||
|
||||
### Hotfix from a release tag
|
||||
|
||||
If `main` contains changes you do not want to release, branch from the affected
|
||||
release tag and cherry-pick only the required fixes. Run CI on that branch, then
|
||||
use the normal release flow with it as the explicit source, choosing a new patch
|
||||
or beta version. Ensure the fixes and changelog also reach `main` and any active
|
||||
`next`, preserving newer development and version changes there. This is a
|
||||
short-lived hotfix branch, not another maintained release track.
|
||||
|
||||
## ACP catalog updates
|
||||
|
||||
ACP catalog work enters a release through an explicit user request:
|
||||
@@ -64,7 +94,7 @@ release push as the changelog and version commit.
|
||||
|
||||
There are two supported release paths:
|
||||
|
||||
1. **Direct stable release**: you are ready to ship the current `main` commit to everyone immediately.
|
||||
1. **Direct stable release**: you are ready to ship the resolved release source to everyone immediately (default `origin/main`).
|
||||
2. **Beta flow**: release candidates on the `beta` channel. Each beta carries its own changelog entry, publishes npm only on the explicit `beta` dist-tag, and stays behind the Stable/Beta switch on `/download`.
|
||||
|
||||
Paseo has one linear release track even though npm dist-tags are independent
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
Paseo 0.8 adds plugin header buttons, custom providers, and richer chat components, alongside fixes for desktop updates and mobile keyboards.
|
||||
|
||||
Before upgrading: desktop now requires macOS 13 or newer. Plugins written for 0.7 need the 0.8 migration guide, including separate client/server entries and the revised composer pill API.
|
||||
|
||||
Added
|
||||
- Added answer forms for Codex questions asked while the agent continues working
|
||||
|
||||
Full notes: https://paseo.sh/changelog
|
||||
@@ -0,0 +1,8 @@
|
||||
Paseo 0.8 adds plugin header buttons, custom providers, and richer chat components, alongside fixes for desktop updates and mobile keyboards.
|
||||
|
||||
Before upgrading: desktop now requires macOS 13 or newer. Plugins written for 0.7 need the 0.8 migration guide, including separate client/server entries and the revised composer pill API.
|
||||
|
||||
Added
|
||||
- Added answer forms for Codex questions asked while the agent continues working
|
||||
|
||||
Full notes: https://paseo.sh/changelog
|
||||
@@ -0,0 +1,8 @@
|
||||
Paseo 0.8 adds plugin header buttons, custom providers, and richer chat components, alongside fixes for desktop updates and mobile keyboards.
|
||||
|
||||
Before upgrading: desktop now requires macOS 13 or newer. Plugins written for 0.7 need the 0.8 migration guide, including separate client/server entries and the revised composer pill API.
|
||||
|
||||
Added
|
||||
- Added answer forms for Codex questions asked while the agent continues working
|
||||
|
||||
Full notes: https://paseo.sh/changelog
|
||||
@@ -0,0 +1,8 @@
|
||||
Paseo 0.8 adds plugin header buttons, custom providers, and richer chat components, alongside fixes for desktop updates and mobile keyboards.
|
||||
|
||||
Before upgrading: desktop now requires macOS 13 or newer. Plugins written for 0.7 need the 0.8 migration guide, including separate client/server entries and the revised composer pill API.
|
||||
|
||||
Added
|
||||
- Added answer forms for Codex questions asked while the agent continues working
|
||||
|
||||
Full notes: https://paseo.sh/changelog
|
||||
+1
-1
@@ -1 +1 @@
|
||||
sha256-8P7kk9B2A81e13IIyyfo8fIoNGlrutfV/WGKAFHscUc=
|
||||
sha256-j8U5S71SJoC+5yh7IjkadrRZaHOYeMsi1z8B6K7j4R4=
|
||||
|
||||
Generated
+28
-27
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "paseo",
|
||||
"version": "0.8.0-beta.1",
|
||||
"version": "0.8.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "paseo",
|
||||
"version": "0.8.0-beta.1",
|
||||
"version": "0.8.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"workspaces": [
|
||||
@@ -36196,7 +36196,7 @@
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@getpaseo/app",
|
||||
"version": "0.8.0-beta.1",
|
||||
"version": "0.8.0",
|
||||
"dependencies": {
|
||||
"@codemirror/commands": "6.10.4",
|
||||
"@codemirror/language": "6.12.4",
|
||||
@@ -37261,12 +37261,12 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@getpaseo/cli",
|
||||
"version": "0.8.0-beta.1",
|
||||
"version": "0.8.0",
|
||||
"dependencies": {
|
||||
"@clack/prompts": "^1.0.0",
|
||||
"@getpaseo/client": "0.8.0-beta.1",
|
||||
"@getpaseo/protocol": "0.8.0-beta.1",
|
||||
"@getpaseo/server": "0.8.0-beta.1",
|
||||
"@getpaseo/client": "0.8.0",
|
||||
"@getpaseo/protocol": "0.8.0",
|
||||
"@getpaseo/server": "0.8.0",
|
||||
"chalk": "^5.3.0",
|
||||
"commander": "^12.0.0",
|
||||
"mime-types": "^2.1.35",
|
||||
@@ -37513,10 +37513,10 @@
|
||||
},
|
||||
"packages/client": {
|
||||
"name": "@getpaseo/client",
|
||||
"version": "0.8.0-beta.1",
|
||||
"version": "0.8.0",
|
||||
"dependencies": {
|
||||
"@getpaseo/protocol": "0.8.0-beta.1",
|
||||
"@getpaseo/relay": "0.8.0-beta.1",
|
||||
"@getpaseo/protocol": "0.8.0",
|
||||
"@getpaseo/relay": "0.8.0",
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -37527,7 +37527,7 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@getpaseo/desktop",
|
||||
"version": "0.8.0-beta.1",
|
||||
"version": "0.8.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@getpaseo/cli": "*",
|
||||
@@ -37538,6 +37538,7 @@
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron/asar": "3.4.1",
|
||||
"@types/node": "24.6.0",
|
||||
"@types/ws": "^8.5.14",
|
||||
"electron": "44.2.0",
|
||||
@@ -37770,7 +37771,7 @@
|
||||
},
|
||||
"packages/expo-two-way-audio": {
|
||||
"name": "@getpaseo/expo-two-way-audio",
|
||||
"version": "0.8.0-beta.1",
|
||||
"version": "0.8.0",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.14",
|
||||
@@ -38666,7 +38667,7 @@
|
||||
},
|
||||
"packages/highlight": {
|
||||
"name": "@getpaseo/highlight",
|
||||
"version": "0.8.0-beta.1",
|
||||
"version": "0.8.0",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "6.12.4",
|
||||
"@codemirror/legacy-modes": "^6.5.3",
|
||||
@@ -38695,14 +38696,14 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@getpaseo/plugin",
|
||||
"version": "0.8.0-beta.1",
|
||||
"version": "0.8.0",
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "^1.4.0",
|
||||
"use-sync-external-store": "^1.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@getpaseo/client": "0.8.0-beta.1",
|
||||
"@getpaseo/protocol": "0.8.0-beta.1",
|
||||
"@getpaseo/client": "0.8.0",
|
||||
"@getpaseo/protocol": "0.8.0",
|
||||
"@types/node": "^20.9.0",
|
||||
"@types/react": "~19.2.0",
|
||||
"react": "19.1.0",
|
||||
@@ -38712,8 +38713,8 @@
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@getpaseo/client": "0.8.0-beta.1",
|
||||
"@getpaseo/protocol": "0.8.0-beta.1",
|
||||
"@getpaseo/client": "0.8.0",
|
||||
"@getpaseo/protocol": "0.8.0",
|
||||
"react": "~19.1.0",
|
||||
"react-native": ">=0.81.5",
|
||||
"zod": "^4.4.3"
|
||||
@@ -38735,7 +38736,7 @@
|
||||
},
|
||||
"packages/protocol": {
|
||||
"name": "@getpaseo/protocol",
|
||||
"version": "0.8.0-beta.1",
|
||||
"version": "0.8.0",
|
||||
"dependencies": {
|
||||
"semver": "^7.8.5",
|
||||
"zod": "^4.4.3"
|
||||
@@ -38762,7 +38763,7 @@
|
||||
},
|
||||
"packages/relay": {
|
||||
"name": "@getpaseo/relay",
|
||||
"version": "0.8.0-beta.1",
|
||||
"version": "0.8.0",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.5.1",
|
||||
"tweetnacl": "^1.0.3",
|
||||
@@ -38980,16 +38981,16 @@
|
||||
},
|
||||
"packages/server": {
|
||||
"name": "@getpaseo/server",
|
||||
"version": "0.8.0-beta.1",
|
||||
"version": "0.8.0",
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "^0.17.1",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.3.246",
|
||||
"@anthropic-ai/sdk": "^0.104.2",
|
||||
"@getpaseo/client": "0.8.0-beta.1",
|
||||
"@getpaseo/highlight": "0.8.0-beta.1",
|
||||
"@getpaseo/plugin": "0.8.0-beta.1",
|
||||
"@getpaseo/protocol": "0.8.0-beta.1",
|
||||
"@getpaseo/relay": "0.8.0-beta.1",
|
||||
"@getpaseo/client": "0.8.0",
|
||||
"@getpaseo/highlight": "0.8.0",
|
||||
"@getpaseo/plugin": "0.8.0",
|
||||
"@getpaseo/protocol": "0.8.0",
|
||||
"@getpaseo/relay": "0.8.0",
|
||||
"@isaacs/ttlcache": "^2.1.4",
|
||||
"@modelcontextprotocol/sdk": "1.29.0",
|
||||
"@opencode-ai/sdk": "1.14.46",
|
||||
@@ -39867,7 +39868,7 @@
|
||||
},
|
||||
"packages/website": {
|
||||
"name": "@getpaseo/website",
|
||||
"version": "0.8.0-beta.1",
|
||||
"version": "0.8.0",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "^1.29.1",
|
||||
"@cloudflare/workers-types": "^4.20260317.1",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "paseo",
|
||||
"version": "0.8.0-beta.1",
|
||||
"version": "0.8.0",
|
||||
"private": true,
|
||||
"description": "Paseo: voice-controlled development environment for local AI coding agents",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/app",
|
||||
"version": "0.8.0-beta.1",
|
||||
"version": "0.8.0",
|
||||
"private": true,
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/cli",
|
||||
"version": "0.8.0-beta.1",
|
||||
"version": "0.8.0",
|
||||
"description": "Paseo CLI - control your AI coding agents from the command line",
|
||||
"bin": {
|
||||
"paseo": "bin/paseo"
|
||||
@@ -28,9 +28,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@clack/prompts": "^1.0.0",
|
||||
"@getpaseo/client": "0.8.0-beta.1",
|
||||
"@getpaseo/protocol": "0.8.0-beta.1",
|
||||
"@getpaseo/server": "0.8.0-beta.1",
|
||||
"@getpaseo/client": "0.8.0",
|
||||
"@getpaseo/protocol": "0.8.0",
|
||||
"@getpaseo/server": "0.8.0",
|
||||
"chalk": "^5.3.0",
|
||||
"commander": "^12.0.0",
|
||||
"mime-types": "^2.1.35",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/client",
|
||||
"version": "0.8.0-beta.1",
|
||||
"version": "0.8.0",
|
||||
"description": "Paseo client SDK package",
|
||||
"files": [
|
||||
"dist",
|
||||
@@ -39,8 +39,8 @@
|
||||
"test": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@getpaseo/protocol": "0.8.0-beta.1",
|
||||
"@getpaseo/relay": "0.8.0-beta.1",
|
||||
"@getpaseo/protocol": "0.8.0",
|
||||
"@getpaseo/relay": "0.8.0",
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -5,6 +5,7 @@ const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
const { setTimeout: delay } = require("node:timers/promises");
|
||||
const { chromium } = require("playwright");
|
||||
const { extractFile } = require("@electron/asar");
|
||||
|
||||
const EXECUTABLE_NAME = "Paseo";
|
||||
const SMOKE_TIMEOUT_MS = 60_000;
|
||||
@@ -805,9 +806,23 @@ async function stopCliDaemon({ appPath, env }) {
|
||||
});
|
||||
}
|
||||
|
||||
function assertLinuxDesktopIdentity(appPath) {
|
||||
if (process.platform === "linux") {
|
||||
const metadata = JSON.parse(
|
||||
extractFile(path.join(appPath, "resources", "app.asar"), "package.json").toString(),
|
||||
);
|
||||
if (metadata.desktopName !== `${EXECUTABLE_NAME}.desktop`) {
|
||||
throw new Error(
|
||||
`Packaged Linux desktop identity ${JSON.stringify(metadata.desktopName)} does not match ${EXECUTABLE_NAME}.desktop`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function smokePackagedDesktopApp({ appPath }) {
|
||||
const executablePath = getExecutablePath(appPath);
|
||||
assertExecutable(executablePath, "Packaged app executable");
|
||||
assertLinuxDesktopIdentity(appPath);
|
||||
ensureLinuxSandboxPermissions(appPath);
|
||||
await smokeColdCliDaemonStart({ appPath });
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/desktop",
|
||||
"version": "0.8.0-beta.1",
|
||||
"version": "0.8.0",
|
||||
"private": true,
|
||||
"description": "Paseo desktop app (Electron wrapper)",
|
||||
"homepage": "https://paseo.sh",
|
||||
@@ -35,6 +35,7 @@
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron/asar": "3.4.1",
|
||||
"@types/node": "24.6.0",
|
||||
"@types/ws": "^8.5.14",
|
||||
"electron": "44.2.0",
|
||||
@@ -43,5 +44,6 @@
|
||||
"unzip-crx-3": "^0.2.0",
|
||||
"vitest": "^4.1.6",
|
||||
"wait-on": "8.0.5"
|
||||
}
|
||||
},
|
||||
"desktopName": "Paseo.desktop"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/expo-two-way-audio",
|
||||
"version": "0.8.0-beta.1",
|
||||
"version": "0.8.0",
|
||||
"description": "Native module for two way audio streaming",
|
||||
"keywords": [
|
||||
"ExpoTwoWayAudio",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/highlight",
|
||||
"version": "0.8.0-beta.1",
|
||||
"version": "0.8.0",
|
||||
"files": [
|
||||
"dist",
|
||||
"!dist/**/*.map",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/plugin",
|
||||
"version": "0.8.0-beta.1",
|
||||
"version": "0.8.0",
|
||||
"description": "Paseo plugin SDK package",
|
||||
"files": [
|
||||
"dist",
|
||||
@@ -65,8 +65,8 @@
|
||||
"use-sync-external-store": "^1.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@getpaseo/client": "0.8.0-beta.1",
|
||||
"@getpaseo/protocol": "0.8.0-beta.1",
|
||||
"@getpaseo/client": "0.8.0",
|
||||
"@getpaseo/protocol": "0.8.0",
|
||||
"@types/node": "^20.9.0",
|
||||
"@types/react": "~19.2.0",
|
||||
"react": "19.1.0",
|
||||
@@ -76,8 +76,8 @@
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@getpaseo/client": "0.8.0-beta.1",
|
||||
"@getpaseo/protocol": "0.8.0-beta.1",
|
||||
"@getpaseo/client": "0.8.0",
|
||||
"@getpaseo/protocol": "0.8.0",
|
||||
"react": "~19.1.0",
|
||||
"react-native": ">=0.81.5",
|
||||
"zod": "^4.4.3"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/protocol",
|
||||
"version": "0.8.0-beta.1",
|
||||
"version": "0.8.0",
|
||||
"description": "Paseo shared protocol schemas and wire types",
|
||||
"files": [
|
||||
"dist",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/relay",
|
||||
"version": "0.8.0-beta.1",
|
||||
"version": "0.8.0",
|
||||
"description": "Paseo relay for bridging daemon and client connections",
|
||||
"files": [
|
||||
"dist",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/server",
|
||||
"version": "0.8.0-beta.1",
|
||||
"version": "0.8.0",
|
||||
"description": "Paseo backend server",
|
||||
"files": [
|
||||
"dist/server",
|
||||
@@ -71,11 +71,11 @@
|
||||
"@agentclientprotocol/sdk": "^0.17.1",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.3.246",
|
||||
"@anthropic-ai/sdk": "^0.104.2",
|
||||
"@getpaseo/client": "0.8.0-beta.1",
|
||||
"@getpaseo/highlight": "0.8.0-beta.1",
|
||||
"@getpaseo/plugin": "0.8.0-beta.1",
|
||||
"@getpaseo/protocol": "0.8.0-beta.1",
|
||||
"@getpaseo/relay": "0.8.0-beta.1",
|
||||
"@getpaseo/client": "0.8.0",
|
||||
"@getpaseo/highlight": "0.8.0",
|
||||
"@getpaseo/plugin": "0.8.0",
|
||||
"@getpaseo/protocol": "0.8.0",
|
||||
"@getpaseo/relay": "0.8.0",
|
||||
"@isaacs/ttlcache": "^2.1.4",
|
||||
"@modelcontextprotocol/sdk": "1.29.0",
|
||||
"@opencode-ai/sdk": "1.14.46",
|
||||
|
||||
@@ -19,6 +19,7 @@ import { toAgentPayload } from "./agent-projections.js";
|
||||
import { projectTimelineRows } from "./timeline-projection.js";
|
||||
import { getOpenAgentTabLabel, PARENT_AGENT_ID_LABEL } from "@getpaseo/protocol/agent-labels";
|
||||
import { formatSystemNotificationPrompt, startAgentRun } from "./agent-prompt.js";
|
||||
import { StaleProviderSessionError } from "./stale-provider-session-error.js";
|
||||
import { ensureAgentLoaded, ensureUnarchivedAgentLoaded } from "./agent-loading.js";
|
||||
import type { StoredAgentRecord } from "./agent-storage.js";
|
||||
import type {
|
||||
@@ -68,6 +69,12 @@ function deferred<T>(): Deferred<T> {
|
||||
return { promise, resolve, reject };
|
||||
}
|
||||
|
||||
async function drainAsyncGenerator<T>(generator: AsyncGenerator<T>): Promise<void> {
|
||||
for await (const _ of generator) {
|
||||
// Drain provider events while AgentManager subscribers observe them.
|
||||
}
|
||||
}
|
||||
|
||||
function waitForAgentLifecycle(
|
||||
manager: AgentManager,
|
||||
agentId: string,
|
||||
@@ -519,6 +526,32 @@ class TestAgentSession implements AgentSession {
|
||||
async close(): Promise<void> {}
|
||||
}
|
||||
|
||||
class ResumeTrackingTestAgentClient extends TestAgentClient {
|
||||
private readonly retryStarted = deferred<void>();
|
||||
|
||||
override async resumeSession(
|
||||
_handle: AgentPersistenceHandle,
|
||||
config?: Partial<AgentSessionConfig>,
|
||||
): Promise<AgentSession> {
|
||||
this.resumeOverrides.push(config);
|
||||
const signalRetryStarted = () => this.retryStarted.resolve();
|
||||
return new (class extends TestAgentSession {
|
||||
override async startTurn(): Promise<{ turnId: string }> {
|
||||
signalRetryStarted();
|
||||
return await super.startTurn();
|
||||
}
|
||||
})({
|
||||
provider: this.provider,
|
||||
cwd: config?.cwd ?? process.cwd(),
|
||||
daemonAppendSystemPrompt: config?.daemonAppendSystemPrompt,
|
||||
});
|
||||
}
|
||||
|
||||
waitForRetryStart(): Promise<void> {
|
||||
return this.retryStarted.promise;
|
||||
}
|
||||
}
|
||||
|
||||
class McpCapableTestAgentSession extends TestAgentSession {
|
||||
override readonly capabilities = {
|
||||
...TEST_CAPABILITIES,
|
||||
@@ -3677,6 +3710,201 @@ test("updateProviderRegistry removes providers omitted from the next registry",
|
||||
expect(removedClient.createSessionCalls).toBe(0);
|
||||
});
|
||||
|
||||
test("retires loaded agents when their plugin provider is replaced", async () => {
|
||||
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-plugin-provider-reload-"));
|
||||
const storage = new AgentStorage(join(workdir, "agents"), logger);
|
||||
const provider = "plugin-provider";
|
||||
|
||||
class OriginalPluginClient extends TestAgentClient {
|
||||
session: CloseRecordingTestAgentSession | null = null;
|
||||
|
||||
constructor() {
|
||||
super(provider);
|
||||
}
|
||||
|
||||
override async createSession(config: AgentSessionConfig): Promise<AgentSession> {
|
||||
const session = new CloseRecordingTestAgentSession(config);
|
||||
session.describePersistence = () => ({
|
||||
provider,
|
||||
sessionId: "plugin-session",
|
||||
});
|
||||
this.session = session;
|
||||
return session;
|
||||
}
|
||||
}
|
||||
|
||||
const original = new OriginalPluginClient();
|
||||
const replacement = new TestAgentClient(provider);
|
||||
const manager = new AgentManager({
|
||||
clients: { [provider]: original },
|
||||
providerDefinitions: { [provider]: { enabled: true } },
|
||||
registry: storage,
|
||||
logger,
|
||||
});
|
||||
const created = await manager.createAgent({ provider, cwd: workdir }, undefined, {
|
||||
workspaceId: undefined,
|
||||
});
|
||||
|
||||
try {
|
||||
manager.updateProviderRegistry({
|
||||
providerDefinitions: { [provider]: { enabled: true } },
|
||||
clients: { [provider]: replacement },
|
||||
retiredProviders: [provider],
|
||||
});
|
||||
|
||||
await manager.waitForAgentClose(created.id);
|
||||
expect(original.session?.closed).toBe(true);
|
||||
expect(manager.getAgent(created.id)).toBeNull();
|
||||
|
||||
const resumed = await ensureAgentLoaded(created.id, {
|
||||
agentManager: manager,
|
||||
agentStorage: storage,
|
||||
logger,
|
||||
});
|
||||
expect(resumed.id).toBe(created.id);
|
||||
expect(replacement.resumeOverrides).toEqual([
|
||||
expect.objectContaining({ cwd: workdir, provider }),
|
||||
]);
|
||||
} finally {
|
||||
await manager.closeAgent(created.id).catch(() => undefined);
|
||||
await manager.flush().catch(() => undefined);
|
||||
await storage.flush().catch(() => undefined);
|
||||
rmSync(workdir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("a prompt after provider replacement reopens the stale session", async () => {
|
||||
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-stale-prompt-"));
|
||||
const storage = new AgentStorage(join(workdir, "agents"), logger);
|
||||
const provider = "plugin-provider";
|
||||
|
||||
class StalePluginSession extends CloseRecordingTestAgentSession {
|
||||
override async startTurn(): Promise<{ turnId: string }> {
|
||||
throw new StaleProviderSessionError("stale-bridge");
|
||||
}
|
||||
}
|
||||
|
||||
class StalePluginClient extends TestAgentClient {
|
||||
constructor() {
|
||||
super(provider);
|
||||
}
|
||||
|
||||
override async createSession(config: AgentSessionConfig): Promise<AgentSession> {
|
||||
const session = new StalePluginSession(config);
|
||||
session.describePersistence = () => ({
|
||||
provider,
|
||||
sessionId: "plugin-session",
|
||||
});
|
||||
return session;
|
||||
}
|
||||
}
|
||||
|
||||
const staleClient = new StalePluginClient();
|
||||
const replacement = new ResumeTrackingTestAgentClient(provider);
|
||||
const manager = new AgentManager({
|
||||
clients: { [provider]: staleClient },
|
||||
providerDefinitions: { [provider]: { enabled: true } },
|
||||
registry: storage,
|
||||
logger,
|
||||
});
|
||||
const created = await manager.createAgent({ provider, cwd: workdir }, undefined, {
|
||||
workspaceId: undefined,
|
||||
});
|
||||
|
||||
try {
|
||||
manager.updateProviderRegistry({
|
||||
providerDefinitions: { [provider]: { enabled: true } },
|
||||
clients: { [provider]: replacement },
|
||||
});
|
||||
|
||||
const dispatch = await startAgentRun(manager, created.id, "continue after reload", logger);
|
||||
expect(dispatch.disposition).toBe("turn_started");
|
||||
await replacement.waitForRetryStart();
|
||||
const result = await manager.waitForAgentEvent(created.id);
|
||||
expect(result.status).toBe("idle");
|
||||
expect(replacement.resumeOverrides).toHaveLength(1);
|
||||
} finally {
|
||||
await manager.closeAgent(created.id).catch(() => undefined);
|
||||
await manager.flush().catch(() => undefined);
|
||||
await storage.flush().catch(() => undefined);
|
||||
rmSync(workdir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("a replacement prompt recovers when the retired session fails to start", async () => {
|
||||
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-stale-replacement-"));
|
||||
const storage = new AgentStorage(join(workdir, "agents"), logger);
|
||||
const provider = "plugin-provider";
|
||||
|
||||
class StaleReplacementSession extends TestAgentSession {
|
||||
private starts = 0;
|
||||
|
||||
override async startTurn(): Promise<{ turnId: string }> {
|
||||
this.starts += 1;
|
||||
if (this.starts > 1) throw new StaleProviderSessionError("stale-bridge");
|
||||
return { turnId: "initial-turn" };
|
||||
}
|
||||
|
||||
override async interrupt(): Promise<void> {
|
||||
if (this.starts > 1) throw new StaleProviderSessionError("stale-bridge");
|
||||
this.pushEvent({
|
||||
type: "turn_canceled",
|
||||
provider: this.provider,
|
||||
turnId: "initial-turn",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class StaleReplacementClient extends TestAgentClient {
|
||||
constructor() {
|
||||
super(provider);
|
||||
}
|
||||
|
||||
override async createSession(config: AgentSessionConfig): Promise<AgentSession> {
|
||||
const session = new StaleReplacementSession(config);
|
||||
session.describePersistence = () => ({ provider, sessionId: "plugin-session" });
|
||||
return session;
|
||||
}
|
||||
}
|
||||
|
||||
const replacement = new ResumeTrackingTestAgentClient(provider);
|
||||
const manager = new AgentManager({
|
||||
clients: { [provider]: new StaleReplacementClient() },
|
||||
providerDefinitions: { [provider]: { enabled: true } },
|
||||
registry: storage,
|
||||
logger,
|
||||
rescueTimeouts: { interruptSessionMs: 20 },
|
||||
});
|
||||
const created = await manager.createAgent({ provider, cwd: workdir }, undefined, {
|
||||
workspaceId: undefined,
|
||||
});
|
||||
|
||||
try {
|
||||
const initial = manager.streamAgent(created.id, "initial");
|
||||
void drainAsyncGenerator(initial);
|
||||
await manager.waitForAgentRunStart(created.id);
|
||||
|
||||
manager.updateProviderRegistry({
|
||||
providerDefinitions: { [provider]: { enabled: true } },
|
||||
clients: { [provider]: replacement },
|
||||
});
|
||||
|
||||
const dispatch = await startAgentRun(manager, created.id, "replacement", logger, {
|
||||
replaceRunning: true,
|
||||
});
|
||||
expect(dispatch.disposition).toBe("turn_started");
|
||||
await replacement.waitForRetryStart();
|
||||
const result = await manager.waitForAgentEvent(created.id);
|
||||
expect(result.status).toBe("idle");
|
||||
expect(replacement.resumeOverrides).toHaveLength(1);
|
||||
} finally {
|
||||
await manager.closeAgent(created.id).catch(() => undefined);
|
||||
await manager.flush().catch(() => undefined);
|
||||
await storage.flush().catch(() => undefined);
|
||||
rmSync(workdir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("createAgent passes explicit model strings through to the provider", async () => {
|
||||
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-"));
|
||||
const storagePath = join(workdir, "agents");
|
||||
|
||||
@@ -77,6 +77,7 @@ import {
|
||||
} from "./agent-run-state.js";
|
||||
import { invokeRewindCapability, type RewindMode } from "./rewind/rewind.js";
|
||||
import { isSystemInjectedEnvelope } from "./agent-prompt.js";
|
||||
import { isStaleProviderSessionError } from "./stale-provider-session-error.js";
|
||||
import { stripInternalPaseoMcpServer, withRuntimePaseoMcpServer } from "./runtime-mcp-config.js";
|
||||
import { resolveCreateAgentTitles } from "./create-agent-title.js";
|
||||
import type { PaseoToolCatalogFactory } from "./tools/types.js";
|
||||
@@ -775,6 +776,7 @@ export class AgentManager {
|
||||
updateProviderRegistry(input: {
|
||||
providerDefinitions: ProviderEnabledMap;
|
||||
clients: ProviderClientMap;
|
||||
retiredProviders?: readonly AgentProvider[];
|
||||
}): void {
|
||||
this.providerEnabled.clear();
|
||||
this.providerDefinitions.clear();
|
||||
@@ -791,6 +793,18 @@ export class AgentManager {
|
||||
this.clients.set(provider, client);
|
||||
}
|
||||
}
|
||||
|
||||
for (const provider of input.retiredProviders ?? []) {
|
||||
for (const agent of this.agents.values()) {
|
||||
if (agent.provider !== provider) continue;
|
||||
void this.closeAgent(agent.id).catch((error) => {
|
||||
this.logger.warn(
|
||||
{ err: error, agentId: agent.id, provider },
|
||||
"Failed to close agent after provider retirement",
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getRegisteredProviderIds(): AgentProvider[] {
|
||||
@@ -2360,6 +2374,13 @@ export class AgentManager {
|
||||
if (pendingRun.settled) {
|
||||
throw error;
|
||||
}
|
||||
if (isStaleProviderSessionError(error)) {
|
||||
pendingRun.start = { status: "failed", error: error.message };
|
||||
agent.pendingReplacement = false;
|
||||
if (!agent.activeForegroundTurnId) agent.lifecycle = "idle";
|
||||
this.runs.settleForegroundRun(agentId, pendingRun.token);
|
||||
throw error;
|
||||
}
|
||||
agent.pendingReplacement = false;
|
||||
const errorMsg = error instanceof Error ? error.message : "Failed to start turn";
|
||||
pendingRun.start = { status: "failed", error: errorMsg };
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
import type { AgentManager, ManagedAgent } from "./agent-manager.js";
|
||||
import type { AgentStorage } from "./agent-storage.js";
|
||||
import { ensureAgentLoaded } from "./agent-loading.js";
|
||||
import { isStaleProviderSessionError } from "./stale-provider-session-error.js";
|
||||
import { getParentAgentIdFromLabels } from "@getpaseo/protocol/agent-labels";
|
||||
import type { ActiveTurnBehavior } from "@getpaseo/protocol/messages";
|
||||
|
||||
@@ -21,7 +22,9 @@ export type AgentRunController = Pick<
|
||||
| "replaceAgentRun"
|
||||
| "steerOrReplaceActiveTurn"
|
||||
| "streamAgent"
|
||||
>;
|
||||
> & {
|
||||
reloadAgentSession(agentId: string): Promise<unknown>;
|
||||
};
|
||||
|
||||
export interface StartAgentRunOptions {
|
||||
replaceRunning?: boolean;
|
||||
@@ -78,6 +81,14 @@ async function startOrReplaceRun(
|
||||
return { iterator, replaced };
|
||||
}
|
||||
|
||||
async function drainAgentRunIterator(
|
||||
iterator: AsyncGenerator<import("./agent-sdk-types.js").AgentStreamEvent>,
|
||||
): Promise<void> {
|
||||
for await (const _ of iterator) {
|
||||
// Events are broadcast via AgentManager subscribers.
|
||||
}
|
||||
}
|
||||
|
||||
export async function startAgentRun(
|
||||
agentManager: AgentRunController,
|
||||
agentId: string,
|
||||
@@ -104,6 +115,26 @@ export async function startAgentRun(
|
||||
if (agentManager.tryRunOutOfBand(agentId, prompt, options?.runOptions)) {
|
||||
return { disposition: "out_of_band" };
|
||||
}
|
||||
try {
|
||||
return await startAgentRunInner(agentManager, agentId, prompt, logger, options);
|
||||
} catch (error) {
|
||||
if (!isStaleProviderSessionError(error)) throw error;
|
||||
logger.info({ agentId, err: error }, "Provider session went stale; reopening from persistence");
|
||||
// The live session belongs to a retired plugin runtime. Reload swaps in a
|
||||
// fresh session on the current runtime while preserving history and labels.
|
||||
await agentManager.reloadAgentSession(agentId);
|
||||
return await startAgentRunInner(agentManager, agentId, prompt, logger, options);
|
||||
}
|
||||
}
|
||||
|
||||
async function startAgentRunInner(
|
||||
agentManager: AgentRunController,
|
||||
agentId: string,
|
||||
prompt: AgentPromptInput,
|
||||
logger: Logger,
|
||||
options?: StartAgentRunOptions,
|
||||
): Promise<{ disposition: PromptDispatchDisposition }> {
|
||||
const snapshot = agentManager.getAgent(agentId);
|
||||
const steered = await steerOrReplaceActiveRun(agentManager, agentId, prompt, options);
|
||||
if (steered?.disposition === "steered") {
|
||||
return steered;
|
||||
@@ -122,8 +153,17 @@ export async function startAgentRun(
|
||||
);
|
||||
void (async () => {
|
||||
try {
|
||||
for await (const _ of iterator) {
|
||||
// Events are broadcast via AgentManager subscribers.
|
||||
try {
|
||||
await drainAgentRunIterator(iterator);
|
||||
} catch (error) {
|
||||
if (!isStaleProviderSessionError(error)) throw error;
|
||||
logger.info(
|
||||
{ agentId, err: error },
|
||||
"Provider session went stale; reopening from persistence",
|
||||
);
|
||||
await agentManager.reloadAgentSession(agentId);
|
||||
const retry = await startOrReplaceRun(agentManager, agentId, prompt, options);
|
||||
await drainAgentRunIterator(retry.iterator);
|
||||
}
|
||||
logger.trace(
|
||||
{
|
||||
|
||||
@@ -8,6 +8,10 @@ import { describe, expect, test } from "vitest";
|
||||
import { createTestLogger } from "../../test-utils/test-logger.js";
|
||||
import type { AgentStreamEvent } from "./agent-sdk-types.js";
|
||||
import { PluginAgentClientRegistry } from "./plugin-provider.js";
|
||||
import {
|
||||
isStaleProviderSessionError,
|
||||
StaleProviderSessionError,
|
||||
} from "./stale-provider-session-error.js";
|
||||
|
||||
const CAPABILITIES = [
|
||||
"prompt.message",
|
||||
@@ -25,6 +29,10 @@ interface ProviderHarnessOptions {
|
||||
function createProviderHarness(options: ProviderHarnessOptions = {}) {
|
||||
let listener: ((event: ProviderEvent) => void) | null = null;
|
||||
let closeCount = 0;
|
||||
let resolveClosed!: () => void;
|
||||
const closed = new Promise<void>((resolve) => {
|
||||
resolveClosed = resolve;
|
||||
});
|
||||
const inputs: ProviderInput[] = [];
|
||||
const emit = (event: ProviderEvent) => listener?.(event);
|
||||
const capabilities = options.capabilities ?? CAPABILITIES;
|
||||
@@ -180,6 +188,7 @@ function createProviderHarness(options: ProviderHarnessOptions = {}) {
|
||||
},
|
||||
async close() {
|
||||
closeCount += 1;
|
||||
resolveClosed();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -191,7 +200,12 @@ function createProviderHarness(options: ProviderHarnessOptions = {}) {
|
||||
},
|
||||
};
|
||||
|
||||
return { registration, inputs, closeCount: () => closeCount };
|
||||
return {
|
||||
registration,
|
||||
inputs,
|
||||
closeCount: () => closeCount,
|
||||
waitForClose: () => closed,
|
||||
};
|
||||
}
|
||||
|
||||
function eventsOfType(events: AgentStreamEvent[], type: AgentStreamEvent["type"]) {
|
||||
@@ -258,6 +272,84 @@ describe("PluginAgentClientRegistry", () => {
|
||||
expect(eventsOfType(events, "turn_failed")).toHaveLength(1);
|
||||
});
|
||||
|
||||
test("closes a stale session after its plugin provider is replaced", async () => {
|
||||
const old = createProviderHarness();
|
||||
const next = createProviderHarness();
|
||||
const registry = new PluginAgentClientRegistry(createTestLogger());
|
||||
|
||||
try {
|
||||
registry.replace([old.registration]);
|
||||
|
||||
const stale = await registry.clients()[old.registration.id]!.createSession({
|
||||
provider: old.registration.id,
|
||||
cwd: "/workspace",
|
||||
});
|
||||
const persistence = stale.describePersistence();
|
||||
expect(persistence).not.toBeNull();
|
||||
|
||||
registry.replace([next.registration]);
|
||||
await old.waitForClose();
|
||||
expect(old.closeCount()).toBe(1);
|
||||
|
||||
await expect(stale.close()).resolves.toBeUndefined();
|
||||
|
||||
const replacement = registry.clients()[next.registration.id];
|
||||
expect(replacement).toBeDefined();
|
||||
const resumed = await replacement!.resumeSession(persistence!, {
|
||||
cwd: "/workspace",
|
||||
});
|
||||
|
||||
await expect(
|
||||
resumed.startTurn("after reload", { clientMessageId: "after-reload" }),
|
||||
).resolves.toEqual({ turnId: "turn-1" });
|
||||
|
||||
expect(next.inputs).toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: "session.open",
|
||||
history: "replay",
|
||||
persistence: {
|
||||
version: 1,
|
||||
data: { token: "root" },
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await resumed.close();
|
||||
} finally {
|
||||
await registry.shutdown();
|
||||
}
|
||||
});
|
||||
|
||||
test("prompting a stale session raises StaleProviderSessionError", async () => {
|
||||
const old = createProviderHarness();
|
||||
const registry = new PluginAgentClientRegistry(createTestLogger());
|
||||
|
||||
try {
|
||||
registry.replace([old.registration]);
|
||||
const stale = await registry.clients()[old.registration.id]!.createSession({
|
||||
provider: old.registration.id,
|
||||
cwd: "/workspace",
|
||||
});
|
||||
|
||||
registry.replace([]);
|
||||
await old.waitForClose();
|
||||
expect(old.closeCount()).toBe(1);
|
||||
|
||||
const failure = await stale
|
||||
.startTurn("after reload", { clientMessageId: "after-reload" })
|
||||
.then(
|
||||
() => null,
|
||||
(error: unknown) => error,
|
||||
);
|
||||
expect(failure).toBeInstanceOf(StaleProviderSessionError);
|
||||
expect(isStaleProviderSessionError(failure)).toBe(true);
|
||||
expect(isStaleProviderSessionError(new Error("Provider connection is closed"))).toBe(false);
|
||||
expect(isStaleProviderSessionError(new Error("boom"))).toBe(false);
|
||||
} finally {
|
||||
await registry.shutdown();
|
||||
}
|
||||
});
|
||||
|
||||
test("adapts callback providers into the existing AgentClient and AgentSession path", async () => {
|
||||
const harness = createProviderHarness();
|
||||
const registry = new PluginAgentClientRegistry(createTestLogger());
|
||||
|
||||
@@ -58,6 +58,7 @@ import {
|
||||
} from "./create-agent-mode.js";
|
||||
import type { ProviderDefinition } from "./provider-registry.js";
|
||||
import { runProviderTurn } from "./providers/provider-runner.js";
|
||||
import { StaleProviderSessionError } from "./stale-provider-session-error.js";
|
||||
|
||||
interface Deferred<Value> {
|
||||
promise: Promise<Value>;
|
||||
@@ -130,6 +131,10 @@ class ProviderRuntime {
|
||||
return this.connection?.capabilities ?? [];
|
||||
}
|
||||
|
||||
get isClosed(): boolean {
|
||||
return this.closed;
|
||||
}
|
||||
|
||||
async isAvailable(): Promise<boolean> {
|
||||
await this.getConnection();
|
||||
return true;
|
||||
@@ -559,6 +564,7 @@ class ProviderRuntimeSession {
|
||||
sessionId: this.providerSessionId,
|
||||
prompt,
|
||||
});
|
||||
if (this.terminal) throw new StaleProviderSessionError(this.id);
|
||||
const pending = deferred<Extract<ProviderEvent, { type: "session.prompt_result" }>>();
|
||||
this.prompts.set(prompt.clientMessageId, pending);
|
||||
try {
|
||||
@@ -568,6 +574,9 @@ class ProviderRuntimeSession {
|
||||
prompt,
|
||||
});
|
||||
return (await pending.promise).result;
|
||||
} catch (error) {
|
||||
if (this.terminal || this.runtime.isClosed) throw new StaleProviderSessionError(this.id);
|
||||
throw error;
|
||||
} finally {
|
||||
this.prompts.delete(prompt.clientMessageId);
|
||||
}
|
||||
@@ -623,6 +632,8 @@ class ProviderRuntimeSession {
|
||||
requestId: randomUUID(),
|
||||
sessionId: this.providerSessionId,
|
||||
});
|
||||
} catch (error) {
|
||||
if (!this.runtime.isClosed) throw error;
|
||||
} finally {
|
||||
this.runtime.removeSession(this.id, this.providerSessionId);
|
||||
}
|
||||
|
||||
@@ -2281,6 +2281,7 @@ describe("ProviderSnapshotManager cwd routing", () => {
|
||||
const withoutPlugin = manager.replacePluginProviders([]);
|
||||
expect(manager.hasProvider(registration.id)).toBe(false);
|
||||
expect(withoutPlugin.clients[registration.id]).toBeUndefined();
|
||||
expect(withoutPlugin.retiredProviders).toEqual([registration.id]);
|
||||
} finally {
|
||||
await manager.shutdown();
|
||||
manager.destroy();
|
||||
|
||||
@@ -191,6 +191,7 @@ export interface AgentManagerProviderState {
|
||||
>
|
||||
>;
|
||||
clients: Partial<Record<AgentProvider, AgentClient>>;
|
||||
retiredProviders?: readonly AgentProvider[];
|
||||
}
|
||||
|
||||
interface ProviderLoadOptions {
|
||||
@@ -402,6 +403,9 @@ export class ProviderSnapshotManager {
|
||||
this.createAgentManagerState(this.generation.definitions, clients);
|
||||
this.pluginProviders.replace(registrations);
|
||||
const plugins = this.pluginProviders.definitions();
|
||||
const retiredProviders = Object.keys(previousPlugins).filter(
|
||||
(provider) => previousPlugins[provider] !== plugins[provider],
|
||||
);
|
||||
const definitions = { ...this.generation.definitions };
|
||||
const changed = new Set<AgentProvider>();
|
||||
for (const provider of new Set([...Object.keys(previousPlugins), ...Object.keys(plugins)])) {
|
||||
@@ -415,7 +419,7 @@ export class ProviderSnapshotManager {
|
||||
const generation = this.createGeneration(definitions, this.providerOverrides);
|
||||
const state = this.createAgentManagerState(definitions, clients);
|
||||
this.installGeneration(generation, clients, changed);
|
||||
return state;
|
||||
return { ...state, retiredProviders };
|
||||
}
|
||||
|
||||
private ensureClient(
|
||||
|
||||
@@ -97,6 +97,88 @@ async function createConnectedSession(
|
||||
}
|
||||
|
||||
describe("Codex app-server provider features", () => {
|
||||
test.each([
|
||||
"gpt-6-astra",
|
||||
"gpt-5.6",
|
||||
"gpt-5.6-sol",
|
||||
"gpt-5.6-terra",
|
||||
"gpt-5.6-luna",
|
||||
"gpt-5.5",
|
||||
"gpt-5.4",
|
||||
])("exposes and sends Fast for %s", async (model) => {
|
||||
const { session, appServer } = await createConnectedSession({ model });
|
||||
try {
|
||||
expect(session.features).toContainEqual(
|
||||
expect.objectContaining({
|
||||
id: "fast_mode",
|
||||
value: false,
|
||||
}),
|
||||
);
|
||||
await session.setFeature?.("fast_mode", true);
|
||||
await session.startTurn("hello");
|
||||
await expect(appServer.waitForTurnStart()).resolves.toMatchObject({
|
||||
model,
|
||||
serviceTier: "fast",
|
||||
});
|
||||
} finally {
|
||||
await session.close();
|
||||
}
|
||||
});
|
||||
|
||||
test.each([
|
||||
"gpt-5.3-codex-spark",
|
||||
"gpt-5.3-codex",
|
||||
"gpt-5.4-mini",
|
||||
"gpt-5.4-nano",
|
||||
"gpt-5.5-pro",
|
||||
"gpt-5",
|
||||
"gpt-4.1",
|
||||
"o3",
|
||||
"o4-mini",
|
||||
"gpt-6-unknown",
|
||||
])("does not expose or restore Fast for %s", async (model) => {
|
||||
const { session, appServer } = await createConnectedSession({
|
||||
model,
|
||||
featureValues: { fast_mode: true },
|
||||
});
|
||||
try {
|
||||
expect(session.features.map((feature) => feature.id)).toEqual(["plan_mode"]);
|
||||
await expect(session.setFeature?.("fast_mode", true)).rejects.toThrow(
|
||||
`Codex fast mode is not available for model '${model}'`,
|
||||
);
|
||||
await session.startTurn("hello");
|
||||
await expect(appServer.waitForTurnStart()).resolves.not.toMatchObject({
|
||||
serviceTier: expect.anything(),
|
||||
});
|
||||
} finally {
|
||||
await session.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("restores Fast on Astra and preserves it when switching supported models", async () => {
|
||||
const { session, appServer } = await createConnectedSession({
|
||||
model: "gpt-6-astra",
|
||||
featureValues: { fast_mode: true },
|
||||
});
|
||||
try {
|
||||
expect(session.features).toContainEqual(
|
||||
expect.objectContaining({
|
||||
id: "fast_mode",
|
||||
value: true,
|
||||
}),
|
||||
);
|
||||
await session.setModel("gpt-5.6-sol");
|
||||
await session.setModel("gpt-6-astra");
|
||||
await session.startTurn("hello");
|
||||
await expect(appServer.waitForTurnStart()).resolves.toMatchObject({
|
||||
model: "gpt-6-astra",
|
||||
serviceTier: "fast",
|
||||
});
|
||||
} finally {
|
||||
await session.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("features returns fast and plan toggles when supported", async () => {
|
||||
const { session } = await createConnectedSession();
|
||||
|
||||
@@ -105,7 +187,7 @@ describe("Codex app-server provider features", () => {
|
||||
type: "toggle",
|
||||
id: "fast_mode",
|
||||
label: "Fast",
|
||||
description: "Priority inference at 2x usage",
|
||||
description: "Priority inference at increased usage",
|
||||
tooltip: "Toggle fast mode",
|
||||
icon: "zap",
|
||||
value: false,
|
||||
@@ -129,7 +211,7 @@ describe("Codex app-server provider features", () => {
|
||||
type: "toggle",
|
||||
id: "fast_mode",
|
||||
label: "Fast",
|
||||
description: "Priority inference at 2x usage",
|
||||
description: "Priority inference at increased usage",
|
||||
tooltip: "Toggle fast mode",
|
||||
icon: "zap",
|
||||
value: true,
|
||||
@@ -250,7 +332,7 @@ describe("Codex app-server provider features", () => {
|
||||
type: "toggle",
|
||||
id: "fast_mode",
|
||||
label: "Fast",
|
||||
description: "Priority inference at 2x usage",
|
||||
description: "Priority inference at increased usage",
|
||||
tooltip: "Toggle fast mode",
|
||||
icon: "zap",
|
||||
value: true,
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
import type { AgentFeature, AgentFeatureToggle } from "../agent-sdk-types.js";
|
||||
|
||||
const CODEX_FAST_MODE_SUPPORTED_MODEL_PREFIXES = ["gpt-5", "gpt-4.1", "o3", "o4-mini"] as const;
|
||||
// Codex Fast is distinct from API Priority processing. Keep model support aligned with
|
||||
// https://developers.openai.com/codex/speed and https://developers.openai.com/codex/models.
|
||||
const CODEX_FAST_MODE_SUPPORTED_MODELS = new Set([
|
||||
"gpt-6-astra",
|
||||
"gpt-5.6",
|
||||
"gpt-5.6-sol",
|
||||
"gpt-5.6-terra",
|
||||
"gpt-5.6-luna",
|
||||
"gpt-5.5",
|
||||
"gpt-5.4",
|
||||
]);
|
||||
|
||||
export const CODEX_FAST_MODE_FEATURE: Omit<AgentFeatureToggle, "value"> = {
|
||||
type: "toggle",
|
||||
id: "fast_mode",
|
||||
label: "Fast",
|
||||
description: "Priority inference at 2x usage",
|
||||
description: "Priority inference at increased usage",
|
||||
tooltip: "Toggle fast mode",
|
||||
icon: "zap",
|
||||
};
|
||||
@@ -30,9 +40,7 @@ export function codexModelSupportsFastMode(modelId: string | null | undefined):
|
||||
if (!normalizedModelId) {
|
||||
return false;
|
||||
}
|
||||
return CODEX_FAST_MODE_SUPPORTED_MODEL_PREFIXES.some(
|
||||
(prefix) => normalizedModelId === prefix || normalizedModelId.startsWith(prefix),
|
||||
);
|
||||
return CODEX_FAST_MODE_SUPPORTED_MODELS.has(normalizedModelId);
|
||||
}
|
||||
|
||||
export function buildCodexFeatures(input: {
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
/** Thrown when a prompt reaches a plugin session whose connection died with its plugin. */
|
||||
export class StaleProviderSessionError extends Error {
|
||||
constructor(sessionId: string) {
|
||||
super(`Provider session ${sessionId} is stale after its plugin reloaded`);
|
||||
this.name = "StaleProviderSessionError";
|
||||
}
|
||||
}
|
||||
|
||||
export function isStaleProviderSessionError(error: unknown): error is StaleProviderSessionError {
|
||||
return error instanceof StaleProviderSessionError;
|
||||
}
|
||||
@@ -202,6 +202,7 @@ function buildRunOutput(params: {
|
||||
type ScheduleAgentManager = Pick<
|
||||
AgentRunController,
|
||||
| "getAgent"
|
||||
| "reloadAgentSession"
|
||||
| "tryRunOutOfBand"
|
||||
| "hasInFlightRun"
|
||||
| "replaceAgentRun"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/website",
|
||||
"version": "0.8.0-beta.1",
|
||||
"version": "0.8.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -271,7 +271,7 @@ Keys use the existing expression syntax and must resolve to a non-empty string o
|
||||
|
||||
An existing session keeps its daemon, agent configuration, target, environment, and tool contracts. Changing those settings for the same key fails with an explanation; choose a different key or **New agent**. Prompts and output destinations belong to each arrival and may change. A worktree branch is chosen when the session is first created and reused on later arrivals.
|
||||
|
||||
Triggers that mint temporary environment credentials, including a `run.github` grant or a connection token in `run.env`, must choose **New agent** when the event has a conversation or uses a custom key. These credentials expire with the execution, and an existing agent's environment cannot be refreshed. Hub-managed reply tools remain available with continuation.
|
||||
A follow-up steers the active agent without extending its runtime deadline or credential expiry. With a `run.github` grant or connection token in `run.env`, follow-ups share the active agent’s credentials. Once all requests finish, Hub revokes leased tokens; the next arrival starts a new agent with fresh credentials. Agents without temporary credentials can be reused after completion.
|
||||
|
||||
### Upgrading
|
||||
|
||||
|
||||
@@ -96,8 +96,6 @@ worktree:
|
||||
|
||||
For agents it dispatched, Hub owns creation, reconnect recovery, output observation, and completion. Agents you start yourself are untouched.
|
||||
|
||||
Restarting Hub lets active agents continue working. Hub reconnects to the same agents and resumes tracking their output and completion. [GitHub credentials retain their original lifetime](/docs/hub/github#restarts-and-credential-lifetime).
|
||||
|
||||
If Hub loses the create response, or the daemon restarts mid-execution, Hub resends the same create intent with the same execution id. The daemon returns the existing agent rather than running the prompt again. An agent that closed or errored is recorded as interrupted; Hub never silently starts a second one.
|
||||
|
||||
## Status
|
||||
|
||||
@@ -37,7 +37,7 @@ steps:
|
||||
${{ paseo.prompt }}
|
||||
```
|
||||
|
||||
The agent can use `git` and `gh` within the declared repositories and permissions. Hub mints the token when the step starts and revokes it when execution ends or its configured duration elapses.
|
||||
The agent can use `git` and `gh` within the declared repositories and permissions. See [agent continuation](/docs/hub/configuration/hub-yml#agent-continuation) for the token lifecycle when requests continue an agent.
|
||||
|
||||
## Fields
|
||||
|
||||
@@ -50,12 +50,6 @@ The agent can use `git` and `gh` within the declared repositories and permission
|
||||
|
||||
Requested authority cannot exceed the GitHub App installation. Activation and dispatch fail clearly when the connection, repository, or permissions cannot be resolved.
|
||||
|
||||
## Restarts and credential lifetime
|
||||
|
||||
Restarting Hub preserves active executions and their existing credentials. Hub reconnects to the same agent and restores the original credential deadlines. A restart does not mint a replacement token or extend its lifetime.
|
||||
|
||||
If Hub is unavailable when a shorter duration elapses, it revokes the overdue token when it returns. GitHub’s own expiry still limits the token lifetime to one hour. Revocation failures are retried after recovery.
|
||||
|
||||
## Agent environment
|
||||
|
||||
Hub supplies `GH_TOKEN` and process-scoped git configuration through environment variables:
|
||||
@@ -79,8 +73,4 @@ env:
|
||||
SOME_TOKEN: "${{ paseo.connections.some-connection.token }}"
|
||||
```
|
||||
|
||||
Hub persists resolved values as private execution data so it can recover after a restart. Authored configuration retains the expressions.
|
||||
|
||||
These values are stored in the Hub database without application-level encryption. Protect database access, storage, and backups as credentials. Hub deletes the execution authority record when the execution becomes terminal; token lease records remain until revocation succeeds or the token expires, so interrupted cleanup can resume. This cleanup is not a guarantee that every copy is erased: retained agent/session data and database backups may still contain resolved values. Token revocation ends access; it does not erase those copies. Backup retention and deletion remain the operator’s responsibility.
|
||||
|
||||
See [Hub security](/docs/hub/security) for provider and host boundaries.
|
||||
Hub resolves the value when it prepares the agent's environment. See [Hub security](/docs/hub/security) for provider and host boundaries.
|
||||
|
||||
@@ -44,7 +44,7 @@ If the daemon is offline or needs a newer Paseo version, the agent selectors sho
|
||||
|
||||
Dashboard triggers default to **Same conversation**. Messages in the same Slack or Discord thread, events on the same GitHub issue or pull request, and events on the same Linear issue continue the existing agent in that project. An event without a conversation starts a new agent.
|
||||
|
||||
If the agent is busy, the new prompt steers its current work. If its workspace is archived, Hub asks Paseo to restore it before sending the prompt. Each arrival still has its own deadline, output limits, and completion status.
|
||||
If the agent is busy, the new prompt steers its current work. Each arrival keeps its own output limits and completion status. If a reusable workspace is archived, Hub asks Paseo to restore it before sending the prompt.
|
||||
|
||||
Choose **Custom key** to group arrivals by an input, or **New agent** to keep them separate. A self-contained trigger document can express the same choice:
|
||||
|
||||
@@ -67,7 +67,7 @@ run:
|
||||
prompt: Handle this request and call finish_execution when complete.
|
||||
```
|
||||
|
||||
The [continuation reference](/docs/hub/configuration/hub-yml#agent-continuation) describes keys and compatibility. The run detail shows whether each arrival created, continued, or restored an agent.
|
||||
The [continuation reference](/docs/hub/configuration/hub-yml#agent-continuation) covers keys and agent reuse. The run detail shows whether each arrival created, continued, or restored an agent.
|
||||
|
||||
Hub includes an `executionId` in each prompt. When using a continuing agent, pass that ID to `reply` and `finish_execution`. These tools act on that arrival's destination and contract; an old or unrelated execution ID is rejected.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user