Skip to content

feat(app): persist dismissed provider subagents across app restarts - #4441

Closed
SezginKahraman wants to merge 1 commit into
getpaseo:mainfrom
SezginKahraman:feat/persist-dismissed-provider-subagents
Closed

feat(app): persist dismissed provider subagents across app restarts#4441
SezginKahraman wants to merge 1 commit into
getpaseo:mainfrom
SezginKahraman:feat/persist-dismissed-provider-subagents

Conversation

@SezginKahraman

Copy link
Copy Markdown

Linked issue

Discussion: #4440 (this is a feature request, not a bug — no issue to close). Opening as a draft per the discussion until there's agreement this is the right shape; see CONTRIBUTING.md guidance on unsolicited PRs.

Type of change

  • Bug fix
  • New feature
  • Enhancement
  • Refactor
  • Docs

Reasoning

"Archive finished" hides completed/failed/canceled provider-owned (Task-tool) subagent rows only in an in-memory hiddenFromTrack Set (packages/app/src/subagents/provider-store.ts), documented as intentional in docs/agent-lifecycle.md. Every reload or app restart wipes it, since refreshProviderSubagents refetches the full descriptor list from the daemon on mount and the daemon has no concept of a "dismissed" provider subagent to filter back out. On a long-running parent that fans out a lot of subagents, this means re-dismissing the same finished rows over and over across reconnects. See #4440 for the full framing.

This PR persists just the hiddenFromTrack set (not the much larger, non-serializable-safe descriptors/timelines maps) to the device's local storage, using the same persist + createValidatedPersistStorage + AsyncStorage pattern already used by sidebar-collapsed-sections-store and workspace-service-route-preferences. The store construction moves into an exported createProviderSubagentStore(storage) factory (matching workspace-service-routes/store.ts) so tests can inject an in-memory storage and exercise a real write → restart → read round trip.

Goals

  • A dismissal made via "Archive finished" for a provider-owned row survives a reload/app restart on the same device.
  • No change to managed Paseo subagent archiving (already daemon-side and correct).
  • No change to descriptors/timelines persistence — those stay in-memory only, refetched/rebuilt from the daemon as before.

Non-goals

  • Not adding a daemon-side "archived" concept for provider subagents — this stays a client-local presentation dismissal, exactly as documented today, just a durable one. It does not propagate to other clients, and does not survive the provider reporting the same subagent id through a different daemon.
  • Not pruning hiddenFromTrack against the live provider list — an existing test (keeps hidden state when a child temporarily disappears from the provider list) already asserts a dismissal must survive a subagent temporarily vanishing from a replaceList response, so the set isn't safe to prune on absence; it only grows. Flagged explicitly in the doc update. A size-bounded eviction policy would need a design decision I didn't want to bundle into this PR.

QA

Commands run:

npm run build:client && npm run build:server   # workspace declarations must be current, see CLAUDE.md
npx vitest run packages/app/src/subagents/provider-store.test.ts packages/app/src/subagents/select.test.ts packages/app/src/subagents/hidden-track-persistence.test.ts
# 28 passed, 0 unhandled rejections
npm run typecheck   # clean across all workspaces
npm run lint -- packages/app/src/subagents/provider-store.ts packages/app/src/subagents/provider-store.test.ts packages/app/src/subagents/hidden-track-persistence.ts packages/app/src/subagents/hidden-track-persistence.test.ts
# 0 warnings/errors

New test survives an app restart instead of resetting Archive finished dismissals (provider-store.test.ts) creates two separate store instances sharing one in-memory storage backend — the second simulates the app restarting — and asserts the dismissal from the first instance is visible in the second after persist.rehydrate(). Confirmed it fails (createProviderSubagentStore is not a function) against the pre-change code and passes after.

One thing I had to work around: @react-native-async-storage/async-storage's web build reads window.localStorage unconditionally, which crashed as an unhandled rejection when the existing provider-store.test.ts/select.test.ts tests (which mutate the real exported singleton, not an injected instance) ran in this package's Node-environment unit test project. resolveHiddenTrackStorage swaps in a no-op storage specifically for that gap (web-targeted evaluation with no window — native and real-browser evaluation are unaffected); covered by hidden-track-persistence.test.ts.

I did not test this manually against a running app (web only, per my role in the linked discussion) — the QA here is the automated round-trip test above, which exercises the real persistence mechanism rather than a mock.

Checklist

  • One focused change
  • npm run typecheck passes
  • npm run lint passes
  • npm run format passes
  • QA evidence
  • Tests added or updated where it made sense

Archive finished currently hides completed/failed/canceled provider-owned
(Task-tool) subagent rows only in useProviderSubagentStore's in-memory
hiddenFromTrack set. This is documented, intentional behavior
(docs/agent-lifecycle.md), but it means every dismissal is silently
undone by a page reload or app restart: refreshProviderSubagents refills
descriptors from the daemon on mount, and nothing in the daemon has a
concept of a dismissed provider subagent to filter them back out.

Wrap useProviderSubagentStore in zustand's persist middleware, storing
only hiddenFromTrack (not the much larger, non-serializable-safe
descriptors/timelines maps) through the same createValidatedPersistStorage
+ AsyncStorage pattern already used by the sidebar collapsed-sections and
workspace-service-route-preferences stores. The store construction moves
into an exported createProviderSubagentStore(storage) factory, matching
workspace-service-routes/store.ts, so tests can inject an in-memory
storage and prove a real write -> restart -> read round trip instead of
only asserting against the in-memory reducer.

The web build of @react-native-async-storage/async-storage reads
window.localStorage unconditionally, which crashes when evaluated
web-side with no window (this package's Node-environment unit tests, or
an SSR context). resolveHiddenTrackStorage guards that one gap with a
no-op storage; native and real-browser evaluation are unaffected.

This is a scoped, deliberate change to previously-documented behavior:
dismissals are still local to the device (no daemon concept of
"archived" for provider subagents, so nothing propagates to other
clients), and the hidden-id set is not pruned against the live provider
list, so it grows for as long as a device keeps dismissing rows. Both are
called out in the doc update. A full fix would need a daemon-side
dismissed/archived concept for provider subagents; that's a larger,
separate change.
@boudra

boudra commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Closing this feature PR. Please describe the workflow and shared need in Discussions, as described in CONTRIBUTING.md.

@boudra boudra closed this Sep 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants