mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-03 23:23:21 +00:00
b280ca53554295b9cf8b60afb08076152c6703bf
2328
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b2bb512b31 |
Let a plugin reload finish without crashing the plugin subprocess (#5231)
* 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> |
||
|
|
067937bace |
Let Hub title workspaces created by its executions (#5302)
* 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 |
||
|
|
d615d3d424 |
Cut the cost of a directory-suggestion scan so Add Project search stops timing out (#5190)
* 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> |
||
|
|
e998e0a082 |
Stop the daemon holding a CPU core on a repository the watcher can't observe (#5170)
* 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 |
||
|
|
ae42b0afb0 |
fix(omp): wait for terminal event after custom messages (#3258)
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> |
||
|
|
829cc5e173 |
Let Claude sessions pass structured launch arguments (#5206)
Expose Claude Agent SDK's documented extraArgs map through providerOptions so Claude-specific flags such as --chrome reach the CLI. Document boolean and valued flags, link the upstream contract, and validate through schema tests, the public session interface, and a real Claude launch. |
||
|
|
48384cafdf |
Create new branches without an upstream so the first push cannot land on main (#5249)
* 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> |
||
|
|
2c7b38bc7e |
Run OpenCode agents under the permission rules they were given (#5296)
* 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> |
||
|
|
222d45a2fc |
fix(plugin-provider): keep request failures from crashing the daemon (#5298)
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. |
||
|
|
c3e1e084a0 |
fix(server): bound watcher work for late ignored directories
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> |
||
|
|
9978988e35 |
fix(server): prevent daemon heap growth after sessions close
Keep AsyncLocalStorage registrations fixed across SessionDelivery lifetimes, preserving request isolation and nonblocking close.\n\nVerified by failing-first regression, isolated memory reproduction, and combined daemon OOM verification with #5222. |
||
|
|
89073d4c5c |
Keep a Claude rewind anchor off subagent messages (#5289)
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> |
||
|
|
897a0abb1a |
Keep one copy of the timeline when an agent is refreshed (#5286)
* 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> |
||
|
|
c4771ca467 |
Rewind a Claude conversation past a turn that got no response (#5285)
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> |
||
|
|
faee1cd952 |
Start the daemon after a reboot hands its old PID to another process (#5277)
Closes #5257 |
||
|
|
ba4595d4e8 |
Start a Cursor agent on a model that has no Fast variant (#5274)
* 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> |
||
|
|
34c9fd03c9 |
fix(codex): expose Fast for GPT-6 Sol and GPT-6 Luna (#5273)
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> |
||
|
|
3054f80050 |
Show MiniMax usage as unavailable when the account has no token plan (#5258)
### 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> |
||
|
|
6d37f7fd9f |
Stop plugins after the agents they serve are closed (#5253)
* 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> |
||
|
|
94ab368e4e |
Give daemon-spawned ACP terminals the agent's identity (#5248)
* fix(acp): give daemon-spawned terminals the agent's launch environment A terminal the daemon opens for an ACP agent is a sibling of the agent process, not a child, so it inherits nothing from it. createTerminal built its environment from the requested terminal env alone, while the transport spawn a hundred lines down already overlays the session's launchEnv. The terminal therefore ran without PASEO_AGENT_ID or PASEO_AGENT_CWD, so `paseo heartbeat create` refused to run and `paseo run` lost the caller, creating child agents with no parent. When the daemon itself was started from inside another Paseo agent, the terminal saw that agent's id instead. Overlay launchEnv beneath the requested terminal env in both the argv and the single-string shell branch, matching the transport spawn's precedence. Fixes #5016 * test(acp): take createSession options as an object The helper had grown a second optional positional parameter, making callers read createSession(undefined, launchEnv). --------- Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> |
||
|
|
90737e1dec |
fix(omp): show a stopped OMP turn as canceled, not failed (#5243)
Pressing Stop on an OMP turn could land in the thread as a failed turn with OMP's interrupt text as the error, because OMP ends the turn with a terminal response that carries "Interrupted by user" in errorMessage and "aborted" in stopReason, and completeTurn classified any errorMessage as a failure. A terminal response whose stop reason is aborted now ends the turn as turn_canceled and terminalizes the turn's in-flight tool calls and subagents, matching what interrupt() already emits when it wins the race with the provider's teardown. A provider-idle state check that resolves after the turn already settled no longer completes that turn, so a stop can no longer emit a second lifecycle event behind the cancellation. Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> |
||
|
|
3eaf0be0b2 |
Send a Claude slash command as the last content block (#5240)
Claude Code expands a slash command only when it is the last content block of the user message. buildAgentPrompt orders blocks as chat history, typed text, images, then rendered attachments, so a "/command" sent alongside a pasted issue link or a screenshot reached the model as literal text: the command never ran, and the model either called the Skill tool on its own or answered without it. toSdkUserMessage now moves the typed block to the end when it parses as a slash command. Attachments rendered as text always carry a mimeType, so a quoted "/command" inside chat history is never mistaken for the invocation. Message order is unchanged when the typed text is not a slash command. Co-authored-by: paseo-bot[bot] <paseo-bot[bot]@users.noreply.github.com> Co-authored-by: Joe Corkerton <joecorkerton@users.noreply.github.com> |
||
|
|
8269a0ea78 |
Keep approvals with the user in every Codex mode but Auto-review (#5239)
A Codex mode preset named its approvals reviewer only for Auto-review. Every other mode left the field off the thread/start and turn/start request, so the Codex app-server fell back to `approvals_reviewer` from the user's global config, or to whatever reviewer the live thread was already using. Default Permissions and Read-only then behaved like Auto-review: escalations were approved by Codex, with no approval card and no notification. The reviewer belongs to the Paseo mode, so the preset now always names it and the request always carries it. An agent with no Paseo mode still inherits the Codex configuration, unchanged. Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> Co-authored-by: HMWCS <121323913+HMWCS@users.noreply.github.com> |
||
|
|
5303313632 |
Keep an archived worktree restorable after a failed re-import (#5238)
* fix(server): keep a workspace's placement when its directory is gone Resolving a workspace read the checkout at its cwd and reconciled the recorded placement against it. A git read answers "not a checkout" for a plain directory and for a directory that is not there, so an archived worktree whose directory had been removed came back as kind "directory" with branch, worktreeRoot and mainRepoRoot null. That rewrite happens before the import operation runs, and the failure path only rolls back a workspace the import created, so a failed re-import left the record erased. Workspace recovery can only recreate a missing directory while the record still says "worktree" with a branch, so the archived worktree stopped being restorable from Paseo and reported workspace_directory_missing instead. Placement is now reconciled only against a directory that is there. The absence is resolved once, where the workspace directory is read, and reconcileWorkspacePlacement refuses to rewrite from it, so the active refresh path is covered by the same rule. The workspace reconciliation pass already skipped missing directories for this reason. Closes #5111 * fix(server): treat a directory lost mid-read as an absence A worktree removed or unmounted while the git read is in flight makes that read report "not a checkout", which erased the placement the same way a directory that was already gone did. The read now counts as evidence only while the directory is there on both sides of it. --------- Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> |
||
|
|
81ad00790f |
Let Stop settle an agent whose Pi or OMP runtime has exited (#5235)
* Let Stop settle an agent whose Pi or OMP runtime has exited AgentSession.interrupt() is documented to reject "only when foreground ownership is still uncertain", and to be a successful no-op when the session is already idle. The Pi and OMP sessions broke that: interrupt() issues `abort` (and `clear_queue` for Pi) over the JSONL RPC transport, and once the child process is gone every request rejects with "<name> process is closed". AgentManager reads that rejection as "the provider still owns the turn", returns `refused`, and leaves the tracked run in place. The user gets "Cannot stop agent <id> because its active run cancellation was not acknowledged", the agent stays pinned at `running`, and pressing Stop again repeats it forever — only a daemon restart clears it. A dead runtime owns nothing: no turn is running and no prompt is queued, which is exactly the postcondition abort and clear_queue promise. Route both through JsonlRpcProcess.requestStopWork, which resolves when the process is gone and still surfaces real failures from a live process. The check covers the process dying while the request is in flight, so a race cannot reintroduce the wedge. * Require an observed child exit before treating stop work as done `disposed` marks the transport finished, not the child gone: `close()` disposes before termination completes, a spawn error disposes without touching the child, and a child can outlive SIGKILL. Keying requestStopWork on it could report a turn canceled while the child was still working. Track the child's exit event instead, and resolve only on that. A disposed transport whose child has not exited still rejects, so the caller keeps refusing a cancellation it cannot prove. Also move the in-flight tests' command detection into named `exitOnCommand` helpers, and reuse the extracted `onOmpCommand` for `replyToCommands` so the OMP file matches the Pi one. * Let a stop survive an stdin failure that precedes the child's exit A dying child breaks its stdin pipe a fraction before it emits `exit`. The broken pipe fails the in-flight abort and starts a termination, and deciding at that moment saw `exited` still false, so the stop was refused for a runtime that was already on its way out. That is the reported wedge again, reached through a narrower race. close() now records the termination it starts, and requestStopWork waits for it before deciding. A child that goes on to exit resolves the stop; a child that outlives SIGKILL still rejects it. --------- Co-authored-by: paseo-bot[bot] <paseo-bot[bot]@users.noreply.github.com> |
||
|
|
290306fd16 |
Read an archived agent's logs after its worktree is removed (#5229)
* Read an archived agent's history without its working directory Loading an archived agent for history rejected when its working directory no longer existed, so `paseo logs <archived-agent>` and `get_agent_activity` failed with "Working directory does not exist" once the workspace archive removed the worktree the agent ran in. Resuming a persisted session already distinguishes an interactive resume from a history load, but residency was settled after the session config was prepared, so the directory check ran for both. Settle residency first and require the directory only for a session that will actually run in it. * Assert the archived transcript is readable, not just loadable * Let archived timeline reads succeed without resurrecting the workspace The auto-archive test asserted that history reads for an archived agent in a removed worktree are rejected. That was the old behavior, not the invariant its comment describes. Assert the invariant instead: the reads succeed, the removed worktree stays removed, no workspace observation comes back, and the next agent lifecycle is unaffected. * Archive the agent only after its turn is finalized The regression test dispatched a run and archived immediately. Dispatch resolves before the provider appends the reply to its history, so the transcript the test reads back could be written after the archive, and the read could find nothing. Wait for the turn to be finalized first. The provider appends the assistant message before it emits turn_completed, so once the turn is finalized the transcript is already on disk. --------- Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> |
||
|
|
3a36cc33c6 |
Keep workspaces on a disk or share that is not mounted (#5227)
* fix(server): keep workspaces whose project root is unreachable Reconciliation archived every active workspace whose cwd returned ENOENT on a single stat, including the pass that runs at daemon startup. An external disk that has not mounted yet, or a network share that is briefly down, made every workspace on it disappear from the sidebar along with its sessions, and nothing brought them back when the directory returned. A missing path is only evidence that a workspace was removed when the project it belongs to is still readable. When the project root is missing too, the whole location is unavailable, and projects already persist through that. Workspaces now follow the same rule. Closes #4982 * fix(server): read project roots after workspace directories Reading the project roots first left a window where a volume could go away between the two reads, marking a project reachable while its workspace looked missing. Reading them after inverts the skew: a volume that disappears mid-pass makes the project unreachable, which withholds an archive instead of causing one. --------- Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> |
||
|
|
17c5050216 |
Show a fork checkout's pull request in the workspace (#5221)
* Resolve a fork checkout's pull request by its head repository A fork checkout's pull request lives in the parent repository, and its head repository is the only thing that tells it apart from every other fork's pull request on the same branch name. The per-checkout lookup asked `gh pr list --repo <parent> --head <forkOwner>:<branch>`, which `gh` cannot answer: it forwards the whole value as a head branch name, so the query always came back empty. Every synchronous refresh then overwrote the status the background poll had resolved, and the workspace showed no pull request. Fork checkouts now resolve through the REST pulls endpoint, the one lookup that takes a head repository, and read each resulting pull request through the ordinary view path. That also removes the disambiguation the bare branch name would have needed: `--head main` already returns thirty pull requests upstream, far past the ten-item candidate page, so a fork checkout on a common branch name would still have lost its own pull request. The three existing fork tests asserted the unanswerable invocation while their fake runner answered it regardless of the head value. They now run against a runner that matches head refs the way `gh` does. * Let a failed fork lookup surface, and keep the checked-out attempt Two corrections to the fork pull request lookup. An operational failure on the REST lookup, a rate limit, a timeout, a DNS failure, was caught and returned as an empty list, so a refresh reported "no pull request" for a checkout that has one. The lookup now only handles what it can act on and lets the rest reach the caller, the same way the non-fork list path does. A fork branch reused for several attempts can carry the checkout's head commit on an older pull request than the three the lookup reads. The page is now requested in full and the attempts carrying the checked-out commit are read first, so the bound on full reads no longer decides which attempt the workspace can see. * Use a function declaration for the head-commit predicate * Use a function declaration for the test pull request helper --------- Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> |
||
|
|
aeb98f813a |
feat(claude): add Opus 5.5 with its model capabilities (#5200)
feat(claude): add Opus 5.5 with its model capabilities Combine the catalog and runtime identity work from #5191 with the per-model effort default from #5197. Claude Code 2.1.280 introduces Opus 5.5 with 1M context, medium effort, and always-on thinking. Resolve prefixed minor model releases before their major version so Opus 5.5 retains its identity and capabilities after a turn. Co-authored-by: Leonardo G. Urci <leonardoguimaraesurci@gmail.com> Co-authored-by: Sebastian Galindo <seb@jexhq.com> Co-authored-by: Rodrigo <rodrigo.parisusao25201@gmail.com> |
||
|
|
4c051388e1 |
Reach every Codex conversation in Import session (#5174)
* fix(codex): page through Codex threads when listing importable sessions Codex answers thread/list with at most 100 rows per response whatever limit is requested, and returns a cursor for the rest. The adapter read the first response and returned, so Import session could only ever offer the newest 100 Codex threads: on a store of 456 threads, a host-wide search that asks for a 500-thread scan saw 100. Follow nextCursor until the caller's window is full, and ask Codex to page by last use so the scanned window matches how the import sheet ranks candidates. Unknown keys are ignored by Codex, so older builds keep their creation-time order. Closes #5073 Co-authored-by: 3ae3ae <83870598+3ae3ae@users.noreply.github.com> * fix(codex): end the thread scan on a page that carries no threads The previous guard only caught a cursor that repeated immediately, so a Codex cursor cycle across two or more pages (A to B to A) kept paging for ever behind a caller that had already timed out. Every page that continues the scan now has to carry rows, which bounds the loop by the requested window whatever cursors Codex hands back. * fix(codex): keep a thread once when the scan sees it on two pages Paging by last use means a thread touched while the scan is running sorts to the front and comes back on a later page, so the same conversation could be offered twice and displace another one inside the window. Identify each thread as it arrives and keep it once. A page that brings nothing new now ends the scan, which also covers a cursor that cycles while still serving rows. --------- Co-authored-by: paseo-bot <paseo-bot@users.noreply.github.com> Co-authored-by: 3ae3ae <83870598+3ae3ae@users.noreply.github.com> |
||
|
|
b2ce2bcb83 |
Count chat Find matches across the whole chat (#5167)
* fix(app): count chat Find matches across the whole chat Chat Find searched the whole chat but its counter read "1 of N in message", where N was the occurrences inside the selected message. The per-message split is an implementation detail of how the client verifies and highlights a hit; a reader counting matches is counting the scope they searched. The counter now reads "3 of 7" for the position across every message that holds a hit, and Next/Previous walk that sequence, crossing message boundaries and wrapping at the ends. Per-message occurrence stays inside the model as navigation state. The total needs a per-message count for messages that are not on screen, so the daemon now reports one. It counts with the same matcher the browser uses on rendered text — query words joined by any whitespace, case-insensitive, never across a Markdown block boundary — which also stops it from reporting locations the client would reject. The client replaces the estimate with the rendered count as each message is revealed, so the total is exact for everything that has been on screen. `count` is optional on the wire and tagged COMPAT(timelineSearchCount): a host that omits it is worth one match per located message until a reveal corrects it. * fix(server): stop chat search reporting matches the browser cannot show Falling back to the whole Markdown source when the per-block scan found nothing reported a match whenever the query words straddled a block boundary — "one target" across "target one\n\ntarget two" — and on syntax the reader never sees. The client always rejected those locations, so before the whole-chat counter the cost was a wasted round trip. Now that the total sums the host's per-message counts, a rejected location inflates the number the reader sees until navigation reaches that message and drops it. The block scan alone is the whole rule. The model's Location derives from the wire type again, with count narrowed to required: the search boundary fills in the COMPAT default, so the interior never re-checks it. |
||
|
|
ee7949ae25 |
fix(pi): honor per-model thinking configuration (#4413)
Derive Pi thinking options from each model's thinkingLevelMap, read Pi's effective level back after create, resume, set_model and set_thinking_level, surface failed state reads instead of persisting the requested level, and show the thinking control for single-level models. Closes #4382 Co-authored-by: therainisme <therainisme@qq.com> Co-authored-by: trim21 <i@trim21.me> |
||
|
|
9696f42264 |
Subscribe only the chats this session opened (#5040)
* fix(app): subscribe only the chats this session opened Open-chat membership was sourced from the persisted workspace layout, which is restored from disk at launch and carries a tab for every chat the user has ever opened on the host. Every member got an authoritative timeline catch-up at session start, and each of those fetches resumed its agent on the daemon: one provider session per historical tab, every launch. Two things followed from each resume. Registration stamped `updatedAt` over the restored timestamp, and the resumed agent started with no attention because the resume path never read the stored flag. Both were persisted, so restarting Paseo rewrote every workspace's last-used time in the sidebar and cleared Ready to review on chats the user never looked at. The timestamp loss was permanent: workspace `statusEnteredAt` is re-derived from `updatedAt` on every daemon start. Keep the open set on the session instead. A chat joins it the first time the user opens it and stays until its tab closes or the session ends, so hiding, switching workspaces, evicting a retained view, backgrounding and reconnect all preserve the subscription, with no recent-agent limit. Layout keeps its release role and loses its source role. Harden the producer as well, so a resume from any caller stays inert: registration adopts restored timestamps rather than stamping now, and resume carries the stored attention through. * fix(agents): keep a resumed agent's updatedAt from moving backwards Dropping the registration stamp exposed a skew the stamp had been erasing. `extractTimestamps` read `lastActivityAt ?? updatedAt`, but the two diverge: renaming, labelling, restoring from archive, marking unread, and clearing attention all move `updatedAt` on an unloaded agent without touching `lastActivityAt`. Eight of 854 agent records on one dev machine carry that skew, up to eight days of it. A resume then handed clients a timestamp older than one they already held, and `acceptAgentDirectoryUpdate` drops any state update that goes backwards. Marking a chat unread without opening it, then opening it, left the unread dot stuck in History, the agent list, Command Center and the tab: the daemon cleared attention and the client refused the update. Read both fields and take the newest, which is already the rule `resolveStoredAgentPayloadUpdatedAt` applies to the list payload. Move that function beside the other stored-record readers as `resolveStoredAgentUpdatedAt` so one rule covers both paths. Also replace the `!options?.updatedAt` inference in `registerSession` with an explicit `restoring` flag. The inference was correct for all four call sites, but it made a second meaning out of a field, and the next caller to seed a timestamp would have opted out of activity stamping silently. * test(app): assert a reloaded layout subscribes only the chat it restores into No e2e reloaded the page with a populated workspace layout and asserted which agents the daemon acknowledged, so the regression this branch fixes had no end-to-end guard. Opening four chats across two workspaces leaves a tab per chat in persisted layout; after a reload only the restored chat may be subscribed, because subscribing resumes that agent on the daemon. Verified it fails when session-context's layout callback is pointed back at replaceVisibleAgentIds: the three restored tabs nobody opened join the acknowledged set. |
||
|
|
92b92a81c8 |
perf(desktop): reduce daemon management memory overhead (#5007)
Desktop and CLI helpers imported daemon bootstrap through the server root, while supervisor configuration retained the full wire schema tree. Expose existing capabilities through narrow package entrypoints and share configuration schemas without loading message definitions. Guard the dependency trees without tree-shaking so unused exports cannot hide the runtime cost. |
||
|
|
047f40e623 |
Install npm plugins and review source updates (#4975)
* feat(plugins): unify source installation and simplify plugin settings * feat(plugins): install npm sources and review CLI updates Treat install selectors as one-time choices. Keep source identity separate from the installed revision, and resolve ordinary updates from the current upstream default. npm only acquires directories; plugin loading remains independent of npm. * fix(plugins): preserve CLI diagnostics and exercise reviewed updates Move new source capability coverage to real browser journeys. Update the requirements regression to exercise preview and apply, and preserve the existing duplicate-selector diagnostic across positional option scopes. * test(plugins): remove unused pending helper * test(plugins): pack registry fixtures with the Windows command runner * test(plugins): assert failed client contribution setup * docs(plugins): consolidate guides and document npm publishing * docs(plugins): introduce publishing choices before npm instructions * test(plugins): own browser journey cleanup in a fixture * feat(plugins): prepare scaffolded packages for publishing |
||
|
|
a8a8da0472 |
fix(plugins): preserve nested provider subagent links (#4970)
* fix(plugins): preserve nested provider subagent links The plugin adapter routed descendants to their managed root but discarded their direct parent when publishing descriptors. Preserve runtime-to-public child identities and forward optional spawning tool IDs. * test(plugins): separate opening and live subagent scenarios * test(server): retry terminal fixture directory cleanup Match the daemon harness cleanup policy so transient Windows EBUSY locks do not fail terminal ownership tests after their assertions complete. |
||
|
|
0f20e6dfe4 |
Keep History search chronological and make matches clear (#4945)
* fix(history): preserve chronology and clarify search matches Keep workspace names primary and highlight each searched field so results explain why they matched. Filter before cursor pagination and retain date buckets. Constrain shared subsequence matching to each whitespace-delimited word while preserving fuzzy abbreviations in Command Center and other consumers. * test(history): reuse response helper in pagination regression * test(history): express browser journey through named helpers * fix(app): start menu entering animation only once the surface is placed Reanimated's web entering path snapshots the element rect at animationend and writes top/left back when it cleans up the keyframe 750ms later. When the 150ms animation ends before anchor and content measurement resolve, that snapshot is the off-screen measuring position and the open menu jumps back to -9999. Remount the surface when its position resolves so the animation starts from the final position. |
||
|
|
a78949d1d4 |
Preserve committed changes when restoring archived workspaces (#4926)
* fix(server): preserve comparison base when restoring worktrees Restore archived worktrees from the workspace's saved comparison base instead of using the feature branch as its own base. Preserve the branch HEAD and allow restore when an exact comparison ref no longer exists. * test(app): isolate draft preference checks from menu animation |
||
|
|
64b1a62ed3 |
Fix voice reply latency and hide internal prompt wrappers (#4927)
* fix(voice): speed up replies and hide prompt wrappers Keep voice prompt cleanup in the client presentation path so provider history and live messages render consistently without rewriting canonical input. Correct native TTS scheduling and threading, and reconnect interruption to speech detection and session cancellation. * test(voice): clarify playback and timeline regression journeys * test(voice): centralize native worker cleanup and repair fixture path * test(app): exercise workspace eviction without reloading the app * test(app): preserve streamed output during workspace eviction Hold the turn until the workspace is evicted instead of dropping all stream events during navigation. Slow CI runners can finish the entire response before navigation ends; waiting for producer completion before release makes that ordering deterministic. * test(app): stabilize subscriptions during workspace eviction Opening another chat replaces the timeline subscription, making buffered frames from earlier generations unroutable. Preopen all chats before holding the stream, finish production before eviction, and assert both early and final response content with no subscription replacement. |
||
|
|
0eac75be7d |
fix(server): stop an expired application lease from permanently killing a plugin session (#4912)
* fix(server): keep plugin sockets out of the application socket lease
A plugin's IPC socket is claimed by the application socket lease as soon as
its client sends its first ping. If that client's heartbeat is then starved
for longer than APPLICATION_SOCKET_LEASE_MS — a blocked event loop, a
SIGSTOP, a sleeping machine — the daemon reaps the socket with "Closing
physical WebSocket with expired application lease".
The plugin never comes back. plugin-process.ts builds its DaemonClient with
`reconnect: { enabled: false }`, so unlike an app or menubar client it cannot
redial, and there is no "Client disconnected; waiting for reconnect" because
no reconnect is ever scheduled. Every later `paseo.*` host API call then
fails with "Transport not connected (status: disconnected)" while
`paseo plugin ls` still reports the plugin as running. Only
`paseo plugin reload <id>` recovers it.
The lease is a backstop for an abandoned application socket. A plugin socket
is IPC to a child this daemon spawned and already supervises through the
child's own close, so heartbeat silence there is not evidence of abandonment
— it only means the child is busy or paused. Reaping it strands a plugin that
was about to resume, which is strictly worse than leaving it attached.
Ordinary sockets are unaffected and still reaped on lease expiry; the second
test pins that down so the exemption cannot widen unnoticed.
Fixes #4883
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(server): let a plugin session recover when the daemon closes its socket
The lease exemption stops the common cause, but any daemon-side close still
stranded a plugin for good: the child's DaemonClient was built with
`reconnect: { enabled: false }`, and even with it enabled the child had
nothing to redial onto, because the runtime attached exactly one
PluginSessionSocket per load and routed frames to that captured const.
Enable reconnect on the plugin client, and have the runtime stand up a
replacement session when a socket closes underneath a live child. Frames now
follow the current socket, and `loaded.sessionSocket` / `sessionClosed` track
the replacement so stop and teardown still await the right attachment. The
session host already supported this — attaching a second socket for the same
plugin id gives it a second, independently owned session.
A plugin on its way out must not be resurrected. Both stop paths
(`stopPluginById`, `stopAll`) clear the catalog entry before they close
anything, so the catalog check is what distinguishes a shutdown from a socket
worth replacing; a dead child is caught by `child.connected`.
The client waits DEFAULT_RECONNECT_BASE_DELAY_MS (1500ms) before redialling
and gives up after DEFAULT_CONNECT_TIMEOUT_MS, so the replacement is attached
long before the first attempt arrives, and a lost race would retry anyway.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor(server): trim the comments on the plugin session fix
The reasoning lives in the commit messages and the PR. Leave only what the
code cannot say for itself: why a plugin socket is not an application socket,
and why the catalog check is what distinguishes a shutdown.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(server): re-attach a plugin session on the child's redial, not on close
Attaching eagerly when the socket closed put a socket in place that nobody
was speaking to yet. Two ways that goes wrong, both reachable under exactly
the conditions #4883 describes - a child whose event loop is starved:
- Nothing sends a hello on the replacement, so the host closes it after
HELLO_TIMEOUT_MS (websocket-server.ts:1310), and that close attached
another. A wedged child drives that cycle until it exits, filling the
plugin's 500-entry log ring with re-attach lines.
- Frames the child had already queued arrive after the close and land on the
replacement while it is still pending, which closes it with
WS_CLOSE_INVALID_HELLO (websocket-server.ts:2151) and attaches another.
Route frames through the current socket instead, and stand a replacement up
only when a hello arrives while the old one is closed - the one point that
can tell leftovers of the dead session from a genuine redial, since the IPC
channel carries no session id. A starved child now attaches nothing until it
wakes up and redials.
Also re-check the guards after the attach await and close the replacement if
they no longer hold, and surface a failed re-attach through appendLog and
notify rather than only a daemon log line, so a plugin left without a session
is visible instead of silently repeating the original bug.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor(server): follow the server style guide and drop test timing
Two review findings against packages/server/CLAUDE.md.
Helper shape (CLAUDE.md: `function` declarations over arrow assignments,
object parameters when a function takes more than one argument). The
re-attach helpers were arrow assignments closing over `loadPlugin` locals, and
two took `(frame, isBinary)` positionally. They are now private methods taking
a single object. The mutable session state they shared - the current socket,
the in-flight flag, the loaded plugin - moves into a PluginSessionBinding the
methods read, which is what made the closures hard to lift out before.
Test determinism (CLAUDE.md: no timing/randomness). The reconnect tests waited
on fixed 500ms sleeps and 15s vi.waitFor polling windows, so they inferred
lifecycle state from elapsed time. The tracked session host now exposes
waitForHellos, resolving on the handshake itself, and the lazy-attach case
runs against a stub child that is driven frame by frame: close, then a stale
frame that must not attach, then a hello that must. No sleeps and no polling
remain in these tests, and the file's suite drops from ~40s to ~11s.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(plugins): verify host API and observation recovery
* test(plugins): await connection and catalog events
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
|
||
|
|
f9fb992dcb |
fix(plugins): use spawnProcess for plugin build commands on Windows (#4776)
runPluginBuild called node:child_process spawn with shell: false, so bare commands like `npm` failed on Windows with `spawn npm ENOENT` because npm is a .cmd script. Route through the shared spawnProcess helper, which already selects shell: true for bare commands and .cmd/.bat scripts on Windows and quotes arguments for cmd.exe. Side effects inherited from spawnProcess, on every platform: - Child env is sanitized via createExternalCommandProcessEnv, stripping ELECTRON_RUN_AS_NODE, ESBUILD_BINARY_PATH and PASEO_* control keys that previously leaked from the daemon into plugin builds. - windowsHide: true, so builds no longer flash a console window on Windows. Adds behavioral tests for preparation.ts. Windows cases use it.runIf and run only on Windows hosts; CI is ubuntu-only. Co-authored-by: ABorakati <ABorakati@users.noreply.github.com> |
||
|
|
80b4387bc6 |
Find text throughout chat history (#4765)
* feat(app): add pane Find to file source * fix(app): float the pane Find widget and tighten its chrome Find pushed the first lines of the file down, ran 480px wide with 32px ghost buttons, and let the replacement input start left of the query input. Float the CodeMirror top panel out of flow so the widget overlays the top right of the pane, move the disclosure toggle into a gutter that spans both rows so the two fields share a leading rail, put the match count inside the query field, and drop the icon controls to the shared pane-toolbar chrome the file bar above already uses. * fix(app): keep the active match visible and drop the Find button The floating widget covered the active match whenever the match sat under it and the document was too short to scroll clear. CodeMirror's own panel scroll margin cannot reveal a first line, so the file integration now owns placement: the widget renders as an overlay beside the editor, the model measures it against the active match, and flips it to the opposite corner only when the current corner collides. Hysteresis keeps navigation from bouncing the widget between corners. Also removes the standalone Find button shown while Find is closed, per user direction. Cmd/Ctrl+F is the entry point. * fix(app): stop the Find placeholder from hiding Go to line Hiding `.cm-panels` suppressed every CodeMirror panel in the file editor, including the Go to line dialog that Mod-Alt-g opens through the same search keymap. Hide only the inert Find placeholder panel instead, by its own class, and leave the shared panel container alone. * feat(app): find text throughout chat history Discover candidate transcript locations on the daemon, then reuse prompt navigation to load history and verify rendered occurrences on the client. Keep search state inside its own modules without changing timeline or task presentation contracts. * fix(app): show actionable chat search failures Keep transport error details in the search model while showing localized retry guidance. Use named client request options for timeline search. |
||
|
|
d8dd189b94 |
ci: simplify and de-flake the test suite (#4868)
* chore(ci): audit packaging costs and repeated test execution Read-only audit: Linux packaging and smokes consume 12-14 minutes; all three CLI shards repeat the 294-test source suite and build the server twice. Preserve the 17 required contexts, move packaging to main, run CLI source tests once, and consolidate repeated E2E setup into journeys. Historical logs and the complete audit were reported outside the repository. * ci: move packaging to main and remove repeated CLI suites PR runners were rebuilding distribution artifacts and executing the entire CLI source suite on all three E2E shards. Keep packaging coverage after merge, let the local E2E runner own its dependency build, and execute source tests once in an existing required check. Root typecheck already validates plugin examples. * test(ci): consolidate journeys and remove repeated startup work Run user journeys with shared setup, retain one Hub CLI contract, and weight the existing Playwright shards using observed costs. Remove implementation-only and ad-hoc measurement checks. Await root-watch closure and publish creation failure commands atomically so teardown and failure injection have completion boundaries. * test(hub): remove incidental observation and speculative shutdown changes Hub control RPCs need a connected client without an agent observation. Preserve the failed-create subscription contract and remove product shutdown changes whose Windows cleanup cause was not reproduced. * fix(client): clear the scheduled retry when a connect attempt opens a transport An explicit connect() during reconnect backoff opened a new transport but left the scheduled retry armed. When it fired on the healthy connection it disposed the transport and reconnected, dropping requests in flight. This is why owned-subscriptions-old-daemon could still fail after #4858 added the explicit reconnect. attemptConnect now owns the rule ensureConnected already applied; the new unit test fails on the old client. * test(e2e): keep the completed-block, font-readiness, and live-page guards inside journeys - agent-stream-ui: the auto-scroll journey anchors on the completed intro block by content and requires its Markdown root to stay the same mounted node while later blocks stream (#1989 guard, reshaped off the tail block). - changes-pane: the code-typography journey holds font loads and requires no diff geometry before they resolve, then applies the font change. - agent-timeline-pagination: the live-output page load runs against a thirty-minute mock stream and gates the release on the running turn, so the status assertion no longer races a ten-second turn. * test(desktop): install the Electron binary once before the unit suite Electron 44 downloads its binary on the first Node require("electron") instead of at npm ci. Vitest workers importing electron-log/main raced that download and extraction (File exists on dist/electron, then "Electron failed to install correctly" in a sibling worker) on both Ubuntu and Windows desktop-tests jobs. Run the package's idempotent install-electron in one process before vitest forks. * ci(desktop-packages): pin actions and smoke packaging-path pull requests Pin checkout, setup-node, and upload-artifact to full commit SHAs. Keep packaging off ordinary PR CI, but run the Linux package smoke on pull requests that touch packages/desktop, nix, or the workflow so a packaging or sandbox regression cannot land on main unchecked. The desktop release workflow already builds and smokes its own artifacts on all platforms. |
||
|
|
b21c004ffe |
Keep open chats current and show reconnection progress (#4863)
* fix(app): keep open chats current and show resume progress Keep open-chat delivery independent of mounted views, freeze hidden chat rendering, and catch up visible chats before hidden ones. Distinguish host reconnection from timeline freshness so cached messages do not silently appear current after resume. * fix(app): prevent sync status from overlapping action errors Share the existing panel toast slot so action errors remain readable during reconnect and catch-up. Verify the interaction and draft retention in the real browser, preserve the tested freeze transition, and refresh the Nix dependency hash for react-freeze. * test(hub): expose complete creation and message retry failures Partial response assertions hid the creation error and allowed duplicate responses with incomplete agent data. Check complete success envelopes, the shared agent snapshot, and null message errors so future failures print the actual error payload. * fix(app): dismiss retained chat menus before freezing An async fork navigates before its closing state commits, so freezing the source chat could leave its portal backdrop blocking the destination. Remove the shared menu surface in the inactive commit before suspension. Keep regression assertions aligned with open-chat subscriptions, per-agent incremental recovery, plugin contribution lifetimes, and rendered image completion. * test(app): expect the reloaded plugin to contribute its pill again A full reload discards the pressed pill with the page, then evaluates the plugin again and its agents snapshot re-adds the pill. The old count-zero assertion only passed on main because the chat was not ready yet when the URL settled; open chats now subscribe from session start, so the pill is rendered by then. * fix(app): suspend hidden chats on web only react-freeze is Suspense with a never-resolving thenable. Suspending a committed subtree hides its host nodes, tears down layout effects, and detaches refs, which changes Fabric ownership of the retained chat. Split the boundary by platform: the web file keeps the two-commit freeze, the native file keeps the chat mounted and live, and the retained-panel signal remains the only thing that stops background work. Restore the mobile-panels rule the branch had rewritten. * fix(creation): never read a receipt while replacing it Admission reads the creation receipt while the runner replaces it atomically. On Windows a rename over a file this process still has open for reading fails with EPERM, and that error marked an otherwise completed creation as failed (#4865). Queue reads and writes per receipt so one owner never overlaps them, and serialize the record when the write is queued because the runner mutates it afterwards. No retry on EPERM: the conflict is removed rather than tolerated. Linux cannot reproduce the rename failure, so the Windows server-tests job (creation/index.test.ts and hub/execution-session.websocket.test.ts) is the verification for this change. |
||
|
|
6215e08efe |
Smooth streaming replies with a continuous left-to-right fade (#4859)
* feat(app): reveal streaming text as one continuous fade front Streaming assistant text is released by word and every character fades in over the same 150 ms envelope from a start time owned by the message. Start times never decrease in reading order, so a line paints as a single front moving left to right, however the text is chunked. Per-word envelopes with an internal stagger let the next word's first letter overtake the previous word's last letter, which read as holes and flashing words at the real frame rate; 100 ms frame strips hid it. Native renderers receive the fade ranges as a prop on a host view wrapping the root Text, so ranges and text land in the same Fabric mount transaction. The earlier binding sent ranges through a separate async module call keyed by view tag and had to reconcile them against the view's text; on a fast phone the UI thread lags JS by several commits, mounts and calls interleave, and every reconciliation heuristic guessed wrong. Native code no longer infers newness or creates start times. The mock load-test provider now separates cycles with a blank line so a cycle heading is not appended to the previous paragraph. * fix(ci): refresh streaming dependencies and isolate E2E setup * test(app): distinguish fade cleanup from Markdown replacement * test(app): detect restarted fades during Markdown updates * docs(app): keep streaming guidance focused on constraints and QA * test(app): control animation time in Android fade assertions Fixed wall-clock sleeps let emulator scheduling select the sampled animation frame. Inject the animator time source and drive paint at exact timestamps within one UI-thread operation; production keeps the system clock. Verify the mid-fade sample and the exact cleanup boundary without sleeps, polling, or retries. * fix(test): compile the desktop renderer before lifecycle checks The lifecycle fixture treated Metro /status as app readiness. Its first Settings click therefore spent its 90-second UI budget compiling the cold bundle. An isolated cold-cache run restricted to one CPU reproduced the click timeout before the app runtime had loaded. Reuse the browser suite's existing bundle warmup before launching Electron, and preserve Metro output in the lifecycle artifacts. The UI timeout, build-request timeout, lifecycle assertions and product behavior are unchanged. CI did not preserve Metro output, so its precise startup timing is not recoverable from the failed run. The retained log makes subsequent startup failures diagnosable without relying only on an empty renderer text dump. Validation: three complete lifecycle E2E runs under private Xvfb displays; existing Metro readiness tests; app typecheck; targeted lint; root format. * test(file-observer): await burst delivery through native recovery The deletion phase required every event within 10 seconds, but native coalescing recovery uses a 30-second safety audit followed by the existing reconciliation debounce. An idle diagnostic excludes that delayed audit and therefore cannot act as a completion barrier. Await the required delivered events instead, preserving every deletion assertion and the existing overall test watchdog. A real-filesystem test with controlled native notifications reproduces 95 outstanding paths at the old deadline and verifies completion when the safety audit delivers them. No production observer behavior changes. * test(file-observer): verify burst recovery through public events The controlled-clock regression constructed a private native backend, bypassing the interface used by consumers. Consolidate recovery coverage into the public observer integration test instead: drive real writes, deletes and directory renames, then await every required public event, including the 450 surviving files at their renamed paths. No public capability was added: subscribe callbacks already express completion. No production behavior or test timeout changes. * test(app): drive Android fade tests through the native host The instrumentation suite constructed TailFadeInAnimator directly, advanced its private clock, and counted FadeInSpan instances. It now mounts the WordFadeHost that React mounts, feeds it the ranges prop the bridge sends, and asserts rendered pixels: hidden before a start time, a monotone front inside and across words, settled frames equal to the baseline, and no frame callbacks left after settling. Span counts and internal timing state are no longer asserted. Choreographer cannot be advanced deterministically from a test and the fade epoch is the message's wall-clock start time, so the host takes a WordFadeFrameClock (time plus frame callbacks) as its smallest public seam; production keeps the Choreographer-backed default. Fixture text is sized in pixels so every sample stays on one line at any density; at 420 dpi the 32sp fixture wrapped and the column helper summed glyphs from the first line. |
||
|
|
0c5f472da1 |
fix(plugins): expose server settings access (#4674)
* fix(plugins): expose server settings access * fix(plugins): isolate settings subscriber state * test(plugins): read persisted settings before clients connect * test(app): reconnect seed client after old daemon restart * test(server): drain file creation events before deletion assertions * test(creation): expose errors when completion fails --------- Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com> |
||
|
|
13979e9c38 |
fix(server): discover fork PRs with URL-valued branch remotes (#4862)
Resolve direct repository URLs as well as named remotes when deriving checkout PR identity, preserving fork-owner filtering and batched polling. |
||
|
|
77c5c8f17e |
Prevent duplicate catalog choices from crashing agent drafts (#4839)
* fix(providers): keep duplicate catalogs from breaking agent drafts Normalize model identities before publication and when materializing older catalogs, preserving the first definition and configured overrides. Keep explicit default variants separate from the base model selection. * fix(providers): preserve saved variant identities across resumes Persist the selection encoding so legacy names remain literal. Refresh provider persistence when a selection changes, before another prompt is sent. * test(app): let fixtures own catalog journey cleanup * fix(providers): preserve the existing default variant contract Keep one Default catalog choice and let the provider resolve an omitted variant. Remove the encoding and persistence machinery; other variant IDs retain their literal meaning. * test(creation): expose failed completion after conflicting requests Creation returns a failed snapshot instead of rejecting. Assert the result before its side effects so platform failures report the underlying error. * test(app): wait for the seed client after daemon restart The browser and seed client reconnect independently. Wait for the seed connection before issuing post-restart RPCs instead of assuming browser readiness covers both clients. * test(app): isolate visible prompts and checkout diff fixtures Inactive tabs retain message DOM, so prompt assertions must inspect visible rows. Ignore the local fixture remote before observation rather than deleting it after the daemon has started tracking the checkout. * fix(file-observer): preserve final deletions when batching events A duplicate creation notification followed by deletion was collapsed into an update, hiding final absence from consumers that distinguish deletes. Keep final deletes while retaining update semantics for atomic replacement. * fix(desktop): wait for Metro HTTP readiness before launching Electron Expo briefly listens while probing port availability. A successful TCP connection can therefore launch Electron before Metro starts and leave it exiting on connection refusal. Require the configured renderer origin to report the running packager status. * test(ci): consolidate fixture and readiness corrections Await the connection and UI states each journey owns, isolate Git fixture contents before observation, and hold real Markdown frames at asserted checkpoints. Keep creation completion diagnostics explicit without claiming the original Windows failure is fixed. |
||
|
|
836f1a9c23 |
fix(ci): consolidate E2E repairs and wait for Metro readiness (#4858)
* fix(desktop): wait for Metro HTTP readiness before launching Electron Expo briefly listens while probing port availability. A successful TCP connection can therefore launch Electron before Metro starts and leave it exiting on connection refusal. Require the configured renderer origin to report the running packager status. * test(ci): consolidate fixture and readiness corrections Await the connection and UI states each journey owns, isolate Git fixture contents before observation, and hold real Markdown frames at asserted checkpoints. Keep creation completion diagnostics explicit without claiming the original Windows failure is fixed. * test(desktop): describe Metro readiness through HTTP states Keep protocol mechanics in the fixture and assert readiness only after a successful packager response. Cover non-success status and unrelated HTTP content without inline synchronization in the test body. |
||
|
|
d63132a37d |
fix(creation): prevent duplicate workspaces and agents on retries (#4442)
* fix(creation): prevent duplicate workspaces and agents on retries * test(creation): cover plugin hooks across receipt replays * fix(creation): recover draft retries without accepting partial workspaces * refactor(creation): name receipt inputs and tag compatibility gate * fix(file-observer): track files announced by native change events * fix(creation): keep initial prompts in daemon-owned creation requests * fix(creation): reuse connection-owned progress and recovery * test(creation): adapt handoff gates to combined requests * ci(desktop): avoid xz compression for smoke packages * ci(desktop): pass valid RPM compression through FPM config * refactor(creation): give legacy and modern requests one identity owner * fix(creation): log observer failures without interrupting creation |