Repository navigation
fix(heartbeat): deliver every heartbeat and count only the user's activity - #18
Conversation
…ivity
Delivery:
- The daemon's {"status":"disabled"} reply counted as delivered, so
heartbeats were dropped while the status bar said ShellTime. They are now
kept, the status bar shows "tracking off", and a notification explains
how to turn it on.
- The queue went out as one message, which servers failed to store past
about 2,800 rows. Messages now carry at most 500, the queue keeps the
newest 5,000, and a flush cut short (the project closing) keeps what it
didn't send. What is left when a project closes goes to the daemon's
resend file.
- The 5-second socket timeout couldn't interrupt blocking socket I/O, so a
daemon that accepted and never answered hung the flush.
Accuracy:
- Typing counts only while the IDE is active and an editor of this project
has focus: reloads from disk (git checkout, a formatter or agent in a
terminal) and the same file open in another project's window don't.
- Tabs restored when a project opens, caret moves while the IDE is in the
background, and files a refactoring saves without opening them no
longer count. Folders the project excludes (build output) are skipped.
- Languages are reported as VS Code language ids, so a language adds up
across editors.
- A file outside the project's base directory counts toward its Git
repository. The branch lookup only reads the repository cache, which the
platform requires on the EDT.
- The config is read from the files the CLI reads (config.yaml, .yml or
.toml plus config.local.*, with the CLI's TOML keys), and the settings
page keeps the file's endpoints.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019PvzR6GVwT4zShCyuHopyi
…d file Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019PvzR6GVwT4zShCyuHopyi
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
☁️ Claude Code cloud · Audit, fix, test, and merge editor activity pipelineAudited all three editor plugins end to end, fixed activity accuracy and reliability issues across the plugins, CLI daemon, and server, expanded regression coverage, and merged the completed changes.
637.9M tokens: 15.9K input · 2.4M output · 625.9M cache read · 9.4M cache write 📝 AI summaryThe session exercised the VS Code, Vim/Neovim, and JetBrains integrations against the Shelltime CLI/service and server, reviewing the full activity path rather than only isolated plugin behavior. Fixes included VS Code heartbeat collection/offline handling and Git metadata logic ( The CLI daemon’s heartbeat resynchronization and timing behavior were corrected and covered with additional tests in View this session on ShellTime → Updated 2026-10-24 15:03 UTC by ShellTime · totals for the whole session so far · the session page is visible to its owner |
… daemon still answers status A stopping daemon closes its listener, then drops a heartbeat message it can no longer queue without replying, so treating every blank reply as delivered lost up to 500 heartbeats. Daemons before 0.1.48 never replied to heartbeats, so a blank reply is still delivered when a status probe answers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019PvzR6GVwT4zShCyuHopyi
…flush on project close stop() only cancelled the flush job, so a flush cancelled mid-send could put its heartbeats back after dispose had drained the queue to the resend file and cleared it. dispose now calls stopAndJoin() first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019PvzR6GVwT4zShCyuHopyi
handleFileSwitch lacked the isActive guard the document and caret handlers have, so the tab a project restores in the background, and tabs a debugger selects there, produced heartbeats. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019PvzR6GVwT4zShCyuHopyi
…ect closed A Flush action or the flush that turning tracking off starts can still be sending when the project closes. Once the queue has been drained for the daemon's resend file, what such a flush doesn't deliver now goes to that file too, instead of into a queue nothing reads again. The sender takes the file as a parameter, so tests never write the daemon's real one. A reply-less close now counts as delivered when the daemon still takes connections, not when it answers status: daemon 0.1.42 took heartbeats without replying and has no status handler. The test task fails a test that hangs after 60 seconds with its stack trace and logs each test's result, instead of stalling CI until its timeout. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019PvzR6GVwT4zShCyuHopyi
MockK runs a coAnswers block in a runBlocking of its own, so a mocked send that suspends blocks its thread outside the flush's coroutine, and cancelling the flush never reaches it. On the IDE's test classpath the two tests that suspend in a send hung, and the threads they left behind timed out every later test that touched kotlin-reflect. The sender now takes a HeartbeatSource (the collector) and a HeartbeatTransport (the socket client), and the test uses plain fakes whose suspend functions run in the sender's coroutine. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019PvzR6GVwT4zShCyuHopyi
On CI two HeartbeatSenderTest cases still time out after the move to fakes, so MockK was not what hung them, and the timeout's trace only shows the test thread parked in runBlocking. ThreadDumpOnTimeout, registered for every test through META-INF/services, adds every thread's stack with its locks, the coroutine dump when the debug agent runs, the JVM arguments and where kotlinx-coroutines and kotlin-reflect come from. CLAUDE.md and the fake's comment no longer blame MockK. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019PvzR6GVwT4zShCyuHopyi
Runs each coroutine primitive the hanging tests use, alone and before any other test, so CI shows which one hangs there. To be removed once the cause is known. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019PvzR6GVwT4zShCyuHopyi
|
What blocks a fix: the same tests pass with plain kotlinx-coroutines 1.9 and kotlin-reflect, and the environment this was written in can't download the IDE, so CI is the only place the hang reproduces. JUnit's timeout trace only shows the test thread parked in Generated by Claude Code |
The plugin runs on the IDE's Kotlin standard library (kotlin.stdlib.default.dependency=false), which is 1.9 on 2024.1, the oldest build pluginSinceBuild allows, and 2.0 on 2024.3. Compiled at the Kotlin 2.3 API level, its coroutines carry debug metadata version 2, which those libraries can't read. In the coroutines' debug mode (on with -ea), reading it threw "Debug metadata version mismatch. Expected: 1, got 2", the dispatcher reported a fatal CoroutinesInternalError and the coroutine was never resumed: on CI every test that resumed a coroutine on another dispatcher, or after a cancellation, hung, and the project service's dispose waits for exactly such a cancelled flush. apiVersion 1.9 writes version 1 metadata and keeps the code to the standard library 2024.1 has. With kotlin-stdlib 2.0.21 and the kotlinx.coroutines debug agent on the test classpath, the harness went from 19 hanging tests to none. The temporary probe tests are removed; ThreadDumpOnTimeout now also names the standard library version. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019PvzR6GVwT4zShCyuHopyi
JUnit extension autodetection also loaded the IntelliJ test framework's own extensions. Its thread leak tracker failed the never-answering daemon test (the fake daemon thread sleeps out its 30 s), and under its interceptors VersionCheckerTest, unchanged and passing on main, timed out and left kotlin-reflect failing every later case. ThreadDumpOnTimeout is now added to the sender and socket tests with @ExtendWith. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019PvzR6GVwT4zShCyuHopyi
VersionCheckerTest mocked URL's constructors, which routes every URL in the JVM through the mock, the class loader's resource reads included. On CI kotlin-reflect first initialized inside that window (MockK asks it whether a mocked type is a value class), read its built-ins through a relaxed mock stream that never ends and hung; every later case then failed with "Recursion detected in a lazy value". It passed on main only while something else had initialized kotlin-reflect first. The test now runs the checker against a local HttpServer, and the encoding case checks the exact query the server receives. The IntelliJ test framework's thread leak tracker runs on these plain JUnit tests whether or not extension autodetection is on, so the previous commit's reason was wrong: it failed the never-answering daemon test because the fake daemon's thread slept out 30 s. The fake daemon now releases and joins its threads in tearDown. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019PvzR6GVwT4zShCyuHopyi
|
Generated by Claude Code |
Summary
An end-to-end check of the editor plugins (plugin → CLI daemon → server) found heartbeats that were silently dropped and activity that wasn't the user's. This PR fixes the JetBrains plugin; the companion PRs are listed below.
Delivery
{"status":"disabled"}counted as delivered. Heartbeats are now kept, the status bar shows ShellTime (tracking off) and a notification explains how to turn code tracking on.OfflineStore).stopAndJoin) before the final flush, and a flush still sending after the queue was drained (a Flush action, or tracking turned off) leaves what it didn't deliver in the resend file.withTimeoutOrNullaround blocking calls), so a daemon that accepted and never answered hung the flush. It now usesrunInterruptible.Accuracy
LanguageIds), so a language adds up across editors.getRepositoryForFileQuick, which doesn't update the repository cache on the EDT.config.yaml,.ymlor.toml, plusconfig.local.*, with the CLI's TOML keys), and the settings page keeps the file's endpoints.Build: Kotlin API version
The plugin runs on the IDE's Kotlin standard library (
kotlin.stdlib.default.dependency=false): 1.9 on 2024.1, the oldest buildpluginSinceBuildallows. Compiled at the Kotlin 2.3 API level (as onmain), its coroutines carry debug metadata version 2, which older libraries can't read (the test runtime has 2.2.0). In the coroutines' debug mode (on with-ea), reading it threwDebug metadata version mismatch. Expected: 1, got 2, the dispatcher reported a fatalCoroutinesInternalErrorand the coroutine was never resumed. On CI that hung every test that resumed a coroutine on another dispatcher or after a cancellation; the project service's dispose waits for exactly such a cancelled flush.build.gradle.ktsnow setsapiVersionto 1.9, which writes version 1 metadata and keeps the code to the standard library 2024.1 has.Testing
The IntelliJ Platform can't be downloaded in the environment this was written in, so this PR's CI is the only build and test run of the platform-bound code.
SocketClient,HeartbeatSender,OfflineStore,ConfigLoader,LanguageIds,VersionChecker, the data classes) were also compiled in a harness with stubbed platform types, whereSocketClientTest(ok/disabled/old reply, no reply from a daemon still listening and from one stopping, no daemon, a daemon that never answers),HeartbeatSenderTest(chunking, requeue order, disabled, cap, cancelled flush,stopAndJoin, a flush failing after the drain, drain),VersionCheckerTest,OfflineStoreTest,ConfigDiscoveryTest,LanguageIdsTestandHeartbeatDataTestpass (49 tests). BreakingstopAndJoinor the save after the drain fails its test.VersionCheckerTest(unchanged onmain) mockedURL's constructors, which routes every URL in the JVM through the mock, the class loader's resource reads included: on CI kotlin-reflect first initialized inside it (MockK asks whether a mocked type is a value class), read its built-ins from a relaxed mock stream and hung. It passed onmainonly while something else initialized kotlin-reflect first. It now runs against a localHttpServer, and the encoding case checks the exact query the server gets.SocketClientTest's fake daemon releases and joins its threads intearDown.ThreadDumpOnTimeout, added with@ExtendWith); that is what named the metadata mismatch.Related
The same end-to-end check, one PR per repository: shelltime/server#567, shelltime/cli#336 (daemon), shelltime/coding-extension-vscode#25, shelltime/coding-extension-jetbrains#18, shelltime/coding-extension-vim#20 and shelltime/web#727. The plugin keeps working with the current daemon and server.
🤖 Generated with Claude Code
https://claude.ai/code/session_019PvzR6GVwT4zShCyuHopyi