livesessions

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Nov 4, 2026 License: MIT Imports: 47 Imported by: 0

Documentation

Overview

Package livesessions tracks the coding-agent sessions running on this machine from the hook events the daemon records, and serves them to the local application so a pull request page and the inbox can show work in progress before any session file is complete.

It reads identifiers only, and one line of text. A hook event never carries prompts or tool input, and the tracker keeps from it the agent, the session id, when the session started and was last active, a normalised event kind, why and since when it waits on its user, with the short message a Claude Code notification showed (masked like a transcript), and the repository, branch and pull request the session's working directory links to, else the one its agent's latest commit or push reaches (work.go). The working directory itself, the transcript path and the terminal the hooks say the session runs in (processes, terminal device and terminal variables, which Focus uses to bring it forward) stay inside the tracker and are never served. Reading session files from disk is unchanged and remains the full history; Transcripts serves a tracked session's transcript, redacted, while it runs, and its list adds each session's title, compaction summary and activity (latest tool calls, files written, latest assistant message), read from the transcript and masked.

Index

Constants

View Source
const (
	TitleRecheckEvery       = 5 * time.Second
	WorkingRecheckEvery     = time.Second
	CursorTitleRecheckEvery = 30 * time.Second
)

Bounds on what is read of a session to describe it. A session is looked at no more than once per TitleRecheckEvery, or WorkingRecheckEvery while it works, so the app follows what it says within a couple of seconds (CursorTitleRecheckEvery for Cursor, whose database is one large file it writes all the time), and its files are read again only when they changed. Claude Code and Codex append to their transcripts, so each read starts where the last one stopped, at most scanPass bytes at a time; until a long transcript is caught up, its last titleTail bytes, where Claude Code rewrites the title as the session goes, name it.

View Source
const (
	// StateRunning is a session whose agent is working: its last event was
	// a start, a prompt or tool activity, and not longer ago than IdleAfter.
	StateRunning = "running"
	// StateIdle is a session waiting for its user (it has a WaitingReason:
	// its turn ended, or it asked for a permission or an answer) or silent
	// for IdleAfter: an agent that crashed sends nothing.
	StateIdle = "idle"
	// StateEnded is a session that sent its end event.
	StateEnded = "ended"
)

States of a tracked session.

View Source
const (
	EventStart  = "start"
	EventPrompt = "prompt"
	EventTool   = "tool"
	// EventWait is the agent asking its user for something: a Claude Code
	// notification or a permission request of either agent.
	EventWait = "wait"
	EventStop = "stop"
	EventEnd  = "end"
)

Normalised kinds of the last event, the same for every agent.

View Source
const (
	// WaitPermission is a session asking to be allowed to run a tool: a
	// Claude Code permission_prompt notification or a permission request of
	// either agent.
	WaitPermission = "permission"
	// WaitInput is a session asking a question (an elicitation dialog, a
	// background agent waiting on input) or any notification of a type baum
	// does not know, which is taken to want the user.
	WaitInput = "input"
	// WaitTurnFinished is a session whose turn ended (a stop, or Claude
	// Code's idle_prompt, which follows one a minute later).
	WaitTurnFinished = "turn_finished"
	// WaitQuiet is a session silent for IdleAfter with no tool call of its
	// own still running: it stopped without a stop (an interrupted turn
	// sends none) or its agent died. The list sets it (settle), since only
	// the transcript says whether a tool call still runs.
	WaitQuiet = "quiet"
)

Why a session waits on its user.

View Source
const (
	IdleAfter   = 10 * time.Minute
	DropAfter   = 2 * time.Hour
	EndedFor    = 24 * time.Hour
	MaxSessions = 200
	// SweepEvery is how often Run applies the timings, which events do not
	// trigger.
	SweepEvery = 30 * time.Second

	// RelinkEvery is the least time between two lookups of a session's
	// repository, branch and pull request: the branch changes while a
	// session runs, and a lookup runs git.
	RelinkEvery = 30 * time.Second
	// NotifyEvery coalesces the notifications of a busy session.
	NotifyEvery = time.Second
	// ActivityNotifyEvery is how far last_activity_at must move, with nothing
	// else changed, for the app to be told: a busy agent's tool events would
	// otherwise make every open window read the list once a second.
	ActivityNotifyEvery = 30 * time.Second
	// AnnounceWithin is how recent a wait must be for Waiting to be told of
	// it: an event the spool delivers late is old news.
	AnnounceWithin = 5 * time.Minute

	// StateFile is the name of the file in StateDir holding the ended
	// sessions, since the hook event record rotates by size and may not reach
	// back EndedFor, and the open ones, which a session waiting for its user
	// outlives the record's DropAfter in.
	StateFile = "live-sessions.json"
)

Timings and bounds. A session is dropped when it has been silent for DropAfter, unless its agent is seen still running (Tracker.Alive), or ended for EndedFor, which is long enough for the Sessions page to show what ended recently; at most MaxSessions are tracked, the least recently active (an ended one first) making room for a new one.

View Source
const (
	RecheckEvery       = time.Second
	CursorRecheckEvery = 5 * time.Second
	MaxCached          = 8
)

Transcript bounds. A session's file is looked at (a stat of each of its files) at most once per RecheckEvery, however many windows ask, and read again only when one of them changed. Cursor keeps every conversation in one database that it writes all the time, and finding one conversation in it reads the whole index, so it is looked at less often. At most MaxCached transcripts are held, the least recently asked for making room.

View Source
const (
	AgentClaudeCode = "claude-code"
	AgentCodex      = "codex"
	AgentCursor     = "cursor"
)

Agents the hooks name, as the tracker keys them.

View Source
const (
	MethodPush   = "push"
	MethodCommit = "commit"
)

Link methods of a pull request found through a session's own Git work rather than its working directory.

Variables

View Source
var ErrAgentGone = errors.New("the session's agent is not running")

ErrAgentGone is a session whose agent no longer runs: it ended without its end event reaching the daemon.

View Source
var ErrNoImage = errors.New("livesessions: no such image")

ErrNoImage is the answer for an image a session's transcript does not hold.

View Source
var ErrNotTracked = errors.New("no such live session")

ErrNotTracked is a session the tracker does not list: not one of this machine's agent sessions, or one that ended more than EndedFor ago.

Functions

This section is empty.

Types

type Activity

type Activity struct {
	// Steps are the latest tool calls, oldest first, at most maxSteps.
	Steps []Step `json:"steps,omitempty"`
	// FilesTouched is the number of distinct files the session wrote, over
	// the whole transcript read so far; absent when none.
	FilesTouched int `json:"files_touched,omitempty"`
	// LastMessage is the latest assistant text, masked, its line breaks kept
	// (block) and cut.
	LastMessage string `json:"last_message,omitempty"`
	// LastPrompt is the latest message the user typed, masked, on one line
	// and cut.
	LastPrompt string `json:"last_prompt,omitempty"`
	// contains filtered or unexported fields
}

Activity is what a session's transcript says it is doing, as the list serves it.

type Answer

type Answer struct {
	Session Session
	// Version changes whenever the transcript's files do.
	Version string
	// Body is the rendered transcript.
	Body []byte
	// Images are the media types of the images pasted with each prompt, by
	// its turn, in order; nil when none were.
	Images map[string][]string
	// contains filtered or unexported fields
}

Answer is a session's transcript as served.

type ClaudeAlive

type ClaudeAlive struct {
	Home string
}

ClaudeAlive is a Tracker's Alive: whether the process of a Claude Code session still runs under home's ~/.claude (empty is the user's home), as FindClaude confirms it. Other agents register no process, so theirs are never confirmed.

func (ClaudeAlive) Alive

func (c ClaudeAlive) Alive(agent, id string) bool

Alive is a Tracker's Alive.

type ClaudeExit

type ClaudeExit struct {
	// Home holds ~/.claude; empty is the user's home.
	Home string
}

ClaudeExit ends Claude Code sessions as /exit does: SIGTERM to the session's process, which then exits in under a second, leaves its terminal at the shell, fires its SessionEnd hook (reason "other") and can be resumed with `claude --resume <id>`. Only a process that reports itself idle is signalled: a busy one would lose the turn it is in.

func (ClaudeExit) End

func (c ClaudeExit) End(_ context.Context, id string) error

End is an Ender.

type ClaudeInbox

type ClaudeInbox struct {
	// Home holds ~/.claude; empty is the user's home.
	Home string
}

ClaudeInbox sends to Claude Code sessions through their cross-session inbox (https://code.claude.com/docs/en/cross-session-messaging): one JSON line on the session's socket, which an idle session takes as a new turn, in its own terminal and transcript. A session's crossSessionInbound setting can hold or refuse it. On macOS and Linux only: on Windows the inbox is a named pipe.

func (ClaudeInbox) Send

func (c ClaudeInbox) Send(ctx context.Context, id, text string) error

Send is a Sender.

type ClaudeProcess

type ClaudeProcess struct {
	PID       int    `json:"pid"`
	SessionID string `json:"sessionId"`
	// Status is "idle" or "busy".
	Status string `json:"status"`
	// Socket is the session's cross-session inbox, a Unix socket.
	Socket string `json:"messagingSocketPath"`
	Kind   string `json:"kind"`
	// ProcStart is when the process started as its system counts it (on
	// Linux, field 22 of /proc/<pid>/stat), and PIDDomain the pid namespace
	// the pid is in ("linux::pid:[…]"): Claude Code records both, so a pid a
	// later process reused, under an entry a killed session left, is told
	// apart from the session's own.
	ProcStart string `json:"procStart"`
	PIDDomain string `json:"pidDomain"`
}

ClaudeProcess is a running Claude Code session as Claude Code registers it in ~/.claude/sessions/<pid>.json.

func FindClaude

func FindClaude(home, id string) (ClaudeProcess, error)

FindClaude is the running process of Claude Code session id under home's ~/.claude/sessions, or ErrAgentGone: an entry naming the session whose process still runs, and is the one that wrote it (sameProcess). Claude Code removes an entry when its session exits; one a killed process left is passed over. A process baum cannot confirm is an error, not ErrAgentGone, so it is neither signalled nor recorded ended.

type CodexQueue

type CodexQueue struct {
	// Codex is the codex command, looked up on PATH when the daemon starts.
	Codex string
}

CodexQueue sends to Codex sessions with Codex's own `codex queue --thread <id> --message <text>` (0.149 and later): the message arrives in the same thread as a turn the user typed, after the current one when the session is busy. A thread whose terminal was closed still runs it, in Codex's shared daemon, which is why the app offers it only after a finished turn.

func NewCodexQueue

func NewCodexQueue() (CodexQueue, bool)

NewCodexQueue is a CodexQueue for the codex command on PATH, or false when there is none.

func (CodexQueue) Send

func (c CodexQueue) Send(ctx context.Context, id, text string) error

Send is a Sender.

type Discovery

type Discovery interface {
	Cached(context.Context, github.Repository) (discovery.Snapshot, error)
}

Discovery reads cached open pull requests and terminal observations without a request to GitHub; *discovery.Service is one.

type Ender

type Ender func(ctx context.Context, id string) error

Ender stops the agent of one of its sessions, by the session's agent and id, as if its user quit it: the agent's own end hook then says it ended. It returns ErrAgentGone when no process of the session is running.

type Focuser

type Focuser interface {
	Predict(agent string, terminal *hookevents.Terminal) string
	Focus(ctx context.Context, agent, id string, terminal *hookevents.Terminal) focus.Result
}

Focuser brings a session forward from where its hooks said it runs; *focus.Focuser is one.

type Link struct {
	// Repository is the registered clone's repository, "owner/name" in
	// lower case.
	Repository string
	// Branch is the branch checked out, empty when detached.
	Branch string
	// PR is the pull request of Branch, 0 when none is known.
	PR                                 int
	PRState                            string
	HeadSHA, Upstream, Method, Status  string
	HeadRepository, UpstreamRepository string
}

Link is where a working directory belongs.

type Linker

type Linker interface {
	Link(ctx context.Context, cwd string) (Link, bool)
}

Linker links a hook's working directory to a repository, branch and pull request, and reports false for a directory in no registered clone.

type Listed

type Listed struct {
	Session
	// Title is the session's name as its agent shows it: Claude Code's title
	// (the one the user set, else the one it generated), Codex's thread
	// name, Cursor's conversation name; else the session's first prompt.
	// One line, masked as transcripts are. Empty only when the agent's
	// files cannot be read.
	Title string `json:"title,omitempty"`
	// Summary is the opening of the session's latest compaction summary (its
	// first section), one line, masked; absent until the agent compacted the
	// session. The transcript route serves the whole summary.
	Summary string `json:"summary,omitempty"`
	// Activity is what the transcript says the session is doing: its latest
	// tool calls, how many files it wrote and its latest message (see
	// Activity). Absent for Cursor sessions and until a transcript has a tool
	// call or assistant text.
	Activity *Activity `json:"activity,omitempty"`
	// Endable is whether the app may end the session (end.go): it waits on
	// its user or went quiet, and baum can stop its agent.
	Endable bool `json:"endable,omitempty"`
	// Messageable is whether the app may send the session a message
	// (message.go): it waits at its prompt, and baum can reach its agent.
	Messageable bool `json:"messageable,omitempty"`
	// Focus is how precisely the focus route can bring the session forward:
	// "session" for its own tab, pane or conversation, "window" for the
	// window or application it runs in. Absent when it cannot, as for a
	// session that ended or whose hooks did not say where it runs.
	Focus string `json:"focus,omitempty"`
}

Listed is a session as the list serves it: the tracker's Session, the title its agent gave it and, after a compaction, what the summary says it is about.

type Location

type Location struct {
	Vendor vendors.Slug
	// ID is the session's id: Cursor's conversation.
	ID string
	// contains filtered or unexported fields
}

Location is where a session's transcript is read from: files of an agent's store, or one conversation of Cursor's database (Locate).

func Locate

func Locate(home, cursorStore, agent, id, path string) (Location, error)

Locate finds the files of the transcript of the agent's session id, whose hooks named path (empty when none did), and checks that they are the agent's own: inside its session store under home, its symbolic links resolved. A Codex session without a path is looked up in its store by id; a Cursor session is read from cursorStore. Errors are *Unavailable.

func (Location) Graph

func (s Location) Graph() (*session.Graph, error)

Graph reads the session's graph with the mapper discovery uses.

func (Location) Path

func (s Location) Path() string

Path is the session's main transcript file, empty for a Cursor conversation.

func (Location) Size

func (s Location) Size() (int64, error)

Size is the total size of the session's own files; zero for a Cursor conversation, which shares one database with every other.

func (Location) Version

func (s Location) Version() (string, error)

Version is a fingerprint of the size and modification time of every file the transcript is read from.

type RecordedLinker

type RecordedLinker interface {
	Relink(context.Context, Session) (Link, bool)
}

RecordedLinker resolves a saved Git observation without reading today's checkout.

type Registry

type Registry interface {
	List(context.Context) ([]repos.Clone, error)
}

Registry lists the registered clones; *repos.Registry is one.

type Resolver

type Resolver struct {
	Registry  Registry
	Discovery Discovery
	// Now is the clock; nil is time.Now.
	Now func() time.Time
	// contains filtered or unexported fields
}

Resolver is the Linker the daemon uses. It follows the existing linkage and adds nothing to it: the directory's clone is the registered clone sharing its git directory (a linked worktree resolves to its clone, as it does for session linkage), the branch is what git has checked out, and the pull request is the open one of the discovery cache whose head is that branch. A cached merged or closed PR can still match the exact same-repository head. It never contacts GitHub and never fetches.

func (r *Resolver) Link(ctx context.Context, cwd string) (Link, bool)

Link implements Linker.

func (*Resolver) LinkCommit

func (r *Resolver) LinkCommit(ctx context.Context, dir, branch, head string) (Link, bool)

LinkCommit implements WorkLinker: the commit head made on branch in the working tree dir, with the remotes the clone has (prlink.At), not what dir has checked out since. It runs git and never fetches.

func (*Resolver) LinkPush

func (r *Resolver) LinkPush(ctx context.Context, dir, remote, branch, head string) (Link, bool)

LinkPush implements WorkLinker: the pushed branch and commit, in the repository the remote's push URL names (a fork's own branch is another branch), as refresh on hook events reads a push. It runs git and never fetches.

func (r *Resolver) Relink(ctx context.Context, s Session) (Link, bool)

Relink uses recorded branch, upstream and HEAD evidence, never the checkout.

type Sender

type Sender func(ctx context.Context, id, text string) error

Sender puts a message the user wrote in the app into one of its agent's sessions, by id, as a new turn of the same conversation, shown in the session's own terminal. It returns ErrAgentGone when no process of the session is running.

type Session

type Session struct {
	Agent string `json:"agent"`
	ID    string `json:"session_id"`
	State string `json:"state"`
	// StartedAt is the session's start event, or its first event the
	// daemon saw when it did not see the start.
	StartedAt      time.Time `json:"started_at"`
	LastActivityAt time.Time `json:"last_activity_at"`
	// LastPromptAt is the latest prompt event, absent before the daemon saw
	// one: what the Sessions page orders running sessions by, as their
	// activity moves with each tool call.
	LastPromptAt time.Time `json:"last_prompt_at,omitzero"`
	// LastEvent is the normalised kind of the latest event (EventStart,
	// EventPrompt, EventTool, EventWait, EventStop or EventEnd).
	LastEvent string `json:"last_event"`
	// WaitingReason is why the session waits on its user (WaitPermission,
	// WaitInput or WaitTurnFinished), absent while it works, once it ended,
	// and for a session that is only silent.
	WaitingReason string `json:"waiting_reason,omitempty"`
	// WaitingSince is when that wait began.
	WaitingSince time.Time `json:"waiting_since,omitzero"`
	// WaitingMessage is the line a Claude Code notification showed for the
	// wait, such as "Claude needs your permission to use Bash": format and
	// control characters dropped, masked as transcripts are and at most
	// maxWaitingMessage characters. Absent when the agent sent none.
	WaitingMessage string `json:"waiting_message,omitempty"`
	// EndedAt is when an ended session sent its end event, absent otherwise.
	EndedAt    time.Time `json:"ended_at,omitzero"`
	Repository string    `json:"repository"`
	Branch     string    `json:"branch,omitempty"`
	// PR is the pull request whose head is Branch, 0 when there is none
	// (yet) or it cannot be told.
	PR                 int          `json:"pr,omitempty"`
	PRState            string       `json:"pr_state,omitempty"`
	HeadSHA            string       `json:"head_sha,omitempty"`
	Upstream           string       `json:"upstream,omitempty"`
	HeadRepository     string       `json:"head_repository,omitempty"`
	UpstreamRepository string       `json:"upstream_repository,omitempty"`
	LinkMethod         string       `json:"link_method,omitempty"`
	LinkStatus         string       `json:"link_status,omitempty"`
	ObservedAt         time.Time    `json:"observed_at,omitzero"`
	WorkHistory        *WorkHistory `json:"work_history,omitempty"`
}

Session is what the application is told of a tracked session.

type Source

type Source interface {
	Recent(limit int) ([]hookevents.Record, error)
	SubscribeCounted(buffer int, match func(hookevents.Event) bool) (events <-chan hookevents.Record, cancel func(), dropped func() uint64)
}

Source is the daemon's record of hook events; *hookevents.Store is one.

type Step

type Step struct {
	Tool   string `json:"tool"`
	Target string `json:"target,omitempty"`
	At     string `json:"at,omitempty"`
}

Step is one tool call: the tool as the transcript names it, normalised for display, and what it was aimed at, masked, on one line and cut, with the time of the transcript entry when it has one (RFC 3339, UTC).

type Tracker

type Tracker struct {
	Source Source
	Linker Linker
	// Notify is called, coalesced to at most one call per NotifyEvery, after
	// the sessions changed. It must not block.
	Notify func()
	Log    *slog.Logger
	// Now is the clock; nil is time.Now.
	Now func() time.Time
	// StateDir, when set, is a private directory where live and ended
	// observations are kept across restarts (StateFile): no local paths or
	// notification message text. An open one silent for DropAfter is restored
	// only while Alive says its agent runs.
	StateDir string
	// Alive, when set, reports whether a session's agent still runs: true
	// only for a process it confirms is the session's. A session silent for
	// DropAfter stays listed while it does, as one waiting for its user may be
	// answered hours later; nil drops it then.
	Alive func(agent, id string) bool
	// Buffer is the subscription's size; zero is 256. A subscriber that falls
	// behind misses events, which Reconcile applies from the record once the
	// subscription reports it missed some.
	Buffer int
	// Mask, when set, masks the secrets and e-mail addresses in a waiting
	// message, as transcripts are masked; nil keeps the text.
	Mask func(string) string
	// Waiting, when set, is called once for each wait for a permission or an
	// answer (WaitPermission, WaitInput) of a session the application is
	// shown, as it begins: not for a finished turn, not for the waits a
	// rebuild replays when the daemon starts, and not for one that began more
	// than AnnounceWithin ago. It is called without the tracker's lock and
	// must not block.
	Waiting func(Session)
	// contains filtered or unexported fields
}

Tracker follows the sessions of the agents whose hooks report to the daemon.

func (*Tracker) Answered

func (t *Tracker) Answered(agent, id string, at time.Time) bool

Answered records that the app answered the permission or question a session waited for, at at: it works again, though the agent reports nothing until the tool call ends. A session with an event newer than at, or that waits for nothing else, is left as it is. It reports whether the session changed.

func (*Tracker) Apply

func (t *Tracker) Apply(_ context.Context, e hookevents.Event) bool

Apply updates the sessions with a hook event and reports whether what the application sees changed enough to tell it. It never waits for git: a new session is hidden until ResolvePending links it.

func (*Tracker) Gone

func (t *Tracker) Gone(agent, id string, at time.Time) bool

Gone records that a session's agent no longer runs, found at at: it ended, as its end event would have said had it reached the daemon. An ended session is left as it is. It reports whether the session changed.

func (*Tracker) Handler

func (t *Tracker) Handler() http.Handler

Handler serves GET /api/live-sessions: {"sessions": [...]} with the Session of each tracked session, the most recently active first. The daemon mounts it behind the same authentication as every application route.

func (*Tracker) Interrupted

func (t *Tracker) Interrupted(agent, id string, at time.Time) bool

Interrupted records that a session's user turned down, in the terminal, the permission or answer it waited for, at at: Claude Code interrupts the turn and sends no stop, so the session waits for its user's next prompt, as at the end of a turn. A session with an event newer than at, or that waits for nothing else, is left as it is. It reports whether the session changed.

func (*Tracker) Lookup

func (t *Tracker) Lookup(agent, id string) (Session, string, bool)

Lookup is a session the application is shown, with the transcript path its hooks last reported (empty when none did, or for an ended session read back from StateFile, which keeps no paths); false when the tracker does not list it.

func (*Tracker) Rebuild

func (t *Tracker) Rebuild(ctx context.Context)

Rebuild replaces the sessions with those the record's events show, so a daemon that restarts keeps showing the sessions that were running. Their links are looked up afterwards, off the caller's goroutine when Run is the caller; ResolvePending does it for a caller without one.

func (*Tracker) Reconcile

func (t *Tracker) Reconcile(ctx context.Context) bool

Reconcile applies the events of the record that the subscription did not deliver (a full buffer drops them) and reports whether what the application sees changed. Applying an event twice changes nothing, so it can run at any time.

func (*Tracker) ResolvePending

func (t *Tracker) ResolvePending(ctx context.Context) bool

ResolvePending looks up the link of each session waiting for one and reports whether what the application sees changed. Run does it in the background after every event that needs it.

func (*Tracker) Resumed

func (t *Tracker) Resumed(agent, id string, at time.Time) bool

Resumed records that a session's Stop hook handed it a message the app queued, at at: the agent goes on with the turn the stop ended, though it may report nothing until it calls a tool or stops again. A stop the tracker records after this, but which happened before at, is the one that delivered the message and is ignored as old news. A session with an event newer than at, or that waits for its user, is left as it is. It reports whether the session changed.

func (*Tracker) Run

func (t *Tracker) Run(ctx context.Context)

Run rebuilds the sessions from the record, then follows new events until ctx ends.

func (*Tracker) Sessions

func (t *Tracker) Sessions() []Session

Sessions returns the tracked sessions, the most recently active first.

func (*Tracker) Sweep

func (t *Tracker) Sweep() bool

Sweep drops the sessions that expired and reports whether what the application sees changed, by that or by a running session going idle.

func (*Tracker) Terminal

func (t *Tracker) Terminal(agent, id string) (*hookevents.Terminal, bool)

Terminal is where the hooks of a session the application is shown last said it runs; nil when none did, and for a session that ended: nothing runs to bring forward.

type Transcripts

type Transcripts struct {
	Tracker *Tracker
	// Render writes a session's transcript; transcript.Renderer.Render is
	// the one every local transcript is written with.
	Render func(ctx context.Context, graph *session.Graph, meta transcript.Meta) ([]byte, error)
	// Home holds the agents' session stores; empty is the user's home.
	Home string
	// CursorStore is Cursor's database; empty is its default global store.
	CursorStore string
	// Now is the clock; nil is time.Now.
	Now func() time.Time
	Log *slog.Logger
	// Enders stop a session's agent, by agent (end.go); an agent without
	// one cannot be ended from the app.
	Enders map[string]Ender
	// Senders put a message from the app into a session, by agent
	// (message.go); an agent without one cannot be messaged.
	Senders map[string]Sender
	// Focus brings a running session forward; nil serves no focus route and
	// lists no session as one it can bring forward.
	Focus Focuser
	// contains filtered or unexported fields
}

Transcripts serves the transcript of a session the Tracker lists, rendered as the review page renders a linked session's, while it runs.

The file read is never named by a request: it is the transcript path the session's hooks reported, and it is read only when, its symbolic links resolved, it is a regular file inside that agent's own session store under the home directory (~/.claude/projects for Claude Code, ~/.codex/sessions for Codex). A Codex session whose hooks named no path is looked up in that store by its id; a Cursor session is read from Cursor's own database by its conversation id, since Cursor keeps no transcript file baum reads.

func (*Transcripts) Get

func (t *Transcripts) Get(ctx context.Context, agent, id string) (Answer, error)

Get answers with the transcript of the tracked session agent/id. An error is ErrNotTracked, an *Unavailable, or the context's.

func (*Transcripts) Handler

func (t *Transcripts) Handler() http.Handler

Handler serves the tracker's list as Tracker.Handler does, each session with its title (Listed), and GET /api/live-sessions/{agent}/{id}/transcript: the transcript of a session the list has, for as long as it has it. The answer is {"available":true,"version","live","transcript","summary"?,"images"?}, the transcript as GET /api/sessions/{id} answers it, the session's latest compaction summary (masked) and the media types of the images pasted with each prompt, by its turn, all left out when the request names the current version (?version=), so a window polling a session that did not change receives a few bytes; or {"available":false,"live", "code":"transcript_unavailable","message"} with why it is not served. A session the tracker does not list is 404.

GET /api/live-sessions/{agent}/{id}/transcript/images/{turn}/{n} is the n-th of those images (from 0), as it was pasted: images are not masked. One the transcript does not hold is 404.

func (*Transcripts) Image

func (t *Transcripts) Image(ctx context.Context, agent, id, turn string, n int) (string, []byte, error)

Image is the n-th image (from 0) pasted with the prompt turn of the tracked session agent/id, and its media type, read from the session's transcript as Get reads it. An error is ErrNotTracked, an *Unavailable, ErrNoImage, or the context's.

func (*Transcripts) Read

func (t *Transcripts) Read(ctx context.Context, agent, id, path, branch string) (Answer, error)

Read answers with the transcript of the session agent/id whether the tracker lists it or not, such as one that ended long ago: from path, the transcript its hooks named when it was captured (empty when none did), on branch. While the tracker lists it, its session and path are the tracker's. The file read is held to the same rules as Get's. An error is an *Unavailable or the context's.

func (*Transcripts) Watch

func (t *Transcripts) Watch(ctx context.Context, notify func())

Watch calls notify, until ctx ends, when what the list serves changed with no hook event to say so: agents report few events during a turn, so a running session's steps, files and message, and a silent session settling (settle), are only in its transcript. The list is built every WorkingRecheckEvery, which reads a working session's files when they grew (only what they appended) and the others' no more often than describe allows.

func (*Transcripts) WriteAnswer

func (t *Transcripts) WriteAnswer(w http.ResponseWriter, r *http.Request, answer Answer, err error)

WriteAnswer writes a transcript answer as the live sessions' transcript route does (Handler), for any route that serves one: the summary is the tracked session's.

type Unavailable

type Unavailable struct{ Reason string }

Unavailable is a tracked session whose transcript is not served, with the reason, a sentence the application shows.

func (*Unavailable) Error

func (u *Unavailable) Error() string

type WorkHistory

type WorkHistory struct {
	Entries []WorkObservation `json:"entries"`
}

type WorkLinker

type WorkLinker interface {
	// LinkCommit links the commit head made on branch ("" when detached) in
	// the working tree dir, from what the hook recorded rather than what dir
	// has checked out now, and reports false when dir is in no registered
	// clone.
	LinkCommit(ctx context.Context, dir, branch, head string) (Link, bool)
	// LinkPush links a push from the working tree dir through remote, of
	// head to branch, and reports false when dir is in no registered clone.
	LinkPush(ctx context.Context, dir, remote, branch, head string) (Link, bool)
}

WorkLinker links the commits and pushes of a session's agent; *Resolver is one. With a Linker that is not one, they link nothing.

type WorkObservation

type WorkObservation struct {
	At                 time.Time `json:"at"`
	HeadRepository     string    `json:"head_repository,omitempty"`
	UpstreamRepository string    `json:"upstream_repository,omitempty"`
	Repository         string    `json:"repository"`
	Branch             string    `json:"branch,omitempty"`
	HeadSHA            string    `json:"head_sha,omitempty"`
	Upstream           string    `json:"upstream,omitempty"`
	PR                 int       `json:"pr,omitempty"`
}

WorkObservation is recorded Git context, not proof of code contribution.

Jump to

Keyboard shortcuts

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