NAME

App::karr::ActivityLog - Activity log writer for karr board operations

VERSION

version 0.600

SYNOPSIS

use App::karr::ActivityLog;
use App::karr::Git;

my $git = App::karr::Git->new(dir => '.');
my $log = App::karr::ActivityLog->new(git => $git, role => 'agent');

$log->log_entry(
    agent   => 'agent-fox',
    action  => 'pick',
    task_id => 5,
    detail  => 'in-progress',
);

DESCRIPTION

Writes append-style JSON log entries to refs/karr/log/<identity> refs. Each entry receives an automatic timestamp if not provided.

The identity is <role>/<email>: the Git user email percent-encoded into a ref name and qualified by a role (user or agent). The role disambiguates a human and an AI agent that share one Git config. It defaults to the KARR_ROLE environment variable, or user.

Identity encoding

Git's ref-name grammar is far narrower than what a mail address may contain, so each component is percent-encoded ("identity", "decode_identity"). [A-Za-z0-9._-] survives literally to keep the common address readable; every other octet becomes %XX, including % itself, which makes the mapping injective -- two different addresses can no longer land on one ref.

Older karr releases replaced every unsafe character with _, which both collided (a b@x and a-b@x shared a ref) and produced names git rejects (a..b@x, x@y.lock). Refs written that way are not rewritten: "entries" reads them alongside the current one so existing history stays visible.

Segments

One identity's log is a chain of refs, not one ref:

refs/karr/log/<role>/<email>          segment 0
refs/karr/log/<role>/<email>+000001   segment 1
refs/karr/log/<role>/<email>+000002   ...

An entry is appended to the newest segment until that segment reaches "segment_max_bytes"; the next entry then opens the following one, and the full segment is never rewritten again. A ref blob is rewritten whole on every append, so a single unbounded log ref made each entry cost a copy of the entire history: 10,000 entries of the ~93 bytes karr writes added up to about 4.6 GB of objects for a 1 MB log, growing quadratically (#171). Segmenting bounds one write by the cap instead of by the history, which makes the total linear.

The layout needs no marker ref and no migration, unlike the encoding switch that refs/karr/meta/encoding and App::karr::Cmd::Repair exist for: which segments a log has is visible in the ref names. A board written before the split has segment 0 and nothing else -- indistinguishable from a board that has not rotated yet -- so it keeps being read and appended to as it was.

Everything that reads the whole log by walking refs/karr/log/* -- karr log, karr context -- picks the segments up unchanged. karr context, which excludes the invoking identity's own entries, uses "owns_ref" rather than an equality test so a rotated segment does not read as another agent.

git

The App::karr::Git instance this log reads and writes refs through. Required.

role

The actor role, user (default) or agent. Read from KARR_ROLE when not given explicitly.

segment_max_bytes

How large (in characters) the active log segment may grow before the next entry opens a new one; 8192 by default. Set explicitly only by the tests, which have to see rotation without writing the thousands of entries the real cap needs.

METHODS

identity

my $id = $log->identity;   # e.g. "agent/getty%40conflict.industries"

The percent-encoded <role>/<email> string keying this actor's log. Always a legal pair of git ref components; see "decode_identity" for the inverse.

decode_identity

my ($role, $email) = App::karr::ActivityLog->decode_identity($id);

Turns an encoded identity -- the part of a refs/karr/log/* ref name below refs/karr/log/ -- back into the role and mail address it was built from.

owns_ref

next if $log->owns_ref($ref);

True when $ref is one of this identity's log refs under the current naming scheme -- segment 0 (refs/karr/log/"identity") or any of its rotated segments. Refs left behind by the pre-#75 schemes are not claimed; the internal _legacy_refs is what reads those.

karr context uses this to leave the invoking identity's own entries out of the cross-agent activity it summarises: comparing against "identity" alone would have counted every rotated segment as somebody else's log the moment one identity's history outgrew a single ref.

log_entry

$log->log_entry(
    agent   => 'agent-fox',
    action  => 'pick',
    task_id => 5,
    detail  => 'in-progress',
    ts      => '2026-05-15T10:00:00Z',  # optional, auto-generated
);

Writes a JSON log line to this identity's newest log segment, opening the next one when that segment has reached "segment_max_bytes" (see "Segments"). The first segment is refs/karr/log/<role>/<encoded_email>.

Returns the result of "write_ref_cas" in Git, or 0 after warning if the entry could not be written. It never dies: by the time a command logs, it has already written the task the entry describes, so a failure here must not take the command down with a half-applied mutation behind it (#75).

entries

my @entries = $log->entries;

Returns the decoded log entries for this identity, oldest first. Refs written under the pre-#75 naming schemes are read first and merged in ahead of the current ref, which is also their chronological order: a board stops being written under an old scheme the moment it is touched by a karr that knows the new one. The current scheme's segments ("Segments") follow in segment order, which is chronological for the same reason: only the newest segment is ever appended to.

last_entry

my $entry = $log->last_entry;

The most recent decoded log entry for this identity, or undef if none.

Reads the refs newest-first and stops at the first one that yields an entry, so on a segmented log this is one small ref read rather than the whole history -- the point of "Segments" being lost if the cheap write path were paid for with an expensive read of the last line.

SUPPORT

Issues

Please report bugs and feature requests on GitHub at https://github.com/Getty/karr/issues.

IRC

Join #langertha on irc.perl.org or message Getty directly.

CONTRIBUTING

Contributions are welcome! Please fork the repository and submit a pull request.

AUTHOR

Torsten Raudssus <getty@cpan.org>

COPYRIGHT AND LICENSE

This software is Copyright (c) 2026 by Torsten Raudssus <torsten@raudssus.de> https://raudssus.de/.

This is free software, licensed under:

The Artistic License 2.0 (GPL Compatible)