Repository navigation
fix(heartbeat): deliver every heartbeat and count only the user's activity - #20
Conversation
…ivity
Delivery:
- The daemon's {"status":"disabled"} reply counted as delivered. Heartbeats
are now kept and :ShellTimeStatus and a warning explain how to turn code
tracking on.
- The queue went out as one message, which servers failed to store past
about 2,800 rows; messages now carry at most 500. A late reply after a
timeout no longer runs the callback twice, which requeued delivered
heartbeats and doubled the queue.
- Heartbeats left when Neovim quits with the daemon down go to the daemon's
resend file.
- Without git on PATH every heartbeat failed; the branch is now read from
HEAD, which also keeps git off the main loop.
Accuracy:
- :bufdo, :argdo, :cfdo and session restore count only the buffer that is
current when they finish, a text change that leaves the buffer
unmodified (a reload from disk) isn't editing, and :wa attributes each
write to its own buffer and cursor. Saves count once per debounce window,
as auto-save plugins write after every change.
- The project is the git repository, so a monorepo's packages don't split
into projects, and languages are reported as VS Code ids.
- An extensionless file under a dotted directory (john.doe/TODO) no longer
reports the path as its language.
- The config is read from the files the CLI reads (config.yaml, .yml or
.toml plus config.local.*), trailing comments and yes/no booleans parse
as the CLI reads them, and turning tracking on in the file applies
without restarting Neovim.
- requiring nvim-dap no longer loads a lazy-loaded plugin, and the update
check leaves the clipboard alone.
Tests never reach a daemon or config on the machine: HOME is a temporary
directory and the default socket goes nowhere.
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
Daemons since 0.1.48 answer every heartbeat message with {"status":"ok"}
or {"status":"disabled"}, and close without a reply when they could not
take the batch (shutting down, or dying before they answer). The plugin
counted that empty reply as delivered, so the batch was neither requeued
nor left in the resend file. An empty reply now probes the daemon with a
status request: if it answers, the daemon is one of 0.1.42-0.1.47, which
handled heartbeats but never replied, and the batch counts as delivered;
otherwise it is 'failed' and kept. Resends are harmless, since the server
dedupes on heartbeat ids.
The send_heartbeats tests that still expected a boolean, and only failed
inside a scheduled callback, now check the status string.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019PvzR6GVwT4zShCyuHopyi
… left The sender kept one in-flight slot that any concurrent send overwrote and whichever finished first cleared. When Neovim exited while a periodic flush of a backlog was between chunks, exit_flush found the queue empty, returned at once, and the chunks not yet sent were neither delivered nor saved. Each send is now tracked by id with its heartbeats and how many the daemon acknowledged. flush_sync waits, within its timeout, until every send in progress has finished, and exit_flush then saves to the resend file what each unfinished send has not delivered, then whatever is still queued. A send exit_flush gave up on no longer requeues its heartbeats when its answer comes late, so they are not both saved and queued. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019PvzR6GVwT4zShCyuHopyi
:ShellTimeDisable started an asynchronous flush and then removed the sender's VimLeavePre handler. With the daemon down the flush failed and requeued, and nothing saved the queue when Neovim quit. Disabling now stops collecting heartbeats, then runs the exit flush: a send that waits up to 1.5 s, with what the daemon doesn't take left in its resend file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019PvzR6GVwT4zShCyuHopyi
Every save was debounced like any other event: a save inside its file's 30 s window only flagged the file's next heartbeat as a write. Continuous editing keeps a window open, so a :w/:wq right after typing was never sent when Neovim exited, or the write landed on a heartbeat from hours later. A save inside the window now builds its write heartbeat right away, at the save's own time and cursor, and holds it as the file's pending write, replacing an earlier one. It goes out ahead of the file's next heartbeat, or with the next flush: heartbeat.flush(), which the periodic flush and the exit flush use, returns the pending writes too, and the pending count includes them. An auto-save plugin still adds at most one write per window, plus one per flush. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019PvzR6GVwT4zShCyuHopyi
Text changes that left the buffer unmodified were skipped as reloads from disk, but undoing back to the saved state (x, then u) clears 'modified' as well, so that edit was dropped. The plugin now stores each buffer's changedtick on BufReadPost and FileChangedShellPost and skips a TextChanged/TextChangedI only at that tick, which is the reload's own change. Checked in headless Neovim with a temporary HOME: a :checktime reload after an outside write fires TextChanged and adds no heartbeat, and 2G x u x u adds one heartbeat per step. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019PvzR6GVwT4zShCyuHopyi
… before it The TOML parser recognised only [table] headers. A config.toml with Endpoints, which go-toml writes as [[ENDPOINTS]] after the root keys, left the parser in the root table, so the endpoint's APIEndpoint and Token replaced the root's and the CLI update check asked the debug endpoint. A [[a.b]] header now appends a new table to the array a.b and the keys after it go there. Parsing a config.toml written by the CLI now gives the root APIEndpoint and Token. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019PvzR6GVwT4zShCyuHopyi
A repository using git's reftable backend keeps its real HEAD in reftable/ and writes a `ref: refs/heads/.invalid` stub to the HEAD file, so every heartbeat from it reported the branch '.invalid'. When HEAD names .invalid or the git directory has reftable/, the branch now comes from `git rev-parse --abbrev-ref HEAD` run in the file's directory through pcall(vim.fn.systemlist), cached per git directory for 30 s, and '' when git is missing or fails. Other repositories still read HEAD without running git. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019PvzR6GVwT4zShCyuHopyi
An empty reply counted as delivered only when the daemon then answered a status request, but daemon 0.1.42 took heartbeats without replying and has no status handler, so its batches were resent forever. The probe now only connects: a daemon that is stopping closes its listener before it drops a message, so the connection fails and the batch is kept. A send exit_flush stopped waiting for no longer goes on with its next chunks, which exit_flush already saved to the offline file. The reftable branch fallback reads git's stdout only, so a warning on stderr can't be taken for the branch. 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 |
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 Neovim plugin; the companion PRs are listed below.
Delivery
{"status":"disabled"}counted as delivered. Heartbeats are now kept; a warning and:ShellTimeStatusexplain how to turn code tracking on.:ShellTimeDisablekeeps the queue the same way.giton PATH every heartbeat threw; the branch is now read fromHEAD, which also keeps git off the main loop (git runs only for reftable repositories, whoseHEADis a stub, cached for 30 s).Accuracy
:bufdo,:argdo,:cfdoand session restore count only the buffer current when they finish; a reload from disk (:checktimeafter an outside change) isn't editing, while an undo back to the saved state still is;:waattributes each write to its own buffer and cursor.:wqalways arrives.sh→shellscript,text→plaintext, ...), and an extensionless file under a dotted directory (john.doe/TODO) no longer reports a path as its language.config.yaml,.ymlor.toml, plusconfig.local.*); trailing comments andyes/nobooleans parse the way the CLI reads them,[[array]]TOML tables no longer overwrite the keys before them, and turning tracking on in the file applies without restarting Neovim.Tests that reached a real daemon
tests/socket_spec.luasent its test heartbeats to/tmp/shelltime.sock, so on a machine with a running daemon they were uploaded to the developer's account.tests/minimal_init.luanow gives the tests a temporary HOME and points connections to the default socket at a path where nothing listens (withXDG_DATA_HOMEpinned so the child processes still find plenary).Testing
./scripts/test.sh: 306 passed, 0 failed (Neovim 0.11.4).:argdoover three files stored only the last; a reload after an outside change stored nothing; the writes of:waarrived with their own cursors; an edit and a save made while the daemon was down reached the server through the resend file after the daemon restarted.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. They can be merged in any order: every plugin keeps working with the current daemon and server, and the daemon with the current server.
🤖 Generated with Claude Code
https://claude.ai/code/session_019PvzR6GVwT4zShCyuHopyi