Skip to content

fix(statusline): expire the achievement banner instead of latching it - #170

Merged
ramarivera merged 2 commits into
mainfrom
fix/achievement-banner-ttl
Jul 28, 2026
Merged

fix(statusline): expire the achievement banner instead of latching it#170
ramarivera merged 2 commits into
mainfrom
fix/achievement-banner-ttl

Conversation

@ramarivera

@ramarivera ramarivera commented Jul 28, 2026

Copy link
Copy Markdown
Owner

The bug

The trophy banner rendered unconditionally from status.json with no TTL, while reactions expire via _sweep_expired_reactions. status.json is shared across all sessions (reactions are per-SID), so a single unlock pinned every session's bubble to that achievement until some path happened to call writeStatusState with no achievement.

Observed live: conflict_resolver ("Diplomat") unlocked at 1785186841582 and the banner was still pinned ~5h later.

The fix

  • server/state.tsStatusState gains achievementAt, stamped on write, 0 when there is no achievement.
  • statusline/buddy-status.sh — the banner expires on the same reactionTTL clock as a reaction. The block moved below the config load since it needs REACTION_TTL.

Compatibility: an explicit 0 never renders; a legacy status.json with no achievementAt at all is treated as fresh and self-heals on the next server write, so existing snapshot fixtures are unaffected.

Tests

4 new cases in statusline/buddy-status.test.ts: fresh shows, older-than-TTL drops, zeroed drops, legacy-missing-field shows.

bun test469 pass, 0 fail.

🤖 Created with the help of AI (Claude Opus 5).


Open in Devin Review

Note

Expire the achievement banner in the statusline after REACTION_TTL seconds

  • The achievement banner previously latched indefinitely once set; it now expires after REACTION_TTL seconds by comparing the current time against a new achievementAt epoch timestamp stored in status.json.
  • server/state.ts writes achievementAt as Date.now() when an achievement is present, or 0 when absent.
  • statusline/buddy-status.sh reads achievementAt and suppresses the banner if its age (in seconds) meets or exceeds REACTION_TTL; setting REACTION_TTL=0 disables expiry for validly stamped achievements.
  • Legacy status.json files without achievementAt treat the achievement as fresh to preserve backward compatibility.
  • Behavioral Change: a zero or malformed achievementAt now suppresses the banner entirely, regardless of REACTION_TTL.

Macroscope summarized ee7ad8c.

Copilot AI review requested due to automatic review settings July 28, 2026 02:54
@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@github-project-automation github-project-automation Bot moved this to Todo in Kanban Jul 28, 2026
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 19 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: b9983cc6-b590-4caf-ab3c-c112ccb6e605

📥 Commits

Reviewing files that changed from the base of the PR and between 318c88b and ee7ad8c.

📒 Files selected for processing (3)
  • server/state.ts
  • statusline/buddy-status.sh
  • statusline/buddy-status.test.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/achievement-banner-ttl

Comment @coderabbitai help to get the list of available commands.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Devin Review found 3 potential issues.

Open in Devin Review

Comment thread statusline/buddy-status.sh Outdated
Comment thread server/state.ts
Comment thread statusline/buddy-status.test.ts

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a statusline bug where the achievement (“trophy”) banner could latch indefinitely across all sessions because it was read from shared status.json without any expiration. The change introduces a timestamp (achievementAt) and makes the statusline only render the banner while it’s fresh (using the same TTL as reactions), with tests covering the expected expiry behavior and a package version bump.

Changes:

  • Add achievementAt to StatusState and stamp it on writes (0 when no achievement is present).
  • Update statusline/buddy-status.sh to expire the achievement banner using reactionTTL (and treat legacy status.json without the field as fresh).
  • Add new test cases for achievement banner expiry behavior.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
statusline/buddy-status.test.ts Adds tests validating banner expiry and legacy behavior.
statusline/buddy-status.sh Expires trophy banner using achievementAt + reactionTTL, avoiding indefinite latching.
server/state.ts Adds achievementAt to persisted status state and stamps it during writeStatusState.
package.json Bumps package version to reflect the fix release.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread statusline/buddy-status.sh
Comment thread statusline/buddy-status.test.ts
ramarivera and others added 2 commits July 28, 2026 05:09
The trophy banner rendered unconditionally from status.json with no TTL,
while reactions expire via _sweep_expired_reactions. status.json is shared
across sessions (reactions are per-SID), so a single unlock pinned every
session's bubble to that achievement until some path happened to call
writeStatusState with no achievement — observed as a "Diplomat" banner
stuck for ~5h after conflict_resolver unlocked.

writeStatusState now stamps achievementAt, and the statusline expires the
banner on the same reactionTTL clock as a reaction. An explicit 0 never
renders; a legacy status.json with no field is treated as fresh and
self-heals on the next server write.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H2GXsqpMvUhxBSNabSqZdk
Review catch (Copilot, Devin): the freshness gate was skipped wholesale when
reactionTTL=0, so an explicit achievementAt of 0 still rendered — contradicting
the field's documented "nothing pending" meaning and re-enabling cross-session
latching for TTL=0 users.

Validity and age are now separate gates. A zeroed or malformed achievementAt
never renders regardless of TTL; reactionTTL=0 disables expiry only, so a
validly stamped achievement still persists as it does for reactions.

Both cases are covered by tests. The zeroed-under-TTL=0 case was confirmed
failing against the previous commit's script before the fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H2GXsqpMvUhxBSNabSqZdk
@ramarivera
ramarivera force-pushed the fix/achievement-banner-ttl branch from 308c855 to ee7ad8c Compare July 28, 2026 03:13
@ramarivera

Copy link
Copy Markdown
Owner Author

Review round 1 addressed — ee7ad8c

One real bug found, fixed. Copilot and Devin independently flagged the same hole from different angles: the freshness gate was skipped wholesale when reactionTTL=0, so an explicit achievementAt: 0 still rendered — contradicting the field's documented "nothing pending" meaning and re-enabling cross-session latching for TTL=0 users. My own code comment claimed the opposite of what the code did.

Validity and age are now separate gates:

  • a zeroed or malformed achievementAt never renders, regardless of TTL;
  • reactionTTL=0 disables expiry only, so a validly stamped achievement still persists — matching how reactions treat TTL=0.

Two tests added. The zeroed-under-TTL=0 case was confirmed failing against the previous commit's script before the fix, so it is a genuine regression test rather than a restatement.

Acknowledged without change (both accurate):

  • Banner lifetime is typically "until the next write", with the TTL as backstop — that is the intent. Hooks write reaction.$SID.json directly and never touch status.json, so the no-write gap is the common case, and it is what produced the original ~5h stuck banner.
  • The expiry tests use the real wall clock. Deliberate — the age math mirrors the existing reaction TTL block, and wiring BUDDY_FAKE_NOW into only the achievement path would put the two TTLs on different clocks. Worth a follow-up that converts both.

Also fixed while here: the branch was accidentally cut from chore/bump-0.9.2 rather than main, so the PR carried the already-merged 0.9.2 release commit. Rebased onto origin/main; the diff is now the three intended files.

bun test471 pass, 0 fail, confirmed across four consecutive runs post-rebase. One earlier run in this session reported 2 failures that I could not reproduce and did not capture names for — flagging it rather than burying it.

🤖 Created with the help of AI (Claude Opus 5).

@ramarivera
ramarivera merged commit 290057d into main Jul 28, 2026
4 checks passed
@github-project-automation github-project-automation Bot moved this from Todo to Done in Kanban Jul 28, 2026
@ramarivera
ramarivera deleted the fix/achievement-banner-ttl branch July 28, 2026 03:17

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ee7ad8c7ba

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if [ "$REACTION_TTL" -gt 0 ] 2>/dev/null; then
ACH_AGE=$(( ($(date +%s) * 1000 - ACHIEVEMENT_AT) / 1000 ))
[ "$ACH_AGE" -ge "$REACTION_TTL" ] && ACH_FRESH=0
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Honor the fake clock when expiring achievements

When snapshot tests set BUDDY_FAKE_NOW, this age calculation still calls the real system clock, even though the script documents that variable as its wall-clock override. For example, an achievement stamped at the real current time remains visible when BUDDY_FAKE_NOW is advanced beyond the TTL, making expiry snapshots nondeterministic and preventing tests from exercising a fixed timestamp. Calculate against the existing NOW value instead.

Useful? React with 👍 / 👎.

@ramarivera ramarivera mentioned this pull request Jul 28, 2026
ramarivera added a commit that referenced this pull request Jul 28, 2026
Ships the achievement-banner expiry fix from #170.

The trophy banner rendered from the shared status.json with no TTL while
reactions expire on a sweep, so a single unlock pinned every session's
bubble until some path happened to call writeStatusState with no
achievement — observed as a "Diplomat" banner stuck for ~5h after
conflict_resolver unlocked.

writeStatusState now stamps achievementAt and the statusline expires the
banner on the same reactionTTL clock as a reaction. Validity and age are
separate gates: a zeroed or malformed stamp never renders regardless of
TTL, and reactionTTL=0 disables expiry only.


Claude-Session: https://claude.ai/code/session_01H2GXsqpMvUhxBSNabSqZdk

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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