idx

ADR 0011: idx destroy Disables Daemon Before Removing Indices

Status

Accepted

Context

idx destroy removes all .idx directories under the project root. With the watch-mode daemon introduced in ADR 0005 and ADR 0006, a running watcher process monitors the filesystem and reacts to file-change events by writing new .idx directories.

When idx destroy was run while a daemon was active, the following race condition occurred:

  1. destroy scans and deletes .idx directories.
  2. The still-running daemon detects a file-change event (or a directory removal event) and immediately re-creates one or more .idx directories.
  3. The project appears to still have index files even after destroy completes.

This made idx destroy appear broken: users found leftover .idx directories even after a seemingly successful destroy.

A second, related bug compounded the problem: DaemonService.Disable only removed the first matching entry for a project path from the daemon state file. If the same path had been registered more than once (e.g. after repeated idx daemon enable . calls), one or more watcher processes remained alive after disable. The daemon state was therefore not accurately representing reality, and subsequent operations (status, disable, destroy) could behave unexpectedly.

Decision

1. Disable daemon before destroy

cobra_commands.go wraps the destroy execution with a preflight daemon-disable step:

func (runner CommandRunner) newDestroyCommand() *cobra.Command {
    return &cobra.Command{
        Use:   "destroy",
        Short: "Destroy index metadata",
        RunE: func(_ *cobra.Command, _ []string) error {
            if err := runner.disableDaemonForDestroy(); err != nil {
                return err
            }
            return runner.destroyCommand.Run()
        },
    }
}

The helper disableDaemonForDestroy calls DaemonService.Disable(".") and silently ignores the following expected non-error conditions:

Any other error (e.g. permission failure, unexpected state corruption) is propagated and aborts the destroy.

2. Remove all duplicate state entries in DaemonService.Disable

DaemonService.Disable was updated to iterate the full project list and remove every entry whose path matches absPath, rather than stopping after the first match. For each removed entry, if the associated process is still running (Enabled && PID > 0), the process is killed.

for _, project := range state.Projects {
    if project.Path != absPath {
        filtered = append(filtered, project)
        continue
    }
    removedCount++
    if project.Enabled && project.PID > 0 {
        proc, findErr := os.FindProcess(project.PID)
        if findErr == nil {
            _ = proc.Kill()
        }
    }
}
if removedCount == 0 {
    return fmt.Errorf("project %q not being monitored", absPath)
}

Decision Drivers

Consequences

Positive

Negative

Operational Notes