locallink

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Nov 13, 2026 License: MIT Imports: 46 Imported by: 0

README

Local session linkage

The desktop app, local PR viewer, review bundles and feature log share this resolver. It reads the clone and local session stores without fetching.

Entire checkpoints discover candidates. Each commit exposes their session IDs as checkpoint_sessions, separately from links. Membership survives missing transcripts, but does not add linked session cards, transcript access, review context or feature attribution.

Links require independent evidence: a successful recorded commit creation (commit_ref) or timed edit overlap (files_overlap). Creation does not prove that the session authored all committed edits. The starting-parent shortcut is disabled locally because concurrent sessions commonly share it. Exact edit-score ties remain unresolved. Contributors outside a checkpoint remain eligible, and checkpoint membership never breaks a tie. The hosted resolver retains its own policy; this package does not change it.

The daemon durably records session-owned Git hooks in the application database (commitattribution, bucket attribution/commits). The shared resolver reads post-commit observations as commit_ref evidence, scoped to the clone's remote repository identities and exact commit SHA. These observations take precedence over transcript guesses about commit creation. Their provenance includes agent, session, hook event and observation time. A missing transcript keeps the link and metadata-only review context; it does not invent conversation or usage. Push observations are retained separately and never establish creation.

Git reflogs supply creation evidence too, for commits, amends, cherry-picks, reverts, applied patches and rebase picks (pull --rebase included). A reflog entry made while one session's shell tool call ran, in the same working tree, establishes that session as the creator. The call must have succeeded (a rejected, denied or failed call, such as git commit with nothing to commit, establishes nothing, and a call that waited for permission counts only if it then succeeded) and its command must write Git: the first Git subcommand (after global options such as -C and -c) is commit, rebase, cherry-pick, revert, am or pull. A call that merely overlaps, such as a long test run or a poll of a running command, establishes nothing. Commits made through a wrapper script (make release) get no reflog evidence and fall back to the other evidence. Subagent calls count for their top-level session. Calls are read from any tool the session graph marks as a command (the execute verb) that records its start and end times; today that is Claude Code and Codex.

A commit object is made once, so only the earliest creation entry of a commit counts. A later entry is a ref moving to an existing commit, and cherry-pick --ff is never creation. The entry's time is the committer time Git recorded, so GIT_COMMITTER_DATE or a script that preserves dates can move it outside the call.

The HEAD logs (the main working tree's and each linked worktree's) are authoritative: a commit one records is placed only by a call in that working tree, and a HEAD log of unknown working tree places nothing. Only a commit no HEAD log records comes from the branch logs, which know no working tree, so only a call in a working tree that no longer exists, on the same branch (the session's recorded branch at the call; unknown matches nothing), may match. This covers plain commits, amends, cherry-picks and reverts of a removed worktree, not its rebase picks (a branch log records only the end of a rebase) and not once the branch is deleted. Reflogs expire with Git's gc (90 days by default).

A call starts in its workdir, else in the nested session's cwd attribute and working directory, then the top-level session's. Its commands are read by agentcmd without tree-sitter: Claude Code's Bash, Codex's exec_command and shell with their workdir, and Codex code-mode exec programs. A code-mode program is stored as its JavaScript plus the command lines it runs; agentcmd reads the program itself, so each tools.exec_command({...}) call is a command with its own workdir and no cd carried between them. A command or workdir the program builds at run time (a template, a variable, a shorthand {workdir}, a spread or computed key) gives no evidence. Another tool marked as a command is read as shell from its command or cmd field. Only commands that run count, in a command substitution or a script fed to a shell too, never quoted text or a heredoc that is only data, and a script that does not parse gives no evidence. A cd or pushd to a literal path moves the directory within its scope (a subshell has its own), and bash -c scripts, shell functions and wrappers (env -C, timeout, nohup, xargs) are followed. A dynamic path, ~, cd - or pushd +N leaves the directory unknown, and so does a cd inside a conditional or loop. git notes and git apply make no commit in a working tree's history. Unplaced, too:

  • commands run on another host or in a container (ssh, docker, kubectl);
  • commands run by find, since -execdir runs them in each match's directory;
  • Git redirected by --git-dir, --work-tree, core.worktree or a config include, and options that only print (--exec-path, --version);
  • a --dry-run;
  • every command of a call that mentions GIT_DIR, GIT_WORK_TREE, GIT_COMMON_DIR or a GIT_CONFIG variable, which agentcmd does not follow, also when spelled with JSON, JavaScript or $'...' escapes;
  • a Windows path in a workdir.

A directory inside a nested repository (a submodule, or a clone within the working tree: a .git entry below the working tree's root) is not placed, since its commits are not in this reflog. Directories map to the deepest existing linked worktree, else a removed worktree kept in an agent's conventional folder inside the main working tree, else the main one.

A command that yielded while it ran (Codex exec_command with a short yield_time_ms, whose result reports a process id as "session_id":N or session ID N) can finish during later polls. Its window then extends to the last successful write_stdin or wait call on the same process, following polls that still find it running until one finds it finished. Where the result does not report the process id (a program that does not print it), the window is the call's own. Only a call that ran one command is extended this way: in a code-mode program with several, a process id may be another command's (a test run after the commit). A poll that finds the process exited with a non-zero status means the command failed, and the call gives no evidence.

A code-mode program that completed says nothing about its commands. It gives evidence only when its printed results show how each went (an exit status or a process id per command) and none exited with a non-zero status. A Codex call whose result reports a non-zero exit gives none either.

When several sessions fit, or none, the commit stays unresolved. Merges are never reflog evidence. A reflog match outranks a hook observation of the same commit, which falls back to guessing the session from the process tree when the agent exports no session id; hook observations still decide when the reflog has no single Git-writing creator. A background command that commits after its tool call returned, and that no poll covers, lies outside every window. Entries are read from the newest 8 MiB of each reflog. ReflogFingerprint hashes the creation entries of the pull request's commits so the evidence and feature caches notice a new one and ignore other reflog activity. A session's Git-writing calls depend on its graph alone, so its session digest keeps them: a session BuildSessionEvidence reuses without mapping still has its windows, and another session never wins a commit just because the reused one was not mapped.

Quiet commits followed immediately by git log on a new line also supply creation evidence when the tool succeeded and its output has no explicit commit-failure diagnostic. The command parser excludes quoted commands, heredoc text, background/negated commands and intervening checkout commands.

Local matching still uses path overlap rather than content equivalence. A matched path set does not establish that every contributor was discovered. Checkpoint IDs provide the narrower pool for later evidence inspection or adjudication without asserting those candidates as contributors.

BuildSessionEvidence maps only the sessions that changed since the process last read them. It keeps a digest per session: what linking reads from the graph and, for a linked session, the evidence built for it with what that evidence depended on besides the transcript. The digest is reused only when all of it is unchanged, so the evidence is the same as a full rebuild. Local session evidence describes the keys. LinkCommits shares the same bounded compact facts cache and skips unchanged session graphs too, including transcripts older than an hour and graphs evicted from the mapping cache. It recomputes observed and reflog creation evidence for the current commits, so changing external evidence still changes the links. If facts needed for an unmapped session are missing, it retries with full mapping. Resolve also selects links using those compact facts, then maps only linked sessions to produce their complete rows and transcript graphs. If a remap changes the selected sessions it retries, with a full read as the final fallback. Unlinked unchanged candidates need no graph.

Run just oss-go test ./internal/locallink/... from the monorepo root. The tests cover concurrent readers/editors, stale sessions, matching-parent false positives, ambiguous edits, outside contributors, checkpoint discovery and offline behavior.

ResolveSummary returns the same redacted session/commit JSON from compact facts, including title, outcome and edited-file count, without retaining linked graphs. The local app uses this for lists and decision attribution. Its lazy LoadGraph reader rechecks linkage against the same pinned commits when a transcript is explicitly opened; a session no longer linked yields no graph. The viewer shares and bounds this lazy read like its other transcript builds, and the daemon applies its existing local-read semaphore. Default Resolve still supplies complete linked graphs for callers that need them.

Digests may also keep an optional context-independent evidence basis (at most 4 MiB per session, inside the existing 64 MiB total). It contains only fields read by the existing evidence projector: session/path/spawn metadata, bounded prompt/latest-reply text, distinct model names, execute inputs and statuses, and file-touch paths. Tool output, assistant content and patch hunks are not kept. New PR paths, checkout placement, caps and thread names are projected again from these inputs without decoding an unchanged source. All retained strings and bytes are detached from the original graph.

Compact facts take priority: optional bases may shed colder optional bases to fit, but never evict facts for admission. Essential data can discard bases first under pressure. Missing, oversized or inadmissible bases retain the full read fallback, which may refill a hot basis. locallink.evidence_basis counters and its bytes gauge distinguish reuse, size rejection and memory pressure.

Documentation

Overview

Package locallink links the coding-agent sessions recorded on this machine to the commits of a pull request, using only this machine: the clone containing a directory supplies the commits and the home-directory and checkpoint stores supply the sessions. It is the local counterpart of the hosted service's linkage resolver and runs the same matcher over the same kinds of evidence.

It never fetches. Commits the clone does not hold make the answer unavailable rather than being downloaded, and nothing here contacts a server. Every piece of text Resolve returns has been redacted the way "the local session reader" redacts its listing.

A caller such as "the reference viewer host" serves the Result. BuildSessionEvidence gives "the bundle assembler" the same linked sessions as bundle sessions, which the bundle's own input redaction masks.

Index

Constants

View Source
const (
	// CodeNotAFile is a path the head does not hold as a file: a directory,
	// a submodule, or no entry at all.
	CodeNotAFile = "not_a_file"
	// CodeNoText is a file whose content is not text (it holds a NUL byte).
	CodeNoText = "no_text"
)

Codes a FileResult can carry, besides those the sessions answer shares.

View Source
const (
	// CodeSample is the built-in sample document, which belongs to no clone.
	CodeSample = "sample"
	// CodeNoHead is a document that does not name a repository and full head
	// and base commit ids.
	CodeNoHead = "no_head"
	// CodeNotAClone is a directory outside a git clone with a working tree.
	CodeNotAClone = "not_a_clone"
	// CodeRemoteMismatch is a clone none of whose remotes is the document's
	// repository.
	CodeRemoteMismatch = "remote_mismatch"
	// CodeCommitsMissing is a clone that lacks the pull request's commits.
	CodeCommitsMissing = "commits_missing"
	// CodePartialClone is a partial clone that has the pull request's commits
	// but not the trees or blobs reading them needs.
	CodePartialClone = "partial_clone"
	// CodeAmbiguousCommit is an abbreviated head or base that names more than
	// one commit in the clone.
	CodeAmbiguousCommit = "ambiguous_commit"
	// CodeTooManyCommits is a pull request with more commits than are listed.
	CodeTooManyCommits = "too_many_commits"
	// CodeError is anything else that stopped the read.
	CodeError = "error"
)

Codes say why a Result is unavailable, for a reader to branch on. The Reason beside each is for people.

View Source
const (
	// CodeNoTextPatch is a file whose change has no text hunks (a binary file,
	// or a change of mode alone), a submodule, or a diff that is not one file's.
	CodeNoTextPatch = "no_text_patch"
	// CodeUnchanged is a path the range does not change.
	CodeUnchanged = "unchanged"
	// CodeTooLarge is a patch over MaxPatchBytes.
	CodeTooLarge = "too_large"
)

Codes a PatchResult can carry, besides those the sessions answer shares.

View Source
const DefaultDigestBytes = 64 << 20

DefaultDigestBytes bounds the memory the session digests take, roughly estimated. A digest is a few kilobytes for most sessions, against the megabytes of the transcript it was read from.

View Source
const EvidenceMaxAge = 10 * time.Minute

EvidenceMaxAge bounds how long BuildSessionEvidence reuses an answer whose inputs look unchanged. What the key cannot see, such as a file created in the checkout that a session touched, shows within this time.

View Source
const MaxFileBytes = 512 << 10

MaxFileBytes is the most of a file ReadFile returns; a longer file is cut there and marked truncated.

View Source
const MaxPatchBytes = 1 << 20

MaxPatchBytes is the largest file patch Patch returns. A larger one is unavailable (CodeTooLarge) rather than sent in part.

Variables

View Source
var SessionWindow = linkage.DefaultConfig().TimeframeTolerance() + 24*time.Hour

SessionWindow is how long before a pull request's first commit a session may have last been written and still be read. The matcher links a commit only to a session whose end, or last write when it recorded none, is within its timeframe tolerance (a day) of the commit, or that created the commit by id, and a session that created a commit was written to at or after it. A day of margin covers clock skew. Older sessions cannot match, and skipping them is what keeps a machine with years of sessions from reading all of them.

Functions

func BuildSessionEvidence

func BuildSessionEvidence(ctx context.Context, req EvidenceRequest) ([]bundle.Session, error)

BuildSessionEvidence returns the bundle sessions for the pull request commits: every session on this machine matched to one of them, as Resolve matches them, baum's own generation runs left out (sessions.IsGenerationRun). They are ordered by linkage confidence, the highest any of the session's edges has, then by start, earliest first and unknown last, then by id.

The sessions are not redacted: the bundle's input redaction (bundle.Redact) masks them with the rest of the bundle before it is trimmed or written, as for every other source. A command event carries only the paraphrase bundle.ParseCommandParaphrase gives, and a touched path only a path inside one of the clone's checkouts, made relative to it; prompt and reply text is bounded by bundle.PromptScanChars.

func CitedFiles

func CitedFiles(document []byte) map[string]struct{}

CitedFiles reads the paths a review document cites, which are the only ones the app's file routes read (GET /api/file, a group's file): the context's references (summary.context.<part>.refs[].path), the hunks' paths and old paths (index.hunks[].path, old_path), and the path of every anchor of type "file" anywhere in the document. A document that does not parse cites none.

func CoauthoredAgents

func CoauthoredAgents(trailers []string) []string

CoauthoredAgents are the agents a commit's Co-Authored-By trailers name by address, each once, in the order they appear; a person or an unknown address names none.

func EmbedPatches

func EmbedPatches(ctx context.Context, dir string, document []byte, maxBytes int64) ([]byte, error)

EmbedPatches is a pull request document that can be read without the clone: each indexed hunk without a diff gets its text from its file's patch, read from the clone containing dir by the document's own repository, head, base and merge base and masked as Patch masks it (review.EmbedHunkPatches), and each without a github_url gets the derived one. A hunk whose patch the clone cannot provide (missing commits, no matching remote, a binary file, a patch over MaxPatchBytes) keeps no diff, and so does every hunk once maxBytes of patch text have been read (0 is no limit). A failure to read the clone that may pass, such as git failing, also leaves the hunk without a diff rather than failing: the document is still the one the store holds. Only the context ending fails it.

The patches are read ahead, embedReads at a time, in the order they are used, and used one by one in that order, so which hunks get text within maxBytes does not depend on which read finishes first. One Patcher reads them all, so the clone is opened and the range resolved once.

func ImagePromptResolver added in v0.2.0

func ImagePromptResolver(s sessions.Session) func(string) (bundle.ImageOrigin, bool)

ImagePromptResolver locates image inputs in the linked root conversation, sharing spawn ownership across the images of one session.

func RedactGraph

func RedactGraph(ctx context.Context, engine *redact.Engine, graph *session.Graph, indent bool) ([]byte, error)

RedactGraph encodes a session graph as JSON and masks every secret and e-mail address in it with redact.Engine.RedactJSON: found over the whole encoded text, so a tool input's key and value are caught together, and in each string as decoded, so a line of a tool's output is read as a line. The result stays valid JSON. "locallink.RedactGraph" prints it indented.

func ReflogFingerprint added in v0.2.0

func ReflogFingerprint(ctx context.Context, repo *gitlocal.Repo, dir string, commits []string) string

ReflogFingerprint describes the creation entries the clone's reflogs hold for the commits, for a cache that must notice a new one. Moving HEAD or a branch without making one of the commits does not change it. A clone whose reflogs cannot be read fingerprints as having no entries. repo, when not nil, is the clone of dir already open.

Types

type Commit

type Commit struct {
	OID     string `json:"oid"`
	Subject string `json:"subject"`
	// Author is the author's name; an e-mail address is never included.
	Author      string `json:"author,omitempty"`
	CommittedAt string `json:"committed_at,omitempty"`
	// Links are the sessions matched to the commit, strongest first.
	Links []Link `json:"links"`
	// CheckpointSessions are candidate IDs recorded by the commit's checkpoints,
	// including sessions whose transcripts could not be read. Membership alone
	// never adds a session to Links, Sessions or Graphs.
	CheckpointSessions []string `json:"checkpoint_sessions,omitempty"`
}

Commit is one commit of the pull request. All text is redacted.

type CommitDetail

type CommitDetail struct {
	// Available is true when Body and Files hold the commit's.
	Available bool
	// Code says why they do not, when they do not.
	Code string
	OID  string
	// Parent is the commit's first parent, "" for a root commit.
	Parent string
	// Body is the commit's message after its subject line and the blank lines
	// that follow it, without its final newline.
	Body  string
	Files []CommitFile
}

CommitDetail is the outcome of ReadCommit.

func ReadCommit

func ReadCommit(ctx context.Context, req CommitRequest) (CommitDetail, error)

ReadCommit reads a commit's message body and the files it changed against its first parent (the empty tree for a root commit) from the clone containing req.Dir, once the clone is matched to req.Repository. It never fetches. A commit that cannot be read from this machine is an unavailable CommitDetail, not an error.

func (CommitDetail) File

func (d CommitDetail) File(path string) (CommitFile, bool)

File is the changed file at path, or the renamed file whose old path is path; false when the commit did not change path.

func (CommitDetail) Stable

func (d CommitDetail) Stable() bool

Stable reports whether the answer holds while the clone keeps the commit: the commit's detail, or one too large to read. The other unavailable answers can change when a person fixes the clone.

type CommitFile

type CommitFile struct {
	// Path is the file's path after the commit (before it, for a deletion).
	Path string `json:"path"`
	// OldPath is the path the file had before a rename.
	OldPath string `json:"old_path,omitempty"`
	// Status is added, modified, deleted or renamed. A change of type (a file
	// to a symlink, say) is modified.
	Status string `json:"status"`
	// Additions and Deletions are the lines the commit added to and removed
	// from the file; both are left out for a binary file.
	Additions *int64 `json:"additions,omitempty"`
	Deletions *int64 `json:"deletions,omitempty"`
	// Submodule and TypeChanged say how to read the file's patch.
	Submodule   bool `json:"-"`
	TypeChanged bool `json:"-"`
}

CommitFile is one file a commit changed.

type CommitLink struct {
	Commit string
	// Session is the session's id and Vendor the agent that ran it.
	Session, Vendor string
	Method          string
	Confidence      float64
}

CommitLink is one session matched to a commit.

func LinkCommits

func LinkCommits(ctx context.Context, dir string, commits []gitlocal.Commit, opts LinkOptions) ([]CommitLink, error)

LinkCommits matches the commits, of any pull requests of the clone containing dir, to the sessions recorded for that clone, exactly as Resolve matches a pull request's commits: by their Entire checkpoints, then the matcher's default steps, baum's own generation runs left out and only sessions written within SessionWindow of the commits read. Nothing is redacted: the links carry ids only.

type CommitPatchRequest

type CommitPatchRequest struct {
	Dir, Repository string
	// Commit is the commit as ReadCommit read it, and File one of its files.
	Commit CommitDetail
	File   CommitFile
}

CommitPatchRequest names one file of a commit whose patch to read.

type CommitRequest

type CommitRequest struct {
	// Dir is a directory inside the clone; "" means the current directory.
	Dir string
	// Repository is the pull request's "owner/name"; the clone must have a
	// remote for it, as Resolve requires.
	Repository string
	// OID is the commit's full id.
	OID string
}

CommitRequest names a commit to read and where.

type EvidenceRequest

type EvidenceRequest struct {
	// Dir is the repository root or any directory inside the clone; ""
	// means the current directory.
	Dir string
	// Commits are the pull request's commits as the clone holds them, oldest
	// first: gitlocal.Repo.Commits from the merge base to the head.
	Commits []gitlocal.Commit
	// Sessions narrows or redirects the stores read. Its PreviousPaths are
	// the directories the repository used to live at (--previous-path).
	Sessions sessions.Options
	// MaxPromptsPerSession and MaxCommandEventsPerSession are the bundle caps
	// of the same names, which the source applies: a session keeps its first
	// prompt and its latest MaxPromptsPerSession-1 (none at 0), and its
	// latest MaxCommandEventsPerSession command events (all at 0).
	MaxPromptsPerSession, MaxCommandEventsPerSession int
	// Warn receives one line for each session store file or checkpoint that
	// could not be read. It defaults to discarding them.
	Warn io.Writer
	// Frozen answers with the evidence this process last built for the same
	// request, without looking at the session stores, when there is one: the
	// commits belong to a pull request that is closed or merged. Only the
	// first frozen request in a process reads the stores.
	Frozen bool
	// Repo, when set, is the clone opened by the caller, which every read of
	// it (Entire checkpoints included) goes through instead of opening its
	// own. It must be the clone containing Dir.
	Repo *gitlocal.Repo
}

EvidenceRequest says which pull request commits BuildSessionEvidence links sessions to, where it looks for them, and how much of each it keeps.

type FileRequest

type FileRequest struct {
	// Dir is a directory inside the clone; "" means the current directory.
	Dir string
	// Repository is the pull request's "owner/name"; the clone must have a
	// remote for it, as Resolve requires.
	Repository string
	// Head is the commit to read the file at, full or abbreviated.
	Head string
	// Path is the repository-relative path of the file.
	Path string
}

FileRequest names one file to read at a pull request's head, and where.

type FileResult

type FileResult struct {
	// Available is true when Content holds the file's text.
	Available bool
	// Content is the file's text, at most MaxFileBytes, with any invalid
	// UTF-8 dropped.
	Content string
	// Truncated says the file goes on past Content.
	Truncated bool
	// Code says why Content is empty, when it is.
	Code string
}

FileResult is the outcome of ReadFile.

func ReadFile

func ReadFile(ctx context.Context, req FileRequest) (FileResult, error)

ReadFile reads one file at req.Head from the clone containing req.Dir, once the clone is matched to req.Repository. It never fetches. A file that cannot be read from this machine is an unavailable FileResult, not an error: a directory, a submodule or a missing path (CodeNotAFile), a binary file (CodeNoText), a file over gitlocal's blob cap (CodeTooLarge), a head the clone lacks. The caller decides which paths may be read; ReadFile only refuses one git would read outside the tree.

func (FileResult) Stable

func (r FileResult) Stable() bool

Stable reports whether the answer holds while the document stays the same: the file's text, or a path that has none at the head. The other unavailable answers can change when a person fixes the clone.

type Link struct {
	Session string `json:"session"`
	// Method is the independent evidence that matched: commit_ref for commit
	// creation or files_overlap for inferred contribution.
	Method     string  `json:"method"`
	Confidence float64 `json:"confidence"`
}

Link is one session matched to a commit.

type LinkOptions

type LinkOptions struct {
	// Repository is the commits' "owner/name"; the clone's remote naming it
	// is the one a missing Entire checkpoint is fetched from.
	Repository string
	// Sessions and Warn are as in Request.
	Sessions sessions.Options
	Warn     io.Writer
}

LinkOptions say where LinkCommits looks.

type PatchRequest

type PatchRequest struct {
	// Dir is a directory inside the clone; "" means the current directory.
	Dir string
	// Repository is the pull request's "owner/name".
	Repository string
	// Head, Base and MergeBase bound the pull request. The patch is from the
	// merge base to the head, or from the base when the document records no
	// merge base. A merge base the clone lacks makes the patch unavailable:
	// the base would give a different diff.
	Head, Base, MergeBase string
	// Path is the repository-relative path of the file at the head.
	Path string
}

PatchRequest says which file's patch to read and where.

type PatchResult

type PatchResult struct {
	// Available is true when Patch holds a text patch.
	Available bool
	// Patch is the file's diff from its first "@@" hunk header on. git's header
	// lines before it ("diff --git", "index", the rename lines, "---" and
	// "+++") are left out; the viewer reads only the hunks.
	Patch string
	// Code says why Patch is empty, when it is.
	Code string
}

PatchResult is the outcome of Patch.

func CommitPatch

func CommitPatch(ctx context.Context, req CommitPatchRequest) (PatchResult, error)

CommitPatch reads one file's patch in a commit, against the commit's first parent (the empty tree for a root commit), as Patch reads a pull request's: rename-aware, from its first "@@" line, git's header lines left out. A submodule, a binary file or a patch over MaxPatchBytes is an unavailable PatchResult. Text is masked as the stored review's hunks are.

func Patch

func Patch(ctx context.Context, req PatchRequest) (PatchResult, error)

Patch is Patcher.Patch with nothing kept between calls.

func (PatchResult) Stable

func (r PatchResult) Stable() bool

Stable reports whether the answer holds until the document changes: a patch, or a file that has no text patch. The other unavailable answers (not a clone, no matching remote, missing commits) can change when a person fixes the clone, so they are asked again.

type Patcher

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

Patcher reads file patches for pull requests. It finds the files a range changes, and so the old name of each renamed one, once for a merge base and head, and keeps that for the last few ranges while the answer holds. It also keeps, for the last few clones and ranges it read, the opened clone, its matched remote and the range's resolved commits, for rangeIdle after the last read, so a further file of a known range costs one git diff. Requests for the same patch of the same clone that arrive while one is read share that read, whichever document or version asked. The zero value is ready. It is safe for concurrent use; Close releases what it keeps.

func (*Patcher) Close

func (p *Patcher) Close()

Close closes every clone the Patcher keeps open, once the reads using it end. The Patcher stays usable: a later read opens the clone again.

func (*Patcher) Patch

func (p *Patcher) Patch(ctx context.Context, req PatchRequest) (PatchResult, error)

Patch reads one file's patch for a pull request from the clone containing req.Dir, once the clone is matched to the pull request's repository as Resolve matches it. It never fetches. A patch that cannot be read from this machine is an unavailable PatchResult, not an error; an error is a failure to read something that should have been readable.

The diff is the one the hosted service reads: -M and -l1000 over the file's path, and over its old path too when the range renamed it, so a renamed file shows only what changed in it. A path the range does not change (a directory is one) is unchanged, and a submodule or a diff that is not one file's (a type change has two sections of its path) has no text patch. The patch is then masked as the bundle masks a stored hunk's text (bundle.RedactFilePatch), so it holds no secret a document would have hidden.

Requests for the same clone, repository, range and path share a read that is running. A request whose shared read failed, its context ended included, reads again.

type Request

type Request struct {
	// Dir is a directory inside the clone; "" means the current directory.
	Dir string
	// Repository is the pull request's "owner/name".
	Repository string
	// Head, Base and MergeBase bound the pull request, as full or abbreviated
	// (7 or more hex digits) commit ids. The pull request's commits are those
	// from the merge base to the head, so MergeBase is used when the clone
	// has it, and Base otherwise. MergeBase may be empty.
	Head, Base, MergeBase string
	// Sessions narrows or redirects the stores read; the zero value reads
	// the defaults. Its Sources do not stop the checkpoint a commit's
	// Entire-Checkpoint trailer names from being read.
	Sessions sessions.Options
	// Warn receives one line for each session store file that could not be
	// read, as "the local session reader" prints them, and for each malformed
	// Entire-Checkpoint value skipped beside a valid one. It defaults to
	// discarding them.
	Warn io.Writer
}

Request says which pull request to link and where to look.

type Result

type Result struct {
	Available bool
	// Code and Reason are set when the result is unavailable.
	Code, Reason string
	// Sessions are those with at least one matched commit, oldest first.
	Sessions []Session
	// Commits are every commit of the pull request, oldest first.
	Commits []Commit
	// Graphs are the linked sessions' graphs by id, as read and unredacted,
	// for a caller that shows a session's transcript; RedactGraph redacts
	// one. They are never part of the JSON.
	Graphs map[string]*session.Graph
	// LoadGraph is set by ResolveSummary. It obtains a graph only for an
	// explicit transcript read with its current linked metadata, rechecking that it is still linked
	// to this request's pinned commits. It is never part of the JSON.
	LoadGraph func(context.Context, string) (Result, error)
}

Result is the outcome of Resolve. Its JSON is

{"available":true,"sessions":[...],"commits":[...]}

or {"available":false,"code":"...","reason":"..."}.

func Resolve

func Resolve(ctx context.Context, req Request) (Result, error)

Resolve reads the pull request's commits from the clone containing req.Dir and the sessions recorded for that clone, and matches them. A pull request that cannot be linked from this machine is an unavailable Result, not an error; an error is a failure to read something that should have been readable.

func ResolveSummary added in v0.2.0

func ResolveSummary(ctx context.Context, req Request) (Result, error)

ResolveSummary returns exactly Resolve's JSON without retaining full graphs. Compact row facts outlive graph-cache eviction; transcript readers may call LoadGraph to revalidate linkage and obtain the graph on demand.

func Unavailable

func Unavailable(code, reason string) Result

Unavailable builds the Result for a pull request that cannot be linked.

func (Result) MarshalJSON

func (r Result) MarshalJSON() ([]byte, error)

MarshalJSON writes the shape documented on Result; an available result always carries both lists, empty or not.

type Row

type Row struct {
	Source      string              `json:"source"`
	Vendor      string              `json:"vendor"`
	ID          string              `json:"id"`
	Title       string              `json:"title,omitempty"`
	Branch      string              `json:"branch,omitempty"`
	StartedAt   *time.Time          `json:"started_at,omitempty"`
	EndedAt     *time.Time          `json:"ended_at,omitempty"`
	Outcome     session.Outcome     `json:"outcome"`
	Commits     map[string][]string `json:"commits,omitempty"`
	EditedFiles int                 `json:"edited_files"`
	Path        string              `json:"path"`
	Notes       []string            `json:"notes,omitempty"`
}

Row is one session as "the local session reader" lists it, and the JSON shape of its --json output. Its text is redacted.

func RowOf

func RowOf(engine *redact.Engine, s sessions.Session) (Row, error)

RowOf summarises s for a listing, redacting its title with engine.

type Session

type Session struct {
	// ID is the vendor's own session id, stable across runs.
	ID string `json:"id"`
	// Source is the store the session was read from and Vendor the agent
	// that ran; they differ for sessions an Entire checkpoint recorded.
	Source      string          `json:"source"`
	Vendor      string          `json:"vendor"`
	Title       string          `json:"title,omitempty"`
	Branch      string          `json:"branch,omitempty"`
	StartedAt   *time.Time      `json:"started_at,omitempty"`
	EndedAt     *time.Time      `json:"ended_at,omitempty"`
	Outcome     session.Outcome `json:"outcome"`
	EditedFiles int             `json:"edited_files"`
}

Session is a linked session. All text is redacted.

Directories

Path Synopsis
Package linktest builds the fixture the local linkage tests share: a real clone with a pull request's worth of commits, and a Claude Code store with one session that created the first of them.
Package linktest builds the fixture the local linkage tests share: a real clone with a pull request's worth of commits, and a Claude Code store with one session that created the first of them.

Jump to

Keyboard shortcuts

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