Skip to content

Keep the Bot queued-message retry pause when its timer fires early - #683

Merged
sambitcreate merged 1 commit into
mainfrom
claude/classify-one-at-a-time
Oct 31, 2026
Merged

sambitcreate merged 1 commit into
mainfrom
claude/classify-one-at-a-time

Conversation

@sambitcreate

@sambitcreate sambitcreate commented Oct 31, 2026 •

Copy link
Copy Markdown
Owner

What

The Bot live view classifies queued messages, so a routine's input stays hidden and the person's own message is listed. When classification fails, it pauses for classifyRetryMs before trying again. When the retry timer fired, it opened that pause outright (notBefore = 0).

Node schedules timers from the event loop's cached time. On a busy loop, a timer can fire before now() reaches the end of the pause, so the next frame classified again too soon. That is the live-projection.test.ts "retries are throttled, saw 3 calls" failure. It is a real bug, not only a test timing problem: under load, a failing classification is retried faster than its pause.

Change

  • The pause (notBefore) is set only when a classification fails or comes back incomplete. Frames that arrive during the pause no longer push it forward.
  • The timer is armed for the rest of the pause. If it fires early against the classifier's own clock, it waits out the remainder before calling retry().

Tests

  • New live-projection.test.ts case: "a retry timer that fires early never opens the classification pause before its time". It drives the classifier with mocked timers and a clock that lags them. It fails on the old code (a second classification inside the pause) and passes now.
  • The whole live-projection.test.ts passes locally (54/54). tsc --noEmit and eslint pass on the touched files.

Flake record (why this PR exists)

The "while classifying fails, a cached routine stays hidden…" test failed on macOS Desktop tests (runtime-subagents):

No doc changes: this is internal retry timing with no user-facing behavior change.


Generated by Claude Code

Flaky test record

  • 2026-10-31, head a120ed7: tests/e2e/workspace-panel.spec.ts:436 "browser creation and background selection preserve address focus while strip navigation moves it" failed in Electron E2E (2/3): the browser address field stayed "inactive" instead of focused for 10 s, and passed on retry, so --fail-on-flaky-tests failed the job. This PR changes only the live-projection classifier timer. Failed job; failed jobs rerun once.

Generated by Claude Code

… early

The retry timer opened the pause outright when it fired. A busy event loop
schedules timers from a stale loop time, so the timer can fire before the
classifier's own clock reaches the end of the pause, and the next frame then
classified again too soon. That is the live-projection 'retries are
throttled' flake seen on many PRs' macOS runtime-subagents lane.

The pause is now set only when a classification fails or comes back
incomplete, and an early timer waits out the remainder before asking for a
retry. A new test drives the classifier with a clock that lags the timers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018VA6RFoM21JGeMe45njSXW
@very-hermes-bot

Copy link
Copy Markdown
Collaborator

Hermes Review Bot

Confidence: 4

Engine: opencode/opencode-go/deepseek-v4.1-flash
Review mode: small
Head: a120ed7a4cfb005908060f7c602c159ee08b423a
Generated: 2026-10-31T15:30:40+00:00
Reviews: 1

Summary

The change reworks the queued-message classifier's retry timing in main/services/bot-runtime/live-projection.ts: notBefore is now set only when a classification fails or returns incomplete, and the timer re-arms itself for the remaining pause when it fires ahead of the classifier's own clock, instead of clearing notBefore and calling retry() immediately. A focused unit test drives the classifier with mocked setTimeout and a lagging clock to prove an early timer defers retry() and a frame inside the pause does not classify. I traced the new timer state machine, both call-site families (createBotLiveProjection's requeue and BotLiveProjector's lane-serialized publish), the two existing throttle/pause tests, and the failure/incomplete/close paths; the logic holds and the new test genuinely fails against the pre-change behavior.

Confidence Score: 4/5

Every branch of armTimer/scheduleRetry/personal was traced against both callers and existing tests, but the suites were not executed in this review, so timing behavior under the real event loop is reasoned rather than observed.

📁 Important Files Changed
  • main/services/bot-runtime/live-projection.ts — the classifier's pause/timer state machine; retry is now gated on notBefore at timer-fire time and the timer is re-armed for the remainder, with notBefore assigned only in the failure/incomplete branch.
  • main/services/bot-runtime/live-projection.test.ts — imports createQueuedInputClassifier and adds a mocked-timer test that fails on the old code by observing a second classification inside the pause.

Findings

No findings.

📊 Sequence Diagram
sequenceDiagram
  participant Caller
  participant Classifier
  participant Timer
  Caller->>Classifier: personal(docs), now >= notBefore
  Classifier->>Classifier: classify() fails / id unanswered
  Classifier->>Classifier: notBefore = now() + retryMs
  Classifier->>Timer: armTimer(notBefore - now())
  Note over Timer: fires before now() reaches notBefore
  Timer->>Classifier: remaining > 0, armTimer(remaining)
  Note over Timer: fires at or after notBefore
  Timer->>Classifier: remaining <= 0, deps.retry()
  Classifier->>Caller: requeue -> personal(docs)
Loading

Machine-Readable Findings

[]

Last reviewed commit: a120ed7a4cfb
Reviews (1) · Comment /hermes review to trigger a new review · /hermes review full for full re-review

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ No new issues found.

Reviewed changes in the shared queued-input classifier and its regression coverage, including desktop and Remote callers.

  • Stable retry deadline: Failed or incomplete classification starts the pause; incoming frames no longer extend it.
  • Early-timer handling: The timer checks the classifier's clock and waits out any remainder before requesting another projection.
  • Deterministic regression coverage: The new test verifies that an early timer neither permits classification nor requests a retry, then confirms recovery at the deadline without another frame.
  • Validation: All 144 desktop and Remote session tests passed, as did npx tsc --noEmit and ESLint on both changed files. Native consumers were inspected but native mobile suites were not run; hosted CI was still pending/in progress when checked.

Pullfrog  | View workflow run | Using gpt-6.1-sol | 𝕏

Copy link
Copy Markdown
Owner Author

Electron E2E (2/3) failed on a120ed7a at tests/e2e/workspace-panel.spec.ts:436 ("browser creation and background selection preserve address focus…"). The browser address field stayed "inactive" for 10 s, then passed on retry, so --fail-on-flaky-tests failed the job. This PR only changes the live-projection classifier retry timer, which this browser-focus spec doesn't exercise. No fix exists for that spec yet. I recorded it in the PR description and reran the failed jobs once.


Generated by Claude Code

@sambitcreate
sambitcreate merged commit 6b5fa75 into main Oct 31, 2026
48 of 50 checks passed
@sambitcreate
sambitcreate deleted the claude/classify-one-at-a-time branch October 31, 2026 19:37
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.

3 participants