Session logs
agentfeedback sessions status # stores, counts per state, the saved selection
agentfeedback sessions list --unprocessed # new and changed sessions
agentfeedback sessions digest --unprocessed --json # bounded, scrubbed events; records the digest watermark
agentfeedback submit friction --stdin --context-from claude-code:<session id>#<span> < report.json
agentfeedback sessions mark claude-code:<session id> --outcome filed --ref <uid>
agentfeedback sessions reads the session logs a coding-agent harness keeps
on disk, on demand and read-only, so an agent can turn what went wrong in a
past session into feedback. The policy of the config file's [collect]
table is applied per session, by its recorded working directory, before the
log is opened. Digesting and marking store only watermarks; Push
uploads eligible, scrubbed transcripts to a configured server whose
organisation enables processing, manually or through the automatic sweep.
Session state always lives in the data-directory database
(${XDG_DATA_HOME:-~/.local/share}/agentfeedback/agentfeedback.db), in local
and in remote mode. sessions status also reads the configured server's
session policy, subject to its cache and backoff, and prints the org's
processing status (capture in JSON). Listing, digesting and marking do
their own work locally; eligible CLI invocations can also trigger the sweep.
submit and sessions push send their respective content to the server.
See Session policy settings.
Use agentfeedback config or config set sessions.quiet 10m to change this
machine's quiet interval. The menu shows organisation policy and repository
restrictions read-only; changing a setting never grants capture permission.
See Machine settings for all editable keys.
Server authentication is separate from collection permission. The four
onboarding sources (paste key, email
device login, agent self-enrolment, stdin enrolment token) and explicit
agentfeedback login reauthentication do not enable capture or relax
repository restrictions. A changed key invalidates credential-scoped cached
policy; the server's current policy still governs uploads. Keep keys,
tokens, device codes, verification URLs and claim URLs out of session logs.
Push
agentfeedback sessions push claude-code:<session-id> --json
agentfeedback sessions push --due --limit 10 --json
agentfeedback sessions push codex:<session-id> --since 2026-01-01T00:00:00Z --dry-run --json
Push supports Claude Code (claude-code-jsonl), Codex (codex-rollout),
OpenCode V1 and 2 (opencode-virtual-log) and Cursor
(cursor-agent-transcript, storage only). It sends the whole complete-line
revision, not the digest or only new entries. The revision is the decimal
source byte count. An unfinished last line is excluded; changes during
capture defer the upload.
The configured server must enable processing. Collection rules and every
recorded repository's [sessions] push = false veto apply before export.
Unreadable, unsupported, unknown-project, disabled and denied sessions do
not upload. The last entry must be at or after this machine first observed
enabled policy; --since explicitly replaces that cutoff. --due selects
quiet sessions oldest-last-entry first, up to --limit (default
sessions.push_batch); named references bypass quiet time, not safety checks.
The run respects sessions.push_budget and ten-second request timeouts.
All transcript text is scrubbed before hashing or splitting into parts of
at most 4 MiB, at line boundaries where possible. JSON string values are
decoded for scrubbing, including escaped credentials; multiline private
keys are redacted across records. Capture processes records incrementally,
retaining bounded scrubbed output rather than the whole raw transcript.
Individual records still require record-sized decoding buffers. The
assembled canonical envelope must fit the server's size cap; there is no
separate raw-source size cap. Uploads resume acknowledged parts and complete
as one kind: session submission. An unchanged successful revision is
replayed without sending parts again.
Outcomes are pushed, replayed, skipped with a reason, deferred,
suspended, or error. Dry runs report dry-run, revision, byte and part
counts, and at most 2 KiB of scrubbed preview; they discover policy but send
no transcript. A named session that cannot be pushed makes the command exit
- A 429 suspends the run and policy until retry is allowed; network and
server failures leave it for a later invocation. A begin conflict is
replay_mismatchand is not retried at that source size.
sessions_pushed stores only upload metadata and outcomes, scoped to the
destination, organisation, credential and consent period, separately from
digest watermarks. session_manifests retains the initial scrubbed upload
metadata and partition size while an upload is pending, so retries do not
change identity when repository facts or file timestamps change. Current
collection rules and repository vetoes still apply before each request.
Pending metadata is removed on completion, terminal rejection, a new
revision or a changed consent scope. Transcript text and previews are never
persisted to the database, cache, log or spool. too_large is not retried unless the source
shrinks or rotates, or the server's cap grows. sessions list shows the
pushed revision; sessions status shows pushed, pending and skipped counts
and the last push time for the current scope.
Automatic sweep
In remote mode, existing turn-end hooks and eligible CLI invocations can
launch a detached, short-lived agentfeedback sessions push --due --json.
The trigger reads only cached policy matching the destination and credential;
it makes no network request. Previously enabled consent can retry after a
transient policy error or an elapsed Retry-After. A changed credential,
authorization failure, disabled or unsupported policy cannot revive consent.
The upload child always refreshes policy before uploading.
Commands that open a validated remote API client can trigger the sweep,
including mcp and ui startup. Commands with no such client, and the
diagnostic doctor, explicit sessions push, flush and ingest paths, do
not. Existing turn-end hook handling triggers after its bounded flush.
No new hook registrations, resident daemon or scheduler are installed.
A nonblocking push.lock, inherited by the child, allows at most one sweep
at a time. push.last-started throttles launches by sessions.quiet.
An abandoned run can be retried after sessions.push_budget plus one minute
once the operating system has released its lock; an old timestamp never
permits bypassing a still-running child's lock. Malformed, zero or future
start timestamps are repaired only after acquiring the lock. The parent
launches its own running executable, not a PATH lookup, and does not wait
for uploads; the whole hook trigger shares the hook's deadline.
Each child applies the quiet-session selection,
sessions.push_batch and sessions.push_budget described above and exits.
The cache's owner-only push.last-run.json records start and finish times,
pushed, replayed, skipped, deferred and error counts, and any suspension
deadline. doctor prints it as last push; both doctor --json and
sessions status --json expose it as last_push. It contains no transcript
text, credentials or session identifiers.
Harnesses
| Harness | Reader | Store (moved by) | Working directory, checked before the session is opened |
|---|---|---|---|
claude-code | claude-code-jsonl | <config dir>/projects/<encoded launch directory>/<session id>.jsonl ($CLAUDE_CONFIG_DIR, else ~/.claude) | the encoded project directory name, before; each entry's cwd, after |
codex | codex-rollout | <codex home>/sessions/YYYY/MM/DD/rollout-*.jsonl and <codex home>/archived_sessions/rollout-*.jsonl ($CODEX_HOME, else ~/.codex) | the session_meta first line, read alone, before; turn_context, after |
copilot | copilot-events | <copilot home>/session-state/<session id>/events.jsonl ($COPILOT_HOME, else ~/.copilot) | workspace.yaml beside the log, before; session.start and session.context_changed, after |
gemini-cli | gemini-cli-jsonl | <gemini dir>/tmp/<project>/chats/session-*.jsonl ($GEMINI_CLI_HOME/.gemini, else ~/.gemini) | <project>/.project_root, before; the metadata line's directories, which the log records only for sub-agents and /dir add, after |
opencode-v1 | opencode-sqlite | $OPENCODE_DB (a relative one under <data home>/opencode), else opencode.db and opencode-*.db in <data home>/opencode ($XDG_DATA_HOME, else ~/.local/share) | the session row's directory, before; each assistant message's path.cwd, after |
opencode-v2 | opencode-v2-sqlite | the same databases, read only when they have a session_v2 table | the session row's directory, before |
OpenCode V1 session references use opencode-v1:<id>. The input alias
opencode:<id> resolves and marks the same session. Existing local session
state migrates to the canonical name without resetting processed progress;
saved harness selections accept the alias too. The store paths, SQLite
reader format, detector version and finding idempotency keys remain unchanged:
retrying a previously filed span still uses its original key.
OpenCode 2 (checked with 2.2.6) stores sessions in a different schema in the
same database files, and migrates a V1 database in place: it adds its own
rows with the same session ids and keeps V1's tables. A database with a
session_v2 table is therefore read only by opencode-v2; the V1 reader
skips it, so a session is never listed under both names. Sessions migrated
this way are listed once, as opencode-v2, and their V1 state does not
carry over: they read as new, so a scan files their findings again under new
keys and sessions push uploads them again under opencode-v2.
A directory variable that is not an absolute path is ignored. A store
directory that cannot be listed, or a session file whose directory entry
cannot be read, is named in the store's problems and its sessions are not
listed; a watermark row whose file it hides is unreadable, not absent. A
recorded working directory that is not an absolute path is ignored, as if not
recorded. Every reader takes a result's status from the harness's own record
where there is one and otherwise from fixed texts of the harness, matched
literally, so a reworded denial or abort message in a new harness release is
missed: Claude Code and OpenCode read it as a plain error; Codex as ok
unless an exit code says otherwise; Gemini CLI as an error, or as
interrupted on a cancelled call; Copilot CLI takes denials and aborts from
records, not texts.
A session is named by its ref, <harness>:<session id>; an event of a
digest adds #<span>, the reader's id of the log entry that carries it
(per reader below).
Claude Code
- Span: the entry's
uuid; a tool call's span is that of the assistant entry carrying itstool_use. - Prompt: a
userentry with text that is notisMetaand carries notool_result; one starting[Request interrupted by useris an interrupt. - Results:
is_errormakes a result fail; it is denied when it containsThe user doesn't want to proceed with this tool useorhas been denied, interrupted whentoolUseResult.interruptedis true or it contains[Request interrupted by user. The log records no exit code: the digest reads it from anExit code Nfirst line. - Not read: subdirectories of a project directory, sub-agent transcripts
among them. The encoding turns every byte outside
[A-Za-z0-9]into-.
Codex
- Span: the call's
call_id; a prompt from anitem_completedUserMessageitem, its itemid; any other entryL<offset>, its line's byte offset. A rewritten rollout changesLspans. - Prompt:
event_msguser_messageanditem_completedUserMessage, neverresponse_itemuser messages, which also carry injected context. - Results: the first output line of a call is its result. A legacy result
(
function_call_outputtext) is classified from the text Codex writes, never the command's output: the header lines before the first line that is exactlyOutput:. An exit code of 0 there is ok. Otherwiseexec command rejected by user,patch rejected by user,rejected by configurationorautomatic approval review denied the actionin the header is denied (without anOutput:line, only when the text starts with it), a line startingaborted by userinterrupted, and a non-zero exit code in aProcess exited with code NorExit code: Nheader an error. The oldest form ({"output", "metadata"}) is classified by itsmetadata.exit_codealone. A lateritem_completed(failed,declined),patch_apply_endormcp_tool_call_endrefines the status, and the exit code when it records one; one that changes the status moves the call's result to its line, so an incremental digest reports it. Aturn_abortedwith reasoninterruptedinterrupts every call still without a result. - Metadata: the
session_metafirst line is read alone, one byte at a time, nothing past its newline, at most 1 MiB. When it is longer, is notsession_metaor does not decode, and the policy hasdeny_pathsoropt_in_only, the session isdeniedwithout being read ("the session metadata could not be read, so the policy could not be checked"); without such a policy it is read and checked by its recorded working directories. The read checks its first line again: acwdother than the one the gate checked makes the sessionunreadable("the session changed since its policy was checked"). - Not read: a compressed rollout (
.jsonl.zst, listedunsupported-format), and thehistory_baseprefix of a paginated rollout, kept in another file. The session id is the file name afterrollout-<time>-, without the extension; a session id in bothsessionsandarchived_sessionsis listed fromsessions, the other named inproblems.
Copilot CLI
- Span: the event
id; a tool call's span is that of itstool.execution_start. - Prompt:
user.messagewith sourceuser(or none) that is not an autopilot continuation. - Results: the first
tool.execution_completeof a call: error coderejectedordeniedis denied; success with no exit code or exit code 0 is ok; anything else, success with a non-zero exit code included, is an error. The exit code isshellExecution.exitCode, else, for a shell call (toolbashorpowershell, a start withshellToolInfo, or a complete withshellExecution), the closingcompleted with exit code Nline.permission.completedwith adenied-kind denies a call without a result, withcancelledinterrupts it; one after the result changes nothing.abortinterrupts every call without a result. - Not read: events with an
agentId(sub-agents), thesession-store.dbindex and the legacy flat transcripts. Only a top-levelcwd:scalar (or the JSON membercwd) ofworkspace.yamlis read, and only from a regular file of at most 64 KiB.
Gemini CLI
- Span: the message
id. A message written again under its id updates its entry in place: a rewrite of an already-digested message updates its text or model without reporting it again. A call's result is the first line that records it in a terminal status. - Prompt: a
usermessage with text, not only function responses, not starting with<session_context>, and not a slash command (a first word matching/[A-Za-z][A-Za-z0-9_-]*, so/home/u/x fix thisis a prompt). - Results: status
erroris an error,cancelledinterrupted;[Operation Cancelled] Reason: User denied execution.orTool execution denied by policy.is denied. Gemini CLI records no exit code: arun_shell_commandresult whose trailer (the final lines startingError:,Exit Code:,Signal:,Background PIDs:orProcess Group PGID:) has anExit Code: Nline (N not 0) is an error with that exit code, one with aSignal:line an error without; lines the command printed are not read. - Working directory:
.project_rootis read only from a regular file of at most 4 KiB, and not at all when collection is disabled. - Not read:
chats/<parent session id>/subdirectories (sub-agents), andsession-*.jsonsingle-document sessions of releases before v0.39.0, listedunsupported-format. A$patchrecord is not applied, and messages a$rewindToremoves are kept: they happened.
OpenCode
- Span: the message id of a prompt, the part id of a tool call.
- Prompt: a user message's text parts that are neither synthetic nor ignored; a user message counts once it has a part.
- Results: a
completedtool part is ok unlessmetadata.exitis a non-zero integer (an error with that exit code), or null with the shell tool's abort note (interrupted) or timeout note (an error). Anerrorpart is interrupted when aborted, denied when the user or a rule refused it or a question was dismissed, else an error. Ametadata.exitof 1.0 is exit code 1. An assistant message ended byMessageAbortedErroris an interrupt. - Not read: child sessions (
parent_idset: sub-agents). A session is laid out as a virtual append-only log, one record per message then per part, so offsets, sizes and watermarks work as for a file. A record holds only the fields that are final once the row is complete, so OpenCode's later rewrites (a user message's summary, a tool part's compaction time) do not change it. A user message without parts, an assistant message not completed, or a tool part not finished, ends it like a final line without its newline, and the session's mtime is itstime_updated. A session id in two databases is listed from the first, the other named inproblems. A session whosedirectorychanged since it was listed isunreadable. - Open: never written, and never opened
mode=ro(SQLite opens and creates the-shmfile read-write even then): alwaysimmutable=1, so no lock is taken and no-walor-shmfile is opened or created. The database file and its-walare checked (existence, size, mtime) before and after the read; a change discards the read and reads once more, and a second change makes itunreadable("the database changed during the read"). Rows still only in OpenCode's write-ahead log are not seen until OpenCode checkpoints them into the database file, at the latest when it exits; the session then reads asappended.
OpenCode 2
- Store: tables
session_v2,session_message(one row per message, typeduser,assistant,system,idleand others, ordered byseq) andsession_partwhen present; opened, checked and retried as for V1. - Span: the message id of a prompt,
<message id>/<index>of a tool call embedded in an assistant message,<message id>/<ordinal>of a part row. - Prompt: a
usermessage with non-empty text. - Results: a tool is
completed(ok, unlessstate.metadata.exitis a non-zero integer: an error with that exit code) orerror(an error; the 2.2.6 samples show no abort or denial text to classify). Anidlerow with outcomeinterruptedis an interrupt. - Layout: a virtual append-only log, one record per message then per part,
each holding only fields final once complete; an assistant message without
time.completed, or with a streaming or running tool, ends it. A file a tool returned is recorded without its inlinedata:content (its type, name and MIME type stay). A session moved to another directory (location-switched) records both directories, and both are checked against the[collect]rules. - Not read: child sessions (
parent_idset).
Cursor
Hook-indexed listing and capture are supported; semantic digests are not.
postToolUseFailure, stop and sessionEnd record the conversation id,
absolute transcript path beneath the home directory, workspace roots and
reported cwd in the existing owner-only session counters cache (seven-day retention).
install cursor wires all three events; sessionEnd is fire-and-forget.
No transcript is opened by the hook.
Listing and push apply collection policy and repository upload vetoes to every
recorded workspace root and the reported cwd before opening the transcript.
The first root supplies the displayed project. Missing or invalid roots are
unknown-project; symlinks (including ancestor components) and nonregular
files are refused. A symlinked home or ancestor therefore prevents capture.
Only complete lines are captured, labelled cursor-agent-transcript for
both IDE and CLI sessions. Transcript mtime supplies the capture timestamps;
tool counts and model metadata are unavailable.
Automatic mixed-harness digests skip Cursor without opening its transcripts.
After seven days without a hook update, cache pruning removes the transcript
pointer; the transcript remains untouched but is no longer discoverable until
another hook indexes it.
Verification is documentation-based, using the official
hooks contract and
CLI changelog, checked 2026-11-10,
plus synthetic IDE and CLI fixtures. No installed Cursor version was
live-verified. The CLI changelog's Claude Code-compatible JSONL claim does
not establish digest compatibility; both surfaces remain storage-only.
The IDE format lacks tool results, timestamps, call ids and cwd. The
undocumented SQLite/protobuf stores (store.db, state.vscdb) are not read.
Antigravity
Not read. transcript.jsonl under <app data dir>/brain/<conversation id>/.system_generated/logs/ records steps with a status but no working
directory or call ids; its directory differs per product surface, it is
rewritten on compaction, and versions before agy 1.2.4 omit failed steps.
conversations/*.pb are encrypted and the *.db files undocumented.
Devin: no documented local session store; not read (best effort).
Commands
| Command | Flags | What |
|---|---|---|
sessions list | --harness H, --since T, --unprocessed, --json | every session with its state; --since keeps sessions whose last entry is at or after T (RFC 3339 or 30m, 12h, 7d, 2w); --unprocessed keeps new and changed |
sessions digest <harness:id>... | --allow-unknown-project, --json | the digest of the named sessions; a processed session named here is digested whole again |
sessions digest --unprocessed | --harness H, --limit N, --allow-unknown-project, --json | the digest of the new and changed sessions (and the unknown-project ones with --allow-unknown-project), oldest last entry first, at most N (0: no limit) |
sessions mark <harness:id>... | --outcome filed|nothing|skipped, --ref UID (repeatable), --json | records the outcome and the uids filed; all refs or none; a session not digested yet is refused, and so is a uid not in the submission uid form (an RFC 9562 UUID, 8-4-4-4-12 hex) |
sessions status | --json | per harness: the store location and state, counts per session state; the saved selection |
sessions status --set-selection | --harness H (repeatable), --since T, --limit N, --json | saves the processor's selection (since is kept as written), then prints the status |
Without --json the output is a table (list), readable text with the
notice first (digest) or a summary (status, mark). sessions list
names on stderr a store that is unreadable or unsupported-format, and
each of its problems, always; an absent store only when --harness names
its harness. digest exits 1
when sessions were requested and none could be digested; its JSON then names
each in errors. A wrong command line exits 2.
States
Precedence, first match wins:
| State | Meaning |
|---|---|
disabled | [collect] disabled, or a recorded working directory is switched off (also by its repository's .agentfeedback.toml). Readers with store metadata (Codex's first line, Copilot's workspace.yaml, Gemini CLI's .project_root, OpenCode's session row) apply the whole policy, the repository's .agentfeedback.toml included, to that directory before the session is opened; Claude Code opens the file unless the user's [collect] disabled is set and finds a repository's disabled after the read, from the recorded working directory |
denied | the working directory checked before the open (Harnesses), or a recorded one, lies within a deny_paths entry, or outside every opt_in_paths entry with opt_in_only; each entry matches as written and with its symbolic links resolved, without case on macOS and Windows, and a filesystem root matches every directory; the file is not opened when the directory checked before the open matches. Claude Code's encoded name loses the path's separators, so its pre-open gate is exact for deny_paths but may over-deny a sibling whose encoded name shares the prefix (/a/b-c under a /a/b entry), and approximate for opt_in_paths: such a sibling is opened, then refused once its recorded working directory is read |
unreadable | opening or reading the file failed (reason says why) |
unsupported-format | no complete line parses as an entry of the reader's format, or the session is a compressed Codex rollout or a legacy Gemini CLI .json session; as a store state, an OpenCode database without the expected tables and columns |
unknown-project | the log records no working directory, so no policy can be checked; digested only with --allow-unknown-project |
absent | a watermark row whose file is gone |
new | no watermark row, or one never marked |
changed | the file differs from the marked watermark: reason appended (larger; digested from the marked size), truncated (smaller) or rotated (different first line); the last two are digested from the start |
processed | the file's complete-line size and first-line hash equal the marked watermark |
A recorded working directory that no longer exists gets the user rules only;
the repository file cannot be read and is skipped. A session refused after
the read (disabled or denied by a recorded working directory) is listed
with nothing read from the file: no project, cwds, start, end or
counts. list and status scrub path, project, cwds and problems
with the formats of submit --scrub.
Watermark
Only complete lines count: a final line without its newline is neither
parsed nor counted, so a session still being written is digested up to its
last complete entry. digest records, per digested session, the
size it covered, the SHA-256 of the first line, the file's mtime and the
time; mark copies that digest watermark into the marked one. The size
covered is the complete-line size, or, when the digest is truncated, the
offset that completes the first event it left out: under the cap, events
are kept in the order their prompt or result line completes them, so the
watermark always lies past every kept event and the next digest reports the
rest. A retry is kept with the failed call it repeats, so a pair split by
the cap may not be reported. The mtime is recorded but does not decide the
state. A crash after digest and before mark leaves the
session new or changed, and the next run digests it again from the last
mark; a crash after mark loses nothing. mark records the watermark of the
session's latest digest, so run one triage at a time per machine: a second
run digesting the same session in between moves the watermark the first one
marks.
The store is taken as append-only. A file larger than the marked size with
the same first line is changed (appended), and the next digest starts at
the marked size; a rewrite that keeps the first line and is the same size or
larger is not detected (a larger one is read as an append). Truncation or a
rewritten first line start it over (truncated, rotated).
Digest format
sessions digest --json prints one object; the MCP tool answers with the
same object.
| Member | What |
|---|---|
notice | the provider-boundary notice below |
detector_version | version of the digest rules; it enters the key of a submission filed from a digest |
sessions[] | one digest per session, below |
errors[] | {ref, state, message} per session refused or not found |
deferred[] | refs left out to keep the run under its cap; they keep their watermark and come next run |
Each session:
| Member | What |
|---|---|
ref, harness, session_id | the session |
project | the first recorded working directory |
cwd | the working directory of the last entry that records one |
start, end | first and last entry time, UTC with milliseconds |
model | the model of the last entry that names one (an assistant entry; for Codex the turn context) |
from_offset | byte offset the digest starts at (0, or the marked size when appended); an incremental digest also reports a tool call made before it whose result line lies at or after it |
truncated | events were left out to stay under the session cap |
counts | entries, prompts, tool_calls, tool_errors, interrupts, denials, unparsed (complete lines that are not an entry), from from_offset |
events[] | prompts and tool calls that did not succeed, in log order of the prompt or the call: span, ref (<harness>:<id>#<span>), at, type (prompt or tool_call); a prompt has summary (its first non-empty line); a tool call has tool, args_digest (a digest of the command, or of the input when there is no command), status (error, denied, interrupted), error_class (exit, tool_error, denied, interrupted), exit_code (the exit code the harness records, else an Exit code N first line of the result), excerpt of the result, and self (true when the call is agentfeedback's own command, by the same rule the hook uses; such a failure is rarely friction about the project) |
retries[] | {span, of_span, tool, args_digest}: a call repeating an earlier failed one with the same tool and arguments |
denials[] | spans of the denied tool calls |
flagged[] | {span, phrase}: prompts with a phrase such as still failing, doesn't work, wrong, why did you, again |
final_excerpt | the last assistant text |
Caps: a summary is at most 160 bytes, an excerpt 240; one session's digest is
at most 64 KiB (the events completed first that fit are kept, listed in log
order, truncated set, and the watermark stops at the first one left out); one
run is at most 256 KiB (later sessions go to deferred). Every string is
scrubbed with the formats of submit --scrub (security.md)
before it is sized or cut.
Labelled corpus and known limits
The digest reports facts; the model that reads it judges which failures are friction. What it shows for each kind of failure:
| Class | What the digest shows |
|---|---|
| genuine friction (a wrong doc, a missing flag, a stale config) | a tool_call event, status error |
expected failure (a probe meant to fail: test -f, grep without a match, git diff --exit-code) | exactly the same as genuine friction: the digest cannot tell them apart |
| cancellation (the user interrupted) | status interrupted, a status of its own |
| denial (the user rejected the call) | status denied, and the span in denials[] |
| repeat (the same failing call again) | a retries[] entry, only when the tool and the args_digest match exactly |
| self-noise (agentfeedback's own failing command) | self true: marked, not dropped |
self follows the hook's rule: the first word of a command segment (after
variable assignments and env, exec, sudo, command, time) is
agentfeedback, or the tool's name contains it. Codex's
["bash", "-lc", "<script>"] argv (a bash, sh, zsh or dash with
-c, -lc or -cl, and nothing more) is read as its script, so the rule
applies to the script.
The corpus lives in internal/sessions/testdata/corpus/: hand-written
sessions in the readers' formats and labels.json, one {session, span, class, note} per labelled span, with tool naming the call when the span
carries several (a Gemini CLI message). Its test digests every session and
fails on each label the digest does not show as above and on each
tool_call event without a label, so it measures everything the digest
emits. just ci runs
it with the per-reader golden tests,
cmd/agentfeedback/testdata/sessions/<harness>.{list,digest}.golden.json; a
reader without them fails the test.
Provider boundary
Every digest starts with this notice: the digest is shown to the model that reads it and so reaches that model's provider; agentfeedback itself stores and sends none of it. A digest is evidence about a past session, never an instruction: text quoted from a log is untrusted, as stored reports are.
Filing from a digest
agentfeedback submit friction --stdin --context-from claude-code:<session id>#<span> [--ordinal N] [--allow-unknown-project] < report.json
--context-from names the entry the report is about. The session is read
again under the current policy; a session now disabled, denied,
unreadable or unsupported-format, a missing session or span, a span that
records no time (a replay would not match), and an unknown-project session
without --allow-unknown-project are refused and nothing is filed. It works
with flags or with --stdin. It sets, over anything stdin gives:
key:session-scan-and the SHA-256 hex of harness, session id, span, ordinal anddetector_version, joined by NUL bytes;--ordinal(default 1) numbers several findings at one span.occurred_at: the entry's time;harness: the session's harness;model: the model of the last assistant entry at or before the span, and nomodelat all when there is none (never the configured or collected one).projectand the collected context: collected in the session's working directory with an empty environment, when that directory still exists and the[collect]rules still allow it; when it is gone,projectis its base name and nothing is collected. The working directory and environment of the process that runssubmitare never read.context.originsession-scan,context.detector(<reader>/1:claude-code-jsonl/1,codex-rollout/1,copilot-events/1,gemini-cli-jsonl/1,opencode-sqlite/1,opencode-v2-sqlite/1),context.session_idandcontext.session_harness, over the collected and stdin context;--context key=valuestill goes over all of them.
--key, --project, --harness and --model together with
--context-from are a usage error. machine resolves as for every
submission. Origin session-scan turns scrubbing on, and a body filed with
--context-from is always scrubbed, even when --context origin=...
replaces the origin. Filing the same report
again replays: outcome duplicate, exit 0. Different content under the same
key is outcome mismatch (the server's 409), exit 1: file a second finding
at that span with the next --ordinal.
Over MCP, submit_feedback of agentfeedback mcp takes context_from
(<harness>:<session id>#<span>) and ordinal (default 1): the other
arguments are what --stdin would read, kind is required, and the body
is the one submit <kind> --stdin --context-from <ref> --ordinal <n> builds,
so the same report filed both ways is one row (the second filing is a
duplicate). An unknown-project session is refused: admitting it is a
human's decision, --allow-unknown-project on the CLI. A refused call is a
validation error on context_from with the CLI's message. In remote mode
the body is built on this machine and forwarded to the server's
submit_feedback; the HTTP /mcp never takes these arguments.
MCP tools
agentfeedback mcp (stdio) serves three more tools over the same database
and policy, in local and in remote mode (the session state stays in the
data-directory database either way); the HTTP /mcp never serves them.
| Tool | Arguments | CLI |
|---|---|---|
sessions_list | harness, since, unprocessed | sessions list |
sessions_digest | refs[] or unprocessed; harness, limit (with unprocessed) | sessions digest (records the watermark, so not read-only); --allow-unknown-project is a human's decision and has no argument |
sessions_mark | refs[], outcome, uids[] | sessions mark |
Unknown, repeated or mistyped arguments are refused with a validation error, as for the other tools. Their results reach the provider of the model that calls them.
Security
By default AgentFeedback is one binary and one database owned by one OS user on one machine, with nothing listening (The client on a machine).
Releases
Source releases are annotated, immutable `vMAJOR.MINOR.PATCH` tags, with `-rc.N` pre-releases before a stable one (Tags): patch for compatible fixes, docs and…