Skip to content

feat(tsmd): idle-quit daemon when locked and silent for 30 minutes#13

Merged
tashian merged 1 commit into
mainfrom
feat/daemon-idle-quit
May 5, 2026
Merged

feat(tsmd): idle-quit daemon when locked and silent for 30 minutes#13
tashian merged 1 commit into
mainfrom
feat/daemon-idle-quit

Conversation

@tashian

@tashian tashian commented May 5, 2026

Copy link
Copy Markdown
Owner

Summary

  • Daemon shuts itself down when the vault is locked AND no RPC has arrived for idleQuitSeconds (default 1800). The next tsm command spawns a fresh daemon from whatever binary is on disk, so upgrades land automatically once the user has been idle long enough.
  • Reverts the shim-side staleness check from feat(npm): restart stale tsmd in the shim, not postinstallΒ #12. That approach had a correctness gap on bun install -g: bun overwrites the binary in place, so the running daemon's argv[0] matches the post-upgrade path and the path-mismatch heuristic skips it. Idle-quit covers in-place upgrades, cross-prefix upgrades, and any future install method without a wrapper-side opinion.
  • Tradeoff: between the upgrade and the idle window, clients keep talking to the old code. tsm daemon stop is the escape hatch when that matters.

Implementation

  • New IdleTracker actor in tsmd/ β€” tracks lastActivityAt, exposes bump() and isIdle(idleSeconds:now:).
  • JSONRPCHandler bumps the tracker on every request (errors and unknown methods included β€” they're still client activity).
  • Daemon owns the tracker, exposes shouldIdleQuit(), and the existing 15 s tick now evaluates the predicate and posts .tsmdShutdown when it fires.
  • Daemon.init grew injectable vault / idleTracker / idleQuitSeconds / tickInterval parameters with defaults that preserve production behavior, so tests can drive Daemon.run() to completion in milliseconds.

Test plan

  • swift test β€” 123 pass / 0 fail (added 13 tests: 6 IdleTrackerTests, 5 DaemonIdleQuitTests including a real Daemon.run() round-trip that verifies the full timer β†’ notification β†’ shutdown β†’ socket-unlinked chain, 2 handler bump tests)
  • go test ./... β€” all packages pass
  • Manual: install via bun, observe a real tsmd, lock vault, leave it for 30 min idle, confirm process exits and socket file is gone
  • Manual: confirm tsm daemon stop still works as the immediate escape hatch

Replaces the staleness-detection logic in the npm shim (#12) with a
self-contained lifecycle on the daemon. The daemon shuts itself down
when its vault is locked AND no client has sent an RPC for the idle
window (default 30 minutes). The next `tsm` command spawns a fresh
daemon from whatever binary is on disk, so an upgrade is picked up
automatically once the user has been idle long enough β€” no shim
heuristics, no postinstall scripts, no path comparisons.

The shim-based approach had a real correctness gap: bun's `bun install
-g` overwrites the binary in place, so the running tsmd's argv[0]
matched the post-upgrade path and the staleness check left it alone.
Idle-quit covers the in-place case, the cross-prefix case, and any
future install method without a wrapper-side opinion.

Tradeoff: between an upgrade and the idle window, clients keep talking
to the old code. `tsm daemon stop` is the escape hatch when that
matters.

Implementation:
- New IdleTracker actor (tracks lastActivityAt, isIdle predicate).
- JSONRPCHandler bumps the tracker on every request.
- Daemon owns the tracker, exposes shouldIdleQuit(), and the existing
  15 s tick now also evaluates the idle predicate and posts
  .tsmdShutdown when it fires.
- Daemon.init grew injectable vault/idleTracker/idleQuitSeconds/
  tickInterval parameters so tests can drive Daemon.run() to completion
  in milliseconds.

Tests: IdleTrackerTests (6), DaemonIdleQuitTests (5, including a real
Daemon.run() round-trip that confirms the timer β†’ notification β†’
shutdown chain), plus two JSONRPCHandler bump tests. Existing 110
tests still pass; total 123.

Reverts the shim-side staleness logic from #12 β€” shim.js is back to
resolving the platform package and exec'ing tsm, nothing else.
@tashian tashian merged commit 429a38a into main May 5, 2026
1 check passed
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.

1 participant