✨ feat(notifications): notify when the maturity gate clears (#587)#597
Conversation
Fire a dedicated maturity-cleared notification the moment an update previously withheld by maturityMode=mature becomes applicable, instead of silently waiting for the next scan's generic update-available. - ✨ feat(store): persist a maturityGatePendingSince marker while an update soaks; reset on candidate identity change, survive recreation via the update lifecycle cache - ✨ feat(maturity): gate-watch detection helper with clear-flag-first exactly-once semantics + a store-only sweep scheduler (DD_MATURITY_SWEEP_CRON, default */5 min, empty disables) - ✨ feat(triggers): maturity-cleared notification kind with its own templates; respects THRESHOLD/AUTO/include/exclude/once like update-available; symmetric dedup so an update is never announced twice; dedup hashes recorded only on confirmed delivery; action triggers unaffected - ✨ feat(events): emitMaturityGateCleared ordered-handler event + audit-trail subscription (dedupe-windowed) + watch-cycle and AgentClient pre-report hooks - ✨ feat(ui): bell support + en rule labels for the new kind - 📝 docs(triggers): document the notification, sweep env var, and timing semantics; CHANGELOG entry - ✅ test: full coverage incl. the previously-untested suppressed→matured transition regression
OOM DoS via unbounded expansion length (GHSA-mh99-v99m-4gvg); bumps the app/ui lockfiles and the e2e override pin. Surfaced by the qlty osv-scanner pre-push gate.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (7)
🚧 Files skipped from review as they are similar to previous changes (6)
📝 WalkthroughWalkthroughAdds maturity-gate lifecycle tracking, policy evaluation, and a configurable cron sweep. Cleared gates emit ordered events that are audited with deduplication and routed through a new Possibly related PRs
🚥 Pre-merge checks | ✅ 2✅ Passed checks (2 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
app/agent/AgentClient.ts (1)
767-774: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winGate detection skipped in the watcher-snapshot reconciliation branch.
handleWatcherSnapshotEvent's pending-report branch callsbuildContainerReportdirectly (bypassingprocessContainer), somaybeEmitMaturityGateClearednever runs for containers reconciled that way. Detection still happens eventually via the scheduled sweep, but it's inconsistent with the other watch/report paths that now await gate detection inline.const pendingContainerReport = this.takePendingWatcherCycleReport(watcherName, container); if (pendingContainerReport) { this.clearPendingFreshState(container.id); - containerReports.push( - await this.buildContainerReport(container, pendingContainerReport.changed), - ); + const report = await this.buildContainerReport(container, pendingContainerReport.changed); + await maybeEmitMaturityGateCleared(report.container); + containerReports.push(report); continue; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/agent/AgentClient.ts` around lines 767 - 774, Update the pending-report reconciliation branch in handleWatcherSnapshotEvent to invoke maybeEmitMaturityGateCleared for the built container report before emitting it, matching the inline gate-detection order in processContainer. Preserve the existing report emission and reconciliation behavior.
🧹 Nitpick comments (3)
app/triggers/providers/Trigger.test.ts (1)
4373-4377: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueDRY the optimistic dispatch microtask flush.
The triple
await Promise.resolve()at Lines 4375-4379, 4447-4449, and 4573-4575 hardcodesdispatchToTriggerOptimistically’s current.then()depth; any addedawait/new chain links will break these tests. Extract a named helper or shared drain function for these assertion sites.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/triggers/providers/Trigger.test.ts` around lines 4373 - 4377, Extract the repeated triple Promise.resolve microtask flushes in the relevant Trigger tests into a named shared drain helper, then use that helper at all optimistic dispatch assertion sites around dispatchToTriggerOptimistically. Keep the helper responsible for draining the required promise chain so tests do not depend on hardcoded .then() depth.app/maturity/scheduler.ts (1)
62-85: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win
init()isn't idempotent — re-invocation leaks the previous cron task.If
init()is called again while acronTaskis already scheduled, the old task is overwritten without calling.stop(), leaving it running forever alongside any new task (duplicate/leaked sweeps).♻️ Proposed fix
export function init(): void { + if (cronTask) { + cronTask.stop(); + cronTask = undefined; + running = false; + } const maturitySweepConfig = getMaturitySweepConfiguration(); const cronExpression = maturitySweepConfig.cron;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/maturity/scheduler.ts` around lines 62 - 85, Make init idempotent by stopping any existing cronTask before scheduling a new one, using the task’s stop method and clearing the prior reference as needed. Update the cronTask handling in init so repeated calls cannot leave the previous maturity sweep running alongside the replacement task.app/maturity/gate-watch.ts (1)
8-13: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winReuse
hasMaturityGatePendingSincefromgate-watch.ts.
scheduler.tsduplicates the predicate instead of importing it. Export it fromgate-watch.ts, keeping aContainerparameter, then import and use it fromscheduler.ts.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/maturity/gate-watch.ts` around lines 8 - 13, Export hasMaturityGatePendingSince from gate-watch.ts without changing its Container parameter or predicate behavior, then remove scheduler.ts’s duplicate maturity-gate check and import and use this shared helper instead.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/event/audit-subscriptions.ts`:
- Around line 361-369: Update the dedupe key in the registerMaturityGateCleared
handler to include payload.container.pendingSince along with the container
identity, so distinct maturity-gate intervals are not suppressed within the
dedupe window. Preserve duplicate suppression for repeated emissions sharing the
same pendingSince value, and add a regression test covering two different
pendingSince values within five minutes.
In `@app/maturity/gate-watch.ts`:
- Around line 25-54: Update maybeEmitMaturityGateCleared to contain failures
from emitMaturityGateCleared: preserve clearing state, but catch handler/emit
errors and return false instead of propagating them to direct callers. Keep the
successful emission path returning true and retain the existing no-op checks.
In `@app/triggers/providers/Trigger.test.ts`:
- Around line 4559-4562: Await the asynchronous initialization in the regression
test by changing the trigger.init() call to await trigger.init(). Keep the
existing setup and later awaited init() calls unchanged, ensuring initialization
completes before the test proceeds and any rejection is observed.
In `@content/docs/current/api/index.mdx`:
- Around line 220-232: Update the documentation text at the default notification
rules description to state that eight rules are created automatically, matching
the eight entries in DEFAULT_NOTIFICATION_RULES and the payload’s total count.
In `@ui/src/locales/en/listViews.json`:
- Around line 110-113: Add the "maturity-cleared": "Maturity Cleared" entry to
the auditView.actions catalog in the English locale file, reusing the existing
maturity-cleared label and preserving the current locale structure.
---
Outside diff comments:
In `@app/agent/AgentClient.ts`:
- Around line 767-774: Update the pending-report reconciliation branch in
handleWatcherSnapshotEvent to invoke maybeEmitMaturityGateCleared for the built
container report before emitting it, matching the inline gate-detection order in
processContainer. Preserve the existing report emission and reconciliation
behavior.
---
Nitpick comments:
In `@app/maturity/gate-watch.ts`:
- Around line 8-13: Export hasMaturityGatePendingSince from gate-watch.ts
without changing its Container parameter or predicate behavior, then remove
scheduler.ts’s duplicate maturity-gate check and import and use this shared
helper instead.
In `@app/maturity/scheduler.ts`:
- Around line 62-85: Make init idempotent by stopping any existing cronTask
before scheduling a new one, using the task’s stop method and clearing the prior
reference as needed. Update the cronTask handling in init so repeated calls
cannot leave the previous maturity sweep running alongside the replacement task.
In `@app/triggers/providers/Trigger.test.ts`:
- Around line 4373-4377: Extract the repeated triple Promise.resolve microtask
flushes in the relevant Trigger tests into a named shared drain helper, then use
that helper at all optimistic dispatch assertion sites around
dispatchToTriggerOptimistically. Keep the helper responsible for draining the
required promise chain so tests do not depend on hardcoded .then() depth.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: e19bd10e-7fca-4d16-b151-8c87eef0b0af
⛔ Files ignored due to path filters (4)
CHANGELOG.mdis excluded by!CHANGELOG.mdapp/package-lock.jsonis excluded by!**/package-lock.json,!**/package-lock.jsone2e/package-lock.jsonis excluded by!**/package-lock.json,!**/package-lock.jsonui/package-lock.jsonis excluded by!**/package-lock.json,!**/package-lock.json
📒 Files selected for processing (39)
app/agent/AgentClient.test.tsapp/agent/AgentClient.tsapp/configuration/index.test.tsapp/configuration/index.tsapp/event/audit-subscriptions.test.tsapp/event/audit-subscriptions.tsapp/event/index.test.tsapp/event/index.tsapp/index.test.tsapp/index.tsapp/maturity/gate-watch.test.tsapp/maturity/gate-watch.tsapp/maturity/scheduler.test.tsapp/maturity/scheduler.tsapp/model/audit.d.tsapp/model/container.test.tsapp/model/container.tsapp/model/maturity-policy.test.tsapp/model/maturity-policy.tsapp/registry/index.test.tsapp/registry/index.tsapp/store/container.test.tsapp/store/container.tsapp/store/notification-history.tsapp/store/notification.tsapp/triggers/providers/Trigger.test.tsapp/triggers/providers/Trigger.tsapp/watchers/providers/docker/container-processing.test.tsapp/watchers/providers/docker/container-processing.tscontent/docs/current/api/index.mdxcontent/docs/current/configuration/actions/update-eligibility.mdxcontent/docs/current/configuration/triggers/index.mdxcontent/docs/current/configuration/watchers/index.mdxe2e/package.jsonui/src/locales/en/listViews.jsonui/src/stores/notifications.tsui/src/types/container.d.tsui/tests/components/NotificationBell.spec.tsui/tests/stores/notificationStore.spec.ts
- 🐛 fix(maturity): contain emit failures inside maybeEmitMaturityGateCleared so a throwing handler can't abort the caller's container-report cycle - 🐛 fix(audit): include pendingSince in the maturity-cleared audit dedupe key so a legitimate second clearance within the window still records - ✅ test(triggers): await trigger.init() in the maturity-cleared batch test - 📝 docs(api): default notification rule count seven → eight - 🔧 config(ui): add the maturity-cleared audit action label to the en catalog
Implements discussion #587: fire a notification the moment an update previously withheld by the maturity gate (
maturityMode: mature) becomes applicable.What was happening
While an update soaks,
updateAvailableis suppressed and every notification stays silent. When the window passes, the genericupdate-availablenotification fires — but only at the next watcher cron tick (default 6-hourly), indistinguishable from a fresh detection, and the whole transition had zero test coverage.What this adds
maturityGatePendingSincemarker persisted while an update soaks: reset on candidate-identity change, survives container recreation via the update lifecycle cache, cleared exactly once when the gate opens.app/maturity/gate-watch.ts) with clear-flag-first exactly-once semantics; emits for any reason the gate opens (clock elapsed, policy change, override, snooze expiry).app/maturity/scheduler.ts): store-only walk (no registry traffic) every 5 minutes by default,DD_MATURITY_SWEEP_CRONto tune, empty string disables. Same check also runs inline at every watch cycle and agent report, before report emission.maturity-clearednotification kind with its own default templates. Dispatch mirrorsupdate-availableexactly: respectsTHRESHOLD,AUTO, include/exclude, agent scoping, andonce. Symmetric dedup againstupdate-available(both directions, recorded only on confirmed delivery) so one update is never announced twice. Action triggers are unaffected and keep applying updates on their normal scan cadence.Tests
Also carries
🔒 security(deps): brace-expansion 5.0.7→5.0.8 (CVE-2026-14257) in app/ui/e2e lockfiles — surfaced by the osv-scanner pre-push gate on this branch.Follow-ups deliberately not in scope
AuditViewfilter dropdown doesn't listcontainer-unhealthy/agent-reconnecttoday either — pre-existing gap, left consistent.Changelog
maturityGatePendingSincecontainer marker with lifecycle-aware stamping, carry-forward, and clearingmaybeEmitMaturityGateClearedexactly-once detection that clears the pending marker and emitsmaturity-clearedwhen the gate opensDD_MATURITY_SWEEP_CRON(getMaturitySweepConfiguration)maturity-clearedordered event plumbing (emitMaturityGateCleared/registerMaturityGateCleared)maturity-cleared, including updated delivery-success bookkeeping and once/dedup behavior againstupdate-availablematurity-clearedwith 5-minute deduplication keyed by container identity (fallback to container name) and incorporatingpendingSincematurity-clearedbrace-expansionfrom 5.0.7 to 5.0.8 across app, UI, and E2E lockfiles (CVE-2026-14257)Concerns
pendingSincehandling matches intended business semanticsmaturity-clearednotification delivery interactions with existingupdate-availablehistory, retries, and batch flush semanticsmaturity-cleared) and audit query behaviorbrace-expansionare updated consistently and installed dependencies resolve cleanly