Version: 4.4.2 (Homebrew) · macOS
Related: the "hook session-start silently spawns a daemon" bug report — that one is about it being undocumented. This one is about whether the daemon should work this way at all.
What I found
A single project directory, observed live:
PID ELAPSED COMMAND
44058 5d 04:22:41 codemap watch daemon <project>
81231 06:54:57 codemap watch daemon <project>
81271 06:54:56 codemap watch daemon <project>
81298 06:54:55 codemap watch daemon <project>
Four daemons watching the same directory. Three of them started within one second of each other. The fourth had been running for five days, long after whatever session started it had ended.
That's two separate defects wearing one costume.
Defect 1 — the "already running?" check is not a guard
Three spawns at :55, :56, :57 means the check is read-then-act with a window in between. Any two invocations that land inside that window both conclude nothing is running and both spawn. Editors, shells, and agent tooling all tend to fire session hooks at once, which is exactly the access pattern that loses this race.
A liveness check is not mutual exclusion. This needs an atomic claim — O_EXCL lock file holding the pid, or a unix socket bind, or flock — where losing the claim means exiting rather than proceeding.
Defect 2 — nothing owns the daemon, so nothing reaps it
The five-day-old process is the interesting one. It outlived its session by days, still watching, still holding state, invisible to the user who caused it. There is no parent that dies and takes it with it, no TTL, no "is my originating session still alive" heartbeat, and no documented way to stop one — --help lists --watch to start but nothing to stop.
Over weeks this accumulates silently. I only noticed because I went looking for something else.
The design question underneath
The part I'd actually reconsider is hook session-start spawning it at all.
A watcher you opt into with --watch and can see is a normal tool. A watcher that appears because you asked for a printout — under a subcommand whose help text is "Show project context" — and then outlives the thing that asked, is the shape that produced four copies. The user never formed an intent to run a background process, so they never form an intent to stop one.
Is the daemon buying anything?
Worth asking honestly, because the costs above are real and the benefit may not be. Measured cold, no daemon, on a ~140-file Go repo and a ~290-file Swift repo:
| command |
runtime |
handoff |
0.59s / 1.00s |
--importers <file> |
0.30s / 0.52s |
--deps |
0.52s / 0.89s |
blast-radius --text |
0.65s / 1.46s |
| bare tree |
0.19s / 0.38s |
Everything a consumer actually wants is sub-second to ~1.5s from cold. The daemon is optimizing a cost that doesn't obviously exist at these repo sizes. It would start to matter on a very large monorepo — but that's an argument for making it opt-in for those users, not the default side effect of a print command.
Options, roughly in increasing order of work
- Remove the spawn from
hook session-start. Print context, exit. Keep --watch for people who want it. Smallest change, removes the whole class.
- Keep the spawn but make it safe and owned — atomic lock claim, a TTL or heartbeat tied to the originating session,
codemap watch stop [--all], and watch status listing every daemon with its age and project.
- Keep it, but earn it — measure the cache hit rate a daemon actually provides. If warm runs aren't meaningfully faster than the numbers above, option 1 wins on evidence.
Happy to be wrong about this if the daemon is carrying weight on repos larger than the ones I tested — but at these sizes I couldn't find the win, and I did find four copies of it.
What still holds up
Worth saying explicitly since this reads as a pile-on: the graph work underneath is sound where the language has real file-level imports. --importers was set-identical to grep -rl (31/31) and deterministic across five runs, and handoff had the best signal-per-token of anything I measured. The problems I hit were all in the surrounding layers — process lifecycle, truncation order, config resolution — not the analysis.
Version: 4.4.2 (Homebrew) · macOS
Related: the "
hook session-startsilently spawns a daemon" bug report — that one is about it being undocumented. This one is about whether the daemon should work this way at all.What I found
A single project directory, observed live:
Four daemons watching the same directory. Three of them started within one second of each other. The fourth had been running for five days, long after whatever session started it had ended.
That's two separate defects wearing one costume.
Defect 1 — the "already running?" check is not a guard
Three spawns at
:55,:56,:57means the check is read-then-act with a window in between. Any two invocations that land inside that window both conclude nothing is running and both spawn. Editors, shells, and agent tooling all tend to fire session hooks at once, which is exactly the access pattern that loses this race.A liveness check is not mutual exclusion. This needs an atomic claim —
O_EXCLlock file holding the pid, or a unix socket bind, or flock — where losing the claim means exiting rather than proceeding.Defect 2 — nothing owns the daemon, so nothing reaps it
The five-day-old process is the interesting one. It outlived its session by days, still watching, still holding state, invisible to the user who caused it. There is no parent that dies and takes it with it, no TTL, no "is my originating session still alive" heartbeat, and no documented way to stop one —
--helplists--watchto start but nothing to stop.Over weeks this accumulates silently. I only noticed because I went looking for something else.
The design question underneath
The part I'd actually reconsider is
hook session-startspawning it at all.A watcher you opt into with
--watchand can see is a normal tool. A watcher that appears because you asked for a printout — under a subcommand whose help text is "Show project context" — and then outlives the thing that asked, is the shape that produced four copies. The user never formed an intent to run a background process, so they never form an intent to stop one.Is the daemon buying anything?
Worth asking honestly, because the costs above are real and the benefit may not be. Measured cold, no daemon, on a ~140-file Go repo and a ~290-file Swift repo:
handoff--importers <file>--depsblast-radius --textEverything a consumer actually wants is sub-second to ~1.5s from cold. The daemon is optimizing a cost that doesn't obviously exist at these repo sizes. It would start to matter on a very large monorepo — but that's an argument for making it opt-in for those users, not the default side effect of a print command.
Options, roughly in increasing order of work
hook session-start. Print context, exit. Keep--watchfor people who want it. Smallest change, removes the whole class.codemap watch stop [--all], andwatch statuslisting every daemon with its age and project.Happy to be wrong about this if the daemon is carrying weight on repos larger than the ones I tested — but at these sizes I couldn't find the win, and I did find four copies of it.
What still holds up
Worth saying explicitly since this reads as a pile-on: the graph work underneath is sound where the language has real file-level imports.
--importerswas set-identical togrep -rl(31/31) and deterministic across five runs, andhandoffhad the best signal-per-token of anything I measured. The problems I hit were all in the surrounding layers — process lifecycle, truncation order, config resolution — not the analysis.