Skip to content

fix(notifications): show one notification per session compaction - #1096

Merged
pascalandr merged 2 commits into
devfrom
fix/compaction-notification-dedupe
Oct 22, 2026
Merged

pascalandr merged 2 commits into
devfrom
fix/compaction-notification-dedupe

Conversation

@pascalandr

Copy link
Copy Markdown
Contributor

Fixes #986: one automatic compaction stacked several identical "Session … was compacted" notifications.

Cause
The notification was not idempotent: every delivered session.compaction.ended created a bubble, plus a history entry when the category is routed there. The same event reaches a window more than once:

  • once per project tab open on its folder, because the server publishes one envelope per owning workspace;
  • again when the event stream replays it after a reconnect.

Fix

  • stores/compaction-notice.ts claims each compaction end once per window, by its durable position (aggregate and sequence, or the event ID without one). It keeps a bounded memory of recent compactions. Later deliveries still update the session status in their tab, but no longer notify.
  • The session's bubble stays unique: a later compaction of the same session replaces the bubble still shown instead of stacking beside it. Each distinct compaction still reaches the history when routed there.

Validation

  • Unit tests for the claims: tabs, replays, next compaction, another session, event-ID fallback and the bound. Added to the CI unit list.
  • Browser test compaction-notification.test.ts, through the real event dispatcher and toaster:
    • one compaction delivered to two tabs on the same folder, plus a replay, gives one bubble and one history entry;
    • the session's next compaction gives one replaced bubble and a second history entry;
    • without the claim the test fails with three bubbles, without the replacement with two.
  • notification-icon-badge and compaction-responsiveness pass. The UI typecheck with its own tsconfig reports 0 diagnostics, and the root npm run typecheck passes.

The user-visible change only removes duplicates, so no help page needs an update.

One automatic compaction stacked several identical "Session … was compacted"
notifications. The notification path was not idempotent: every delivered
session.compaction.ended created a bubble (and a history entry when routed
there). The same event reaches a window once per project tab open on its folder
(the server publishes one envelope per owning workspace) and again when the
event stream replays it after a reconnect.

stores/compaction-notice.ts claims each compaction end once per window by its
durable position (aggregate and sequence, or the event ID without one), within
a bounded memory of recent compactions; later deliveries still update the
session's status in their tab but no longer notify. The session's notification
bubble is also kept unique: a later compaction of the same session replaces the
bubble still shown instead of stacking beside it, while each distinct
compaction still reaches the history when routed there.

Validation: unit tests for the claims (tabs, replays, next compaction, other
session, event-ID fallback, bound). A browser regression delivers one
compaction to two tabs on the same folder plus a replay through the real event
dispatcher and toaster, and asserts one bubble and one history entry, then one
replaced bubble and a second history entry for the next compaction; without
the claim it shows three bubbles, without the replacement two.
notification-icon-badge and compaction-responsiveness pass; UI own-tsconfig
typecheck and root typecheck pass.

Fixes #986

@pascalandr pascalandr left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Independent gatekeeper review of exact HEAD 9165c80195af92d51a287441f1960fe684fc1879 against origin/dev.

[P3] Release expired compaction toast handles

packages/ui/src/stores/session-events.ts:605-612 (module-level map at line 63): every distinct session that compacts leaves a ToastHandle in compactionToasts for the entire window lifetime. Bubble expiry/manual dismissal, session deletion, and project removal never delete these entries; even History/System-only routing stores a no-op handle. Thus a long-lived window accumulates one retained key/handle per compacted session, independently of the bounded 256-entry claim set and 50-entry notification history. Remove the entry after its bubble lifetime (guard cleanup by handle identity so an older expiry cannot remove a replacement), and avoid retaining no-bubble handles; cover expiration/replacement cleanup with a regression check.

Validation: UI npx tsc --noEmit -p tsconfig.json and root npm run typecheck both passed. The requested unit command passed 3 tests; the requested three browser files passed all 12 tests. CI explicitly lists the unit test and discovers the new browser test through tests/browser/*.test.ts.

Identity/routing audit: the server relay forwards the same native event to each owning workspace and the client SSE path preserves id/durable. The native v2.1.2 compaction aggregate is sessionID; Bus sequences are persisted in event_sequence, incremented for new events, and retained on replay. Notification claiming precedes all screen/history/system delivery, while per-tab session status updates still run on duplicates. No additional actionable findings identified.

Gatekeeper review of #1096 found that the per-session bubble map kept a toast handle for every session that ever compacted, for the window's lifetime, including no-op handles when the category is routed away from the screen.

The one-bubble-per-session logic moves to createSessionBubbles in stores/compaction-notice.ts: it replaces the bubble still shown, forgets each one when its duration ends unless a newer one replaced it, and retains nothing for a notice that shows no bubble. Unit tests cover replacement, identity-guarded expiry and off-screen routing; the browser regression, notification-icon-badge and compaction-responsiveness pass, as do the UI own-tsconfig typecheck and root typecheck.

@pascalandr pascalandr left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed exact HEAD 585140abfe6b9379a19186a5846fd9da1b2c4273 against origin/dev after fetching the branch.

Zero actionable P0–P3 findings

The prior P3 is addressed: session bubble handles expire with identity-guarded cleanup, replacements dismiss the previous bubble, and off-screen routing retains no handle. Rechecked the complete PR diff, identity preservation through the relay/SSE path, notification routing, and CI test discovery.

Validation passed: UI own-tsconfig npx tsc --noEmit -p tsconfig.json, root npm run typecheck, all 5 compaction-notice unit tests, and all 12 tests across the requested compaction-notification, notification-icon-badge, and compaction-responsiveness browser files. No installed-app or shared-daemon interaction.

@pascalandr
pascalandr merged commit 333d734 into dev Oct 22, 2026
13 of 14 checks passed
@pascalandr
pascalandr deleted the fix/compaction-notification-dedupe branch October 22, 2026 13:34
pascalandr added a commit that referenced this pull request Oct 22, 2026
## Problem

Since #1096, the UI unit job on `dev` fails in two
`instances-pending-compaction.test.ts` tests, with `TypeError:
toast.dismiss is not a function` (also in #1096's own PR run).

#1096 shows one bubble per session, so a later compaction end dismisses
the session's earlier bubble. These tests end several compactions in one
session. Under Node with tsx, `import toast from "solid-toast"` yields
the CommonJS module object, which has no `dismiss`. The fixture replaced
only `toast.custom`, so the dismissal threw.

The app is not affected: Vite resolves the real `toast` function, which
has `dismiss`.

## Fix

The fixture also replaces `toast.dismiss` with a no-op and restores it
after each test, as `session-lifecycle-signals.test.ts` already does.
Test-only change.

## Validation

- Reproduced on unmodified `dev` (`333d73452`): 2 of 7 tests in the file
fail.
- With the fix:
- `instances-pending-compaction`, `compaction-notice` and
`session-lifecycle-signals`: 18/18;
  - full UI unit suite: 1694/1694;
  - UI typecheck.
pascalandr added a commit that referenced this pull request Oct 22, 2026
## Summary

- Qualify OpenCode **2.1.3** (latest stable, 22 October) with the
existing locked **2.1.0 client/plugin** dependencies, and **recommend**
it.
- Keep the technical **minimum at 2.1.2**: nothing CodeNomad consumes
changed, so the minimum has no new reason to move.
- A 2.1.2 daemon stays ready and gets an optional upgrade offer. 2.1.3
is the `tested` version. Newer and custom labels keep authenticated
contract recognition.
- The reviewed-contract fast path lists 2.1.3 beside the 2.1.2 minimum.
- The recommended version is updated in the help (all ten locales), both
READMEs, `dev-docs/OPENCODE_COMPATIBILITY.md` and the architecture
skill.

## Contract review (v2.1.2 → v2.1.3, 66 commits)

- **Authenticated OpenAPI:** 119 → **121 paths**, 144 → **146
operations**. Every consumed operation is unchanged. The only changes:
  - added `GET /api/experimental/voice/models`;
- added `POST /api/experimental/voice/session/{sessionID}/instructions`;
- an optional hosted `session.id` in the `POST
/api/experimental/voice/session` response.

  None of these is on the instance proxy allowlist, so they stay closed.
- **Published declarations:** the client adds only `voice.models` /
`voice.instructions`. The schema changes only typing (agent `color`
decoding, `Durable` as a tagged union). The plugin is unchanged.
- **Windows managed services** move their default port from **45278 to
28894** ([#57428](anomalyco/opencode#57428)).
CodeNomad discovers the daemon through the selected CLI's `service
status` and the service registration, and assumes no port.
- Live check: this machine's shared daemon was upgraded to 2.1.3 and
restarted on 28894, and the running CodeNomad desktop kept working
through it.
- **Core:** mostly refactors, plus behaviour fixes that the native
suites below exercise:
  - compaction summary output limit;
  - `keepID` validated as a message ID;
  - MCP media kept out of attachments;
  - only changed instruction entries resent;
  - relative-directory V1 migration;
  - legacy `apply_patch` permission mapped to `edit`.

## Isolated Windows qualification

Base: dev `333d73452`, rebased on `853b14f2f` (#1097) for the final UI
unit run; Node **24.20.0**; exact official CLI **2.1.3**; unchanged
locked client/plugin **2.1.0**.

- **Build and checks:** production server/UI/native plugin builds,
server and root typechecks, native bundle guard.
- **Native runtime (9/9):**
  - Code Mode job ordering, plugin runtime, automation;
  - complete pruning, history, ownership, worktree and event relay;
  - loaded-only pending snapshot;
- Missions contract, family control, native and Compare/Wayfinder
trajectory.
- **Sessions (15/15):** environment, fork, aside, interrupt, lifecycle,
blank cleanup, prompt skills, provider usage/accounts/selection, service
usage, web search, plugin packages, output limit, Promise-tool failure.
- **Recurring (5/5):** all **11 recurring Mission journeys**, subagent
depth, **2 MiB** compaction, permission receipts.
- **Migrations (9/9):**
- history from **2.0.3, beta-19271, 2.0.32, 2.1.0, 2.1.1 and 2.1.2** to
2.1.3;
  - Git degraded/init;
  - temporary workspaces.
- **Installation (3/3), with the 2.1.3 recommendation:**
  - fresh install of the 2.1.2 minimum;
- native **2.0.15 → 2.1.3** upgrade, after which the already-running
daemon keeps its PID and 2.0.15 version;
  - explicit OpenCode **1.18.10 → 2.1.3** replacement.
- **After the recommendation change:**
  - admission/transport/update/setup units **36/36**;
  - full server **2,463 passed, 8 skipped**, no failure or cancellation;
  - UI units **1,694/1,694** (with #1097);
  - focused browser **126/126**;
  - native pruning with the real rendered UI;
  - the exact CI invocation of the Code Mode regression (without tsx);
  - ten-locale help checks.

The UI unit run on `333d73452` itself had two failures, in
`instances-pending-compaction.test.ts` (`toast.dismiss is not a
function`). They come from #1096 and also fail on unmodified `dev`;
#1097 fixes that fixture.

This is Windows automation evidence. In CI, `latest` now resolves to
2.1.3: the compatibility job runs the native suites against the minimum
and latest on Windows, and the migration and Git-degraded certification
runs them on Ubuntu, Windows and macOS. WSL, packaged desktop,
installers/updater end-to-end and real mobile devices keep their
separate release gates.
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.

[Bug]: Duplicate notifications on automatic compaction

1 participant