fix: address 15 additional hang vectors found during deep audit (#45)

MCP server (5 fixes):
- Move semaphore acquisition inside goroutine so main loop stays
  responsive when all slots are taken
- Add 10s write timeout to sendResponse to prevent pipe deadlock
  when Claude Code pauses reading stdout
- Send fallback JSON-RPC error when json.Marshal fails instead of
  silently swallowing the error and leaving caller waiting forever
- Silence unknown notification methods (req.ID == nil) instead of
  sending unsolicited error responses that may desync the host
- Return MCP isError content for tool failures instead of top-level
  JSON-RPC error, matching the MCP specification

Vector/embedding (3 fixes):
- Move EmbedBatchWithContext call before writeMu.Lock in AddDocuments
  so ONNX inference runs outside the write lock
- Replace singleflight.Do with DoChan + ctx select in both
  getOrComputeEmbedding and UnifiedSearch so callers can bail out
  independently when their context expires
- Add activeQueries atomic counter; skip cache warming when user
  queries are in-flight; reduce warming timeout from 5s to 2s

Hooks (4 fixes):
- Cap EnsureWorkerRunning to 15s hard deadline with context; reduce
  StartupTimeout from 30s to 10s; reduce port-in-use retries
- Fix nil dereference panic in user-prompt hook when initResult is
  nil (non-JSON worker response); use comma-ok assertions
- Use package-level hookClient/healthClient with DisableKeepAlives
  to prevent FD leaks in short-lived hook processes
- Set SysProcAttr{Setpgid: true} to detach worker from hook process
  group, preventing kill-cascade from Claude Code

Worker/DB (3 fixes):
- Replace os.Exit(0) in MCP config watcher with context cancellation
  for clean protocol shutdown
- Add 60s context.WithTimeout around ProcessObservation calls in
  processAllSessions to prevent hung CLI subprocesses from blocking
  the queue processor forever
- Set explicit PRAGMA wal_autocheckpoint=1000 and add PASSIVE WAL
  checkpoint to Optimize() to prevent checkpoint stalls

Adds 20+ regression tests across all fix areas.
This commit is contained in:
2026-05-26 14:29:34 +01:00
parent de5796bbe6
commit a81482d06a
15 changed files with 952 additions and 92 deletions
+9 -6
View File
@@ -59,7 +59,7 @@ func main() {
}()
// Start file watchers for config changes
startWatchers()
startWatchers(cancel)
telemetry.Send("claude-mnemonic", Version)
@@ -68,18 +68,21 @@ func main() {
log.Info().Str("project", *project).Str("version", Version).Str("worker", workerURL).Msg("Starting MCP server")
if err := server.Run(ctx); err != nil {
if err == context.Canceled {
log.Info().Msg("MCP server shut down (config change or signal)")
return
}
log.Fatal().Err(err).Msg("MCP server error")
}
}
// startWatchers initializes file watchers for config.
func startWatchers() {
// Watch config file for changes (triggers process exit for restart)
func startWatchers(cancel context.CancelFunc) {
// Watch config file for changes (triggers graceful shutdown via context cancellation)
configPath := config.SettingsPath()
configWatcher, err := watcher.New(configPath, func() {
log.Warn().Str("path", configPath).Msg("Config file changed, exiting for restart...")
time.Sleep(100 * time.Millisecond) // Give logs time to flush
os.Exit(0)
log.Warn().Str("path", configPath).Msg("Config file changed, shutting down gracefully...")
cancel() // Triggers ctx.Done() in server.Run(), which drains in-flight requests
})
if err != nil {
log.Warn().Err(err).Msg("Failed to create config watcher")