Skip to content

feat(persona): persona distillation surface (doc 06 launch milestone)#75

Merged
senamakel merged 11 commits into
mainfrom
agent-memory
Jul 14, 2026
Merged

feat(persona): persona distillation surface (doc 06 launch milestone)#75
senamakel merged 11 commits into
mainfrom
agent-memory

Conversation

@senamakel

Copy link
Copy Markdown
Member

Summary

Implements doc 06 — Persona Distillation (goals P1, P2, P5, P6, P7, P8, P9, P10, P11): a new ingestion + distillation surface that turns a person's local coding-agent history (Claude Code, Codex), their agent instruction files (CLAUDE.md / AGENTS.md / .cursorrules / copilot), and their git commit history into a durable persona memory layer — compiled into a 5–10k-token, prompt-ready persona/PERSONA.md context pack.

Everything is gated behind a new default-off persona Cargo feature and builds on existing seams (flavoured trees, ChatProvider/Summariser/EmbeddingBackend traits, SyncStateStore pattern, the PII/secret guard). Nothing under persona/ names a concrete provider.

What's included

  • P1 — evidence model (persona/types.rs): PersonaSourceKind, EvidenceTier (T0–T3), 7 PersonaFacets + default asks, content-addressed evidence ids, constructor-enforced redaction via the composite sanitize_text (secrets + PII).
  • P7 — OpenRouter reference provider (providers/openrouter.rs, behind providers-http): the crate's first concrete ChatProvider + Summariser + EmbeddingBackend (closes the C3/M3 seam). SecretString key, retry/backoff, fail-fast 4xx, per-run cost/call budget cutoff. Wiremock-tested + live-verified.
  • P2 — Claude Code + Codex readers: streaming, user-turn-only extraction (prompts T2, corrections T1 with assistant context), vendor/tool/sidechain exclusion. Live-measured 99.3% / 98.8% byte reduction on the real corpus.
  • P5 — instruction-file reader: rule-granular, verbatim T0 directives with global-vs-repo scope + content-sha change detection.
  • P6 — git-history reader (behind git-diff): author-filtered commit-message batches (T2) + bounded sampled diffs (T3).
  • P8 — pipeline + compiler: LLM digest map (strict-JSON, windowed, soft-fallback) → 7 facet flavoured trees → deterministic compiler (fixed section order, Directives budget-protected & near-verbatim, per-facet clamp, [5k,10k] total ceiling, strength annotations).
  • P9 — checkpointing & budgets: persona-local PersonaStateStore + file-backed store, backfill/incremental modes, run-budget cutoff with clean checkpoint & correct resume.
  • P10 — harness & config: examples/persona_harness.rs (backfill|incremental|compile|status), declarative PersonaConfig, .env.example updated.
  • P11 — verification: tests/persona_e2e.rs (wiremock OpenRouter, pack structure / tier order / resume / cost assertion), quality rubric + first-run scorecard (docs/plan/06-persona-rubric.md).
  • Perf: the network-bound digest map runs concurrently (bounded, order-preserving buffered) while folds stay serial — configurable via digest_concurrency.

Verification

  • 1126 base-crate tests pass (no regression); 46 persona unit + 3 integration e2e + 6 provider wiremock tests, all green. cargo fmt clean; default build unaffected (feature default-off).
  • Live run over this machine's real transcripts + CLAUDE.md + git via OpenRouter (DeepSeek v4 Flash): 40 sessions → 774 observations → 6.8 KB PERSONA.md for $0.063. Reproduces 8/8 independently-known preferences with no fabrications.

Follow-ups (out of scope, tracked separately)

  • P3 (opencode / Cursor SQLite readers) and P4 (ChatGPT / Claude.ai export readers) — the plan's explicit follow-on sources.
  • A full uncapped backfill (~4,000 sessions ≈ $6) once concurrency is tuned.

🤖 Generated with Claude Code

https://claude.ai/code/session_019sJbtz8aPD1AjpNz4uZ6sL

senamakel and others added 10 commits July 14, 2026 05:25
New default-off `persona` feature and `src/memory/persona/` module. types.rs
defines PersonaSourceKind, EvidenceTier (T0-T3 confidence ladder), PersonaFacet
(7 lenses + default asks), EvidenceSource, PersonaEvidence, DigestObservation,
SessionDigest. Evidence ids are content-addressed (sha256(source_id ‖
excerpt)[..32]); the excerpt is constructor-redacted via the composite
sanitize_text (secrets + PII) with a private field so evidence cannot be built
from unredacted text. Unit-tested.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019sJbtz8aPD1AjpNz4uZ6sL
providers/openrouter.rs (behind providers-http) implements ChatProvider (JSON
mode), Summariser (plain-text folds honouring the flavoured-tree ask), and
EmbeddingBackend against OpenRouter's OpenAI-compatible endpoints. SecretString
key handling; retry/backoff on transport + 429/5xx; fail-fast on 4xx with the
status embedded; per-run usage/cost accumulation with a hard budget cutoff.
Wiremock suite (happy, JSON digest, 429 retry, 402 fail-fast, budget cutoff,
embeddings, summariser) + live-verified against deepseek-v4-flash and
text-embedding-3-small. Nothing under persona/ references it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019sJbtz8aPD1AjpNz4uZ6sL
Streaming line-at-a-time readers under persona/readers/ emitting redacted
PersonaEvidence, one RawSession per file. Keep user-authored turns only:
genuine prompts (T2), corrections/interrupts (T1, carrying ~200 chars of the
preceding assistant turn), with byte accounting. Exclude tool_result turns,
sidechains, meta, local-command/system-reminder scaffolding (Claude Code) and
the developer role + environment_context/user_instructions/subagent
wrappers (Codex). Provenance from cwd + session id.

Live-verified on this machine: 99.3% byte reduction over 201 Claude Code
files, 98.8% over 256 Codex rollouts. Unit-tested with inline fixtures.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019sJbtz8aPD1AjpNz4uZ6sL
readers/instruction.rs discovers CLAUDE.md / AGENTS.md / .cursorrules /
.github/copilot-instructions.md across configured roots (repo-scoped via
nearest .git ancestor) plus explicit global files, and splits each into
rule-granular verbatim chunks (top-level bullets with nested continuations,
paragraphs; headings and fenced code dropped) as T0 directives evidence.
content_sha() feeds P9 change detection. Unit-tested.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019sJbtz8aPD1AjpNz4uZ6sL
readers/git_history.rs (git-diff feature) discovers repos under configured
roots, author-filters by a configured email set, and emits two evidence
streams: message-style batches (~100 commits/unit, subject+body+stats, T2,
oldest-first) and a bounded sample of small-commit diffs (small commits first,
hard size/count caps, T3 — corroborates only). Synthetic-repo tests via git2.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019sJbtz8aPD1AjpNz4uZ6sL
- distill.rs (map): one chat_for_json per session → SessionDigest of per-facet
  prescriptive observations. Strict-JSON schema in the prompt, prose/fence-
  tolerant parsing, char-windowing for oversized sessions, soft-fallback (a
  failed/garbage digest skips the session, never aborts).
- reduce.rs: fold observations into the 7 facet flavoured trees (one leaf per
  facet per digest, tier-weighted), fold verbatim T0 directives into the
  directives tree, then seal + compile each root. Tracks per-facet observation
  and distinct-scope counts.
- compile.rs: deterministic pack assembler — identity header + trait summary,
  fixed section order (Directives budget-protected first), per-facet clamp,
  total [5k,10k] ceiling, strength annotations. Writes persona/PERSONA.md.

Mock-LLM map→reduce→compile e2e green on the offline ConcatSummariser path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019sJbtz8aPD1AjpNz4uZ6sL
- state.rs: persona-local PersonaStateStore trait (mirrors the SyncStateStore
  pattern without pulling the feature-gated sync/reqwest stack) + a
  JSON-file-backed FileStateStore. Coarse cursors: (mtime,len) per transcript,
  content sha per instruction file, HEAD-sha+author-hash watermark per repo.
- config.rs: declarative PersonaConfig (source roots with platform defaults,
  author emails, model ids, per-facet asks, per-facet+total token budgets, run
  budgets, git tunables) + ask resolution.
- pipeline.rs: Backfill (oldest-first) / Incremental (cursor-skip) orchestration
  over instructions → transcripts → git, run-budget cutoff with clean
  checkpoint, plus compile_only (no-LLM re-assembly). RunReport surfaces
  counts/skips/spend.

Pipeline e2e green: backfill→incremental resume, budget cutoff, compile-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019sJbtz8aPD1AjpNz4uZ6sL
- examples/persona_harness.rs: backfill|incremental|compile|status subcommands,
  wiring PersonaConfig + FileStateStore + the OpenRouter provider (chat +
  summariser), reporting counts/spend/wall-clock. .env.example documents the
  OPENROUTER_API_KEY and PERSONA_* knobs. Cargo [[example]]/[[test]] entries.
- Prune target/node_modules/.git/.claude/vendor/build dirs from instruction +
  git discovery — large speedup and avoids worktree/submodule double-counting.
- Keep T0 directives near-verbatim: collect them out of the LLM fold, persist to
  persona/directives.md, and render the Directives section from them directly so
  explicit rules survive exactly regardless of summariser (fixes an LLM-fold
  rewrite surfaced by the wiremock e2e). compile_only reloads them.
- tests/persona_e2e.rs: pipeline over fixtures against a wiremock OpenRouter —
  pack structure, tier order, incremental resume, cost assertion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019sJbtz8aPD1AjpNz4uZ6sL
- docs/plan/06-persona-rubric.md: manual quality rubric + first-run scorecard
  (8/8 known preferences reproduced, no fabrications, $0.063 / 774 observations).
- doc 06 §6.10: first-live-run note + documented deviations.
- Fix: seed verbatim directives from the persisted store at the start of every
  run so an incremental run (which cursor-skips instruction files) still emits
  the Directives section; regression test added.
- cargo fmt across the persona surface.

Launch milestone (P1,P2,P5,P6,P7,P8,P9,P10,P11) complete and green: 46 persona
unit tests + 3 integration e2e + 6 provider wiremock tests, plus a live run
against this machine's real transcripts/instructions/git via OpenRouter.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019sJbtz8aPD1AjpNz4uZ6sL
The network-bound digest (LLM map) step dominated wall-clock (~46 min for 40
sessions run sequentially). Digests now run concurrently with bounded, order-
preserving `buffered` (config `digest_concurrency`, default 8) while folds stay
serial (SQLite writes + seals must not interleave); trees still fold oldest-
first.

Cursor commits are deferred to a per-session token applied only AFTER a session
is folded, so a budget-truncated tail is re-processed on the next run rather
than silently skipped — fixing a resume regression the read-all-then-digest
restructure would otherwise introduce (covered by the incremental-resume e2e).

46 persona unit + 3 integration e2e green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019sJbtz8aPD1AjpNz4uZ6sL
@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 24 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ec60e2ab-8c1e-4817-815e-d2968ef350e8

📥 Commits

Reviewing files that changed from the base of the PR and between 51d94d6 and c4a96cc.

📒 Files selected for processing (36)
  • .env.example
  • .gitignore
  • Cargo.toml
  • docs/plan/06-persona-distillation.md
  • docs/plan/06-persona-rubric.md
  • examples/persona_harness.rs
  • src/memory/mod.rs
  • src/memory/persona/compile.rs
  • src/memory/persona/compile_tests.rs
  • src/memory/persona/config.rs
  • src/memory/persona/config_tests.rs
  • src/memory/persona/distill.rs
  • src/memory/persona/distill_tests.rs
  • src/memory/persona/mod.rs
  • src/memory/persona/pipeline.rs
  • src/memory/persona/pipeline_tests.rs
  • src/memory/persona/readers/claude_code.rs
  • src/memory/persona/readers/claude_code_tests.rs
  • src/memory/persona/readers/codex.rs
  • src/memory/persona/readers/codex_tests.rs
  • src/memory/persona/readers/git_history.rs
  • src/memory/persona/readers/git_history_tests.rs
  • src/memory/persona/readers/instruction.rs
  • src/memory/persona/readers/instruction_tests.rs
  • src/memory/persona/readers/jsonl.rs
  • src/memory/persona/readers/mod.rs
  • src/memory/persona/reduce.rs
  • src/memory/persona/reduce_tests.rs
  • src/memory/persona/state.rs
  • src/memory/persona/state_tests.rs
  • src/memory/persona/types.rs
  • src/memory/persona/types_tests.rs
  • src/memory/providers/mod.rs
  • src/memory/providers/openrouter.rs
  • src/memory/providers/openrouter_tests.rs
  • tests/persona_e2e.rs

Comment @coderabbitai help to get the list of available commands.

@senamakel

Copy link
Copy Markdown
Member Author

Follow-on sources P3 (opencode/Cursor) and P4 (ChatGPT/Claude.ai exports) tracked in #76.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f827735fe1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/memory/persona/distill.rs Outdated
for window in windows(session) {
match digest_window(provider, session, &window).await {
Ok(mut obs) => observations.append(&mut obs),
Err(e) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve cursors when provider calls fail

Catching every digest_window error here turns hard provider failures (for example OpenRouter budget exhaustion or 401/403 non-retryable errors) into an empty digest; digest_and_fold then still marks the selected session processed and writes its file/repo cursor. In that scenario an incremental rerun skips the transcript or repo even though no observations were folded, so the evidence is lost until a manual backfill or state reset.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c4a96cc. digest_session now returns Result: a hard chat_for_json failure (budget/401/403/transport) bubbles as Err and digest_and_fold does not commit that session's cursor, so it's retried next run; only a received-but-unparseable response stays a soft empty digest (which does commit, since retrying reproduces it). Added RunReport.sessions_failed and regression test hard_provider_failure_does_not_commit_cursor.

Comment thread src/memory/persona/pipeline.rs Outdated
// Seed verbatim directives from the persisted store so an incremental
// run (which cursor-skips unchanged instruction files) still emits the
// Directives section. fold_directives dedups on re-read.
state.directives = super::compile::read_directives(self.config);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Rebuild persisted directives when files change

Seeding state.directives from the persisted directives.md before reading current instruction files makes incremental updates append-only. If a user removes or edits a rule in AGENTS.md/CLAUDE.md, the old line is already present, fold_directives only appends missing rules, and the stale directive is written back into PERSONA.md.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c4a96cc. Dropped the append-only seed from the persisted directives.md; instruction files are now always re-read every run (cheap, no LLM) so directives are rebuilt fresh and removed/edited rules drop out of the pack. Regression test removed_directive_drops_out_on_rerun.

Comment thread src/memory/persona/reduce.rs Outdated
Comment on lines +172 to +173
let seq = state.next_seq();
let chunk = persona_chunk(&scope, seq, leaf_text, timestamp);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use stable persona chunk sequence IDs

The persona chunk id includes seq, but this counter is reset for each run. When an incremental run re-reads an unwatermarked source (for example a repo after a budget-truncated pass) while earlier transcript files are now skipped, the same observation is assigned a different seq and therefore a different chunk_id; tree insertion dedupes by chunk id, so the same evidence can be folded repeatedly.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c4a96cc. The persona chunk id now derives its sequence from sha256(scope‖content) instead of the per-run counter, so the same observation maps to a stable chunk_id across runs and tree insertion dedupes it. Regression test persona_chunk_id_is_content_stable_across_runs.

Three correctness fixes from PR #75 review:

- Provider failures no longer lose evidence (was P1): digest_session now returns
  Result — a hard chat_for_json failure (budget/401/403/transport) bubbles as
  Err and the pipeline does NOT commit that session's cursor, so it is retried
  next run. A received-but-unparseable response stays a soft empty digest (does
  commit). RunReport gains sessions_failed.
- Directives rebuilt fresh each run (was P2): dropped the append-only seed from
  the persisted store; instruction files are always re-read (cheap, no LLM) so
  removed/edited rules drop out of the pack.
- Content-stable chunk ids (was P3): persona chunk id derives its sequence from
  sha256(scope‖content) instead of a per-run counter, so re-reading the same
  evidence dedupes on tree insertion instead of re-folding.

Regression tests added for all three (hard_provider_failure_does_not_commit_
cursor, removed_directive_drops_out_on_rerun, persona_chunk_id_is_content_
stable_across_runs). 49 persona unit + 3 e2e green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019sJbtz8aPD1AjpNz4uZ6sL

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c4a96ccc6b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

let head_value = serde_json::Value::String(head);
let last = sessions.len() - 1;
for (i, session) in sessions.into_iter().enumerate() {
let commit = (i == last).then(|| (key.clone(), head_value.clone()));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Delay repo watermark until all git sessions succeed

For git repos that produce multiple RawSessions (multiple message batches and/or the diff sample), this assigns the repo HEAD watermark only to the final session. If an earlier batch hits a hard provider error but the final batch succeeds, digest_and_fold will still write this watermark, and the next incremental run skips the whole repo even though the failed batch's evidence was never folded. This is fresh evidence beyond the prior cursor-failure comment: the repo-level commit is attached to a different later pending item than the failed one.

Useful? React with 👍 / 👎.

score,
};
let factory = TreeFactory::flavoured(scope, ask.to_string());
factory.insert_leaf(config, &leaf, summariser).await?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Deduplicate leaves beyond the current buffer

This relies on stable chunk_ids to make re-scans idempotent, but insert_leaf only reaches append_to_buffer, whose duplicate check is limited to the current L0 buffer. Because each pipeline run later force-seals and clears that buffer, a backfill rerun or any unwatermarked repo/file re-scan appends the same already-sealed chunk again and folds duplicate evidence into the persona tree. Fresh evidence beyond the prior chunk-id finding is that stable ids do not protect sealed summaries.

Useful? React with 👍 / 👎.

Comment on lines +192 to +193
if !state.directives.is_empty() {
super::compile::write_directives(self.config, &state.directives)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Persist empty directive sets to clear stale cache

After the last instruction rule is removed or all instruction files disappear, state.directives is empty, so this guard leaves the old persona/directives.md untouched. The run's immediate pack omits the rule, but a later compile_only() reads that stale file and resurrects deleted directives into PERSONA.md; the fresh evidence beyond the earlier directive comment is this empty-set cache write being skipped.

Useful? React with 👍 / 👎.

Comment on lines +99 to +101
fn charge(&mut self) {
self.sessions += 1;
self.calls += 1;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Charge the LLM budget per windowed call

The run budget records one LLM call per selected session here, but digest_session calls chat_for_json once per 12k-character window. With a large transcript, or with any ChatProvider that does not independently enforce the same limit, the pipeline can exceed max_llm_calls while still treating the session as a single charged call, so the configured call/cost ceiling is not reliable.

Useful? React with 👍 / 👎.

@senamakel senamakel merged commit 4f0f582 into main Jul 14, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant