Repository navigation
fix(notifications): show one notification per session compaction - #1096
Conversation
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
left a comment
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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.
## 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.
## 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.
Fixes #986: one automatic compaction stacked several identical "Session … was compacted" notifications.
Cause
The notification was not idempotent: every delivered
session.compaction.endedcreated a bubble, plus a history entry when the category is routed there. The same event reaches a window more than once:Fix
stores/compaction-notice.tsclaims 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.Validation
compaction-notification.test.ts, through the real event dispatcher and toaster:notification-icon-badgeandcompaction-responsivenesspass. The UI typecheck with its own tsconfig reports 0 diagnostics, and the rootnpm run typecheckpasses.The user-visible change only removes duplicates, so no help page needs an update.