Port registry: global coordination across projects

This is an implementation detail of slice give-me-a-port. It is not part of the public API and is invisible to template scripts and programs.

Problem

The base problem I'm trying to solve is what port can I use that is free right now. Some kind of slice give-me-a-port.

Now many would say its bonus points for giving me a port will still be free over the next week or so. To those that say bind() is all you need I say "expect more from your tools." I never know when I'm going to get to show someone one of the several ideas I'm juggling day to day, so I like to keep multiple worktrees in demo ready state.

Now you should have an idea why I really want slice give-me-a-port to avoid port conflicts not just within a single project but across every project on my laptop.

Now technically all I need to do is write down a list of all the projects using thinslice, and then every time I need a new port

  1. Start with the ~65k ports for 127.0.0.1
  2. I can scan all of those /**/+state+/*/thinslice.toml files on my laptop.
  3. after subtracting out the "claimed" ports, find one port not open using bind()

My assumption is that a developer laptop has thousands of available ports and only a handful in active use. I don't actually know how coordinate with the OS. I'm not going to build in a list of "avoid" other programs. I think thinslice will work with just a crude best-effort registry.

I'm going to ignore the problem of the registry and reality drifting apart by handwaving towards some sort of slice doctor that can rebuild the global registry from scratch. When thinslice thinks you can use a port, but then say a week later you discover some other program wants to use that port too... my plan is to have you manually run slice give-me-a-port again and hand edit the specific thinslice.toml file in conflict, and then rebuild the global registry cache.

Solution

slice maintains a global cache as the port_assignments table inside a SQLite database at:

$XDG_DATA_HOME/thinslice/thinslice.db

Falling back to ~/.local/share/thinslice/thinslice.db when XDG_DATA_HOME is unset, per the XDG Base Directory Specification.

SQLite is not required, I just felt like it. Technically all we need is list of claimed ports, that could just be a text file. However, I'm the kind of person who likes collecting time series data and graphing things. Once I decided to have some kind of state file, I might as well make it SQLite just in case I get a free weekend and want to stash more debugging data, like command timings.

Schema

CREATE TABLE port_assignments (
    port         INTEGER NOT NULL,
    project_root TEXT    NOT NULL,  -- absolute path to the +clone+ parent dir
    slot_name    TEXT    NOT NULL,  -- name of the slot inside +state+
    process_display_name TEXT NOT NULL,  -- key from [process.*] in thinslice.toml
    assigned_at  TEXT    NOT NULL,  -- ISO-8601 timestamp
    PRIMARY KEY (port)
);

A port appears at most once. The combination of (project_root, slot_name, process_display_name) identifies what owns it.

Read/write lifecycle

On slice give-me-a-port: reads all rows from port_assignments to build the global exclusion set, then combines it with the session-local reservation list (ports already issued in the current template run) and the bind() test. Picks the first port not excluded by any of these three checks. Writes the chosen port to the registry immediately with a provisional entry (process_display_name = "__pending__") so that subsequent calls within the same template run see it as taken even if slice state new has not yet written thinslice.toml.

On slice state new completing successfully: replaces any __pending__ rows for this slot with final entries sourced from the [process.*] sections of the written thinslice.toml.

On slice state new failing or being interrupted: the session tempfile is cleaned up by slice state new, and provisional __pending__ rows for that run are deleted. A crash before cleanup leaves orphan rows, which the reconciliation pass handles.

Reconciliation

Registry entries can become stale when slots are deleted outside of slice commands (e.g., rm -rf +state+/slot-a). slice give-me-a-port runs a best-effort reconciliation pass before reading the exclusion set, with a 100 ms wall-clock budget:

  1. For each row in port_assignments, check whether <project_root>/+state+/<slot_name>/thinslice.toml exists and still declares that port for that process.
  2. Delete any row where the file is missing or no longer claims the port.
  3. Stop early if the budget is exhausted — leave remaining rows for next time or for slice doctor.

The pass is intentionally shallow. Its goal is to prevent the most common sources of drift (deleted slots, renamed projects) without making slice give-me-a-port slow. It is not a consistency guarantee.

For a full, unbounded reconciliation across all known projects, run slice doctor. This rescans every thinslice.toml file referenced in the registry, rebuilds the global view from scratch, and reports any discrepancies it finds.

slice debug

slice debug is a namespace of diagnostic commands for inspecting thinslice internals. These are not part of normal workflow; they exist to help understand what thinslice thinks is true when something seems wrong.

slice debug port <number>

Looks up a specific port in the registry and reports what thinslice believes owns it, or confirms it is unregistered. Useful when a process is failing to bind and you want to know if thinslice has already claimed that port for another slot.

slice doctor

Runs an unbounded reconciliation pass: scans every thinslice.toml referenced in the registry, removes stale entries, and reports what it changed. Run this if slice debug registry shows entries you do not recognize, or after manually deleting slots or moving project directories.