Skip to content

fix(heartbeat): deliver every heartbeat and count only the user's activity - #20

Merged
AnnatarHe merged 10 commits into
mainfrom
claude/loving-cerf-xohla2
Oct 24, 2026
Merged

AnnatarHe merged 10 commits into
mainfrom
claude/loving-cerf-xohla2

Conversation

@AnnatarHe

@AnnatarHe AnnatarHe commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

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 :ShellTimeStatus 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 reply arriving after the timeout ran the callback twice, requeuing delivered heartbeats (measured: 600 → 1,200 → 2,400 pending). The callback now runs once.
  • Heartbeats left when Neovim quits with the daemon down go to the daemon's resend file. The exit flush waits for every send in progress and saves what each left, and :ShellTimeDisable keeps the queue the same way.
  • An empty reply counts as delivered only when the daemon still takes connections: daemons 0.1.42–0.1.47 took heartbeats without replying, and a current daemon skips the reply only while stopping, when its listener is already closed.
  • Without git on PATH every heartbeat threw; the branch is now read from HEAD, which also keeps git off the main loop (git runs only for reftable repositories, whose HEAD is a stub, cached for 30 s).

Accuracy

  • :bufdo, :argdo, :cfdo and session restore count only the buffer current when they finish; a reload from disk (:checktime after an outside change) isn't editing, while an undo back to the saved state still is; :wa attributes each write to its own buffer and cursor.
  • Saves count once per debounce window, since auto-save plugins write after every change, but a save inside the window is kept with its own time and sent with the next flush, so the last save before :wq always arrives.
  • The project is the git repository, so a monorepo's packages don't split into projects. Languages are VS Code ids (sh → shellscript, text → plaintext, ...), and an extensionless file under a dotted directory (john.doe/TODO) no longer reports a path as its language.
  • The config comes from the files the CLI reads (config.yaml, .yml or .toml, plus config.local.*); trailing comments and yes/no booleans 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.
  • A lazy-loaded nvim-dap is no longer loaded by the first heartbeat, and the update check leaves the clipboard alone.

Tests that reached a real daemon

tests/socket_spec.lua sent 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.lua now gives the tests a temporary HOME and points connections to the default socket at a path where nothing listens (with XDG_DATA_HOME pinned so the child processes still find plenary).

Testing

  • ./scripts/test.sh: 306 passed, 0 failed (Neovim 0.11.4).
  • End to end with a real headless Neovim driven over RPC against a local daemon and server: the stored rows have the right file, language, project root, branch and cursor; :argdo over three files stored only the last; a reload after an outside change stored nothing; the writes of :wa arrived 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

claude added 10 commits October 24, 2026 11:12
…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
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
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ⚠️ Failed 2026-10-24T13:28:17.205943Z
We couldn’t complete this review. Comment @codex review to try again.
463c1ab PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@shelltime

shelltime Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

☁️ Claude Code cloud · Audit, fix, test, and merge editor activity pipeline

Audited 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.

💰 Cost 🔢 Tokens ⏱️ Duration 🧠 Model 💬 Prompts ✏️ Lines
$230.80 637.9M 8h 1m (3h 36m active) claude-opus-5-5 3 +3101 / −298

637.9M tokens: 15.9K input · 2.4M output · 625.9M cache read · 9.4M cache write

📝 AI summary

The 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 (src/heartbeat/collector.ts, src/heartbeat/offline.ts, src/utils/git.ts), plus Vim socket lifecycle, initialization, TOML parsing, and Git utilities (lua/shelltime/socket.lua, init.lua, and utils/*). JetBrains received test-infrastructure improvements for diagnosing coroutine/test timeouts.

The CLI daemon’s heartbeat resynchronization and timing behavior were corrected and covered with additional tests in daemon/heartbeat_resync.go, heartbeat_resync_more_test.go, and cc_info_timer_test.go. Server-side activity handling and coding-activity timeseries/schema behavior were also updated and validated, notably in server/handler/activity.go and related tests. End-to-end and targeted regression checks were added or run across repositories, including Vim socket tests and temporary independent review/verifier suites. The resulting changes across the editor extensions, CLI, and server were then merged together.

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

@AnnatarHe
AnnatarHe merged commit 588e61c into main Oct 24, 2026
3 checks passed
@AnnatarHe
AnnatarHe deleted the claude/loving-cerf-xohla2 branch October 24, 2026 13:54
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