| name | session-recap |
| description | Extract a compact recap from previous pi or Claude Code session JSONL without reading raw JSONL. Use for 'last session', 'what was I doing', continuity restore, and pi-internal GPT vs ACP recall via --source pi --harness gpt|acp. Preserves the skip-before-size edge case; retry with --min-kb 0 when a real recent session is below the default size floor. |
session-recap — extract a session summary
Extract only user/assistant text from a session JSONL.
Never read a raw JSONL directly — it dumps ~50KB of JSON noise into context.
Multi-harness corpus: handles pi and Claude Code sessions. The unset-source runtime
fallback is Claude Code → claude, every other surface → pi; only those first two
callers are truly harness-matched. Codex, Antigravity, and other third surfaces write
neither indexed corpus, so they must choose --source explicitly and normally use
--skip 0. Inside --source pi, use --harness gpt|acp|all to distinguish pi native
GPT/Codex from entwurf Claude/Opus. --source claude still means Claude Code only.
Corpus filters (aligned with andenken session-indexer.ts — 0d4432b "tighten
corpus … >300KB, drop tmp + legacy"; the pi filename spec was re-pinned to the current
native form on 2026-08-10). Same discipline as session
embeddings, so recap usually sees substantive sessions instead of probes. Three filters
exist, and their order is part of the contract:
- Structural filters (applied before skip):
- tmp dirs excluded (both runtimes) — pi
--tmp…-- / claude -tmp… scratch.
- pi current native filename only —
<created-at>_<UUIDv7>.jsonl, pi's present-day
session id. No backward compatibility (GLG ruling 2026-08-10): the older garden-id
form (_YYYYMMDDTHHMMSS-<6hex>), UUIDv4, _delegate-, and _entwurf- are not
OR'd back in — corpus admission is one current spec. pi stopped emitting garden-id
suffixes on 2026-08-06; a filter still demanding them made every pi session after that
date invisible to both recap and the andenken session index. Filenames do not carry
identity: the garden id ↔ nativeSessionId ↔ transcriptPath join belongs to the
entwurf meta-record, and neither recap nor andenken reimplements it. claude is always
UUID, so no filename filter.
- Pi harness filter
--harness gpt|acp|all (applied after skip, before size):
gpt = pi native OpenAI/Codex (openai-codex / gpt-*), acp = entwurf
Claude (entwurf / claude-*). Unknown pi sessions pass only with all.
- Size filter
--min-kb 300 (applied after skip + harness): drops short test/probe
fragments from what's shown. This is a heuristic, not truth: a real GPT/Codex
session can be below 300KB. Disable with --min-kb 0 when the expected session is
missing or the header looks stale.
Why size filter runs after skip (bug fix 2026-06-19). --skip 1 drops the current
live session, identified by the invariant current session = newest mtime (true on
any harness — it's the file being written right now). Early in a session that file is
still small (<300KB). If the size filter ran first it would drop the current session
from the list, so --skip 1 would then drop the most-recent real session instead and
recap would surface a stale one. So: structural filters → skip on the full recency list
→ optional pi harness filter → size filter on the survivors.
Size-floor edge case. If --source pi --harness gpt (or acp) returns no session or
an older-than-expected header, do not switch project names or read raw JSONL. First
retry the same command with --min-kb 0 (or a lower floor such as --min-kb 100). The
300KB default is there to suppress probes; it is allowed to hide a real but shorter
conversation.
This skill is the low-level extractor under /recall. Single repo/session restore lives
here; multi-axis recall (cross-project, day-query, journal §/llmlog) follows
commands/recall.md. (The old slash name /recap collided with a Claude Code built-in
and was renamed 2026-05-12.)
API
python3 {baseDir}/scripts/session-recap.py -p <PROJECT> -m 15
| Flag | Default | Description |
|---|
-p, --project NAME | all | Project filter (exact match). Always specify. |
-m, --messages N | 20 | Last N messages per session |
-s, --sessions N | 1 | Last N sessions |
-c, --chars N | 300 | Max chars per message |
-a, --all-projects | - | Include all projects |
--commits | off | Include git commit commands |
--cost | off | Session cost summary |
--skip N | 1 | Skip newest N sessions (the current one) |
-f, --format | text | text or json |
--source | runtime fallback | pi, claude, all. Unset → Claude Code=claude, every other surface=pi; third surfaces should set explicitly |
--harness | all | pi-internal filter: gpt, acp, all. Use with --source pi or all |
--min-kb N | 300 | Size floor, size > N*KB. 0 disables; use it when a real recent GPT/ACP session is below the default floor |
--device NAME | all | Session-corpus device filter (oracle, thinkpad). Implies --skip 0 unless you pass --skip yourself. Only meaningful with a corpus configured — see below |
--session-file PATH | — | Exact selection. One absolute .jsonl path. Bypasses every discovery filter; cannot be combined with any of them |
Session corpus — other machines' sessions
ANDENKEN_SESSION_CORPUS (set in ~/.env.local, shared with andenken's indexer)
points at the gathered corpus, where every device's sessions live under
<corpus>/<device>/ keeping the runtime's own path shape. Unset → live stores only,
exactly as before.
The variable is the switch; ~/.env.local is its SSOT. When the variable is
absent from the process environment, recap reads that one key out of the file
before giving up. A process environment is captured once at login, so a line added
to .env.local afterwards is invisible to every session, daemon and agent that
started earlier — measured 2026-09-03, a shell carried every other
ANDENKEN_SESSION_* but not ANDENKEN_SESSION_CORPUS, and the corpus was silently
off while semantic-memory kept returning corpus paths that --session-file then
refused. Setting the variable to the empty string (export ANDENKEN_SESSION_CORPUS=)
is still a deliberate live-only opt-out and wins over the file.
That opt-out is read-side only. andenken's sync-sessions.sh has the same
fallback (its own ANDENKEN_SESSION_CORPUS guard near the top of the script, read
2026-09-03) but tests with -z, so an empty value there is treated as unset and the
file wins. Same variable, two readings of the empty string: recap/improve-agent stay
on live stores, the indexer still gathers. Use the empty string to scope a recap, not
to disable the corpus system-wide.
Discovery reads live ∪ corpus, not corpus instead of live. andenken's indexer
replaces the live stores because sync-sessions.sh gathers first and so owns the
corpus's freshness; recap has no gather step. Corpus-only discovery would drop this
machine's current session whenever it was written after the last gather, and that
silently breaks the --skip 1 invariant (current session = newest mtime) the whole
size-filter ordering above exists to protect.
Copies of one session held by two devices are folded by basename (UUID session id):
larger file wins — a transcript only grows, so the larger copy holds strictly more turns
— and a size tie breaks on the lexicographically smaller path, which keeps the choice
stable across machines. Same rule as andenken dedupeByBasename. Measured 2026-09-02:
2,104 live → 2,567 after union (+463 oracle), 0.07s.
The header shows provenance as [claude@oracle] / [pi:gpt@oracle]; live sessions carry
no @ suffix. A device says where a session was collected, not where it was created.
The two machines exchanged an rsync -a with mtimes preserved, so origin is not
recoverable — use it to label and filter, never to rank.
--device — what it selects, and why it skips nothing
--device filters before the dedupe fold, so it names the copy under that device,
not "the winner that happened to come from there". Two consequences, both measured
2026-09-03 on the live corpus:
- On this machine,
--device thinkpad is the corpus copy, not the live one. Of
2,575 discovered sessions the winning copy was live for 2,106 and oracle for 469
— zero thinkpad-corpus winners, because a live transcript is always at least as
large as its gathered copy. --device thinkpad is therefore how you reach a
session that was deleted from the live store and survives only in the append-only
corpus; for anything still live it just names the other copy of the same session.
--device implies --skip 0. --skip 1 exists to drop "the session being
written right now", identified as newest mtime. That session is in the live store
and carries no device, so it can never appear in a device-filtered list — skipping
one there throws away the other machine's genuine newest session instead. Measured:
-p agent-config --device oracle hid the 2026-09-02T19:08 session (69f08580)
entirely until --skip 0. An explicit --skip N is still honoured.
Exact selection — --session-file
The flags above search for a session. --session-file names one. Use it when
another tool already found the exact session — today that means semantic-memory, whose
results carry the session file. Without this flag search hands you a precise answer and
recap goes back to guessing "the recent session of this project".
python3 {baseDir}/scripts/session-recap.py --session-file /abs/path/session.jsonl -m 20
What it bypasses — all of it, deliberately. tmp exclusion, the pi native
filename spec, --min-kb, and --skip 1. Dropping skip means exact selection can
target the live current session — sometimes what you want, but never by accident.
Reading your own live transcript may show an inverted 기간 line because the file is
still being appended and is not strictly time-ordered; the extracted text is unaffected.
It refuses rather than guesses. Combining it with -s/-p/-a/--skip/--source/ --harness/--min-kb is an error (exit 2), not a silent override — those flags describe a
search that is no longer happening. Named errors, each a different fact: not absolute ·
not .jsonl · a symlink · unreadable · outside ~/.pi/agent/sessions,
~/.claude/projects, and (when set) the corpus roots · parsed 0 messages (exit 1 — the path is a real session file
but holds no readable turns; nothing is printed to stdout, so you can never write a
header for a session you did not actually read).
The header contract is unchanged, plus one 파일: line carrying the full path, so the
answer rules below apply identically.
Seam contract — what this flag does and does not join
| Signal | Owner | Consumed here? |
|---|
file / sessionFile (path) | semantic-memory | Yes — --session-file |
line / lineNumber | semantic-memory | No. Different address space |
entwurf-peek → recap is still an open seam. peek resolves a citizen's transcript
path internally but never emits it: peek prints only <parent>/<name> and has no
--json, and situation --json rows carry no transcript path. So a garden id cannot be
turned into a --session-file argument yet. Do not describe that path as available.
semantic-memory reports sessionFile:lineNumber where lineNumber is the raw JSONL
line (blank and non-message records included). recap counts filtered messages. The
two never line up, so recap does not take a line anchor and will not grow one. To read
around a semantic hit, that is semantic-memory's own job:
semantic-memory search-sessions ... --with-excerpt. Division of labor: the hit
neighborhood belongs to semantic-memory, the exact session's spine belongs to recap.
Examples
python3 {baseDir}/scripts/session-recap.py -p agent-config -m 15
python3 {baseDir}/scripts/session-recap.py -p dictcli -m 20 --cost
python3 {baseDir}/scripts/session-recap.py -a -m 10
python3 {baseDir}/scripts/session-recap.py -p notes -s 3 -m 10
python3 {baseDir}/scripts/session-recap.py -p nixos-config --commits
python3 {baseDir}/scripts/session-recap.py -p agent-config -m 15 --source pi
python3 {baseDir}/scripts/session-recap.py -p entwurf -m 15 --source pi --harness gpt
python3 {baseDir}/scripts/session-recap.py -p entwurf -m 15 --source pi --harness acp
python3 {baseDir}/scripts/session-recap.py -p agent-config -m 15 --source claude
python3 {baseDir}/scripts/session-recap.py -p agent-config -m 15 --source all
python3 {baseDir}/scripts/session-recap.py -p agent-config -m 15 --min-kb 0
Choosing -p (project name)
Default rule: the last directory component of CWD.
project = repo directory name (~/repos/gh/agent-config → agent-config).
| CWD | -p value |
|---|
~/repos/gh/agent-config | agent-config |
~/repos/work/some-proj | some-proj |
/home/junghan (home) | home |
User intent overrides the CWD rule
In these cases don't use the CWD basename mechanically — use the project of the
context the user pointed at:
- "home 디렉토리 분신", "Entwurf", "분신 기록" →
-p home
- "COS" / 비서실장 session →
-p cos
- a named repo steward session → that repo name (
andenken, notes, entwurf, …)
When unsure:
ls -lt ~/.pi/agent/sessions/ | head
inspect the recent session dirs and confirm the user's stated task matches a recent
session name.
Without -p, you get the single newest session across all projects — possibly a
different repo's.
Workflow: "what was I just doing?"
Step 0: First decide if the user means home / Entwurf / COS / a specific repo steward.
Step 1: python3 {baseDir}/scripts/session-recap.py -p <PROJECT> -m 15
(pi/Claude Code: unset source follows that runtime; third surfaces: set it explicitly)
Step 2: Verify the target via the header (`═══ project [source] (file...) ═══` or
`═══ project [pi:gpt|pi:acp] (...) ═══`) and the first 1–3 messages.
Step 3: If empty, stale, or too short → rerun the SAME axis with --min-kb 0 first
(small recent session), then widen to --source all → -s 3 --skip 0
Step 4: Summarize from the verified output only.
Escalation: multi-axis recall
Don't stop at session-recap — escalate to the /recall protocol when:
- The retrieved session is short (1-turn entwurf / smoke / "Reply OK").
- The user says "어제 전체", "오늘 이어서", "기억축", "compact 없이", "나를 리콜".
- The current repo session is right but cross-project recall (agent-config / andenken /
voscli …) looks important.
- A journal
§repo marker or llmlog may be the real spine of the work.
Escalation order: session-recap → extract proper nouns from the output →
two-pass session_search → if needed day-query (gitcli --summary, denotecli day,
lifetract, calendar) → report both the axis you saw and the one you didn't.
Why the runtime fallback? To continue prior work under pi or Claude Code, read that
same runtime's sessions (pi under pi, claude under Claude Code — automatic). Codex,
Antigravity, and other third surfaces have no matching indexed source, so the fallback to
pi is only a compatibility default, not a claim that pi is their current transcript.
Historically Claude Code produced many 1–2 message stubs, which argued for preferring
pi; the >300KB size filter now removes those stubs, so claude sessions also retain