handshake

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 19, 2026 License: MIT Imports: 5 Imported by: 0

Documentation

Overview

Package handshake is the daemon↔client version contract.

A ty install is several processes of the same program: the daemon that runs agents, the TUI, and every short-lived CLI invocation. They are supposed to be the same build reading the same database, talking to the same tmux server, resolving the same Claude config dir. When they are not, the symptom is never "version mismatch" — it is an agent that resumes into an empty session, a hook that writes a status nobody reads, or a fix that is built, installed and apparently ignored because the daemon from twenty minutes ago is still the one executing tasks.

So the daemon writes down what it is and where it lives, and clients read that record at startup and say so out loud.

Index

Constants

View Source
const (
	CheckBuild = "daemon-handshake"
	CheckEnv   = "daemon-env"
)

Check ids produced by Compare. They are part of `ty doctor --json`'s stable shape, so they are constants rather than literals scattered around.

View Source
const ContractFingerprint = "955cf702ca9431fd"

ContractFingerprint pins the contract inputs Protocol covers. See contract_test.go; it prints the replacement value when the inputs move.

View Source
const Protocol = 4

Protocol is the version of the daemon↔client contract this build speaks.

It is NOT the release version: it moves only when the daemon and a client must agree on something that changed underneath them. Bump it by one — never reusing a number — when any of these change:

  • db.SchemaVersion: the daemon writes rows an older client cannot read, or reads columns a newer client has stopped writing.
  • executor.ClaudeHookEvents: the set of Claude hook events ty installs into a task worktree, or the payload those hooks send back to `ty claude-hook`. A daemon that installs hooks a client does not understand silently loses status transitions.
  • the tmux conventions in internal/tmuxctl: the agent server socket name, the pane tags, the daemon session and window naming. Two builds that disagree here cannot find each other's agents.

Those three inputs are fingerprinted by TestContractFingerprint. Change one without bumping Protocol and the test fails, printing the fingerprint to paste back in — so the bump is a deliberate act, not a thing you remember. 2: the Claude hook set gained SessionStart/UserPromptSubmit/StopFailure/ SessionEnd, and hook payloads are now checked against the session that owns the task. A daemon that installs those hooks against a client too old to handle them loses the transitions they carry.

3: per-task turn counters (task_turns, db.SchemaVersion 2). The hooks that advance them run the DAEMON's binary, so a daemon from before this writes no counters — and a newer client that sends a prompt and waits for the reply then waits for something that cannot happen. It has to be able to say so.

4: attachment bytes moved to local files (db.SchemaVersion 3). Older executors would read the empty compatibility blob and silently lose files.

Variables

This section is empty.

Functions

func RecordPath

func RecordPath(pidFile string) string

RecordPath is where a daemon whose pid file is pidFile writes its record — beside the pid file, alongside the existing .mode and .lock siblings.

func Write

func Write(path string, r Record) error

Write saves r to path, atomically: a client must never read a half-written record and conclude the daemon is a build that does not exist.

Types

type Finding

type Finding struct {
	ID       string
	Severity Severity
	Summary  string
	Detail   string
}

Finding is one comparison result, shaped so `ty doctor` can render it directly as a check.

func Compare

func Compare(daemon *Record, client Record) []Finding

Compare reports how a client lines up with the running daemon.

daemon == nil means the daemon left no readable record. That is not an error: it is what every ty from before this handshake looks like, and what a remote or placed host running a slightly older ty looks like from here. Such a daemon is reported as info and is never blocked — see the package docs on mixed fleets.

It always returns exactly two findings, CheckBuild then CheckEnv, so `ty doctor --json` has the same check ids on every machine.

type Record

type Record struct {
	Version    string    `json:"version"`  // build, e.g. "0.9.3" or "dev"
	Protocol   int       `json:"protocol"` // Protocol of the build that wrote this
	PID        int       `json:"pid"`      // the daemon process
	StartedAt  time.Time `json:"started_at"`
	Executable string    `json:"executable"` // resolved path of the running binary

	// The environment two processes must agree on to see the same world.
	ClaudeConfigDir string `json:"claude_config_dir"`
	TmuxSocket      string `json:"tmux_socket"` // "" means tmux's default server
	DBPath          string `json:"db_path"`
}

Record is what a daemon writes down about itself, and what a client compares itself against. Every field is stable JSON: an older ty must be able to read a newer record without choking, so fields are only ever added.

func Read

func Read(path string) (*Record, error)

Read loads a record. A missing file is reported as os.ErrNotExist, which callers treat as "the daemon predates the handshake" rather than an error.

type Severity

type Severity string

Severity ranks a finding. The zero value is intentionally invalid so a finding always carries an explicit one.

const (
	// SeverityOK means the two agree.
	SeverityOK Severity = "ok"
	// SeverityInfo is worth printing but is not a problem — chiefly a daemon
	// too old to have left a record at all.
	SeverityInfo Severity = "info"
	// SeverityWarning is a divergence that can bite but does not break the
	// contract: same protocol, different build; or a differing environment.
	SeverityWarning Severity = "warning"
	// SeverityError is a broken contract: different protocols.
	SeverityError Severity = "error"
)

func Worst

func Worst(sevs ...Severity) Severity

Worst returns the most severe of the given severities, SeverityOK for none.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL