archive

package
v0.10.0 Latest Latest
Warning

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

Go to latest
Published: Aug 25, 2026 License: MIT Imports: 30 Imported by: 0

Documentation

Overview

Package archive implements the transcript archive: one git repository shared by all of a user's machines, holding each machine's raw transcript trees under a top-level directory per machine (`<machine>/<source>/...`). The repo is the durable home for raw session bytes — local index dbs stay disposable derived caches.

All git knowledge, clone-layout knowledge, and machine-manifest handling live behind this package's small interface; nothing else in the codebase learns git. Git runs via the system binary through the unexported run seam (one real exec adapter, a fake for unit tests).

Index

Constants

View Source
const PrivacyWarning = `` /* 213-byte string literal not displayed */

PrivacyWarning is printed by `archive init`. Transcripts contain whatever was pasted into sessions; no host-side detection of repo visibility is attempted (unreliable across git hosts) — this warning is the mechanism.

Variables

View Source
var ErrBusy = errors.New("another rawclaw archive sync is already running on this machine")

ErrBusy reports that another rawclaw process on this machine holds the archive sync lock. Callers treat it as a clean "someone else is already syncing" no-op, not a failure: the holder's run (or the next sync) covers the same trees.

Functions

func AcquireAutosyncToken

func AcquireAutosyncToken(now time.Time) bool

AcquireAutosyncToken reports whether a background sync may spawn now, and atomically claims the slot when it may. The token is claimed at SPAWN time (not child completion), so a burst of invocations spawns one child even while that child is still running. Claiming = removing the stale token and re-creating it O_EXCL: of N racing processes exactly the creators win, and the rare double-winner (remove/create interleaving) costs one extra sync — the token rate-limits, it does not guard correctness (the flock does).

func AutosyncLogPath

func AutosyncLogPath() string

AutosyncLogPath is <state-dir>/archive/autosync.log — where the detached sync child's output lands (its receipt trail). The spawner redirects the child's stdout+stderr here; nothing is ever written to the invoking terminal.

func SSHDestination

func SSHDestination(machine string) (string, error)

SSHDestination resolves a machine name to the ssh destination `rawclaw live` dials: the config's ssh-map entry when one exists, else the name itself (letting ~/.ssh/config aliases carry user/port/key). Live peek works without a configured archive, so this reads the config file leniently — a missing file, or one without the archive's own remote/name fields, still resolves (an ssh-map-only config is valid for live). A config that EXISTS but cannot be parsed is an error: silently dropping a user's ssh map would dial the wrong destination and then blame the name.

Types

type Archive

type Archive struct {
	// contains filtered or unexported fields
}

Archive is a configured transcript archive: a remote, this machine's dir name, and the local clone the push/pull verbs operate on. Obtain one via Load (nil when the feature is unconfigured) or Init.

func Init

func Init(ctx context.Context, remote, name string) (*Archive, error)

Init bootstraps the archive: clones the remote (an empty remote clones fine; its default branch is born on the first push), registers this machine (a human-readable dir name + a manifest carrying the stable machine id), pushes the registration, and writes the config Load reads from then on.

Init refuses a machine dir already claimed by a DIFFERENT machine_id — pick another name with --name. A live config also refuses (with a pointer at the file to remove); after state loss (config gone, machine-id intact) re-init against the same remote is idempotent — the machine reclaims its own dir.

func Load

func Load() (*Archive, error)

Load resolves the archive configuration (env + config file). It returns (nil, nil) when unconfigured — every caller treats nil as "feature off", so the zero state costs one nil-check. RAWCLAW_ARCHIVE=off force-disables a configured archive. A present-but-unreadable config is an error: the user configured the feature and deserves to know it broke.

func (*Archive) ClonePath

func (a *Archive) ClonePath() string

ClonePath returns the local clone path the verbs operate on.

func (*Archive) ForeignProjectMatches

func (a *Archive) ForeignProjectMatches(project string) []string

ForeignProjectMatches reports the foreign machine names whose dir name or scope labels contain the project substring — the delete verb's guard. Foreign sessions are read-only from every box (no cross-machine delete in v1), so a delete filter that reaches into a foreign machine's scopes must be named, never silently ignored. Matching mirrors what search shows the user: the machine name and the "<machine>/<label>" scope labels — labels rebuilt here from the same layout claudeScopes/codexScopes read, rather than through Scopes() itself, because Scopes() ingests every scope into its cache db (far too heavy for a delete-time guard). Offline, clone-only; an absent clone matches nothing. A machine matches only if it actually holds at least one matching SESSION-bearing scope (or its name matches and it holds any sessions at all): naming a machine with nothing in it would send the user chasing sessions that do not exist.

func (*Archive) ForeignSessionMatches

func (a *Archive) ForeignSessionMatches(id string) []string

ForeignSessionMatches reports the foreign machine names holding a session the given id addresses (lifecycle.MatchesSessionID: exact id, or a >=8-char prefix) — the positional delete's counterpart to ForeignProjectMatches. Same posture: offline, clone-only, best-effort — an absent clone matches nothing, and the caller turns a foreign-only hit into the read-only refusal naming the origin machine.

func (*Archive) LookupScopes

func (a *Archive) LookupScopes() []view.Scope

LookupScopes enumerates the same foreign scopes as Scopes WITHOUT ingesting anything: each scope's DBP names the cache db a previous search-time ingest would have built; a scope never ingested simply fails to open read-only at the caller. This is the cheap path for point lookups (e.g. resolving a --resume prefix) where walking and indexing every foreign tree would be far too heavy. No staleness git probe runs either (Stale stays false — lookups don't report freshness), which keeps this path free of child processes, so it needs no watchdog ctx.

func (*Archive) Name

func (a *Archive) Name() string

Name returns this machine's top-level dir name in the archive.

func (*Archive) Pull

func (a *Archive) Pull(ctx context.Context, throttle bool) (pulled bool, err error)

Pull refreshes the clone from the remote (re-cloning it if it is missing — deleting a corrupt clone and pulling is the documented recovery). With throttle=true it no-ops unless the last successful pull is older than pullThrottleWindow, judged by the stamp file's mtime in the state dir; the explicit CLI verb passes false and always pulls. pulled reports whether the remote was actually consulted: true after any successful refresh — including "already up to date" and a still-empty remote (its branch is born on the first push; nothing-there is a verified-fresh state) — false only on a throttled skip, so callers can render the two honestly.

func (*Archive) PushLocal

func (a *Archive) PushLocal(ctx context.Context) (PushReport, error)

PushLocal copies this machine's transcript trees into the clone, commits, and pushes (pull --rebase + push, bounded retries). Idempotent; safe mid-session (transcripts are append-only, so a half-written file in the archive is valid and superseded by the next push). Returns a report for status output and logging.

func (*Archive) Remote

func (a *Archive) Remote() string

Remote returns the configured archive remote URL.

func (*Archive) Scopes

func (a *Archive) Scopes(ctx context.Context, reindex bool) []view.Scope

Scopes enumerates the clone's FOREIGN machine dirs as ready-to-search scopes: each foreign machine's Claude project dirs and Codex cwd-groups, ingested through the existing index paths into their own namespaced cache dbs, with origin_machine stamped from the dir's manifest. Our OWN dir is excluded — the live local tree is fresher and already indexed; that exclusion is what makes cross-machine dedup a non-event. A missing clone yields nil (enumeration never touches the network; `archive pull` is the refresh path). reindex forces a full rebuild of the scope dbs, mirroring the local scopes. ctx bounds the per-machine staleness git probes (dirStale), so they die with the caller's watchdog like every other git child.

func (*Archive) SetTagExporter added in v0.6.0

func (a *Archive) SetTagExporter(fn TagExporter) *Archive

SetTagExporter wires the local-tag source used by PushLocal. Returns the Archive for call chaining at the cli seam.

func (*Archive) Status

func (a *Archive) Status(ctx context.Context) (StatusReport, error)

Status reports clone path, remote, last push/pull (with own-sync overdue flags), and per-machine last-new-content times. A missing clone is a reported state (CloneOK=false, no machines), not an error — `archive pull` is the repair path.

type Config

type Config struct {
	Remote string `json:"remote"` // git remote URL of the archive repository
	Name   string `json:"name"`   // this machine's top-level dir name in the repo

	// SSH optionally maps a machine name to the ssh destination `rawclaw live`
	// dials (e.g. "box-a": "user@10.0.0.5"). Unmapped names default to the
	// name itself, so an ~/.ssh/config Host alias needs no entry here.
	SSH map[string]string `json:"ssh,omitempty"`
}

Config is the archive configuration persisted in the state dir by `archive init` and read back by Load.

type MachineStatus

type MachineStatus struct {
	Name       string    // top-level dir name in the archive
	MachineID  string    // stable machine id from the dir's manifest
	Own        bool      // this machine's own dir
	LastCommit time.Time // last commit touching the dir (zero = none yet) — last NEW CONTENT, not liveness
}

MachineStatus is one machine dir's recorded state in the clone.

type PushReport

type PushReport struct {
	Copied    int  // files copied into the clone this push
	Removed   int  // tombstoned own sessions removed from the clone this push
	TagFiles  int  // tag files written into <machine>/tags/ this push
	Committed bool // a commit was created (false = nothing changed)
	Pushed    bool // the commit reached the remote
	Retries   int  // rebase-retry rounds needed before the push landed
}

PushReport summarizes one PushLocal run for status output and logging.

type StatusReport

type StatusReport struct {
	Remote       string          // configured remote URL
	Clone        string          // local clone path
	CloneOK      bool            // a COMPLETED clone exists (sentinel present — ensureClone's own predicate)
	LastPush     time.Time       // last successful push sync from this machine, incl. verified no-ops (zero = never)
	LastPull     time.Time       // last successful pull on this machine (zero = never)
	PushOverdue  bool            // a recorded push sync exists but is older than the window (never ≠ overdue)
	PullOverdue  bool            // a recorded pull exists but is older than the window (never ≠ overdue)
	Machines     []MachineStatus // one entry per machine dir in the clone, own first
	TagConflicts []string        // sessions whose cross-machine tags disagreed; the winner is deterministic and every side's tag file is retained
}

StatusReport is the raw material for `archive status` and doctor-style output: where the archive lives, when this machine last synced, and when each machine's dir last received new content. Status is an OFFLINE read — recorded state only (config, stamp files, the clone's git history); it never fetches.

The only staleness verdicts here are the own-sync overdue flags: this machine knows first-hand when ITS last successful push/pull ran. A foreign machine's freshness is deliberately NOT judged — from the clone alone an idle-but-healthy machine (nothing new to commit) and a dead one are indistinguishable, so per-machine state is reported as the honest LastCommit ("last new content") and nothing more.

type TagExporter added in v0.6.0

type TagExporter func() ([]TagFile, error)

TagExporter returns THIS machine's locally-authored tag files. It is injected from the cli seam (SetTagExporter) because collecting local tags means enumerating local scopes + reading the store — deps the archive package deliberately does not carry (internal/scopes imports archive; the reverse would cycle). nil = the archive was built without the tag feature wired, so push simply skips tag export (transcripts still sync).

type TagFile added in v0.6.0

type TagFile struct {
	SessionID     string       `json:"session_id"`
	OriginMachine string       `json:"origin_machine"`
	Segments      []TagSegment `json:"segments,omitempty"`
	Verdict       *TagVerdict  `json:"verdict,omitempty"`
}

TagFile is one session's tagging, serialized as `<machine>/tags/<session>.json`. It is the whole authored unit for that session on the writing machine: the segment set plus the optional verdict, stamped with the origin machine id the cross-machine ingest resolves on. One file per session — a re-tag overwrites it (dedup by path), and it diffs cleanly in git.

type TagSegment added in v0.6.0

type TagSegment struct {
	StartUUID string  `json:"start_uuid"`
	EndUUID   string  `json:"end_uuid,omitempty"`
	Topic     string  `json:"topic"`
	Summary   string  `json:"summary,omitempty"`
	TaggedAt  float64 `json:"tagged_at,omitempty"`
}

TagSegment is one topic segment in a TagFile.

type TagVerdict added in v0.6.0

type TagVerdict struct {
	Verdict  string  `json:"verdict"`
	Source   string  `json:"source"`
	TaggedAt float64 `json:"tagged_at,omitempty"`
}

TagVerdict is a session's verdict in a TagFile (e.g. routine + floor|agent).

Directories

Path Synopsis
Package archivetest stands up throwaway transcript-archive fixtures: an isolated HOME with one local Claude project, a configured archive against a local bare remote, and one FOREIGN machine dir pushed by a simulated second machine and pulled into the local clone.
Package archivetest stands up throwaway transcript-archive fixtures: an isolated HOME with one local Claude project, a configured archive against a local bare remote, and one FOREIGN machine dir pushed by a simulated second machine and pulled into the local clone.

Jump to

Keyboard shortcuts

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