Skip to content

One learning file per session, or one per PR - #748

Merged
shhdwi merged 1 commit into
mainfrom
claude/one-learning-per-session-9c2df4
Nov 7, 2026
Merged

shhdwi merged 1 commit into
mainfrom
claude/one-learning-per-session-9c2df4

Conversation

@anirudhkumar-nanonets

@anirudhkumar-nanonets anirudhkumar-nanonets commented Nov 2, 2026 •

Copy link
Copy Markdown
Collaborator

The issue: One long Claude session that saves a learning three times leaves three files in .trail/learnings/, when it should leave one file per session, or one per PR when the session goes on to another PR.
Why: trail note wrote a new file on every save, because it never knew which session was saving or what that session had already written.
The fix: trail note remembers the file each Claude session wrote on each branch, so a later save adds to that file under the same headings, and a save on a new branch (which a new PR needs) starts a new file.

Moves: share of saves that go into the session's existing learning instead of a new file. Measured by the new into property on note_saved in PostHog (Metabase db 11, posthog_event_full_view): into = session over all note_saved. Value today: none, nothing measured it before this PR (Metabase checked 2026-11-02).

What changed

  • src/notes/session-learning.ts (new): which file a session wrote is kept on this machine in ~/.trail/repos/<repo>/session-learnings.json, keyed by sessionKey (Claude Code's CLAUDE_CODE_SESSION_ID, else its newest transcript) and scope, with the branch it was written on. Nothing about the session goes in the learning file. addToLearning folds a later save in under the same ## headings and skips lines already there; the title, id and file name stay, costs add up and touches join. Sessions idle for a week are dropped from the map.
  • src/cli.ts: trail note adds to the session's file when there is one on the same branch, leaves that file out of the repeat check (a second save in one session would otherwise be refused as a repeat of the first), and prints ✓ added to the learning this session already saved.
  • A new file as before when: there is no session id (Cursor, Codex, a plain terminal), the branch changed, the earlier file is gone, it is always-follow, or the save uses --replaces or --always-follow.
  • note_saved gets into (new / session), in src/telemetry/contract.ts and TELEMETRY.md.
  • The header of src/notes/notes.ts said a learning is never edited after it's written; it now says a later save in the same session adds to it.

Tests

  • test/trail-note-session.test.ts: three saves in one session leave one file with all three in it and the first id; another branch, another session and no session each start a new file; the heading merge.
  • test/trail-note-gate.test.ts clears CLAUDE_CODE_SESSION_ID, so the repeat-check case doesn't pick up the Claude session of whoever runs the tests.
  • Full suite with a scratch HOME: 3,997 pass and 141 fail, the parser and extraction set that fails in a worktree resolving the main checkout's node_modules (no web-tree-sitter-depth there). Every one of them also fails on origin/main (a44da3d) run the same way, so nothing new fails, and none are about notes.
  • By hand, in a fresh repo with one session id: three saves left three files on main and one file on this branch; a fourth save after git checkout -b started a second file.

trail check

One conflict on src/cli.ts, a word match with the learning that replaces: holds the old learning's id. This change keeps that: --replaces still writes a new file whose replaces: is the id, and the repeat check still reads liveLearnings and compares ids.

Merge order

#739 also edits next to note_saved (it adds note_asked right after it in src/telemetry/contract.ts and TELEMETRY.md), so the two conflict on those two lines. Merge #739 first; this branch then keeps both sides. #727 merges clean with this one.

🤖 Generated with Claude Code

@trailhq-graft

trailhq-graft Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

🌱 graft blast radius

3 areas changed → 12 areas can be affected. 70 dependent symbols, depth 2.
Tests: no test reaches Note Command; 1 area updated its tests.
Tag: @shhdwi — 15 of 15 areas · @qoole — Workspace Graph, Engine · @afeddersen — Telemetry

flowchart TB
  A0(("Skills Learning System<br/>19 symbols"))
  A1(("Claude Integration<br/>17 symbols"))
  A2(("Notes Memory<br/>11 symbols"))
  A3(("Repository Discovery<br/>7 symbols"))
  A4(("Cloud Workspace Sync<br/>6 symbols"))
  AX(("7 smaller areas<br/>10 symbols"))
  classDef reached fill:#D9EDF3,stroke:#3AA7C9,stroke-width:1.5px,color:#0E313C;
  class A0,A1,A2,A3,A4 reached;
  classDef tail fill:#EEF2F3,stroke:#9AA4A9,stroke-width:1px,color:#3A4247;
  class AX tail;
Loading
Can be affected Symbols Nearest hop Reached from
Skills Learning System 19 src/skills/cli-skills-1b.ts:L1-L155 cli-skills-1b.ts — imports, depth 1 Session Learning, Telemetry Contract
Claude Integration 17 src/claude/format.ts:L1-L416 format.ts — imports, depth 1 Session Learning, Telemetry Contract
Notes Memory 11 src/notes/ask-notes.ts:L1-L23 ask-notes.ts — imports, depth 1 Session Learning, Telemetry Contract
Repository Discovery 7 src/init/keep.ts:L1-L387 keep.ts — imports, depth 1 Session Learning, Telemetry Contract
Cloud Workspace Sync 6 src/cloud/lookup-activity.ts:L1-L60 lookup-activity.ts — imports, depth 1 Session Learning, Telemetry Contract
Learning Validation 2 src/check/learning-edits.ts:L1-L111 learning-edits.ts — imports, depth 1 Session Learning
6 smaller areas 8 Telemetry, Workspace Graph, Ask Interface, … see below
Who knows this code — 3 people across 15 areas
Area Who knows it
Session Learning · changed @shhdwi — 3 commits, last 7d ago
Telemetry Contract · changed @shhdwi — 1 commit, last 10d ago
Note Command · changed @shhdwi — 51 commits, last 5d ago
Skills Learning System · affected @shhdwi — 11 commits, last 7d ago
Claude Integration · affected @shhdwi — 29 commits, last 6d ago
Notes Memory · affected @shhdwi — 7 commits, last 6d ago
Repository Discovery · affected @shhdwi — 10 commits, last 6d ago
Cloud Workspace Sync · affected @shhdwi — 28 commits, last 6d ago
…7 further areas

Ownership is git history over each area's own files, weighted towards recent work (120-day half-life). Merge commits and bots are dropped, and you are dropped from your own PR. A name with no @ has no GitHub handle in its commit email — tag them by hand, or add a .mailmap entry. A suggestion from history, not a CODEOWNERS rule.

All 70 dependent symbols, grouped by area

Skills Learning System — 19 symbols in 19 files

  • src/skills/cli-skills-1b.ts:L1-L155 — cli-skills-1b.ts (imports, depth 1)
    111: let files: string[];
  • src/skills/cli-skills-report-1b.ts:L1-L192 — cli-skills-report-1b.ts (imports, depth 1)
    40: return p.git && p.key ? p : null;
  • src/skills/follow-learnings.ts:L1-L309 — follow-learnings.ts (imports, depth 1)
    10: * - at recall, a learning that only repeats a step is said once, in the run
  • src/skills/lesson-gate.ts:L1-L320 — lesson-gate.ts (imports, depth 1)
    8: * and lets the command run. The CLI half (`keepLessonCli`) never trusts the
  • src/skills/lessons.ts:L1-L401 — lessons.ts (imports, depth 1)
    4: * fix got past. A lesson on a skill rides at the end of the skill's run
  • src/skills/skills.ts:L1-L479 — skills.ts (imports, depth 1)
    140: let files: string[] = [];
  • src/skills/cli-skills-report.ts:L1-L157 — cli-skills-report.ts (imports, depth 2)
    76: const from = r.source ? ` from a run of ${plural(r.source.commands, 'command')}, ${r.source.failed ? `${r.source.failed} failed` : 'none failed'}` : '';
  • src/skills/cli-skills.ts:L1-L376 — cli-skills.ts (imports, depth 2)
    5: * person's: run by an agent (`personAtTerminal` says no), they print who may
  • src/skills/compile.ts:L1-L489 — compile.ts (imports, depth 2)
    2: * Compile: from a run sheet to a skill (design 4.5, spec 10's copy rules).
  • src/skills/fold.ts:L1-L370 — fold.ts (imports, depth 2)
    3: * one change to a skill, worked out from the skill as it is and the run that
  • src/skills/offer-lesson.ts:L1-L125 — offer-lesson.ts (imports, depth 2)
    75: const { run, verdict, st, ctx, channel } = i;
  • src/skills/offer-lines.ts:L1-L95 — offer-lines.ts (imports, depth 2)
  • src/skills/offer-moment.ts:L1-L307 — offer-moment.ts (imports, depth 2)
    43: import { runCheckout } from './run-checkout.js';
  • src/skills/offer-repair.ts:L1-L203 — offer-repair.ts (imports, depth 2)
    5: * keep the run as a skill, compiled exactly as moment 2 compiles it (R58),
  • src/skills/runblock.ts:L1-L240 — runblock.ts (imports, depth 2)
    2: * What a skill brings back (design 4.15, spec 11): the run block a job or
  • src/skills/save-gate.ts:L1-L363 — save-gate.ts (imports, depth 2)
    6: * `trail skills add`, denies it or writes a receipt and lets it run. The CLI
  • src/skills/save-verify.ts:L1-L390 — save-verify.ts (imports, depth 2)
    9: * its offset. Every step must have run here and worked, read from the
  • src/skills/update-plan.ts:L1-L209 — update-plan.ts (imports, depth 2)
    3: * (phase 2 design 4.11, R14): the run sheet, the skill file as it is now, its
  • src/skills/versions-fs.ts:L1-L218 — versions-fs.ts (imports, depth 2)
    4: * folder and rename, pruning, lesson files by id and kind, and swapping a

Claude Integration — 17 symbols in 17 files

  • src/claude/format.ts:L1-L416 — format.ts (imports, depth 1)
    37: return [C.muted(`◤ ${brand()} · not built · run `) + C.text(cmd('graft build'))];
  • src/claude/hooks.ts:L1-L580 — hooks.ts (imports, depth 1)
    9: import { noteAuthor, teammatesOn } from '../notes/git-facts.js';
  • src/claude/prompt-hook.ts:L1-L307 — prompt-hook.ts (imports, depth 1)
    9: * answers it also brings the learnings that touch the files its hits point into,
  • src/claude/session-metrics.ts:L1-L215 — session-metrics.ts (imports, depth 1)
    37: import type { AgentHost } from '../telemetry/contract.js';
  • src/claude/skills-wiring.ts:L1-L116 — skills-wiring.ts (imports, depth 1)
    16: import { hookMsBucket } from '../telemetry/contract.js';
  • src/claude/state.ts:L1-L165 — state.ts (imports, depth 1)
    30: * nothing the person typed is kept (spec section 6). Optional so files
  • src/claude/hook-io.ts:L1-L140 — hook-io.ts (imports, depth 2)
  • src/claude/prompt-skills.ts:L1-L327 — prompt-skills.ts (imports, depth 2)
    26: import { lastGoodRun } from '../trace/run-verdict.js';
  • src/claude/skill-use.ts:L1-L80 — skill-use.ts (imports, depth 2)
    2: * A coding agent using one of the repo's skills, as a run on the workspace.
  • src/claude/start-init.ts:L1-L74 — start-init.ts (imports, depth 2)
  • src/claude/start-skills.ts:L1-L269 — start-skills.ts (imports, depth 2)
    12: * of: the newest of the epoch just closed, live at the boundary, its run
  • src/claude/stats.ts:L1-L27 — stats.ts (imports, depth 2)
  • src/claude/statusline.ts:L1-L39 — statusline.ts (imports, depth 2)
  • src/claude/stop-skills.ts:L1-L236 — stop-skills.ts (imports, depth 2)
    2: * The skills part of Stop (design 3.5): judge the open run, settle credit,
  • src/claude/stop.ts:L1-L216 — stop.ts (imports, depth 2)
    47: * (Cursor and Copilot CLI run these hooks with transcripts of their own), or
  • src/claude/sync-run.ts:L1-L48 — sync-run.ts (imports, depth 2)
    46: // Run only when executed directly (node dist/claude/sync-run.js <dir> [timeoutMs]), not on import.
  • src/claude/walls-hook.ts:L1-L247 — walls-hook.ts (imports, depth 2)
    26: import { gitOf } from '../skills/anchors-git.js';

Notes Memory — 11 symbols in 11 files

  • src/notes/ask-notes.ts:L1-L23 — ask-notes.ts (imports, depth 1)
    7: import { findNotes, listNotes } from "./notes.js";
  • src/notes/credit.ts:L1-L92 — credit.ts (imports, depth 1)
    44: * works on the same files, and the head start their agents would get, which
  • src/notes/every-repo.ts:L1-L89 — every-repo.ts (imports, depth 1)
    68: let files: string[];
  • src/notes/find-notes.ts:L1-L62 — find-notes.ts (imports, depth 1)
    37: export function findNotes(notes: Note[], query: string, files: string[] = [], limit = 2): NoteHit[] {
  • src/notes/lesson-index.ts:L1-L132 — lesson-index.ts (imports, depth 1)
    5: * files a warm read is one readdir and one lstat a file.
  • src/notes/memory-notes.ts:L1-L280 — memory-notes.ts (imports, depth 1)
    22: import { noteAuthor } from "./git-facts.js";
  • src/notes/note-gate.ts:L1-L215 — note-gate.ts (imports, depth 1)
    5: * files they name), one about setting trail up isn't about the repo, status
  • src/notes/pr-reviews.ts:L1-L577 — pr-reviews.ts (imports, depth 1)
    31: import { noteAuthor } from "./git-facts.js";
  • src/notes/replace.ts:L1-L41 — replace.ts (imports, depth 1)
    7: import { notePlace, shortDate, whoseLearning, type Note } from "./notes.js";
  • src/notes/team-check.ts:L1-L134 — team-check.ts (imports, depth 1)
    26: * The code `text` is about, as `touches:` entries: files it names that exist
  • src/notes/guardrails.ts:L1-L41 — guardrails.ts (imports, depth 2)

Repository Discovery — 7 symbols in 7 files

  • src/init/keep.ts:L1-L387 — keep.ts (imports, depth 1)
    64: /** Where a keep writes: the checkout (phase 0's notes) and its repo key; the run it answers; PR-review findings in memory. */
  • src/init/look-back-mine.ts:L1-L214 — look-back-mine.ts (imports, depth 1)
    3: * replay through 1a's fold, the miners, Claude Code's how-to files and the
  • src/init/run.ts:L1-L445 — run.ts (imports, depth 1)
    7: * call (and phase 0's PR-review read) may run, unless `--plain` or
  • src/init/candidates.ts:L1-L225 — candidates.ts (imports, depth 2)
    7: * newest checked run, then up to 2 more; the first that compiles wins.
  • src/init/discover-worker.ts:L1-L109 — discover-worker.ts (imports, depth 2)
    87: // A worker that can't load this module (Node 20 runs a .ts source only on the main thread) never started: run it here.
  • src/init/replay-runs.ts:L1-L479 — replay-runs.ts (imports, depth 2)
    5: * run's verdict here is the verdict the live hooks reach on the same lines.
  • src/init/review-screen.ts:L1-L316 — review-screen.ts (imports, depth 2)
    47: plainNames: ' · names are plain: to name them with Claude, run trail init --look-back in your own terminal',

Cloud Workspace Sync — 6 symbols in 6 files

  • src/cloud/lookup-activity.ts:L1-L60 — lookup-activity.ts (imports, depth 1)
    3: * the tokens the map saved against reading the files whole. Shared by the CLI's
  • src/cloud/nudge.ts:L1-L505 — nudge.ts (imports, depth 1)
    47: * picked up by the next command they run. */
  • src/cloud/onboarding.ts:L1-L721 — onboarding.ts (imports, depth 1)
    16: * files this person works on, from git
  • src/cloud/workspace-sync.ts:L1-L674 — workspace-sync.ts (imports, depth 1)
    6: * that git has: the ones committed at HEAD and the ones on teammates' pushed
  • src/cloud/other-repos.ts:L1-L150 — other-repos.ts (imports, depth 2)
    30: /** Claude Code sessions run there. */
  • src/cloud/pair.ts:L1-L368 — pair.ts (imports, depth 2)
    18: import { noteAuthor } from "../notes/git-facts.js";

…10 further symbols not listed.

Test signal per changed area — 1 ✓ · 1 ✗ · 1 –

Reached = a node under a test path has a resolved edge into the changed symbol. It undercounts anything called indirectly — through a CLI, a spawned process or a dynamic import — so read a low ratio as “look here”, never as a coverage gate.

  • ✓ Session Learning — 1 of 8 reached · 2 test files changed here: test/trail-note-gate.test.ts, test/trail-note-session.test.ts
    • not reached: sum, headingKey, rememberSessionLearning, sections, sessionLearning, storePath, trimBlank
  • – Telemetry Contract — 8 test files reach it, none changed here
  • ✗ Note Command — 0 of 1 reached · no test file reaches it
    • not reached: runNoteCommand
130 test suites also reference this code

130 symbols, kept out of the diagram and the table so they cannot crowd out the areas a reviewer has to look at.

  • test/ask-fusion.test.ts
  • test/ask-index.test.ts
  • test/ask.test.ts
  • test/claude-copy-dashes.test.ts
  • test/claude-format.test.ts
  • test/claude-hook-timeouts.test.ts
  • test/claude-hooks-1b-review-fixtures.ts
  • test/claude-hooks-1b-review-trust.test.ts
  • test/claude-hooks-1b-review.test.ts
  • test/claude-hooks-1b.test.ts
  • test/claude-hooks-perf.test.ts
  • test/claude-hooks-skills.test.ts
  • test/claude-hooks.test.ts
  • test/claude-host-standdown.test.ts
  • test/claude-journeys-3.test.ts
  • test/claude-journeys-learnings.test.ts
  • test/claude-post-bash-walls.test.ts
  • test/claude-pre-bash-guard.test.ts
  • test/claude-prompt-learnings.test.ts
  • test/claude-prompt-private.test.ts
  • …110 more

⚠️ 2 changed files not in the graph (.trail/learnings/2026-11-02-one-learning-per-session-later-saves-anirudhkumar.md, TELEMETRY.md) — no parser claims the extension, or the index predates the file.

graft blast · origin/main...HEAD · depth 2 · 8 changed files

Open the interactive graph → — click an area to see its dependent symbols at file:line.

github-actions Bot added a commit that referenced this pull request Nov 2, 2026
@shhdwi
shhdwi merged commit e667da1 into main Nov 7, 2026
8 checks passed
github-actions Bot added a commit that referenced this pull request Nov 7, 2026
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.

2 participants