activity

package
v0.5.4 Latest Latest
Warning

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

Go to latest
Published: Oct 22, 2026 License: Apache-2.0 Imports: 12 Imported by: 0

Documentation

Overview

Package activity is the append-only record of what an agent actually did.

Everything else in this system is something a model chose to tell us. A memory is stored because the agent called `remember`; a checkpoint exists because it called `checkpoint`. That works right up until the model does not call them, and the failure is silent — a session that decided three things and wrote none of them down looks, from the vault, exactly like a session that never happened.

This is the other half. It records events the agent does not get a vote on: the prompts a person typed, the tools that ran, the turns that ended. The host reports them through hooks whether or not the model would have thought to mention them, which makes this the one part of the record that cannot be forgotten.

Three deliberate choices:

JSONL on disk, one file per month. Not SQLite. The log is meant to be read by things that are not us — grep, jq, a script, the user at three in the morning wondering what happened. A database would be faster to query and would make that impossible, and "your history is a file you own" stops being true the moment the only reader is our binary. Monthly files keep `activity/2026-09.jsonl` a name a person can guess and keep any single file bounded.

Append-only, never rewritten. A log that edits itself is not evidence.

No model, ever. Recording is arithmetic and string handling. A summariser here would turn a factual record into a plausible one, and cost a hook budget it does not have.

Index

Constants

View Source
const (
	KindPrompt  = "prompt"   // a person typed something
	KindTool    = "tool"     // a tool ran and returned
	KindTurnEnd = "turn-end" // the agent finished a turn
	KindNotice  = "notice"   // the host wants the user's attention
	KindStart   = "session-start"
	KindEnd     = "session-end"
	KindBlocked = "permission" // the agent asked to do something and is waiting
	KindWarn    = "warning"    // something the hook path could not report any louder
)

Event kinds. Plain strings, so a host that grows a new event type does not need a migration here — an unknown kind still records, still greps, and still prints. The set below is what Claude Code's hooks can currently tell us.

View Source
const Dir = "activity"

Dir is the vault subdirectory holding the log.

View Source
const Retention = 30 * 24 * time.Hour

Retention is how long a month's log is kept.

The log records every prompt and every tool call a host reported, which is the most sensitive thing in the vault and the one part of it the user never chose line by line. Keeping it forever was never a decision anyone made — it was the absence of one, and `logos activity` said the log "rolls off under capture retention" while nothing pruned anything.

Thirty days measured from the end of the month a file covers, so a file is deleted only once everything in it is past the window. Whole files, never lines: rewriting a month to drop its first week would make the log something that edits itself, and an append-only record is the only kind that is evidence.

Variables

This section is empty.

Functions

func Append

func Append(vault string, e Event) error

Append writes one event. It creates the month's file if needed and never rewrites what is already there.

Errors are returned rather than swallowed, but callers on a hook path should treat them as advisory: a log that fails to write must not break the session it is recording. The hook decides that; this function only reports.

func Decided added in v0.4.8

func Decided(vault string) bool

Decided reports whether this vault has an answer of its own, so setup can ask once and never again. A vault grandfathered on its existing log counts as decided: it has been recording for weeks, and re-offering the switch every time setup is re-run is how a user flips it by accident.

func Disclose added in v0.4.5

func Disclose(vault string) (string, error)

Disclose returns the one-time sentence telling the user their prompts and tool calls are being written down, and where, and how to stop — empty once it has been said, or when there is nothing being recorded to disclose.

Asking is not saying. The caller prints it and then calls MarkDisclosed, so a crash in between repeats the notice rather than losing it. The other order reads tidier and is wrong: this is the one notice where a user who never sees it has been recorded without being told, which no amount of care about the redaction makes acceptable, and a disclosure shown twice costs only a line.

func MarkDisclosed added in v0.4.5

func MarkDisclosed(vault string) error

MarkDisclosed records that the sentence Disclose returned has actually been delivered. Separate from Disclose so that the marking happens after the saying, never before it.

func Projects

func Projects(vault string) ([]string, error)

Projects lists the projects that appear in the log, busiest first. This is what makes the log browsable without knowing what to ask for.

func Prune added in v0.4.8

func Prune(vault string, now time.Time) error

Prune deletes every month of the log that is past Retention.

Exported so reading the log ages it out as well as writing to it. Pruning only on append meant a vault that stopped being written to — the user moved on, or turned recording off — kept every old month forever, while setup and `logos activity` had both told its user the entries are deleted. A promise about deletion that only holds for active vaults is not one.

func Recording added in v0.4.5

func Recording(vault string) bool

Recording reports whether this vault accepts new activity events.

Three states, not two, because there are three genuinely different vaults.

off marker  — the user said no. Never record.
on marker   — the user said yes, at setup or with `logos activity on`.
neither     — nobody was ever asked.

The last is the interesting one. Defaulting it to "record" is what #88 was about: every prompt and tool call written down forever, disclosed nowhere, while the README said "nothing is observed". Defaulting it to "don't" would silently switch off the installs that have been recording since the plugin shipped, and a user who has been relying on `logos activity` for a fortnight should not find it empty because we changed our minds.

So an un-asked vault is grandfathered on the evidence: if the log already has events, this install was recording before the question existed and keeps doing it. If it does not, nobody has anything invested and the answer is no until someone says otherwise. New installs are opt-in; existing ones are undisturbed; neither is decided silently against the user.

Since 1g the plugin's first session in a repository answers for a vault nobody asked, turning the log on and saying so in the same notice; see activityNotice in cmd/logos. An answer already given is never overridden.

func SetRecording added in v0.4.5

func SetRecording(vault string, on bool) error

SetRecording turns the log on or off for this vault.

Either answer leaves a marker, because the third state is "nobody has been asked" and an explicit yes has to be distinguishable from it — otherwise saying yes on an empty vault would record nothing and read as a broken switch.

func ShortID added in v0.5.3

func ShortID(s string) string

ShortID trims a session UUID to something a person can compare at a glance. Atlas prints "session 8c4f" for the same reason: the full UUID is noise in every row and identifying in none of them.

Types

type Event

type Event struct {
	TS      int64  `json:"ts"`
	Kind    string `json:"kind"`
	Project string `json:"project,omitempty"`
	Session string `json:"session,omitempty"`
	// Agent is the host that reported the event ("claude-code"), not the model.
	Agent string `json:"agent,omitempty"`
	// Tool is the tool's name for KindTool, empty otherwise.
	Tool string `json:"tool,omitempty"`
	// Summary is one line a human can read without expanding anything. It is
	// the whole point of the row; a log whose rows need decoding is a log
	// nobody scans.
	Summary string `json:"summary,omitempty"`
	// Private counts the <private> spans kept out of Summary. A count of its
	// own, not words on the end of the line: the auto checkpoint is built
	// from summaries, and a receipt in one became part of its task, a
	// command and a file name.
	Private int `json:"private,omitempty"`
	// Extra carries whatever the host sent that we did not model. Kept so the
	// record stays lossless even where our schema is not.
	Extra map[string]any `json:"extra,omitempty"`
}

Event is one line of the log.

The fields are small and fixed on purpose. This file is appended to from a hook on the critical path of every tool call, so it holds what is cheap to know and true without interpretation — and puts anything host-specific in Extra rather than growing a column per host.

func FromHook

func FromHook(event string, raw []byte, project string) (Event, error)

FromHook builds an Event from a host hook payload. event is the host's event name; raw is the JSON it wrote to the hook's stdin. project is used when the payload does not carry enough to work one out.

func Read

func Read(vault string, q Query) ([]Event, error)

Read returns matching events, newest first.

A malformed line is skipped rather than fatal. The log is appended to by hooks running in whatever shell the user has, and one truncated line from a machine that lost power should cost that line, not the month.

type Query

type Query struct {
	Project string
	Kind    string
	Tool    string
	Since   time.Time
	Until   time.Time
	// Limit caps the result, keeping the *newest* matches. A limit applied
	// while scanning would keep the oldest, which is the opposite of what
	// anyone asking for "the last 20" means.
	Limit int
}

Query narrows the log. A zero value matches everything.

Jump to

Keyboard shortcuts

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