Skip to content

Take down deleted learnings, and sync each turn - #674

Merged
shhdwi merged 2 commits into
mainfrom
fix/safe-learning-sync
Oct 23, 2026
Merged

shhdwi merged 2 commits into
mainfrom
fix/safe-learning-sync

Conversation

@shhdwi

@shhdwi shhdwi commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

Rebuilt on current main. Main now uploads only learnings committed at HEAD and already keeps learnings that look like they hold a secret on the laptop, so this PR is just the two pieces that were still missing.

What changes

  • Deleted learnings come down from the web. If a learning or skill was at HEAD at the last sync on the same branch and isn't anymore, it's sent as removed. That covers a deleted file, and one now kept back because it looks like it holds a secret. Switching branches never takes anything down.
  • The server hears how many were kept back. The count of learnings kept back for a secret is sent as held, so the server can show it.
  • Sync at the end of each turn. The Stop hook starts a background sync, so a learning committed mid-session reaches the team at the end of that turn. The turn's skill runs and searches go with it, instead of waiting for the next session.

The server already accepts removed and held.

Also: a Windows lock fix. On Windows, opening a lock file that another process is still deleting fails with EPERM or EBUSY instead of EEXIST, and withLock treated that as an error. That broke the "real processes writing skills.json" test on Windows CI in 2 of 3 runs, and it can drop a real user's skill update. It's now treated as a lock someone else holds (Windows only), with a unit test in test/lock-held.test.ts.

Tested

  • New test in test/trail-cloud.test.ts:
    • A deleted file stays up until the delete is committed, then comes down, and only once.
    • A learning that picks up a secret comes down and is counted as held, and the key never leaves the laptop.
    • A branch switch takes nothing down.
  • Full suite: 3999/4010 pass. Two failures are the known graft-to-trail tests that only fail on this machine. The third is a compaction test that passes on its own (5/5), so it's flaky under load.
  • End to end on a sandbox laptop against a local server: I committed a learning and synced (status kept), then deleted it, committed and synced again (status removed).

🤖 Generated with Claude Code

@trailhq-graft

trailhq-graft Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

🌱 graft blast radius

3 areas changed → 6 areas can be affected. 65 dependent symbols, depth 2.
Tests: Stop Event has tests the diff did not touch; 2 areas updated their tests.
Tag: @anirudhkumar-nanonets — 4 of 9 areas · Rushabh Nagda — Workspace Sync, Cloud Pairing

flowchart TB
  A0(("Skill State Management<br/>36 symbols"))
  A1(("Initialization<br/>9 symbols"))
  A2(("Claude Integration<br/>8 symbols"))
  A3(("Trace Logging<br/>7 symbols"))
  A4(("Cloud Pairing<br/>3 symbols"))
  AX(("1 smaller area<br/>2 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
Skill State Management 36 src/skills/lesson-gate.ts:L165-L172 markLessonReceiptUsed — calls, depth 1 File Locking
Initialization 9 src/init/init-record.ts:L354-L366 updateInitRecord — calls, depth 1 File Locking
Claude Integration 8 src/claude/hooks.ts:L480-L560 main — calls, depth 1 Stop Event, File Locking
Trace Logging 7 src/trace/housekeeping.ts:L70-L85 housekeeping — calls, depth 1 File Locking
Cloud Pairing 3 src/cloud/other-repos.ts:L102-L135 setUpPicked — calls, depth 1 Workspace Sync
CLI 2 src/cli.ts:L2139-L2213 workspaceCommand — calls, depth 1 Workspace Sync
Who knows this code — 2 people across 9 areas
Area Who knows it
Workspace Sync · changed Rushabh Nagda — 2 commits, last 2d ago · @anirudhkumar-nanonets — 1 commit, last 10d ago
Claude Integration · affected @anirudhkumar-nanonets — 16 commits, last yesterday
Cloud Pairing · affected Rushabh Nagda — 1 commit, last 3d ago · @anirudhkumar-nanonets — 1 commit, last 10d ago
CLI · affected @anirudhkumar-nanonets — 61 commits, last yesterday
File Locking · changed only you — nobody else has touched these files
Stop Event · changed only you — nobody else has touched these files
Skill State Management · affected only you — nobody else has touched these files
Initialization · affected only you — nobody else has touched these files
…1 further area

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 65 dependent symbols, grouped by area

Skill State Management — 36 symbols in 13 files

  • src/skills/lesson-gate.ts:L165-L172 — markLessonReceiptUsed (calls, depth 1)
    167: withLock(path, () => {
  • src/skills/receipts.ts:L102-L109 — markReceiptUsed (calls, depth 1)
    104: withLock(path, () => {
  • src/skills/settings.ts:L69-L81 — update (calls, depth 1)
    71: const done = withLock(path, () => {
  • src/skills/state.ts:L95-L112 — updateSkillState (calls, depth 1)
    98: const done = withLock(path, () => {
  • src/skills/store-fs.ts:L84-L89 — tryStoreLock (calls, depth 1)
    88: return withLock(join(skillsDir(env), '.store'), fn, waitMs);
  • src/skills/update-gate.ts:L179-L186 — markUpdateReceiptUsed (calls, depth 1)
    181: withLock(path, () => {
  • src/skills/value-line.ts:L131-L146 — updateValue (calls, depth 1)
    135: const done = withLock(path, () => {
  • src/skills/lesson-gate.ts:L254-L317 — keepLessonCli (calls, depth 2)
  • src/skills/lesson-gate.ts:L186-L203 — lessonGatePre (calls, depth 2)
  • src/skills/lesson-gate.ts:L240-L251 — startWorked (calls, depth 2)
  • src/skills/offer-retire.ts:L47-L57 — noteFiring (calls, depth 2)
  • src/skills/save-gate.ts:L288-L362 — cliAdd (calls, depth 2)
  • src/skills/save-gate.ts:L71-L76 — saveGateFor (calls, depth 2)
  • src/skills/settings.ts:L158-L163 — noteGovernor (calls, depth 2)
  • src/skills/settings.ts:L138-L148 — noteSkillsStarted (calls, depth 2)
  • src/skills/settings.ts:L152-L156 — pauseRepo (calls, depth 2)
  • src/skills/settings.ts:L111-L122 — setOffers (calls, depth 2)
  • src/skills/settings.ts:L124-L129 — setSkillOff (calls, depth 2)
  • src/skills/settings.ts:L103-L108 — setSkills (calls, depth 2)
  • src/skills/staleness.ts:L92-L123 — cueFlush (calls, depth 2)
  • src/skills/staleness.ts:L150-L160 — markMissingSaid (calls, depth 2)
  • src/skills/staleness.ts:L135-L147 — markUnusedSaid (calls, depth 2)
  • src/skills/state.ts:L117-L124 — change (calls, depth 2)
  • src/skills/store-fs.ts:L167-L184 — startSkill (calls, depth 2)
  • src/skills/store-fs.ts:L75-L81 — underStoreLock (calls, depth 2)
  • src/skills/store-held.ts:L112-L118 — dropHeld (calls, depth 2)
  • src/skills/store-held.ts:L103-L110 — markHeldShown (calls, depth 2)
  • src/skills/store.ts:L252-L277 — moveStatus (calls, depth 2)
  • src/skills/update-gate.ts:L274-L344 — cliUpdate (calls, depth 2)
  • src/skills/update-gate.ts:L214-L228 — updateGate (calls, depth 2)
  • src/skills/value-line.ts:L177-L186 — markValueSaid (calls, depth 2)
  • src/skills/value-line.ts:L152-L164 — noteValue (calls, depth 2)
  • src/skills/versions.ts:L261-L308 — adoptIfCopied (calls, depth 2)
  • src/skills/versions.ts:L104-L106 — clearPending (calls, depth 2)
  • src/skills/versions.ts:L119-L199 — commitLocked (calls, depth 2)
  • src/skills/versions.ts:L228-L254 — writeProposal (calls, depth 2)

Initialization — 9 symbols in 7 files

  • src/init/init-record.ts:L354-L366 — updateInitRecord (calls, depth 1)
    358: const done = withLock(path, () => {
  • src/init/keep.ts:L372-L385 — claimKeep (calls, depth 1)
    374: const done = withLock(path, () => {
  • src/init/candidates.ts:L207-L224 — seedNeverShown (calls, depth 2)
  • src/init/handover.ts:L55-L90 — initHandover (calls, depth 2)
  • src/init/keep.ts:L183-L251 — keepItems (calls, depth 2)
  • src/init/keep.ts:L326-L369 — keepCommand (calls, depth 2)
  • src/init/look-back-mine.ts:L118-L213 — lookBack (calls, depth 2)
  • src/init/miners.ts:L129-L129 — WallsAdd (references, depth 2)
  • src/init/run.ts:L183-L370 — finishLookBack (calls, depth 2)

Claude Integration — 8 symbols in 7 files

  • src/claude/hooks.ts:L480-L560 — main (calls, depth 1)
    554: if (event === 'stop') { await handleStopEvent(input, dir); return; }
  • src/claude/post-bash.ts:L168-L185 — postToolOffer (calls, depth 2)
  • src/claude/prompt-skills.ts:L73-L109 — promptSkills (calls, depth 2)
  • src/claude/start-init.ts:L49-L65 — keptNotice (calls, depth 2)
  • src/claude/start-skills.ts:L76-L92 — sessionStartSkillsRun (calls, depth 2)
  • src/claude/stop-skills.ts:L220-L226 — notePool (calls, depth 2)
  • src/claude/stop-skills.ts:L52-L68 — stopSkillsRun (calls, depth 2)
    65: hookError('stop', e, env);
  • src/claude/walls-hook.ts:L139-L194 — wallsAtStop (calls, depth 2)

Trace Logging — 7 symbols in 5 files

  • src/trace/housekeeping.ts:L70-L85 — housekeeping (calls, depth 1)
    76: withLock(mark, () => {
  • src/trace/pool.ts:L76-L101 — notePoolDay (calls, depth 1)
    80: const done = withLock(path, () => {
  • src/trace/run-sheets.ts:L52-L57 — updateIndex (calls, depth 1)
    53: withLock(indexPath(env), () => {
  • src/trace/skill-session.ts:L215-L246 — withSkillSession (calls, depth 1)
    218: const done = withLock(path, () => {
  • src/trace/wall-state.ts:L217-L238 — noteWalls (calls, depth 1)
    225: const r = withLock(path, () => {
  • src/trace/run-sheets.ts:L110-L137 — pruneRunSheets (calls, depth 2)
  • src/trace/run-sheets.ts:L60-L95 — writeRunSheet (calls, depth 2)

…5 further symbols not listed.

Test signal per changed area — 2 ✓ · 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.

  • ✓ File Locking — 2 of 2 reached · 1 test file changed here: test/lock-held.test.ts
  • ✓ Workspace Sync — 2 of 3 reached · 1 test file changed here: test/trail-cloud.test.ts
    • not reached: currentBranch
  • ⚠ Stop Event — 1 of 1 reached · 1 test file reaches it, none changed here
65 test suites also reference this code

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

  • test/claude-hooks-1b-review-trust.test.ts
  • test/claude-hooks-1b.test.ts
  • test/claude-journeys-2.test.ts
  • test/claude-journeys-3.test.ts
  • test/claude-journeys-update.test.ts
  • test/claude-post-bash-walls.test.ts
  • test/claude-start-skills-1b.test.ts
  • test/claude-stop-moments.test.ts
  • test/claude-stop.test.ts
  • test/cli-1b.test.ts
  • test/cli-skills.test.ts
  • test/init-b6-review.test.ts
  • test/init-keep-cli-review.test.ts
  • test/init-keep-cli.test.ts
  • test/init-keep-gate-1b.test.ts
  • test/init-keep-shell-noise.test.ts
  • test/init-keep.test.ts
  • test/init-miners.test.ts
  • test/init-record.test.ts
  • test/init-relay-review.test.ts
  • …45 more

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

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

Rebuilt on main, which now uploads only learnings committed at HEAD and
already keeps secret-looking ones local. What was still missing:

- A learning or skill HEAD had at the last sync on the same branch, and
  no longer has (deleted, or now left out for a secret), is sent as
  removed, so it comes down from the web. Switching branches never takes
  anything down. The count left out for a secret goes up as held.
- The Stop hook starts a background sync, so a learning committed
  mid-session, and the turn's skill runs and searches, reach the team at
  the end of the turn, not at the next session.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@shhdwi
shhdwi force-pushed the fix/safe-learning-sync branch from 86ae818 to 4269193 Compare October 23, 2026 11:35
@shhdwi shhdwi changed the title Keep secret-looking learnings local, take down deleted ones, sync each turn Take down deleted learnings, and sync each turn Oct 23, 2026
@shhdwi shhdwi closed this Oct 23, 2026
@shhdwi shhdwi reopened this Oct 23, 2026
@shhdwi
shhdwi changed the base branch from release/0.24.1 to main October 23, 2026 11:39
@shhdwi shhdwi closed this Oct 23, 2026
@shhdwi shhdwi reopened this Oct 23, 2026
github-actions Bot added a commit that referenced this pull request Oct 23, 2026
On Windows, opening a lock file another process is deleting fails with
EPERM or EBUSY (delete pending), not EEXIST. withLock threw on it, which
failed the concurrent skills.json test on Windows CI two runs in three,
and can drop a real user's skill update. Treat it as a held lock.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
github-actions Bot added a commit that referenced this pull request Oct 23, 2026
@shhdwi
shhdwi merged commit c62f0ee into main Oct 23, 2026
8 checks passed
github-actions Bot added a commit that referenced this pull request Oct 23, 2026
@shhdwi shhdwi mentioned this pull request Oct 23, 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.

1 participant