First-run + resume session briefs, kept to the user channel#223
Conversation
…ser channel Adds two SessionStart briefs and ensures every user-facing notification stays out of the model's prompt context. Briefs - cold-start-brief: first-run insight mined from local history. CTA and cadence are sign-in-aware — anonymous users get a sign-in nudge that re-surfaces at most once/24h until they convert; signed-in users get a one-time onboarding brief, after which resume-brief takes over. - resume-brief: "where you left off" for signed-in users, from the latest project summary. Query bounded at 1.5s so it can't stall the hook; pointer skips boilerplate and returns the first prose sentence. Channel safety (a user flagged our credit notice as a prompt-injection) - The notifications banner is userVisibleOnly across the board: welcome / savings / briefs render to systemMessage, never additionalContext. Mined and summary-derived prose can no longer enter the model's context. - capture.ts no longer writes a "HIVEMIND ALERT ... tell the user ... top up at <url>" notice into additionalContext on capture failure. The credit-exhausted condition reaches the user via the SessionStart banner. - balance-exhausted / low-balance notifications are userVisibleOnly. - session-start no-auth note is neutral/factual (dropped the imperative "ask the user to run /hivemind:login"). - localMinedRule unregistered: the cold-start signup brief owns the single anonymous conversion slot, so no double login nudge. Tests: channel-split regression (resume prose hits systemMessage, never additionalContext), prose extraction, updated welcome/savings channel assertions.
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughThe PR refactors the session notification pipeline by removing mid-session credit signaling and local-mining-derived rules, introducing cold-start and resume brief generators that provide contextual session-startup messaging, and ensuring all user-facing content (briefs and banners) routes through the user-visible channel only. ChangesSession Notification Pipeline Refactoring and Context Briefs
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 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: 3
🧹 Nitpick comments (3)
src/notifications/sources/cold-start-brief.ts (2)
121-126: ⚡ Quick winAdd defensive check for state directory existence.
writeFileSyncat line 124 will throw if the parent directory (~/.claude) doesn't exist. While the code reads from~/.claude/projectsearlier (making it likely to exist), adding an explicitmkdirSyncguard would prevent edge-case failures.🛡️ Proposed defensive mkdir
+import { existsSync, readdirSync, statSync, writeFileSync, readFileSync, openSync, readSync, closeSync, mkdirSync } from "node:fs"; +import { join, dirname } from "node:path"; ... function writeState(sessionsScanned: number): void { try { + const stateFile = STATE_FILE(); + const dir = dirname(stateFile); + if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); writeFileSync( - STATE_FILE(), + stateFile, JSON.stringify({ lastBriefTs: new Date().toISOString(), fireReason: "first_run", sessionsScanned }), ); } catch (e: unknown) { log(`writeState failed: ${(e as Error).message}`); } }🤖 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 `@src/notifications/sources/cold-start-brief.ts` around lines 121 - 126, The writeState function currently calls writeFileSync(STATE_FILE(), ...) which will throw if the parent directory (~/.claude) does not exist; before writing, ensure the directory exists by resolving the directory via path.dirname(STATE_FILE()) and calling mkdirSync(dir, { recursive: true }) (or an equivalent async check) to create the parent directory, then proceed with writeFileSync; update the writeState function to perform this defensive mkdir check using path and fs utilities.
30-30: ⚖️ Poor tradeoffConsider asynchronous file operations to avoid blocking the event loop.
The module uses synchronous fs operations extensively (
readFileSync,readdirSync,statSync, etc.), which can block Node's event loop during I/O. While theHARD_TIMEOUT_MSbudget and chunked-read strategy mitigate unbounded work, switching to async operations withPromise.racefor timeout would maintain responsiveness.🤖 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 `@src/notifications/sources/cold-start-brief.ts` at line 30, The file currently imports and uses blocking fs functions (readFileSync, readdirSync, statSync, writeFileSync, openSync, readSync, closeSync); replace those with their async equivalents from fs/promises or the asynchronous stream APIs (readdir, stat, readFile, writeFile, open + filehandle.read/close or createReadStream) inside the ColdStartBrief code paths, and wrap each long-running I/O operation or the whole processing routine with a Promise.race against the existing HARD_TIMEOUT_MS to enforce the timeout; update functions that perform chunked reads to use async reads or streams (using FileHandle.read or a readable stream) and ensure proper try/finally close semantics and error propagation so behavior matches current logic but without blocking the event loop.tests/claude-code/resume-brief.test.ts (1)
20-48: ⚡ Quick winConsider adding test cases for additional sentence-ending punctuation.
The current tests thoroughly cover period-ending sentences and edge cases, but the regex pattern
/^.*?[.!?](\s|$)/also supports!and?. Consider adding test cases for:
- Sentence ending with
!→ ensures exclamation marks are recognized- Sentence ending with
?→ ensures question marks are recognized- Multiple sentences in prose → verifies only the first is returned
- Empty string input → confirms graceful empty-string return
📝 Suggested additional test cases
it("handles exclamation mark sentence endings", () => { const s = "## What Happened\nGreat progress today! More work tomorrow."; expect(firstProseSentence(s)).toBe("Great progress today!"); }); it("handles question mark sentence endings", () => { const s = "## What Happened\nWhat's the plan? We'll figure it out."; expect(firstProseSentence(s)).toBe("What's the plan?"); }); it("returns only the first sentence when multiple exist", () => { const s = "## What Happened\nFirst sentence here. Second sentence. Third."; expect(firstProseSentence(s)).toBe("First sentence here."); }); it("returns empty string for empty input", () => { expect(firstProseSentence("")).toBe(""); });🤖 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 `@tests/claude-code/resume-brief.test.ts` around lines 20 - 48, Tests only assert period-ending sentences but the regex in firstProseSentence supports ! and ? and edge cases; add unit tests covering exclamation and question endings, multiple sentences to ensure only the first is returned, and an empty-string input to ensure it returns "" so behavior matches the regex. Add tests referencing firstProseSentence such as: a prose line ending with "!" returning that sentence, a line ending with "?" returning that sentence, a multi-sentence prose returning only the first sentence, and calling firstProseSentence("") expecting "". Ensure these new cases follow the existing test structure and edge-case patterns used in the file.
🤖 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 `@src/notifications/sources/cold-start-brief.ts`:
- Around line 121-130: The writeState function always hardcodes fireReason as
"first_run", which mislabels subsequent anonymous re-nudges; change writeState
to accept a fireReason parameter (e.g., 'first_run' | 're_nudge' | string),
write that value to the JSON instead of the constant, and update every call site
(the cold-start notifier caller that currently invokes writeState(...) around
the 450s) to pass the appropriate reason (pass 'first_run' on initial run and
're_nudge' for 24-hour re-notifications); keep using STATE_FILE() and the same
JSON shape and preserve the existing try/catch logging in writeState.
- Line 254: The code that computes projDirName uses a hardcoded "/" separator
which breaks on Windows; update the extraction in cold-start-brief.ts to use
Node's path utilities (e.g., path.sep or better path.dirname/path.basename)
instead of splitting on "/", so replace the split("/") logic that assigns
projDirName with a path-aware approach (use path.basename(path.dirname(path)) or
split using path.sep) to reliably get the project directory name across
platforms.
In `@src/notifications/sources/resume-brief.ts`:
- Around line 128-144: The SQL identifier for the Deeplake table is used without
validation in pickResumeBrief (cfg?.tableName ?? "memory") and can lead to SQL
injection when interpolated into DeeplakeApi.query; validate the table name
using the repository's sqlIdent() (or a strict identifier regex) before building
the query, and if validation fails, log an error (include context: table value
and source) and return null instead of proceeding to call DeeplakeApi.query;
update pickResumeBrief to reference the validated table identifier and avoid
using sqlStr() for identifiers.
---
Nitpick comments:
In `@src/notifications/sources/cold-start-brief.ts`:
- Around line 121-126: The writeState function currently calls
writeFileSync(STATE_FILE(), ...) which will throw if the parent directory
(~/.claude) does not exist; before writing, ensure the directory exists by
resolving the directory via path.dirname(STATE_FILE()) and calling
mkdirSync(dir, { recursive: true }) (or an equivalent async check) to create the
parent directory, then proceed with writeFileSync; update the writeState
function to perform this defensive mkdir check using path and fs utilities.
- Line 30: The file currently imports and uses blocking fs functions
(readFileSync, readdirSync, statSync, writeFileSync, openSync, readSync,
closeSync); replace those with their async equivalents from fs/promises or the
asynchronous stream APIs (readdir, stat, readFile, writeFile, open +
filehandle.read/close or createReadStream) inside the ColdStartBrief code paths,
and wrap each long-running I/O operation or the whole processing routine with a
Promise.race against the existing HARD_TIMEOUT_MS to enforce the timeout; update
functions that perform chunked reads to use async reads or streams (using
FileHandle.read or a readable stream) and ensure proper try/finally close
semantics and error propagation so behavior matches current logic but without
blocking the event loop.
In `@tests/claude-code/resume-brief.test.ts`:
- Around line 20-48: Tests only assert period-ending sentences but the regex in
firstProseSentence supports ! and ? and edge cases; add unit tests covering
exclamation and question endings, multiple sentences to ensure only the first is
returned, and an empty-string input to ensure it returns "" so behavior matches
the regex. Add tests referencing firstProseSentence such as: a prose line ending
with "!" returning that sentence, a line ending with "?" returning that
sentence, a multi-sentence prose returning only the first sentence, and calling
firstProseSentence("") expecting "". Ensure these new cases follow the existing
test structure and edge-case patterns used in the file.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 60c8182d-1bd6-404e-8b57-3964ad909025
📒 Files selected for processing (10)
src/deeplake-api.tssrc/hooks/capture.tssrc/hooks/session-notifications.tssrc/hooks/session-start.tssrc/notifications/sources/cold-start-brief.tssrc/notifications/sources/primary-banner.tssrc/notifications/sources/resume-brief.tstests/claude-code/notifications-primary-banner.test.tstests/claude-code/notifications.test.tstests/claude-code/resume-brief.test.ts
| function writeState(sessionsScanned: number): void { | ||
| try { | ||
| writeFileSync( | ||
| STATE_FILE(), | ||
| JSON.stringify({ lastBriefTs: new Date().toISOString(), fireReason: "first_run", sessionsScanned }), | ||
| ); | ||
| } catch (e: unknown) { | ||
| log(`writeState failed: ${(e as Error).message}`); | ||
| } | ||
| } |
There was a problem hiding this comment.
fireReason is always "first_run" but anonymous users can fire multiple times.
The writeState function hardcodes fireReason: "first_run", but anonymous users can receive re-nudges every 24 hours (lines 440-441). This makes the persisted state misleading for debugging or telemetry.
📊 Proposed fix to distinguish first-run from re-nudge
-function writeState(sessionsScanned: number): void {
+function writeState(sessionsScanned: number, isFirstRun: boolean): void {
try {
writeFileSync(
STATE_FILE(),
- JSON.stringify({ lastBriefTs: new Date().toISOString(), fireReason: "first_run", sessionsScanned }),
+ JSON.stringify({
+ lastBriefTs: new Date().toISOString(),
+ fireReason: isFirstRun ? "first_run" : "renudge",
+ sessionsScanned
+ }),
);
} catch (e: unknown) {
log(`writeState failed: ${(e as Error).message}`);
}
}Then update the call site at line 454:
- writeState(sessions.length);
+ writeState(sessions.length, !hadState);🤖 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 `@src/notifications/sources/cold-start-brief.ts` around lines 121 - 130, The
writeState function always hardcodes fireReason as "first_run", which mislabels
subsequent anonymous re-nudges; change writeState to accept a fireReason
parameter (e.g., 'first_run' | 're_nudge' | string), write that value to the
JSON instead of the constant, and update every call site (the cold-start
notifier caller that currently invokes writeState(...) around the 450s) to pass
the appropriate reason (pass 'first_run' on initial run and 're_nudge' for
24-hour re-notifications); keep using STATE_FILE() and the same JSON shape and
preserve the existing try/catch logging in writeState.
| const projectCwd = first.cwd ?? last.cwd; | ||
| if (last.ts < cutoff) return null; | ||
|
|
||
| const projDirName = path.split("/").slice(-2, -1)[0] ?? "unknown"; |
There was a problem hiding this comment.
Path separator assumption breaks on Windows.
Line 254 hardcodes "/" as the path separator, which fails on Windows where paths use "\". The project directory name extraction will return incorrect results.
🔧 Proposed fix using path.sep
+import { join, sep } from "node:path";
...
- const projDirName = path.split("/").slice(-2, -1)[0] ?? "unknown";
+ const projDirName = path.split(sep).slice(-2, -1)[0] ?? "unknown";🤖 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 `@src/notifications/sources/cold-start-brief.ts` at line 254, The code that
computes projDirName uses a hardcoded "/" separator which breaks on Windows;
update the extraction in cold-start-brief.ts to use Node's path utilities (e.g.,
path.sep or better path.dirname/path.basename) instead of splitting on "/", so
replace the split("/") logic that assigns projDirName with a path-aware approach
(use path.basename(path.dirname(path)) or split using path.sep) to reliably get
the project directory name across platforms.
| const table = cfg?.tableName ?? "memory"; | ||
| const api = new DeeplakeApi( | ||
| creds.token, | ||
| creds.apiUrl ?? "https://api.deeplake.ai", | ||
| creds.orgId, | ||
| creds.workspaceId ?? "default", | ||
| table, | ||
| ); | ||
|
|
||
| // Most recent summary for THIS project by THIS user. summary != '' | ||
| // skips placeholder rows; ORDER BY last_update_date DESC takes the | ||
| // latest. LIMIT 1 — we only need the one resume pointer. | ||
| const rows = await withTimeout( | ||
| api.query( | ||
| `SELECT summary, project, last_update_date FROM "${table}" ` + | ||
| `WHERE project = '${sqlStr(project)}' AND author = '${sqlStr(creds.userName)}' ` + | ||
| `AND summary <> '' ORDER BY last_update_date DESC LIMIT 1`, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Check DeeplakeApi.query signature and usage patterns
# Search for DeeplakeApi class definition and query method signature
ast-grep --pattern $'class DeeplakeApi {
$$$
query($$$) {
$$$
}
$$$
}'
# Search for all query() call sites to see if any use parameterization
rg -n -C3 'api\.query\(' --type tsRepository: activeloopai/hivemind
Length of output: 44147
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Locate DeeplakeApi implementation
rg -n --hidden --no-ignore -S "class DeeplakeApi" src tests || true
rg -n --hidden --no-ignore -S "knownTablesOrNull\\(" src tests || true
rg -n --hidden --no-ignore -S "ensureSessionsTable\\(" src tests || true
# If class not found, locate the file exporting DeeplakeApi
rg -n --hidden --no-ignore -S "export (class|default function|const) DeeplakeApi" src tests || true
# 2) Print the relevant part of resume-brief.ts (table construction + query)
sed -n '1,220p' src/notifications/sources/resume-brief.ts
# 3) Find loadConfig() / HIVEMIND_TABLE / tableName validation (if any)
rg -n --hidden --no-ignore -S "HIVEMIND_TABLE|tableName|loadConfig\\(" src || true
fd -a "loadConfig" src || true
fd -a "*config*" src/notifications/sources || true
# 4) If sqlStr() is in this module or imported, locate it
rg -n --hidden --no-ignore -S "function sqlStr|const sqlStr|sqlStr\\(" src/notifications/sources/resume-brief.ts src || trueRepository: activeloopai/hivemind
Length of output: 36790
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect DeeplakeApi.query and any identifier escaping/validation helpers
sed -n '240,520p' src/deeplake-api.ts
echo "----"
sed -n '520,720p' src/deeplake-api.ts
# Inspect sql identifier helper usage (sqlIdent) if present
rg -n "function sqlIdent|sqlIdent\\(" src/deeplake-api.ts src/utils -S || true
echo "----"
sed -n '1,120p' src/utils/sql.ts
# Inspect config.ts to see how tableName/HIVEMIND_TABLE is sourced/validated
sed -n '1,120p' src/config.ts
sed -n '120,240p' src/config.ts
# Inspect resume-brief.ts around table/query
sed -n '110,180p' src/notifications/sources/resume-brief.ts
# Inspect tests for invalid SQL identifier behavior (for ensureSessionsTable)
sed -n '280,420p' tests/claude-code/sessions-table.test.tsRepository: activeloopai/hivemind
Length of output: 34596
SQL injection risk: unvalidated table identifier in resume-brief.ts
In src/notifications/sources/resume-brief.ts (lines ~127-144), pickResumeBrief uses cfg?.tableName ?? "memory" (sourced from process.env.HIVEMIND_TABLE in loadConfig) directly in FROM "${table}" without any identifier validation. sqlStr() only escapes string literals, and DeeplakeApi.query accepts a raw SQL string (no parameterized queries), so a malicious HIVEMIND_TABLE containing " can break out of the quoted identifier and modify the query.
Fix: validate table with the repo’s existing sqlIdent() (or equivalent strict regex) before interpolating; if invalid, log and return null.
🤖 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 `@src/notifications/sources/resume-brief.ts` around lines 128 - 144, The SQL
identifier for the Deeplake table is used without validation in pickResumeBrief
(cfg?.tableName ?? "memory") and can lead to SQL injection when interpolated
into DeeplakeApi.query; validate the table name using the repository's
sqlIdent() (or a strict identifier regex) before building the query, and if
validation fails, log an error (include context: table value and source) and
return null instead of proceeding to call DeeplakeApi.query; update
pickResumeBrief to reference the validated table identifier and avoid using
sqlStr() for identifiers.
- tests: update the bundle-artifact (built session-notifications.js) cases to the new user-only channel — welcome renders to systemMessage and never to additionalContext (the source-level cases were already updated; these run the compiled bundle, which CI rebuilds). - resume-brief: validate the table identifier with sqlIdent() before interpolating into FROM "<table>". sqlStr escapes literals, not identifiers, so a HIVEMIND_TABLE containing a double-quote could break out — bail to a plain welcome on an invalid name. - cold-start-brief: split jsonl/cwd paths on both / and \ so project-label derivation is correct on Windows. - cold-start-brief: writeState records fireReason "first_run" vs "renudge" instead of always "first_run", so the anonymous 24h re-nudge isn't mislabelled in state.
Coverage ReportScope: files changed in this PR. Enforced threshold: 90% per metric (per file via
File Coverage — 10 files changed
Generated for commit ddc52db. |
|
Reviewed end-to-end. The client-channel fix is solid:
One gap I'd flag before declaring the injection vector closed: Two ways to close it:
(Disregard my earlier mention of CodeRabbit findings — I checked HEAD and the path-separator + fireReason ones are already fixed in |
…atim
renderBrief now emits generic copy ('I found context from your recent
sessions...') instead of the mined signal description + verbatim snippet.
The signal is kept only as a fire/skip gate. Removes the surveillance-y
'your last session ended with "<quote>"' first-contact message.
…rver-push injection gap) Kamo's review caught that toClient() didn't set userVisibleOnly, so server-pushed rows from GET /me/notifications — including the deeplake-api low-balance 'top up to avoid service interruption' push — still reached the model's additionalContext. Same prompt-injection shape as the client-side notices we already fixed, different source. Mark all backend rows userVisibleOnly: the body is server-controlled free text shown as a banner; no server push should enter the agent prompt. Defense-in-depth regardless of any deeplake-api Targets change. Tests: backend-source cases now assert systemMessage + additionalContext undefined; added a billing-class regression (low-balance push reaches the user, never the model).
|
Re-reviewed against |
Reorder drainSessionStart so the welcome/savings banner leads, then low-balance / backend / rule notifications follow. Previously the queued low-balance notice rendered above 'Welcome back', which read backwards.
…nto banner Low-balance warnings were produced on the SDK query path and enqueued, so they only surfaced at the NEXT SessionStart — a session behind — and never co-occurred with the first-run brief (the queue was empty on a fresh session). They could also double with anything live. Now: org-stats reads X-Activeloop-Balance-Cents from its own SessionStart call (OrgStats.balanceCents), and pickPrimaryBanner merges a low-balance line straight into the (userVisibleOnly) welcome/savings banner — shown the moment it's detected, in the message the user is already reading. - Remove signalLowBalanceFromHeader + the query-path enqueue + flag (the lagging path). Hard exhaustion (402) still flows through the balance-exhausted queue notice. - Scope the live line to the soft warning (0 < bal < $2); exhausted stays on its 402 path to avoid a double. - Drop deeplake-api-low-balance.test.ts (tested the removed path); add coverage for the header parse and the live banner merge.
Completes bce3f95 (which only carried the test deletion): the actual org-stats balance-header read, the pickPrimaryBanner appendBalance merge, and removal of the lagging SDK signalLowBalanceFromHeader enqueue, plus the new header-parse and banner-merge tests.
Summary
Adds two SessionStart briefs and makes a hard rule that every user-facing notification stays out of the model's prompt context. The channel-safety half is prompted by a user flagging our credit notice as a prompt-injection attempt — it was.
Briefs
# Session/metadata/**Label**boilerplate and returns the first prose sentence (verified against real summaries).Channel safety (the important part)
A user's agent flagged repeated
HIVEMIND ALERTsystem-reminders as prompt-injection. They were right — it was our owncapture.tswriting"HIVEMIND ALERT … Tell the user clearly: … top up at <url>"into the model'sadditionalContexton every failed capture.userVisibleOnlyacross the board — welcome / savings / briefs render tosystemMessage, neveradditionalContext. Mined and summary-derived prose can no longer reach the model.capture.tsno longer emits the mid-session credit notice into the model channel (no safe user channel exists for PostToolUse, so we don't smuggle it via the model). The credit-exhausted condition reaches the user via the SessionStart balance banner.balance-exhausted/low-balancenotifications →userVisibleOnly.session-startno-auth note is neutral/factual (dropped the imperative "ask the user to run /hivemind:login").localMinedRuleunregistered: the cold-start signup brief owns the single anonymous conversion slot — no double login nudge.Tests
systemMessagebut neveradditionalContext.tests/claude-code+tests/sharedsuites green (2833 tests);tsc --noEmitclean.Known follow-ups (minor, same class, not in this PR)
updateNoticeandlocalMinedNoteare still appended tosession-start'sadditionalContext— milder (no "tell the user / pay"), but should move to the user-only path.additionalContextwrite contains billing /hivemind login/hivemind:update-style user-directed strings.🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes
New Features
Bug Fixes
Tests