* fix(opencode): reconnect v2 sessions after helper exits
A helper exit aborted the session lifetime, so every later prompt failed even though OpenCode can serve the same native session from another helper. Keep the session open, fail the active turn, and acquire and configure the current helper on the next OpenCode operation.
* fix(opencode): forget exited v2 helper generation
A dead shared helper could remain selected as the current generation. Clear that selection at exit so the first lazy reconnect acquires a live helper.
* feat(auth): authenticate daemon clients through hello
Give local CLI and desktop clients a per-run credential while remote clients send the host password inside the protocol. Keep relay passwordless admission for this mobile release so existing saved hosts keep working until the follow-up enforcement release.
Move password recovery for saved hosts into host settings and keep pairing credentials in the add flow. Include isolated web QA evidence for both flows.
* fix(auth): preserve Hub bootstrap and local credential expectations
Hub-owned sockets already carry admission; keep their hello handling synchronous through session bootstrap while resolving credentials asynchronously for ordinary sockets. Keep the resolver out of lightweight configuration imports.
Update CLI expectations for same-home credentials, let a rejected connection yield to another usable connection, and clear pairing secrets when add-flow targets change.
* fix(pairing): retain passwords across edits to the same target
Track the last valid pairing host while a URI is edited. This preserves an entered password for whitespace and partial edits but clears it when a different host becomes the target.
* test(auth): align local credential checks across platforms
Expect the CLI to authenticate through the credential in its selected home, and verify POSIX file modes only where Node exposes them.
* feat(app): move host password into a settings row with a modal
The host page rendered an always-visible password form above Name on every
host. Replace it with a Password row under a Security section below
Appearance; tapping it opens a modal with Save and, when a password is saved,
Clear. The connection-error callout keeps its guidance and gains a Set
password button that opens the same modal. Drop the unused
pairing.hostPassword body/reopen/saveError strings.
* chore: remove committed QA screenshots
QA evidence stays out of the repository; the PR body describes it in words.
* feat(app): keep the host password out of the host page
The saved password is connection data, entered only when a host is added. A
Password setting on the host page reads as the daemon's own password, so the
row, modal, and callout button are gone. A rejected host shows the daemon's
reason with guidance to remove the host and add it again with the password.
useHostMutations no longer exposes setHostPassword; relay imports still set it
inside the runtime.
* feat(app): show host connection errors in an error alert
HostConnectionError renders the existing Alert with the connection error as
its title. Auth rejections add the re-add guidance as the description; other
errors show the title only. The Overview and Connections pages share it.
* style(app): soften alert borders to half-opacity accent
* feat(app): size alerts on the button scale
Alert takes a size prop (xs/sm/md/lg, default md) whose geometry lives next to
the button's in control-geometry: one font size for title and description, the
button icon size centered on the first text line, button padding and radius,
and a one-line alert as tall as the button of the same size. Consumers pick
sm among settings rows, cards, panels, and modals, and md when the alert is
the whole page or sheet state.
* style(app): roomier, rounder alerts with a tight title block
Alert sm padding returns to the original 12/16 and every size is at least
borderRadius.xl. Title and description sit a small gap apart with no line
height override. The icon slot carries a zero-width text strut in the alert's
font so the icon centers on the first line for titled, title-only, and
description-only alerts.
* refactor(app): align the alert icon with a centered lead row
Drop the zero-width strut. The icon and the alert's first text (title, else
description) share one row centered on each other; the description and actions
sit below, indented by the icon slot width plus gap so they start at the lead
text's left edge.
* refactor(app): drop the unused Alert icon override
* 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
* 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>
A provider that extends codex with its own OPENAI_BASE_URL starts its
threads under its own Codex model provider id. Codex's thread/list returns
only the model provider its config selects unless the request names
others, and the import scan named none, so those sessions were missing
from the import list. After switching the default provider in the Codex
config, stock Codex listed them instead and imported them onto itself.
Ask Codex for the custom provider's own threads when listing its
importable sessions. Stock Codex keeps its current listing.
Closes#5381
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
A failed write to daemon.log (for example ENOSPC on a full disk) made the
supervisor's rotating log stream emit an unhandled 'error' event, which
killed the supervisor; the worker then exited on lost supervisor liveness.
The supervisor now treats daemon.log as best effort: a failed stream is
reported once on stderr, dropped, and reopened on the next log line, so
logging resumes once the file is writable again.
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
Opening an archived ACP agent whose worktree was removed spawned the
provider in the missing directory. The session spawn had no listener for
the child's error event, so Node's ENOENT became an uncaught exception
and the daemon restarted, then crashed again when the client retried.
The session spawn now rejects initialization on a spawn error, the same
way the provider probe already did, and both share one helper.
Closes#5435
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
Co-authored-by: qinkangdeid <qinkangdeidd@gmail.com>
A provider that extends claude and sets CLAUDE_CONFIG_DIR in its env
writes transcripts and settings.json under that directory, but the
daemon looked them up under its own CLAUDE_CONFIG_DIR or ~/.claude.
Agents on that provider reopened with an empty timeline after a daemon
restart, their sessions were missing from the import list, and models
from its settings.json were missing from the catalog.
Resolve the config directory from the same environment the provider is
launched with, and drop the test-only configDir client option.
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
OpenCode adds text to user messages on its own behalf (continue prompts after
compaction or a subtask, MCP resource contents, plugin prompts) and flags that
text `synthetic: true`. Its own UI hides it. Paseo built user bubbles from every
text part, so reopening an OpenCode chat showed those messages as if the user
had typed them, and appended synthetic text to the user's own message.
Closes#5425
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
Pi's navigateTree without a summary only moves the session's leaf in
memory and appends nothing to the session file. Pi reopens a session at
the file's last entry, so after a daemon restart the resumed chat showed
the messages the rewind had removed.
After navigating, the Paseo extension now appends a paseo_rewind custom
entry on the new branch. Custom entries carry no model context, and the
file's last entry now sits on the rewound branch, so Pi reopens there.
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
* List a generic ACP agent's slash commands in a new agent's / menu
Generic ACP agents advertise slash commands with available_commands_update
after session/new. The new-agent / menu opens a short-lived session and asks
for its commands right after session/new, so it always got an empty list,
which the app keeps for the draft. Wait for the first batch, bounded by the
existing initial-commands timeout, as the Cursor, Kiro, and TRAE clients do.
Closes#4759
* Keep the generic ACP command test independent of scheduling
* Wait for agent state writes before removing MCP test temp dirs
The blocking send_agent_prompt timeout test removed its temp dir while
AgentManager background tasks and AgentStorage writes were still in
flight, which intermittently failed cleanup with ENOTEMPTY on Linux CI.
Flush both before removing the dir in the tests that use real storage.
---------
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
* Notify a caller once when it prompts a child that is still running
create_agent and a background send_agent_prompt each armed their own
finish notification for the same child and caller. When the second
prompt replaces the child's running turn, the child never goes idle in
between, so its one finish fired both and the caller got the same
notification twice. Arming again for a child and caller now replaces
the earlier notification.
Closes#5397
* Declare the notification test helper as a function
---------
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
* Rewind a Pi chat to the message you pick after an earlier rewind
After a rewind, Pi's session file keeps the abandoned branch. Paseo
captured user entry ids from the whole file but replayed history from
the current branch, pairing them by position. Each abandoned user
message shifted later rows onto an earlier entry id, so rewinding a
replayed row went back further than the selected message.
Capture user entries from the same compaction-aware branch Pi replays,
so the nth capture is the nth replayed user message.
* Assert Pi entry captures as data with named test helpers
* Name the fields of Pi user entry test fixtures
* Rewind a Pi row whose entry compaction summarized away
The capture fed both replay pairing and the rewind target lookup. Narrowing
it to the context branch broke rewinding a live row from before /compact,
because that row's entry is off the branch. The extension now reports the
session's user entries for the lookup apart from the branch entries replay
pairs with.
---------
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
* fix(omp): preserve described select options
* test(omp): assert exact option arrays instead of toMatchObject
toMatchObject ignores extra/missing keys, so the described-select and
malformed-metadata tests passed even with option-index misalignment or
an unfiltered whitespace-only description. Switched to toStrictEqual
on the options array so both are actually caught.
* fix(server): notify the caller when a blocking send_agent_prompt outlasts its wait
An agent-scoped send_agent_prompt with background: false only armed the
finish notification for background sends. When the 30s wait timed out,
the tool returned "still running" and nothing was armed, so the child's
result never reached the caller. The wait now reports whether it timed
out, and a timed-out wait on a still-running agent arms the same
notification a background send gets and says so in guidance. A blocking
send that finishes inside the wait still returns its result without a
notification.
* refactor(server): declare the finish-notification helper as a function
---------
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
A Pi agent created without a model took the first entry of Pi's model
list, which ignores defaultProvider/defaultModel in Pi's settings. The Pi
catalog now marks the model a fresh Pi session reports in get_state as
the default, so the daemon's default-model fallback picks it.
Closes#4918
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
* Reconnect OpenCode agents to the new server after the old one exits
When the OpenCode server process exits, Paseo starts the next one on a
new port, but existing sessions kept their client bound to the old port
and failed every turn with ECONNREFUSED until the agent was reloaded.
A session now marks its connection dead when the server exits and
connects to the current server before its next turn, rewind, history
read, mode or command lookup.
Closes#5307
* Settle the exited OpenCode server's events before reconnecting
A queued exit from the old server could otherwise fail a turn started on
the new one.
* Re-add a reconnected OpenCode session's MCP servers on the new server
reconnect() reset mcpConfigured but left mcpSetupPromise holding the
first server's resolved setup, so the next turn awaited it and never
called mcp.add on the new server. Track MCP setup with one field that
reconnect clears.
* Re-run CI after an unrelated macOS file-observation flake
* Re-run CI after an unrelated website WebKit timeout
---------
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
readPersistedConfig, used by paseo daemon config, status, pair, and
set-password, parsed and validated config.json without the error
wrapping loadPersistedConfig applies, so an invalid file surfaced as a
bare SyntaxError or ZodError. Both readers now share one parse step
that names the file and, for schema errors, the field.
Closes#5328
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
* fix(daemon): report the local daemon's server id when a password is set
Since #4575, `paseo daemon status` only learned the server id from an
authenticated connection. The desktop app runs that command without the
daemon password, so a password-protected desktop daemon reported no server
id, the app treated it as remote, and hid Open in editor.
The worker now announces its server id with readiness and the supervisor
publishes it in the instance record next to the listen address, so status
reports the running daemon's identity from local state.
Closes#5300
* fix(cli): omit serverId from status until a daemon publishes one
* chore: rerun CI after macOS file observation flake
---------
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
`paseo daemon start` runs the supervisor with its output discarded, and the
supervisor opens its log only after config.json loads and the PID lock is
held. A failure before that point went to stderr only, so the launcher
pointed at a daemon.log that was never written and showed no cause.
Startup failures now also go to the log path the launcher reports, so the
error message tails the real cause.
Closes#5327
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
* fix(server): discover ANTHROPIC_DEFAULT_FABLE_MODEL from Claude settings
* Retrigger CI after website dev server startup timeout
* test(server): assert the built-in Fable entry through the fetched catalog
---------
Co-authored-by: Noah <noahguerin20@gmail.com>
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
* Reject missing and non-directory paths for local create_workspace
The create_workspace MCP tool with isolation local persisted a workspace
and project for a path that does not exist or is a regular file. The
app's workspace create request already rejects those paths with
"Directory not found"; the tool now applies the same check before
provisioning.
* Make the local create_workspace e2e assertions path-format independent
* Rethrow filesystem errors other than a missing path in local create_workspace
* Express the local create_workspace e2e test through daemon and workspace helpers
* Clean up the offline MCP daemon when its startup fails
---------
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
* fix(server): keep original file names for chat uploads
Uploads replaced every character outside ASCII letters, digits, dot,
dash, and space with an underscore, so CJK, Korean, accented, and
parenthesized names reached disk and the agent as underscores.
Replace only characters no file system accepts in a file name, and
shorten names past 255 bytes while keeping the extension.
* test(server): make upload separator test independent of the daemon OS
---------
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
* Load config.json saved with a UTF-8 byte order mark
Windows Notepad saves UTF-8 with a BOM. Both config readers passed the
text straight to JSON.parse, so the daemon refused to start and CLI
config commands failed with "Unexpected token".
Closes#4683
* Strip the config BOM with an anchored replace
---------
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
A supervisor killed between creating paseo.pid and writing its contents
leaves an empty file. Every later start, status, and stop then failed
with "Cannot read daemon state", so the daemon stayed down until someone
deleted the file by hand.
An empty lock names no owner, so after the existing read retries it now
reads as no lock, and acquiring the lock removes it before creating a new
one. A lock with unparseable contents still refuses, since it may belong
to a supervisor this version cannot read.
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
* Start the daemon when the schedules directory holds a file that is not a schedule
One unreadable .json file in the schedules directory rejected
ScheduleStore.list(), which ScheduleService.start() awaits during daemon
bootstrap, so the daemon never started. The store now skips a file it
cannot read as a schedule and logs its path once, the way agent storage
treats an invalid agent record.
Closes#4745
* Skip only schedule files with invalid content
A schedule file that cannot be read now fails the listing as before,
instead of being skipped as if it were not a schedule. An invalid file
is reported again when it reappears after being removed or fixed.
* Retrigger CI after an unrelated macOS observation-test flake
---------
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
* test(server): wait for repository observation before asserting ref refresh
The first checkout facts read can begin before the metadata watcher registers. The test sent an event through an optional watcher lookup at that point and silently dropped it, so a slow setup left only the initial snapshot. Hold the first read to reproduce the ordering and wait for observation setup and the initial refresh before emitting the ref event.
* test(server): isolate ref refresh from background fetch
An asynchronous background fetch could also schedule a structural refresh or populate the known-ref set. Give this test an empty fetch result and wait for that fetch to complete so the second facts read proves the injected watcher event caused it.
* test(server): require the registered repository watcher in the assertion
* Keep the plugin IPC channel open until provider closes report
closeProviderConnection removed the connection from the registry before
awaiting the provider's close, so shutdown's sweep could not see a close
already in flight. With a provider whose close does real work, the
subprocess finished shutting down and called process.disconnect() while
that close was still running, and the trailing provider.closed send hit a
closed channel and took the subprocess down with an unhandled
ERR_IPC_CHANNEL_CLOSED.
Keep the connection registered until its close has reported, and share
one close promise per connection, so shutdown waits for an in-flight
close instead of disconnecting underneath it. The close now also owns
reporting its own outcome, so it never rejects into teardown.
* Harden the reload teardown test for Windows and clean shutdown
Normalize line endings before patching the example provider, so the
fixture also builds from a CRLF checkout. Wait for the in-flight turn and
archive the agent before stopping the daemon, so teardown no longer
rejects a pending provider request into an unhandled rejection.
* Verify plugin reload teardown and reject sends during close
* Synchronize plugin reload teardown test with close lifecycle
---------
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
* fix(server): let Hub title created workspaces
Agent creation can provision a workspace under hub.execute, but a separate title request required workspace.manage. Permit the same execution authority for title changes and their responses, and lock the Hub request contract with socket and permission tests.
* test(server): keep Hub title repro free of conditional flow
* fix(server): stop re-deriving path state for every scanned directory
Directory suggestion scans were CPU-bound in path bookkeeping rather than
in reading the tree. A 20,000-entry scan of a 197,000-directory home took
6.9s, of which readdir accounted for 50ms; the rest went to repeated path
normalization.
Three redundant passes are removed:
- Discovery re-derived Git-ignore state by walking every ancestor of every
entry. Traversal only descends through entries that already passed that
check, so only the entry itself can be newly ignored.
- Discovery re-derived root containment for every child. Only a symlink can
resolve outside the tree being walked; every other child is its parent's
path plus a name, and the parent was already proved inside.
- The directory listing cache swept itself for expired keys on every miss
once it reached its cap, an O(cache size) pass per directory read. Reads
already reject stale entries by expiry and by directory metadata.
The same scan now takes 1.1s. The new test expresses the budget against a
plain readdir walk of the same tree, so it means the same thing on CI as on
a developer machine: the ratio was 12x and is now under 3x.
Refs #4879
* test(server): skip the scan-cost budget on Windows
The tree it needs is deeper than MAX_PATH allows, and the directory
listing cache it measures is disabled on Windows because directory
metadata cannot validate it there.
* test(server): count containment derivations instead of timing the scan
The previous budget compared wall-clock scan time against a readdir walk
of the same tree. packages/server/AGENTS.md requires tests to be
deterministic, and a ratio between an I/O-bound walk and a CPU-bound scan
moves with filesystem caching and runner load.
isPathInsideRoot now counts its calls behind start/stopPathContainmentMetrics,
matching the existing git command metrics. Scanning a symlink-free tree of
320 entries derived containment 1,760 times before this branch and derives
it none now, which is the defect stated exactly rather than inferred from a
stopwatch.
A second test covers the other direction: a symlink that leaves the root is
still checked, and still excluded from results.
* fix(server): keep the Git-ignore ancestor walk for symlinked entries
A symlink resolves to a path whose ancestors were never checked during
traversal, so a link into an ignored directory was discovered. Named
children keep the single-entry check.
* test(server): declare the scan helper as a function
---------
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
* fix(server): back off degraded git polling on an unchanged workspace
When the recursive watcher declines a repository (over 250,000 entries on
macOS/Windows, or any watcher setup failure), WorkspaceGit falls back to
polling. That fallback rescheduled itself every 5 seconds for as long as the
workspace existed, with no client or agent involved. On a repository large
enough to defeat the watcher each tick is an expensive `git status`, so the
daemon held about a core indefinitely.
Double the gap after every tick whose workspace snapshot came back identical,
up to a 60-second ceiling, and reset to the 5-second base on the first change.
Both degraded loops — working tree and repository metadata — share the rule.
Explicit reads still force a refresh, so the ceiling only bounds how long an
edit made outside Paseo stays undiscovered.
Fixes#5102
* test(server): cover repository-metadata degraded backoff, document failed-tick semantics
* test(server): drive the recovery-refresh assertion off the fake clock, not waitFor
* test(server): declare metadata poll counter as a function
Custom messages may precede the prompt user message during an active OMP turn. Treating them as completion terminalized the turn while the provider continued working.
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
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.
* fix(server): create new branches without an upstream
A new branch Paseo creates in place ran `git checkout -b <new> <base>`,
which lets git decide the upstream. Git sets `origin/<base>` whenever the
base is a remote-tracking ref (what the default-branch resolver returns
when the local default branch is absent) or when `branch.autoSetupMerge`
is `inherit`. Every later push reads that upstream, so the work lands on
the default branch: `git push -u origin <branch>` under
`push.default=tracking` pushes to `main`, and Paseo's own push resolves
`branch.<name>.merge` and pushes `HEAD:refs/heads/main`.
Pass `--no-track`, the same promise the worktree path already makes with
`git worktree add -b <branch> --no-track <base>`.
* test(server): read the upstream with for-each-ref so a git failure cannot read as absent
* test(server): use a function declaration for the clone fixture git runner
---------
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
* fix(opencode): set permission rules on the OpenCode session
OpenCode stores permission rules on the session and its prompt_async
request schema has no permission field, so rules built from provider
options and Hub tool grants were dropped on every turn: an agent
configured with external_directory: allow still asked before reading
outside its worktree.
Send the rules where OpenCode reads them — on session.create, and on
session.update when resuming a session Paseo did not just create — and
stop attaching them to prompts. A resume that cannot apply its rules now
fails instead of running under a more permissive policy than configured.
The real-server test is taken from Gurvan Campion's #5295, which reached
the same diagnosis independently.
Co-authored-by: Gurvan Campion <32047986+gurvancampion@users.noreply.github.com>
* test(opencode): use function declarations for the permission test helpers
---------
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
Co-authored-by: Gurvan Campion <32047986+gurvancampion@users.noreply.github.com>
Observe provider response rejection before transport acceptance completes, while preserving the original promise and errors returned to callers. Cover early failure, send rejection, closure, cancellation, and successful retries through the public provider interface.
Closes#4781.
Refresh Git exclusions when ignored directories appear after subscription and bound Linux and native classification work. Preserve tracked-file notifications, ignore-update barriers, and watcher recovery.
Fixes#4558. Supersedes #4808 by @Marcus172; this change incorporates six commits originally authored by Marek Rom and adds verified follow-up fixes. The independent session-context leak is fixed by #5208. Combined isolated daemon verification passed.
Co-authored-by: Marek Rom <marek.marcus.rom@gmail.com>
Task subagent frames stream on the same query as the conversation, and
rememberTranscriptProgress recorded any assistant frame as the current turn's
rewind anchor. When a turn's last assistant frame came from a subagent, the
anchor held a uuid that lives on the subagent's sidechain rather than on the
session's own chain, so forkSession rejected it with "Message <uuid> not found
in session" and every later rewind in that conversation failed.
Skip frames that carry a parent_tool_use_id when recording anchors. The
persisted history path already skips sidechain entries; the live stream now
applies the same rule.
Closes#5284
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
* Clear the committed timeline when an agent refresh rehydrates from disk
Refreshing a loaded agent reloads it with `rehydrateFromDisk`, which wipes
the in-memory timeline so provider history can be re-streamed. It left the
committed timeline in place, and that is what `getTimelineRows` reads, so
every refresh appended a second full copy of the history: N refreshes, N
copies of every message.
Wipe both timelines on that path, the way the force-hydrate path already
does, so the re-streamed history replaces the old rows instead of stacking
on them.
* Replace the committed timeline from the replay instead of wiping it first
Deleting the committed rows on the reload path meant a history replay that
failed halfway left the agent with no transcript, and a retry appended a
fresh replay to whatever partial rows it had written.
Collect the whole replay before touching either store, then drop the rows a
previous hydration committed and write the new ones. A failed stream now
leaves the committed timeline exactly as it was, and this covers the replay
that runs when an agent is loaded after a daemon restart, which duplicated
the same way a refresh did.
---------
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
A turn that dies before the model emits its first token leaves its rewind
anchor without an assistant message id. resolveConversationRewindTarget read
only the anchor immediately before the target, so that one turn made every
later rewind in the conversation throw "assistant response id was not
observed" for good.
Such a turn carries no conversation state, so walk back to the most recent
turn that did answer and fork there, falling back to a fresh session when
none did.
The rewind SDK now reaches the session as an option so the regression test
can drive revertConversation without forking a real Claude session.
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
* Start an ACP session when a stored feature value no longer applies
Replaying stored config onto a new ACP session already tolerates a model
the session cannot select, warning and keeping the provider default.
Feature values were the exception: they were written unconditionally, and
either Paseo's own "does not expose ACP feature" guard or the provider's
-32602 rejection escaped out of initializeNewSession, so no agent was
created.
Cursor's `fast` is a per-model option, so switching a draft from a
fast-capable model to one without a fast variant left a stored value the
new session cannot take, and agent creation failed.
Apply stored feature values best-effort at session start, matching the
model path. A user-initiated setFeature still reports the failure.
Co-authored-by: gengjiawen <technicalcute@gmail.com>
* Tolerate a provider's invalid-params only after a model switch
A stale option snapshot is only possible when the replay switched the
model, since the model write answers with an empty response. Without a
switch the options came straight from session/new, so a rejected write
is a real failure the user needs to see.
---------
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
Co-authored-by: gengjiawen <technicalcute@gmail.com>
The Codex Fast allowlist covered only gpt-6-astra in the GPT-6 family, so
selecting GPT-6 Sol or GPT-6 Luna published no fast_mode feature and the
composer had no Fast control to render. OpenAI documents Fast for Astra,
Sol, and Luna, and the app server's model/list advertises the fast speed
tier for all three.
Add both model IDs to the allowlist and to the parameterized feature test
covering the toggle, the restore path, and the serviceTier sent on turn
start.
The same change was proposed independently in #5220 and #5268.
Closes#5271
Co-authored-by: Ivan Malison <IvanMalison@gmail.com>
Co-authored-by: basilkhowaja <bk08432@st.habib.edu.pk>
### Linked issue
Closes#5247
### Type of change
- [x] Bug fix
- [ ] New feature
- [ ] Enhancement
- [ ] Refactor
- [ ] Docs
### Reasoning
A user whose MiniMax account has no active token plan opened the Usage page and saw a red **Error** badge on the MiniMax card with a block of raw JSON as its body: `[{"expected":"array","code":"invalid_type","path":["model_remains"],...}]`.
MiniMax reports application-level failures inside an HTTP 200 body — `base_resp.status_code` is non-zero and `model_remains` comes back as `null`. Two things went wrong with that response:
- `MiniMaxQuotaResponseSchema` declared `model_remains` as `z.array(...).optional()`, which admits a *missing* key but not `null`, so `.parse()` threw on a perfectly ordinary no-subscription response.
- `fetchUsage()` only checked `res.ok`, so an API-level error returned with HTTP 200 was never inspected in the first place.
The rejection propagated to `ProviderUsageService`, which stores `reason.message` as the card's error text. For a `ZodError`, that message *is* the serialized issue list — which is how the JSON blob reached the card.
This accepts `null` for `model_remains`, parses `base_resp`, and returns the plain unavailable result when the status code is non-zero, matching how a failed HTTP response is already handled in the same method. The API's own `status_msg` goes to the debug log rather than to the card: an account without a token plan is a normal state, not an error, so it should read as "Unavailable" like an unconfigured provider does — not as a red Error.
### Goals
- A MiniMax account with no active token plan renders as **Unavailable** with no error text.
- `model_remains: null` is accepted wherever it appears, with or without a `base_resp`.
- Application-level errors carried in an HTTP 200 body are detected rather than treated as success.
- Accounts that do have a plan keep rendering their interval and weekly windows unchanged.
### Non-goals
- **Generic error rendering in the Usage card.** The issue also asks that structured errors (ZodError and friends) be mapped to a concise message in the UI. That changes what every provider's card shows on schema drift, so it is a design decision for the maintainer rather than part of this fix. With the producer corrected, no ZodError arises on this path; if another provider's response shape drifts, raw issue JSON can still reach the card. Left open for the maintainer.
- Surfacing MiniMax's `status_msg` in the UI. It is logged at debug instead, so the card does not turn red for a normal no-subscription account.
### QA
Reproduced and verified through the real `ProviderUsageService` with the exact payload from the report, in `packages/server/src/services/quota-fetcher/service.test.ts`.
**Before** — `reports MiniMax accounts with no active token plan as unavailable` fails, and the failure output is the reported symptom verbatim:
```
- Expected + Received
- "status": "unavailable", + "status": "error",
- "error": null, + "error": "[
+ {
+ \"expected\": \"array\",
+ \"code\": \"invalid_type\",
+ \"path\": [ \"model_remains\" ],
+ \"message\": \"Invalid input: expected array, received null\"
+ }
+ ]",
```
**After** — the full quota-fetcher suite passes: `Tests 71 passed (71)`.
Three tests were added, two of which are red without the provider change (confirmed by stashing only `minimax.ts`):
- `reports MiniMax accounts with no active token plan as unavailable` — the reported response (`model_remains: null` + `base_resp` 2062) → `status: "unavailable"`, `error: null`.
- `reports a null MiniMax token plan as unavailable even without base_resp` — `model_remains: null` alone.
- `still reads MiniMax windows when base_resp reports success` — guards against the new status check rejecting healthy accounts.
Also run locally: `npm run typecheck --workspace=@getpaseo/server`, `npx oxlint packages/server/src/services/quota-fetcher/`, `npx oxfmt --check packages/server/src/services/quota-fetcher/` — all clean.
No UI change, so no screenshots: with the provider fixed, the card takes its existing "Unavailable" rendering, the same one an unconfigured provider already gets.
### Checklist
- [x] Plugin changes follow the SDK import boundaries (if applicable)
- [x] One focused change
- [x] `npm run typecheck` passes
- [x] `npm run lint` passes
- [x] `npm run format` passes
- [x] QA evidence
- [x] Tests added or updated where it made sense
## Supersedes
- [PR #5252 — fix(quota): handle MiniMax accounts with no active token plan (#5247)](https://github.com/getpaseo/paseo/pull/5252) — Same diagnosis and same reported workflow. That PR returns `status: "error"` carrying MiniMax's `status_msg`, which keeps the red Error badge on the card for an account that is simply unsubscribed; this branch returns the plain unavailable result the issue asks for. It also adds cross-provider `ZodError` summarizing in `ProviderUsageService`, which is the design decision left as a non-goal above.
Co-authored-by: Matteo Pietro Dazzi <ilteoood@gmail.com>
* Close agents before stopping the plugins that serve them
A clean daemon shutdown stopped every plugin before it closed any agent.
Stopping a plugin withdraws its provider registrations, which pushes an
empty registration set through the agent provider registry, shuts the
plugin provider client down, and closes each still-open session with
`connectionClosed()`. That is reported as `session.runtime_failed` and
translated to `turn_failed`, so an agent whose last turn had completed
successfully was persisted with `lastError: "Provider connection closed"`
and came back after a restart carrying a failure it never had.
Stop tracking plugin provider registrations first so plugin teardown can
no longer withdraw a provider from under an open agent, and stop the
plugins themselves after `closeAllAgents()` and the provider runtime
shutdown have already made every session terminal. Closing the sessions
while their plugin is still running also means `session.close` now
reaches the provider.
Fixes#4877
* Bound each agent close so a silent provider cannot stall shutdown
Closing an agent asks its provider to close the session and waits for the
answer, and that request is only rejected when the provider runtime shuts
down. With agents now closing before the runtime does, a provider that
never answers `session.close` would leave `closeAllAgents()` waiting with
nothing left to unblock it.
Give each close a deadline. Whatever outlives it is abandoned and logged,
and the provider runtime shutdown that runs next rejects the request that
was still pending.
Also type the test fixture's `send` input as `ProviderInput`.
* Move the shutdown tests into the plugin lifecycle suite
Both tests exercise plugin lifecycle against a real daemon, which
lifecycle.e2e.test.ts already covers, so they go there rather than into a
suite of their own.
Drop the wall-clock assertion on how long stop() took. What the test
establishes is that stop() returns at all, and the suite timeout is what
enforces that; measuring elapsed time only added a second bound that a
loaded machine could trip.
---------
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>