feat(persona): persona distillation surface (doc 06 launch milestone)#75
Conversation
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
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 24 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (36)
Comment |
|
Follow-on sources P3 (opencode/Cursor) and P4 (ChatGPT/Claude.ai exports) tracked in #76. |
There was a problem hiding this comment.
💡 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".
| for window in windows(session) { | ||
| match digest_window(provider, session, &window).await { | ||
| Ok(mut obs) => observations.append(&mut obs), | ||
| Err(e) => { |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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.
| // 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); |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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.
| let seq = state.next_seq(); | ||
| let chunk = persona_chunk(&scope, seq, leaf_text, timestamp); |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
💡 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())); |
There was a problem hiding this comment.
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?; |
There was a problem hiding this comment.
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 👍 / 👎.
| if !state.directives.is_empty() { | ||
| super::compile::write_directives(self.config, &state.directives)?; |
There was a problem hiding this comment.
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 👍 / 👎.
| fn charge(&mut self) { | ||
| self.sessions += 1; | ||
| self.calls += 1; |
There was a problem hiding this comment.
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 👍 / 👎.
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-readypersona/PERSONA.mdcontext pack.Everything is gated behind a new default-off
personaCargo feature and builds on existing seams (flavoured trees,ChatProvider/Summariser/EmbeddingBackendtraits,SyncStateStorepattern, the PII/secret guard). Nothing underpersona/names a concrete provider.What's included
persona/types.rs):PersonaSourceKind,EvidenceTier(T0–T3), 7PersonaFacets + default asks, content-addressed evidence ids, constructor-enforced redaction via the compositesanitize_text(secrets + PII).providers/openrouter.rs, behindproviders-http): the crate's first concreteChatProvider+Summariser+EmbeddingBackend(closes the C3/M3 seam).SecretStringkey, retry/backoff, fail-fast 4xx, per-run cost/call budget cutoff. Wiremock-tested + live-verified.git-diff): author-filtered commit-message batches (T2) + bounded sampled diffs (T3).[5k,10k]total ceiling, strength annotations).PersonaStateStore+ file-backed store,backfill/incrementalmodes, run-budget cutoff with clean checkpoint & correct resume.examples/persona_harness.rs(backfill|incremental|compile|status), declarativePersonaConfig,.env.exampleupdated.tests/persona_e2e.rs(wiremock OpenRouter, pack structure / tier order / resume / cost assertion), quality rubric + first-run scorecard (docs/plan/06-persona-rubric.md).buffered) while folds stay serial — configurable viadigest_concurrency.Verification
cargo fmtclean; default build unaffected (feature default-off).CLAUDE.md+ git via OpenRouter (DeepSeek v4 Flash): 40 sessions → 774 observations → 6.8 KBPERSONA.mdfor $0.063. Reproduces 8/8 independently-known preferences with no fabrications.Follow-ups (out of scope, tracked separately)
🤖 Generated with Claude Code
https://claude.ai/code/session_019sJbtz8aPD1AjpNz4uZ6sL