Created
September 9, 2026 08:02
-
-
Save Gipphe/5873166b108e714dabdf94a528655e9e to your computer and use it in GitHub Desktop.
Tricorder MCP demonstration Claude Code chat
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| ▐▛███▛█ Claude Code v2.1.260 | |
| ▝▜██████▀ Sonnet 5 · Claude Pro | |
| ▝▝ ▝▝ ~/projects/tweag/tricorder | |
| ❯ Currently, the Tricorder daemon will remain running in the background forever once started, until the user | |
| themselves calls tricorder stop or otherwise kills the process. It would instead be preferable if the daemon | |
| would shut down by itself after a certain amount of idle time. This duration should be configurable through | |
| the .tricorder.yaml config file. Call the option "idle_timeout_seconds". Every request to the daemon should | |
| refresh the timeout and make it count down anew. The default timeout duration, if none is specified, should be | |
| 300 seconds. Use the Tricorder MCP server to run Tricorder while working on this feature. Don't use cabal | |
| directly. You can start off by starting the Tricorder daemon and verifying that it _currently_ builds OK. The | |
| implementation should take into account the long-lasting connections from tricorder ui clients. Make the | |
| idle_timeout_seconds option part of Session, and have it reevaluated when Session is reloaded. | |
| ● I'll start by loading the Tricorder MCP tools and checking the current build status, then explore the | |
| daemon/session architecture. | |
| Searched for 5 patterns, read 7 files, ran 2 shell commands (ctrl+o to expand) | |
| ● Now I have enough context. Let me check the Delay/TimeUnit Second type usage and existing tests structure for | |
| Session, to follow conventions when adding the new field and tests. | |
| Searched for 1 pattern, ran 1 shell command (ctrl+o to expand) | |
| ● Write(tricorder/src/Tricorder/Session/IdleTimeout.hs) | |
| ⎿ Wrote 13 lines to tricorder/src/Tricorder/Session/IdleTimeout.hs | |
| 1 module Tricorder.Session.IdleTimeout (IdleTimeout (..)) where | |
| 2 | |
| 3 import Data.Aeson (FromJSON, ToJSON) | |
| 4 import Data.Default (Default (..)) | |
| 5 | |
| 6 | |
| 7 newtype IdleTimeout = IdleTimeout {getIdleTimeout :: Int} | |
| 8 deriving stock (Eq, Generic, Show) | |
| 9 deriving (FromJSON, ToJSON) via Int | |
| … +3 lines (ctrl+o to expand) | |
| ● Now update Config.hs: | |
| ● Update(tricorder/src/Tricorder/Session/Config.hs) | |
| ⎿ Added 1 line | |
| 19 , generateWithHpack :: Bool | |
| 20 , testMemoryLimit :: Maybe Text | |
| 21 , hooks :: Maybe Hooks | |
| 22 + , idleTimeoutSeconds :: Int | |
| 23 } | |
| 24 deriving stock (Eq, Generic, Show) | |
| 25 deriving (FromJSON) via WithDefaults (QuietSnake Config) | |
| ● Update(tricorder/src/Tricorder/Session/Config.hs) | |
| ⎿ Added 1 line | |
| 38 , generateWithHpack = True | |
| 39 , testMemoryLimit = Nothing | |
| 40 , hooks = Nothing | |
| 41 + , idleTimeoutSeconds = 300 | |
| 42 } | |
| ● Now update Session.hs: | |
| ● Update(tricorder/src/Tricorder/Session.hs) | |
| ⎿ Added 1 line | |
| 22 import Tricorder.Session.Config (Config (..)) | |
| 23 import Tricorder.Session.GenerateWithHpack (GenerateWithHpack (..)) | |
| 24 import Tricorder.Session.Hooks (Hooks) | |
| 25 +import Tricorder.Session.IdleTimeout (IdleTimeout (..)) | |
| 26 import Tricorder.Session.ReplBuildDir (ReplBuildDir (..)) | |
| 27 import Tricorder.Session.Target (Target, definesCustomPrelude, resolveTargets) | |
| 28 import Tricorder.Session.TestTarget (TestTarget, resolveTestTargets) | |
| ● Update(tricorder/src/Tricorder/Session.hs) | |
| ⎿ Added 1 line | |
| 47 , testTimeout :: TestTimeout | |
| 48 , generateWithHpack :: GenerateWithHpack | |
| 49 , hooks :: Hooks | |
| 50 + , idleTimeout :: IdleTimeout | |
| 51 } | |
| 52 deriving stock (Eq) | |
| 53 | |
| ● Update(tricorder/src/Tricorder/Session.hs) | |
| ⎿ Added 1 line | |
| 65 , testTimeout = def | |
| 66 , generateWithHpack = def | |
| 67 , hooks = def | |
| 68 + , idleTimeout = def | |
| 69 } | |
| 70 | |
| 71 | |
| ● Update(tricorder/src/Tricorder/Session.hs) | |
| ⎿ Added 1 line | |
| 132 , testTimeout = TestTimeout cfgFile.testTimeout | |
| 133 , generateWithHpack = GenerateWithHpack cfgFile.generateWithHpack | |
| 134 , hooks | |
| 135 + , idleTimeout = IdleTimeout cfgFile.idleTimeoutSeconds | |
| 136 } | |
| 137 | |
| 138 | |
| Read 1 file (ctrl+o to expand) | |
| ● Write(tricorder/src/Tricorder/Daemon/IdleTimer.hs) | |
| ⎿ Wrote 97 lines to tricorder/src/Tricorder/Daemon/IdleTimer.hs | |
| 1 -- | Shuts the daemon down after a period of inactivity. | |
| 2 -- | |
| 3 -- Every client request 'touch'es the timer, restarting the countdown. A | |
| 4 -- long-lived connection (e.g. a @tricorder ui@ client streaming 'Watch' | |
| 5 -- updates) brackets its lifetime with 'connectionOpened' \/ 'connectionClosed' | |
| 6 -- instead: as long as one such connection is open, the countdown is | |
| 7 -- suspended, since the connection itself carries no further per-message | |
| 8 -- activity to touch the timer with. | |
| 9 module Tricorder.Daemon.IdleTimer | |
| 10 ( IdleTimer | |
| … +87 lines (ctrl+o to expand) | |
| ● Update(tricorder/src/Tricorder/Daemon/IdleTimer.hs) | |
| ⎿ Added 8 lines, removed 10 lines | |
| 85 $ "Idle for " <> show secs <> "s with no active connections, shutting do | |
| wn." | |
| 86 exitSuccess | |
| 87 | |
| 88 - interpretWith act \env -> \case | |
| 89 - Touch -> touchNow lastActivity | |
| 90 - ConnectionOpened -> localSeqUnlift env \_ -> atomically do | |
| 91 - modifyTVar' openConnections (+ 1) | |
| 92 - ConnectionClosed -> localSeqUnlift env \_ -> atomically do | |
| 93 - modifyTVar' openConnections (max 0 . subtract 1) | |
| 94 - where | |
| 95 - touchNow lastActivity = do | |
| 96 - now <- currentTime | |
| 97 - atomically $ writeTVar lastActivity now | |
| 88 + interpretWith act \_ -> \case | |
| 89 + Touch -> do | |
| 90 + now <- currentTime | |
| 91 + atomically $ writeTVar lastActivity now | |
| 92 + ConnectionOpened -> | |
| 93 + atomically $ modifyTVar' openConnections (+ 1) | |
| 94 + ConnectionClosed -> | |
| 95 + atomically $ modifyTVar' openConnections (max 0 . subtract 1) | |
| ● Now remove the unused localSeqUnlift import. | |
| ● Update(tricorder/src/Tricorder/Daemon/IdleTimer.hs) | |
| ⎿ Added 1 line, removed 1 line | |
| 25 import Effectful (Effect) | |
| 26 import Effectful.Concurrent (Concurrent) | |
| 27 import Effectful.Concurrent.STM (atomically, modifyTVar', newTVarIO, readTVar, writeTVar) | |
| 28 -import Effectful.Dispatch.Dynamic (interpretWith, localSeqUnlift) | |
| 28 +import Effectful.Dispatch.Dynamic (interpretWith) | |
| 29 import Effectful.TH (makeEffect) | |
| 30 | |
| 31 import Atelier.Effects.Conc qualified as Conc | |
| Read 1 file (ctrl+o to expand) | |
| ● Update(tricorder/src/Tricorder/Daemon/IdleTimer.hs) | |
| ⎿ Added 12 lines, removed 4 lines | |
| 89 Touch -> do | |
| 90 now <- currentTime | |
| 91 atomically $ writeTVar lastActivity now | |
| 92 - ConnectionOpened -> | |
| 93 - atomically $ modifyTVar' openConnections (+ 1) | |
| 94 - ConnectionClosed -> | |
| 95 - atomically $ modifyTVar' openConnections (max 0 . subtract 1) | |
| 92 + ConnectionOpened -> do | |
| 93 + now <- currentTime | |
| 94 + atomically do | |
| 95 + modifyTVar' openConnections (+ 1) | |
| 96 + writeTVar lastActivity now | |
| 97 + ConnectionClosed -> do | |
| 98 + -- Reset the countdown from here, so it doesn't fire immediately | |
| 99 + -- using activity that predates however long the connection was open. | |
| 100 + now <- currentTime | |
| 101 + atomically do | |
| 102 + modifyTVar' openConnections (max 0 . subtract 1) | |
| 103 + writeTVar lastActivity now | |
| ● Now wire this into Daemon/Main.hs and Socket/Server.hs. | |
| ● Update(tricorder/src/Tricorder/Daemon/Main.hs) | |
| ⎿ Added 1 line | |
| 41 import Tricorder.Daemon.DaemonInfo qualified as DaemonInfo | |
| 42 import Tricorder.Daemon.EvalCommentRunner qualified as EvalCommentRunner | |
| 43 import Tricorder.Daemon.Hpack.Effect qualified as Hpack | |
| 44 +import Tricorder.Daemon.IdleTimer qualified as IdleTimer | |
| 45 import Tricorder.Daemon.TestRunner qualified as TestRunner | |
| 46 import Tricorder.Session.Command qualified as Repl | |
| 47 import Tricorder.Socket.Server qualified as Server | |
| ● Update(tricorder/src/Tricorder/Daemon/Main.hs) | |
| ⎿ Added 1 line | |
| 79 . inputSession | |
| 80 . runReader @CacheConfig.Config def | |
| 81 . DaemonInfo.runInput | |
| 82 + . IdleTimer.runIdleTimer | |
| 83 . runCacheTtl @ModuleName @PackageId | |
| 84 . runCacheTtl @(PackageId, SourceQuery) @SourceLookup.ModuleSourceResult | |
| 85 . runProcessIO | |
| ● Now update Socket/Server.hs to touch on every request and bracket the Watch connection. | |
| ● Update(tricorder/src/Tricorder/Socket/Server.hs) | |
| ⎿ Added 1 line | |
| 22 | |
| 23 import Tricorder.Build (BuildId, BuildPhase, BuildState (..), Diagnostic) | |
| 24 import Tricorder.Daemon.DaemonInfo (DaemonInfo) | |
| 25 +import Tricorder.Daemon.IdleTimer (IdleTimer) | |
| 26 import Tricorder.Runtime (SocketPath (..)) | |
| 27 import Tricorder.Session.Command (Repl) | |
| 28 import Tricorder.Socket.Protocol | |
| ● Update(tricorder/src/Tricorder/Socket/Server.hs) | |
| ⎿ Added 1 line | |
| 52 import Tricorder.Build qualified as Build | |
| 53 import Tricorder.Build.EvalComment qualified as Eval | |
| 54 import Tricorder.Build.Test qualified as Test | |
| 55 +import Tricorder.Daemon.IdleTimer qualified as IdleTimer | |
| 56 import Tricorder.Socket.Protocol qualified as Protocol | |
| 57 import Tricorder.Waiters qualified as Waiters | |
| 58 | |
| ● Now add the type-signature constraints and the actual touch/bracket logic across main, acceptTrigger, | |
| handleConnection, and dispatch. | |
| ● Update(tricorder/src/Tricorder/Socket/Server.hs) | |
| ⎿ Added 1 line | |
| 69 , FileSystem :> es | |
| 70 , GhcPkg :> es | |
| 71 , Hackage :> es | |
| 72 + , IdleTimer :> es | |
| 73 , Input BuildId :> es | |
| 74 , Input DaemonInfo :> es | |
| 75 , Input Repl :> es | |
| ● Update(tricorder/src/Tricorder/Socket/Server.hs) | |
| ⎿ Added 1 line | |
| 97 , FileSystem :> es | |
| 98 , GhcPkg :> es | |
| 99 , Hackage :> es | |
| 100 + , IdleTimer :> es | |
| 101 , Input BuildId :> es | |
| 102 , Input DaemonInfo :> es | |
| 103 , Input Repl :> es | |
| ● Update(tricorder/src/Tricorder/Socket/Server.hs) | |
| ⎿ Added 2 lines | |
| 127 , FileSystem :> es | |
| 128 , GhcPkg :> es | |
| 129 , Hackage :> es | |
| 130 + , IdleTimer :> es | |
| 131 , Input BuildId :> es | |
| 132 , Input DaemonInfo :> es | |
| 133 , Input Repl :> es | |
| ... | |
| 141 => Handle | |
| 142 -> Eff es () | |
| 143 handleConnection h = do | |
| 144 + IdleTimer.touch | |
| 145 line <- readLine h | |
| 146 case decode (BSL.fromStrict (encodeUtf8 line)) of | |
| 147 Nothing -> sendJson h (ErrorResponse "invalid request") | |
| ● Update(tricorder/src/Tricorder/Socket/Server.hs) | |
| ⎿ Added 9 lines, removed 1 line | |
| 162 , FileSystem :> es | |
| 163 , GhcPkg :> es | |
| 164 , Hackage :> es | |
| 165 + , IdleTimer :> es | |
| 166 , Input BuildId :> es | |
| 167 , Input DaemonInfo :> es | |
| 168 , Input Repl :> es | |
| ... | |
| 179 dispatch query h = case query of | |
| 180 Status (StatusQuery False) -> respondOnce h | |
| 181 Status (StatusQuery True) -> respondWhenDone h | |
| 181 - Watch -> watchStream h | |
| 182 + Watch -> withLongLivedConnection $ watchStream h | |
| 183 Source moduleNames -> respondSource moduleNames h | |
| 184 DiagnosticAt dq -> respondDiagnostic dq.index h | |
| 185 Quit waiters -> quit h waiters | |
| 186 + where | |
| 187 + -- 'Watch' connections (used by e.g. @tricorder ui@) stay open for as | |
| 188 + -- long as the client cares to watch, sending no further requests to | |
| 189 + -- touch the idle timer with. Treat the connection itself as activity | |
| 190 + -- for as long as it's open. | |
| 191 + withLongLivedConnection action = | |
| 192 + IdleTimer.connectionOpened *> action `finally` IdleTimer.connectionClosed | |
| 193 | |
| 194 | |
| 195 quit | |
| Read 1 file (ctrl+o to expand) | |
| ● Update(tricorder/src/Tricorder/Socket/Server.hs) | |
| ⎿ Added 3 lines, removed 2 lines | |
| 188 -- long as the client cares to watch, sending no further requests to | |
| 189 -- touch the idle timer with. Treat the connection itself as activity | |
| 190 -- for as long as it's open. | |
| 191 - withLongLivedConnection action = | |
| 192 - IdleTimer.connectionOpened *> action `finally` IdleTimer.connectionClosed | |
| 191 + withLongLivedConnection action = do | |
| 192 + IdleTimer.connectionOpened | |
| 193 + action `finally` IdleTimer.connectionClosed | |
| 194 | |
| 195 | |
| 196 quit | |
| Read 1 file (ctrl+o to expand) | |
| ● Good. Now let's build and check for type errors via Tricorder MCP. | |
| Read 1 file, called tricorder 5 times, ran 2 shell commands (ctrl+o to expand) | |
| ● Update(tricorder/test/Unit/Tricorder/SessionSpec.hs) | |
| ⎿ Added 5 lines, removed 3 lines | |
| 3 import Atelier.Config (LoadedConfig (..)) | |
| 4 import Atelier.Effects.FileSystem (runFileSystemState) | |
| 5 import Atelier.Effects.Input (runInputConst) | |
| 6 -import Atelier.Effects.Log (Message (..), Severity (..), runLogWriter) | |
| 7 -import Data.Aeson (Value (Null)) | |
| 6 +import Atelier.Effects.Log (Message (..), Severity (..), runLogNoOp, runLogWriter) | |
| 7 +import Data.Aeson (Value (Null), object, (.=)) | |
| 8 import Distribution.PackageDescription.Parsec (parseGenericPackageDescriptionMaybe) | |
| 9 import Effectful (runPureEff) | |
| 10 import Effectful.Reader.Static (runReader) | |
| ... | |
| 13 import Test.Hspec | |
| 14 | |
| 15 import Tricorder.Runtime (ProjectRoot (..)) | |
| 16 -import Tricorder.Session (loadSession) | |
| 16 +import Tricorder.Session (Session (..), loadSession) | |
| 17 import Tricorder.Session.CabalFile (CabalFile (..)) | |
| 18 +import Tricorder.Session.IdleTimeout (IdleTimeout (..)) | |
| 19 import Unit.Tricorder.Session.Helpers (libWithPreludeCabal, preludeOnlyLibCabal) | |
| 20 | |
| 21 | |
| 22 spec_Session :: Spec | |
| 23 spec_Session = do | |
| 24 describe "loadSession" testLoadSession | |
| 25 + describe "loadSession idleTimeout" testIdleTimeout | |
| 26 | |
| 27 | |
| 28 testLoadSession :: Spec | |
| ● Update(tricorder/test/Unit/Tricorder/SessionSpec.hs) | |
| ⎿ Added 20 lines | |
| 59 . runReader (ProjectRoot "/") | |
| 60 . runInputConst (LoadedConfig Null) | |
| 61 $ loadSession | |
| 62 + | |
| 63 + | |
| 64 +testIdleTimeout :: Spec | |
| 65 +testIdleTimeout = do | |
| 66 + it "defaults to 300 seconds when unset" do | |
| 67 + (loadSessionWith (LoadedConfig Null)).idleTimeout `shouldBe` IdleTimeout 300 | |
| 68 + | |
| 69 + it "reads idle_timeout_seconds from the session config" do | |
| 70 + let cfg = LoadedConfig $ object ["session" .= object ["idle_timeout_seconds" .= (5 :: In | |
| +t)]] | |
| 71 + (loadSessionWith cfg).idleTimeout `shouldBe` IdleTimeout 5 | |
| 72 + where | |
| 73 + loadSessionWith cfg = | |
| 74 + runPureEff | |
| 75 + . runLogNoOp | |
| 76 + . evalState @(Map FilePath ByteString) mempty | |
| 77 + . runFileSystemState | |
| 78 + . runInputConst ([] :: [CabalFile]) | |
| 79 + . runReader (ProjectRoot "/") | |
| 80 + . runInputConst cfg | |
| 81 + $ loadSession | |
| Called tricorder 5 times, ran 3 shell commands (ctrl+o to expand) | |
| ● This reverted the working tree back to baseline (the stash restored it correctly). Let me check test status | |
| now. | |
| Called tricorder, searched memories (ctrl+o to expand) | |
| ● Confirmed: this is caused by my changes, not pre-existing flakiness. Let me restore my work and dig into the | |
| actual failure. | |
| Searched for 2 patterns, read 1 file, called tricorder 5 times, ran 7 shell commands (ctrl+o to expand) | |
| ● Found it — the .cabal file wasn't regenerated after adding the two new modules, so cabal build/cabal repl (used | |
| by the nested test-runner) doesn't know they exist, producing an incomplete .so. Let me fix this per the | |
| CLAUDE.md instructions. | |
| Searched for 1 pattern (ctrl+o to expand) | |
| ● Confirmed — no matches. Let's regenerate with nix-hpack. | |
| Ran 2 shell commands (ctrl+o to expand) | |
| ● The .cabal file now includes both new modules. Let's restart the daemon to force a clean rebuild against the | |
| corrected .cabal file, then verify tests pass. | |
| Called tricorder 2 times, searched memories (ctrl+o to expand) | |
| ● All 379 tests pass now, including my two new idleTimeout tests (reads idle_timeout_seconds from the session | |
| config: OK). Let's confirm zero failures overall and check the daemon's own build status once more. | |
| Called tricorder, ran 1 shell command (ctrl+o to expand) | |
| ● All four test suites pass and there are 0 build diagnostics. Now let's do a final review of the complete diff | |
| for correctness and consistency before wrapping up. | |
| Ran 2 shell commands (ctrl+o to expand) | |
| ● This all looks correct and clean. Let's also double check the README/docs for config options to see if | |
| idle_timeout_seconds should be documented alongside other session config keys. | |
| Searched for 2 patterns (ctrl+o to expand) | |
| ● This doc needs updating to document the new option. Let me check the surrounding context and add an entry. | |
| Read 1 file (ctrl+o to expand) | |
| ● I'll add the new option to both the example block and the description list, following the test_timeout style | |
| since it has similar "0 disables" semantics. | |
| ● Update(docs/configuring-tricorder.md) | |
| ⎿ Added 1 line | |
| 23 test_timeout: 10 | |
| 24 generate_with_hpack: true | |
| 25 test_memory_limit: 10mb | |
| 26 + idle_timeout_seconds: 300 | |
| 27 hooks: | |
| 28 start: | |
| 29 before: echo "starting" >> log.txt | |
| ● Update(docs/configuring-tricorder.md) | |
| ⎿ Added 6 lines | |
| 66 units: `kb`, `mb`, `gb`, `tb` and `pb`, as well their binary variants `kib`, | |
| 67 `mib`, `gib`, `tib` and `pib`. The unit `B` is equivalent to no unit, and | |
| 68 signifies a raw byte count. | |
| 69 +- `idle_timeout_seconds`: Number of seconds the daemon will stay running with | |
| 70 + no client activity before it shuts itself down. Defaults to `300` seconds. | |
| 71 + Set to `0` to disable, and let the daemon run indefinitely. Every request | |
| 72 + from a client resets the countdown. A `tricorder ui` client's long-lived | |
| 73 + watch connection counts as continuous activity for as long as it stays | |
| 74 + connected, so the daemon won't shut down while it's being watched. | |
| 75 - `hooks`: Shell scripts to run for certain events. Each property marks an | |
| 76 event that will take place. Each hooks may have a `before` and an `after` | |
| 77 script to run. The `before` script is run just before the event takes place, | |
| ● Now let's do a final status check to make sure the doc change didn't break anything (it shouldn't, since it's | |
| outside watched source dirs) and get a clean final summary. | |
| Called tricorder, searched memories (ctrl+o to expand) | |
| ● Clean build, all 4 test suites passing. Let's review the final diff summary. | |
| Ran 1 shell command (ctrl+o to expand) | |
| ● Implementation is complete and verified (0 diagnostics, all 4 test suites passing including the new tests). | |
| Summary of changes: | |
| - Tricorder.Session.IdleTimeout (new): IdleTimeout newtype, defaults to 300s. | |
| - Session.Config / Session: added idle_timeout_seconds config field (default 300), threaded into Session and | |
| re-evaluated on every loadSession call (i.e. on config reload), per your request. | |
| - Tricorder.Daemon.IdleTimer (new): an effect tracking last-activity time and an open-connection counter via | |
| STM TVars, with a background poller (1s cadence) that calls Exit.exitSuccess once idle for | |
| idle_timeout_seconds with no open connections. Reads the timeout from Session on each check, so config | |
| reloads apply live. 0 disables it (matching the existing test_timeout convention). | |
| - Socket.Server: every request touches the timer (handleConnection); the long-lived Watch connection (used by | |
| tricorder ui) instead brackets itself with connectionOpened/connectionClosed, so watching the UI suspends the | |
| countdown for as long as the client stays connected. | |
| - Daemon.Main: wired IdleTimer.runIdleTimer into the effect stack. | |
| - Regenerated tricorder.cabal via nix-hpack (required — new modules weren't picked up otherwise, which was the | |
| cause of a build failure I ran into and root-caused along the way). | |
| - Added tests in SessionSpec.hs covering the default and config-driven values. | |
| - Documented idle_timeout_seconds in docs/configuring-tricorder.md. | |
| One thing worth flagging: I didn't write an automated test that exercises the actual shutdown/reset timing | |
| behavior of runIdleTimer itself (it's concurrency- and wall-clock-heavy — real exitSuccess, real background | |
| thread), since that's better verified by manual/integration testing than a unit test using fake clocks. Let me | |
| know if you'd like a targeted test for that using runClockList/runExitNoOp, or if you'd like to manually verify | |
| by setting a short idle_timeout_seconds in .tricorder.yaml and watching the daemon shut itself down. | |
| ✻ Churned for 15m 8s · done fredag 28. aug., 13:42 | |
| ❯ Update the changelog. | |
| Searched for 1 pattern, read 1 file (ctrl+o to expand) | |
| ● Adding an entry under [Unreleased] > Added, matching the existing style. | |
| ● Update(tricorder/CHANGELOG.md) | |
| ⎿ Added 6 lines | |
| 9 | |
| 10 ### Added | |
| 11 | |
| 12 +- The daemon now shuts itself down after a period of inactivity, configurable | |
| 13 + with `idle_timeout_seconds` (defaults to 300 seconds; `0` disables it). Any | |
| 14 + client request resets the countdown, and a `tricorder ui` client's | |
| 15 + long-lived watch connection counts as continuous activity for as long as | |
| 16 + it's connected. See [Configuring Tricorder](/docs/configuring-tricorder.md) | |
| 17 + for more information. | |
| 18 - Use the `test_memory_limit` configuration value to specify the maximum memory | |
| 19 a test suite is allowed to consume. This is enforced through GHC's `-M` RTS | |
| 20 option. | |
| Read 1 file (ctrl+o to expand) | |
| ● That's just an index — no changes needed there. Updated tricorder/CHANGELOG.md under [Unreleased] > Added with | |
| an entry for the new idle_timeout_seconds feature. | |
| ✻ Baked for 14s · done fredag 28. aug., 13:42 | |
| ❯ Add a withConnection or something like that to IdleTimer as a bracket-like alternative to manually calling | |
| connectionOpened and connectionClosed. | |
| Read 1 file (ctrl+o to expand) | |
| ● Update(tricorder/src/Tricorder/Daemon/IdleTimer.hs) | |
| ⎿ Added 2 lines | |
| 11 , touch | |
| 12 , connectionOpened | |
| 13 , connectionClosed | |
| 14 + , withConnection | |
| 15 , runIdleTimer | |
| 16 ) | |
| 17 where | |
| ... | |
| 27 import Effectful.Concurrent (Concurrent) | |
| 28 import Effectful.Concurrent.STM (atomically, modifyTVar', newTVarIO, readTVar, writeTVar) | |
| 29 import Effectful.Dispatch.Dynamic (interpretWith) | |
| 30 +import Effectful.Exception (finally) | |
| 31 import Effectful.TH (makeEffect) | |
| 32 | |
| 33 import Atelier.Effects.Conc qualified as Conc | |
| ● Update(tricorder/src/Tricorder/Daemon/IdleTimer.hs) | |
| ⎿ Added 9 lines | |
| 52 makeEffect ''IdleTimer | |
| 53 | |
| 54 | |
| 55 +-- | Bracket a long-lived connection's lifetime with 'connectionOpened' \/ | |
| 56 +-- 'connectionClosed', so the countdown stays suspended for as long as | |
| 57 +-- @action@ runs, even if it throws. | |
| 58 +withConnection :: (IdleTimer :> es) => Eff es a -> Eff es a | |
| 59 +withConnection action = do | |
| 60 + connectionOpened | |
| 61 + action `finally` connectionClosed | |
| 62 + | |
| 63 + | |
| 64 -- | Run the idle timer, shutting the process down once | |
| 65 -- @idle_timeout_seconds@ (read from 'Session', re-read on every check so | |
| 66 -- config reloads apply live) elapses with no activity and no open | |
| ● Now update the module doc comment reference and Server.hs to use it. | |
| ● Update(tricorder/src/Tricorder/Daemon/IdleTimer.hs) | |
| ⎿ Added 4 lines, removed 4 lines | |
| 2 -- | |
| 3 -- Every client request 'touch'es the timer, restarting the countdown. A | |
| 4 -- long-lived connection (e.g. a @tricorder ui@ client streaming 'Watch' | |
| 5 --- updates) brackets its lifetime with 'connectionOpened' \/ 'connectionClosed' | |
| 6 --- instead: as long as one such connection is open, the countdown is | |
| 7 --- suspended, since the connection itself carries no further per-message | |
| 8 --- activity to touch the timer with. | |
| 5 +-- updates) brackets its lifetime with 'withConnection' instead: as long as | |
| 6 +-- one such connection is open, the countdown is suspended, since the | |
| 7 +-- connection itself carries no further per-message activity to touch the | |
| 8 +-- timer with. | |
| 9 module Tricorder.Daemon.IdleTimer | |
| 10 ( IdleTimer | |
| 11 , touch | |
| Read 1 file (ctrl+o to expand) | |
| ● Update(tricorder/src/Tricorder/Socket/Server.hs) | |
| ⎿ Added 4 lines, removed 8 lines | |
| 179 dispatch query h = case query of | |
| 180 Status (StatusQuery False) -> respondOnce h | |
| 181 Status (StatusQuery True) -> respondWhenDone h | |
| 182 - Watch -> withLongLivedConnection $ watchStream h | |
| 183 - Source moduleNames -> respondSource moduleNames h | |
| 184 - DiagnosticAt dq -> respondDiagnostic dq.index h | |
| 185 - Quit waiters -> quit h waiters | |
| 186 - where | |
| 182 -- 'Watch' connections (used by e.g. @tricorder ui@) stay open for as | |
| 183 -- long as the client cares to watch, sending no further requests to | |
| 184 -- touch the idle timer with. Treat the connection itself as activity | |
| 185 -- for as long as it's open. | |
| 191 - withLongLivedConnection action = do | |
| 192 - IdleTimer.connectionOpened | |
| 193 - action `finally` IdleTimer.connectionClosed | |
| 186 + Watch -> IdleTimer.withConnection $ watchStream h | |
| 187 + Source moduleNames -> respondSource moduleNames h | |
| 188 + DiagnosticAt dq -> respondDiagnostic dq.index h | |
| 189 + Quit waiters -> quit h waiters | |
| 190 | |
| 191 | |
| 192 quit | |
| Searched for 1 pattern (ctrl+o to expand) | |
| ● Still used elsewhere, good. Now let's rebuild and verify. | |
| Called tricorder, ran 1 shell command (ctrl+o to expand) | |
| ● Clean build, all 4 test suites still passing. IdleTimer now exports a withConnection bracket helper, and | |
| Socket.Server uses it for the Watch connection instead of manual connectionOpened/connectionClosed + finally. | |
| ✻ Worked for 48s · done fredag 28. aug., 13:44 | |
| ❯ Actually, can't each of the cases in dispatch just use IdleTimer.withConnection? | |
| Searched for 1 pattern, read 1 file (ctrl+o to expand) | |
| ● Update(tricorder/src/Tricorder/Socket/Server.hs) | |
| ⎿ Added 1 line, removed 2 lines | |
| 140 ) | |
| 141 => Handle | |
| 142 -> Eff es () | |
| 143 -handleConnection h = do | |
| 144 - IdleTimer.touch | |
| 143 +handleConnection h = IdleTimer.withConnection do | |
| 144 line <- readLine h | |
| 145 case decode (BSL.fromStrict (encodeUtf8 line)) of | |
| 146 Nothing -> sendJson h (ErrorResponse "invalid request") | |
| ● Update(tricorder/src/Tricorder/Socket/Server.hs) | |
| ⎿ Added 1 line, removed 5 lines | |
| 178 dispatch query h = case query of | |
| 179 Status (StatusQuery False) -> respondOnce h | |
| 180 Status (StatusQuery True) -> respondWhenDone h | |
| 181 - -- 'Watch' connections (used by e.g. @tricorder ui@) stay open for as | |
| 182 - -- long as the client cares to watch, sending no further requests to | |
| 183 - -- touch the idle timer with. Treat the connection itself as activity | |
| 184 - -- for as long as it's open. | |
| 185 - Watch -> IdleTimer.withConnection $ watchStream h | |
| 181 + Watch -> watchStream h | |
| 182 Source moduleNames -> respondSource moduleNames h | |
| 183 DiagnosticAt dq -> respondDiagnostic dq.index h | |
| 184 Quit waiters -> quit h waiters | |
| Read 1 file (ctrl+o to expand) | |
| ● Update(tricorder/src/Tricorder/Socket/Server.hs) | |
| ⎿ Removed 1 line | |
| 161 , FileSystem :> es | |
| 162 , GhcPkg :> es | |
| 163 , Hackage :> es | |
| 164 - , IdleTimer :> es | |
| 164 , Input BuildId :> es | |
| 165 , Input DaemonInfo :> es | |
| 166 , Input Repl :> es | |
| Read 1 file (ctrl+o to expand) | |
| ● Now removing the now-unused Touch operation entirely, since every request goes through withConnection | |
| uniformly. | |
| ● Update(tricorder/src/Tricorder/Daemon/IdleTimer.hs) | |
| ⎿ Added 5 lines, removed 7 lines | |
| 1 -- | Shuts the daemon down after a period of inactivity. | |
| 2 -- | |
| 3 --- Every client request 'touch'es the timer, restarting the countdown. A | |
| 4 --- long-lived connection (e.g. a @tricorder ui@ client streaming 'Watch' | |
| 5 --- updates) brackets its lifetime with 'withConnection' instead: as long as | |
| 6 --- one such connection is open, the countdown is suspended, since the | |
| 7 --- connection itself carries no further per-message activity to touch the | |
| 8 --- timer with. | |
| 3 +-- Every client connection brackets its lifetime with 'withConnection', | |
| 4 +-- restarting the countdown on open and again on close. This means the | |
| 5 +-- countdown stays suspended for as long as any connection is open, | |
| 6 +-- including a long-lived one like a @tricorder ui@ client streaming | |
| 7 +-- 'Watch' updates. | |
| 8 module Tricorder.Daemon.IdleTimer | |
| 9 ( IdleTimer | |
| 11 - , touch | |
| 10 , connectionOpened | |
| 11 , connectionClosed | |
| 12 , withConnection | |
| ● Update(tricorder/src/Tricorder/Daemon/IdleTimer.hs) | |
| ⎿ Added 3 lines, removed 5 lines | |
| 38 | |
| 39 | |
| 40 data IdleTimer :: Effect where | |
| 41 - -- | Record activity, restarting the idle countdown. | |
| 42 - Touch :: IdleTimer m () | |
| 43 - -- | Mark a long-lived connection as open, suspending the idle countdown | |
| 44 - -- until every such connection is closed again. | |
| 41 + -- | Mark a connection as open, suspending the idle countdown until every | |
| 42 + -- open connection is closed again. | |
| 43 ConnectionOpened :: IdleTimer m () | |
| 46 - -- | Mark a previously opened long-lived connection as closed. | |
| 44 + -- | Mark a previously opened connection as closed. | |
| 45 ConnectionClosed :: IdleTimer m () | |
| 46 | |
| 47 | |
| ● Update(tricorder/src/Tricorder/Daemon/IdleTimer.hs) | |
| ⎿ Added 1 line, removed 1 line | |
| 48 makeEffect ''IdleTimer | |
| 49 | |
| 50 | |
| 51 --- | Bracket a long-lived connection's lifetime with 'connectionOpened' \/ | |
| 51 +-- | Bracket a connection's lifetime with 'connectionOpened' \/ | |
| 52 -- 'connectionClosed', so the countdown stays suspended for as long as | |
| 53 -- @action@ runs, even if it throws. | |
| 54 withConnection :: (IdleTimer :> es) => Eff es a -> Eff es a | |
| ● Now update the interpreter to drop the Touch case. | |
| ● Update(tricorder/src/Tricorder/Daemon/IdleTimer.hs) | |
| ⎿ Removed 3 lines | |
| 93 exitSuccess | |
| 94 | |
| 95 interpretWith act \_ -> \case | |
| 96 - Touch -> do | |
| 97 - now <- currentTime | |
| 98 - atomically $ writeTVar lastActivity now | |
| 96 ConnectionOpened -> do | |
| 97 now <- currentTime | |
| 98 atomically do | |
| ● Now let's view the final file to double-check correctness. | |
| Read 1 file (ctrl+o to expand) | |
| ● Update the small comment in the doc that still says "long-lived connections": | |
| ● Update(tricorder/src/Tricorder/Daemon/IdleTimer.hs) | |
| ⎿ Added 2 lines, removed 2 lines | |
| 59 | |
| 60 -- | Run the idle timer, shutting the process down once | |
| 61 -- @idle_timeout_seconds@ (read from 'Session', re-read on every check so | |
| 62 --- config reloads apply live) elapses with no activity and no open | |
| 63 --- long-lived connections. A timeout of zero or less disables shutdown. | |
| 62 +-- config reloads apply live) elapses with no open connections. A timeout of | |
| 63 +-- zero or less disables shutdown. | |
| 64 runIdleTimer | |
| 65 :: ( Clock :> es | |
| 66 , Conc :> es | |
| ● Now let's rebuild and check for diagnostics/test results. | |
| Called tricorder, ran 1 shell command (ctrl+o to expand) | |
| ● Clean build, all 4 test suites passing. Simplified IdleTimer: every connection now goes through withConnection | |
| uniformly (wrapping all of handleConnection), which subsumes the old touch/Watch-only special-casing — it also | |
| fixes a latent gap where a slow Status(awaitDone=True) or Source lookup could have let the countdown expire | |
| mid-request, since now the countdown is suspended for the full duration of every request, not just touched at | |
| connection-open. | |
| Ran 1 shell command (ctrl+o to expand) | |
| ● The touch primitive is gone entirely — IdleTimer now only exposes connectionOpened, connectionClosed, and | |
| withConnection, and dispatch no longer needs the IdleTimer constraint at all since bracketing happens once at | |
| handleConnection. | |
| ✻ Baked for 1m 39s · done fredag 28. aug., 13:47 | |
| ❯ Make IdleTimeout a newtype over Second instead of Int. Use AsRawUnit to derive the JSON instances. | |
| Read 1 file, called tricorder (ctrl+o to expand) | |
| ● Write(tricorder/src/Tricorder/Session/IdleTimeout.hs) | |
| ⎿ Added 3 lines, removed 2 lines | |
| 1 module Tricorder.Session.IdleTimeout (IdleTimeout (..)) where | |
| 2 | |
| 3 +import Atelier.Time (AsRawUnit (..), Second) | |
| 4 import Data.Aeson (FromJSON, ToJSON) | |
| 5 import Data.Default (Default (..)) | |
| 6 | |
| 7 | |
| 7 -newtype IdleTimeout = IdleTimeout {getIdleTimeout :: Int} | |
| 8 +newtype IdleTimeout = IdleTimeout {getIdleTimeout :: Second} | |
| 9 deriving stock (Eq, Generic, Show) | |
| 9 - deriving (FromJSON, ToJSON) via Int | |
| 10 + deriving (FromJSON, ToJSON) via AsRawUnit Second | |
| 11 | |
| 12 | |
| 13 instance Default IdleTimeout where | |
| ● Now update Session.hs to convert Int -> Second when constructing IdleTimeout. | |
| Searched for 1 pattern (ctrl+o to expand) | |
| ● Update(tricorder/src/Tricorder/Session.hs) | |
| ⎿ Added 1 line, removed 1 line | |
| 132 , testTimeout = TestTimeout cfgFile.testTimeout | |
| 133 , generateWithHpack = GenerateWithHpack cfgFile.generateWithHpack | |
| 134 , hooks | |
| 135 - , idleTimeout = IdleTimeout cfgFile.idleTimeoutSeconds | |
| 135 + , idleTimeout = IdleTimeout (fromIntegral cfgFile.idleTimeoutSeconds) | |
| 136 } | |
| 137 | |
| 138 | |
| ● Now update IdleTimer.hs's pattern match and log message (Second's Show already appends "s"). | |
| Read 1 file (ctrl+o to expand) | |
| ● Update(tricorder/src/Tricorder/Daemon/IdleTimer.hs) ⎿ Added 3 lines, removed 1 line | |
| 89 pure $ connections <= 0 && diffUTCTime now idleSince >= fromIntegral secs | |
| 90 when shouldExit do | |
| 91 Log.info | |
| 92 - $ "Idle for " <> show secs <> "s with no active connections, shutting do | |
| -wn." | |
| 92 + $ "Idle for " | |
| 93 + <> show secs | |
| 94 + <> " with no active connections, shutting down." | |
| 95 exitSuccess | |
| 96 | |
| 97 interpretWith act \_ -> \case | |
| Searched for 1 pattern, called tricorder, ran 2 shell commands (ctrl+o to expand) | |
| ● Rebuild timestamp (11:50:18) is fresh, module count 193, 0 diagnostics, all 4 suites passing. IdleTimeout is | |
| now newtype IdleTimeout = IdleTimeout {getIdleTimeout :: Second} with FromJSON/ToJSON derived via AsRawUnit | |
| Second, and both Session.hs (config conversion) and IdleTimer.hs (log message) updated accordingly. | |
| ✻ Baked for 1m 25s · done fredag 28. aug., 13:50 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This comment was marked as off-topic.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.