<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Nate Nelson</title>
    <description>The latest articles on DEV Community by Nate Nelson (@wynelson94).</description>
    <link>https://dev.to/wynelson94</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3885307%2F98c70f86-1589-4c2f-ad13-3b3898dab65c.png</url>
      <title>DEV Community: Nate Nelson</title>
      <link>https://dev.to/wynelson94</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/wynelson94"/>
    <language>en</language>
    <item>
      <title>My memory tool said "no session history." The session had 2,526 lines.</title>
      <dc:creator>Nate Nelson</dc:creator>
      <pubDate>Thu, 23 Apr 2026 06:13:02 +0000</pubDate>
      <link>https://dev.to/wynelson94/my-memory-tool-said-no-session-history-the-session-had-2526-lines-1lha</link>
      <guid>https://dev.to/wynelson94/my-memory-tool-said-no-session-history-the-session-had-2526-lines-1lha</guid>
      <description>&lt;p&gt;&lt;em&gt;Source of truth for this post is the repo: &lt;a href="https://github.com/Wynelson94/longhand/blob/main/docs/devto-dogfood-post.md" rel="noopener noreferrer"&gt;github.com/Wynelson94/longhand/blob/main/docs/devto-dogfood-post.md&lt;/a&gt;. Edits go through git.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Yesterday I asked Claude Code to pull up where we'd left off on a project I'd been working on a few hours earlier. It's a project called &lt;a href="https://github.com/Wynelson94/bsoi-mesh-kit" rel="noopener noreferrer"&gt;bsoi-mesh-kit&lt;/a&gt; — a local STL validator I'm building for a service bureau. The recall tool I built, &lt;a href="https://github.com/Wynelson94/longhand" rel="noopener noreferrer"&gt;Longhand&lt;/a&gt;, is supposed to handle exactly this question.&lt;/p&gt;

&lt;p&gt;The response came back:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;recall_project_status("bsoi-mesh-kit")&lt;/code&gt; → &lt;code&gt;"No session history found for this project."&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Except: there were &lt;strong&gt;four JSONL transcripts on disk&lt;/strong&gt; for that project, including a &lt;strong&gt;2,526-line&lt;/strong&gt; work session from earlier that day where I'd shipped three version bumps, invited a collaborator, and patched a Pantheon Slicer config bug. The session was real. Longhand had captured none of it.&lt;/p&gt;

&lt;p&gt;The rest of this post is the diagnosis and the two releases that came out of it. It's written as a self-contained case study in building a tool that can catch itself in a lie.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Longhand is, in one paragraph
&lt;/h2&gt;

&lt;p&gt;Longhand is a Python CLI + MCP server that reads Claude Code's session transcripts (&lt;code&gt;~/.claude/projects/**/*.jsonl&lt;/code&gt;), indexes every tool call / file edit / thinking block into SQLite + ChromaDB, and exposes semantic recall via MCP tools. Zero API calls. Local-only. The pitch in one line: &lt;em&gt;the model doesn't need to carry the memory — the disk does.&lt;/em&gt; The longer pitch &lt;a href="https://github.com/Wynelson94/longhand/discussions/3" rel="noopener noreferrer"&gt;is here&lt;/a&gt;. Installed on PyPI: &lt;code&gt;pip install longhand&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: confirm the failure is real
&lt;/h2&gt;

&lt;p&gt;First thing I checked was the raw file system:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; ~/.claude/projects/-Users-natenelson/ | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"823dd358|002f6297|e6a3b13f"&lt;/span&gt;
002f6297-129e-4d09-b112-c48bd777e3ba.jsonl
823dd358-f32f-4d73-a481-38a05b378966.jsonl
e6a3b13f-3912-4ee3-b9aa-fa4fc509cb29.jsonl

&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; ~/.claude/projects/-Users-natenelson/823dd358&lt;span class="k"&gt;*&lt;/span&gt;.jsonl
    2526 /Users/natenelson/.claude/projects/-Users-natenelson/823dd358-f32f-4d73-a481-38a05b378966.jsonl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;2,526 lines on disk. Now what does SQLite have?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;sqlite3 ~/.longhand/longhand.db &lt;span class="s2"&gt;"
    SELECT session_id, project_path, project_id
    FROM sessions
    WHERE transcript_path LIKE '%823dd358%'
       OR transcript_path LIKE '%002f6297%'
       OR transcript_path LIKE '%e6a3b13f%';"&lt;/span&gt;

e6a3b13f-3912-4ee3-b9aa-fa4fc509cb29 | /Users/natenelson |
002f6297-129e-4d09-b112-c48bd777e3ba | /Users/natenelson |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things jumped out:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The big session (&lt;code&gt;823dd358&lt;/code&gt;) isn't in the &lt;code&gt;sessions&lt;/code&gt; table at all.&lt;/strong&gt; Never ingested.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The two shorter sessions are ingested but have &lt;code&gt;project_id = NULL&lt;/code&gt;&lt;/strong&gt; and a &lt;code&gt;project_path&lt;/code&gt; of &lt;code&gt;/Users/natenelson&lt;/code&gt; — my home directory, not the project.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Two distinct failure modes in one dataset. Time to understand each.&lt;/p&gt;

&lt;h2&gt;
  
  
  Root cause A: SessionEnd hook didn't fire on the big session
&lt;/h2&gt;

&lt;p&gt;Longhand ingests new sessions via a Claude Code &lt;code&gt;SessionEnd&lt;/code&gt; hook that runs &lt;code&gt;longhand ingest-session&lt;/code&gt;. The hook was installed and pointed to the right binary. But &lt;code&gt;823dd358&lt;/code&gt; — the most important session of the day — never got captured by it.&lt;/p&gt;

&lt;p&gt;I don't know exactly why the hook didn't fire (Claude Code's exit paths are varied, and a few of them skip &lt;code&gt;SessionEnd&lt;/code&gt;). What I know is &lt;strong&gt;there was no retry, no log, no detection mechanism&lt;/strong&gt;. If a hook silently fails, the only way to notice is to manually query something that should have been there and find it missing.&lt;/p&gt;

&lt;p&gt;That's the dogfood failure in one sentence: the tool that was supposed to give me observability into my past work silently lost an entire work session, and I only noticed because I happened to ask about that specific session the next day.&lt;/p&gt;

&lt;h2&gt;
  
  
  Root cause B: project inference was using the first-event cwd
&lt;/h2&gt;

&lt;p&gt;For the two sessions that &lt;em&gt;did&lt;/em&gt; get ingested, the &lt;code&gt;project_id&lt;/code&gt; was NULL because &lt;code&gt;project_path&lt;/code&gt; was &lt;code&gt;/Users/natenelson&lt;/code&gt;. Why?&lt;/p&gt;

&lt;p&gt;Claude Code launched from my home directory. So the transcript's &lt;strong&gt;first event&lt;/strong&gt; had &lt;code&gt;cwd=/Users/natenelson&lt;/code&gt;. Later events — after I &lt;code&gt;cd&lt;/code&gt;'d into the project — had &lt;code&gt;cwd=/Users/natenelson/Projects/bsoi-mesh-kit&lt;/code&gt;. But Longhand's ingest pipeline only looked at the first event.&lt;/p&gt;

&lt;p&gt;A quick scan of the big session confirmed the multi-cwd pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;cwds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;823dd358-....jsonl&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;obj&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;cwd&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;cwds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# =&amp;gt; {
#   '/Users/natenelson',
#   '/Users/natenelson/Projects/bsoi-mesh-kit',
#   '/Users/natenelson/Projects/bsoi-ops',
# }
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any session where I &lt;code&gt;cd&lt;/code&gt; between repos mid-session got misattributed. And since &lt;code&gt;recall_project_status&lt;/code&gt; filters &lt;code&gt;WHERE project_id = ?&lt;/code&gt;, NULL-project rows are invisible to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The v0.6.0 fix
&lt;/h2&gt;

&lt;p&gt;Four changes shipped together:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Mode-of-cwd project inference.&lt;/strong&gt; Tally every event's &lt;code&gt;cwd&lt;/code&gt;, filter out &lt;code&gt;$HOME&lt;/code&gt; and any path that doesn't walk up to a project marker (&lt;code&gt;.git&lt;/code&gt;, &lt;code&gt;pyproject.toml&lt;/code&gt;, &lt;code&gt;package.json&lt;/code&gt;, …), pick the mode. Multi-project sessions get attributed to the repo where most of the work happened.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_pick_best_project_cwd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;home_resolved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;home&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;counts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;resolved_cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cwd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cwd&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;cwd&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;cwd&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;resolved_cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cwd&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;resolved_cache&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;resolved_cache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
                &lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;resolved_cache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;home_resolved&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;resolved_cache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;find_project_root_strict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# returns None if no marker
&lt;/span&gt;        &lt;span class="n"&gt;resolved_cache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;most_common&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;counts&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. A new &lt;code&gt;longhand reconcile [--fix]&lt;/code&gt; command.&lt;/strong&gt; Walks &lt;code&gt;~/.claude/projects/*/*.jsonl&lt;/code&gt;, diffs against the &lt;code&gt;sessions&lt;/code&gt; table, buckets into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fully indexed&lt;/li&gt;
&lt;li&gt;Ingested but &lt;code&gt;project_id IS NULL&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Missing from sessions entirely&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With &lt;code&gt;--fix&lt;/code&gt; it re-ingests the problem buckets. Idempotent (upsert + size-check skip). This is the safety net that was missing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. A &lt;code&gt;stale&lt;/code&gt; flag on &lt;code&gt;recall_project_status&lt;/code&gt;.&lt;/strong&gt; So the next time a caller queries a project with un-ingested transcripts, they see &lt;code&gt;stale: true&lt;/code&gt; and a reason string pointing at &lt;code&gt;reconcile --fix&lt;/code&gt; — not silence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Fixed a pre-existing bug in &lt;code&gt;discover_sessions&lt;/code&gt;.&lt;/strong&gt; It was &lt;code&gt;rglob&lt;/code&gt;-ing all JSONLs under &lt;code&gt;~/.claude/projects&lt;/code&gt;, including subagent transcripts (in &lt;code&gt;*/subagents/&lt;/code&gt; subdirs) and pytest temp dirs. On my machine this was inflating "missing" counts from 28 → 650. The fix is three lines and one regret about not catching it sooner.&lt;/p&gt;

&lt;p&gt;Then I ran &lt;code&gt;longhand reconcile --fix&lt;/code&gt; against my own live DB. &lt;strong&gt;33 sessions re-ingested, 0 errors.&lt;/strong&gt; The 2,526-line &lt;code&gt;823dd358&lt;/code&gt; session got correctly attributed to bsoi-mesh-kit. &lt;code&gt;recall_project_status&lt;/code&gt; started returning real narrative. 182 tests passing. Tagged v0.6.0, pushed — PyPI Trusted Publishing does the release:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git push &lt;span class="nt"&gt;--follow-tags&lt;/span&gt; origin main
&lt;span class="c"&gt;# ... 45 seconds later ...&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;&lt;span class="nv"&gt;longhand&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;0.6.0  &lt;span class="c"&gt;# live&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: audit the fix
&lt;/h2&gt;

&lt;p&gt;I then asked Claude — in the same session — to give me a "full audit full honesty" of what I'd just shipped. &lt;strong&gt;This is the part that matters.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Claude wrote back a multi-page critique. Some of it was flattering (release pipeline, test discipline). Some of it was not:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"The narrative generator leaks garbage into authoritative-looking output. Look at what &lt;code&gt;recall_project_status("bsoi-mesh-kit")&lt;/code&gt; returned after I fixed everything:&lt;/em&gt;&lt;/p&gt;


&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Outcome: **fixed** · can you pull my bsoi-ops from my git and review the whole program

Recent commits (10)
- cc5f72f no message (today)
- `` no message (today)    ← blank commit hash
- `` no message (today)
... (8 more blanks)
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;&lt;em&gt;The 'fix summary' is pulling a raw user question. The commit list has nine empty entries. Agents will read this as ground truth."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And another:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Drift detection is 2.3 seconds per &lt;code&gt;recall_project_status&lt;/code&gt;. On every call, we scan all 59 JSONLs looking for cwd matches. That's going to bite at 500+ sessions."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Four classes of issue came out of that audit. Four more fixes — all traceable to the audit's specific findings — shipped as v0.7.0 within the same session:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Narrative cleanup.&lt;/strong&gt; Commits with empty hashes now get dropped at the extractor (no row written), in SQL (filter), AND in the narrative (render-time guard). The "last fix" trailer now sources from the most-recent episode's &lt;code&gt;fix_summary&lt;/code&gt; instead of the outcome classifier's buggy &lt;code&gt;summary&lt;/code&gt; field.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;longhand doctor&lt;/code&gt; grew a "Recent ingest (7d)" row&lt;/strong&gt; that counts on-disk JSONLs in the last week vs sessions-table rows and emits a red ✗ with &lt;code&gt;reconcile --fix&lt;/code&gt; hint when ratio &amp;lt; 0.5. Catches the next silent-hook-failure the moment the user runs &lt;code&gt;doctor&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A filesystem-backed drift cache.&lt;/strong&gt; &lt;code&gt;_detect_project_drift&lt;/code&gt; now reads &lt;code&gt;(transcript_path, mtime) → set[canonical_paths]&lt;/code&gt; from &lt;code&gt;~/.longhand/cache/jsonl_project_map.json&lt;/code&gt;, keyed on mtime so file edits invalidate automatically. Warm &lt;code&gt;recall_project_status&lt;/code&gt; on my live DB dropped from &lt;strong&gt;2,333ms → 68ms — 34×&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;search&lt;/code&gt; auto-scopes when the query names a project.&lt;/strong&gt; If the query hits a known project at fuzzy-match score ≥0.8 and the caller didn't pass a project filter, the search is pre-scoped to that project's events. The response wraps in &lt;code&gt;{auto_scoped_to, auto_scope_hint, hits}&lt;/code&gt; so agents can tell the filter applied (and override it if wrong).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;git push --follow-tags&lt;/code&gt; → PyPI → v0.7.0 live. 197 tests passing. 45 seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  The meta point
&lt;/h2&gt;

&lt;p&gt;Two meaningful releases in one session. Both were driven by a failure the tool itself surfaced. Both were audited by the tool itself after shipping. &lt;strong&gt;The tool is its own test harness.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the shape I didn't expect when I started. I thought I was building a memory tool — something that stores and retrieves past work. What I actually ended up with is a &lt;strong&gt;memory tool that can audit its own memory&lt;/strong&gt;. When it fails, it fails loudly enough (or I can make it fail loudly enough, on demand) that the failure itself becomes a seed for the next fix.&lt;/p&gt;

&lt;p&gt;The industry pitch is "bigger context windows will solve memory." I keep arguing the inverse: the disk already has the memory; you just need a tool that reads it honestly. The last two days have been me testing "reads it honestly" against its own bugs. The tool passed — but only because I forced it to audit itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's still broken
&lt;/h2&gt;

&lt;p&gt;Since this is a dev.to post and not marketing copy, here's the list of things v0.7.0 doesn't fix. These will probably be v0.8.0:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;fix_summary&lt;/code&gt; still looks rough upstream.&lt;/strong&gt; The narrative now pulls from &lt;code&gt;episode.fix_summary&lt;/code&gt; correctly, but that field itself contains raw thinking-block text with "Intent:" prefixes and mid-code truncations. Fix is ~20 lines in the episode extractor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hook is still a single point of failure.&lt;/strong&gt; &lt;code&gt;doctor&lt;/code&gt; now flags silent failures, but only when the user thinks to run &lt;code&gt;doctor&lt;/code&gt;. A recall-first user never sees it. Should be inlined into &lt;code&gt;recall&lt;/code&gt; and &lt;code&gt;recall_project_status&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-project sessions are winner-takes-all.&lt;/strong&gt; A session that spent 51% in project A and 49% in project B attributes only to A. Many-to-many attribution is the right shape; it's not built yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-scope threshold is a magic &lt;code&gt;0.8&lt;/code&gt;.&lt;/strong&gt; Not calibrated across ambiguous queries yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;22 CLI commands + 16 MCP tools is too many.&lt;/strong&gt; Needs a v1.0 prep pass.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;&lt;span class="nv"&gt;longhand&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;0.7.0
longhand setup         &lt;span class="c"&gt;# ingest existing Claude Code history + install hook + register MCP&lt;/span&gt;
longhand recall &lt;span class="s2"&gt;"that bug I fixed last week"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're already on an older version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--upgrade&lt;/span&gt; longhand
longhand reconcile &lt;span class="nt"&gt;--fix&lt;/span&gt;   &lt;span class="c"&gt;# replay historical sessions with corrected attribution&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The source is at &lt;a href="https://github.com/Wynelson94/longhand" rel="noopener noreferrer"&gt;github.com/Wynelson94/longhand&lt;/a&gt; (MIT). Issues and discussions welcome. If you install it and find a silent failure of your own, please file it — that's the feedback loop that made these two releases happen.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you built a tool that stores AI session history, how would you test that it's not lying to you? That's the problem Longhand is trying to solve. v0.7.0 is the third time it caught itself; it probably won't be the last.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claude</category>
      <category>python</category>
      <category>opensource</category>
      <category>ai</category>
    </item>
    <item>
      <title>Why I built a lossless alternative to AI memory summarization</title>
      <dc:creator>Nate Nelson</dc:creator>
      <pubDate>Sat, 18 Apr 2026 00:10:40 +0000</pubDate>
      <link>https://dev.to/wynelson94/why-i-built-a-lossless-alternative-to-ai-memory-summarization-40cl</link>
      <guid>https://dev.to/wynelson94/why-i-built-a-lossless-alternative-to-ai-memory-summarization-40cl</guid>
      <description>&lt;p&gt;Why I built a lossless alternative to AI memory summarization&lt;/p&gt;

&lt;p&gt;Every AI memory tool I tried summarized my sessions before giving them back to me.&lt;/p&gt;

&lt;p&gt;I'd spend an hour debugging a gnarly webhook bug with Claude Code. A week later I'd come back, ask about it, and get a three-sentence LLM summary. The actual fix? Gone. The reasoning trace? Gone. The five wrong attempts before the right one? Summarized into "you worked on webhook authentication."&lt;/p&gt;

&lt;p&gt;Summarization is a lossy decision disguised as a convenience. An LLM decides what's worth remembering, and I never get to see what it threw away.&lt;/p&gt;

&lt;p&gt;I built Longhand because I didn't want that tradeoff anymore.&lt;/p&gt;

&lt;h2&gt;
  
  
  The industry is racing in the wrong direction
&lt;/h2&gt;

&lt;p&gt;The mainstream answer to AI memory is "make the context window bigger." 1M tokens. 2M tokens. Context-infinite. Every model lab is pushing the same axis: make the model carry more state.&lt;/p&gt;

&lt;p&gt;This is the wrong abstraction. The model doesn't need to carry the memory. The disk does.&lt;/p&gt;

&lt;p&gt;Storage is a solved problem. SQLite shipped in 2000. ChromaDB shipped two years ago. Both run on a laptop. The "AI memory crisis" is artificial — an industry-wide assumption that memory must live where inference happens, even though it makes the whole system more expensive, less private, and more vendor-locked.&lt;/p&gt;

&lt;h2&gt;
  
  
  The state of the world, unfiltered
&lt;/h2&gt;

&lt;p&gt;Here's what most people don't realize: Claude Code already writes rich logs of every session. Every tool call. Every file edit. Every thinking block. All of it, verbatim, to JSONL files in &lt;code&gt;~/.claude/projects/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Those files contain a forensic-level record of your entire collaboration with the model. Nothing is lossy. Nothing is summarized. It's just sitting there on your disk, right now, for every session you've ever had.&lt;/p&gt;

&lt;p&gt;The problem is two-fold.&lt;/p&gt;

&lt;p&gt;First, Claude Code rotates those files off disk after a few weeks. If you don't capture them, they're gone.&lt;/p&gt;

&lt;p&gt;Second, every memory tool that tries to "use" them does so by summarizing — asking another LLM to compress the session into a paragraph before handing it back. Which is the lossy move I was trying to avoid in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture
&lt;/h2&gt;

&lt;p&gt;Longhand takes the opposite path. It reads the JSONL files verbatim and indexes them into two local stores:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SQLite&lt;/strong&gt; for structured events — every tool call, edit, commit, thinking block as a typed row with a timestamp and session ID&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ChromaDB&lt;/strong&gt; for semantic search — vector embeddings of episode summaries and conversation segments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Auto-ingestion runs via a &lt;code&gt;SessionEnd&lt;/code&gt; hook that Claude Code fires after every session. Once-off backfill ingests your existing history on install. The data persists forever after that — even after Claude Code rotates the source JSONL off disk, Longhand has its own copy.&lt;/p&gt;

&lt;p&gt;Recall is exposed as an MCP server. Claude Code itself gets 17 tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;recall&lt;/code&gt; — fuzzy natural-language query ("that stripe webhook fix from last week")&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;search_in_context&lt;/code&gt; — find text across sessions, with surrounding conversation&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;get_session_timeline&lt;/code&gt; — chronological replay of a session&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;replay_file&lt;/code&gt; — reconstruct the exact state of a file at any point in any session&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;find_commits&lt;/code&gt;, &lt;code&gt;get_file_history&lt;/code&gt;, &lt;code&gt;recall_project_status&lt;/code&gt;, and 10 more&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you ask Claude "do you remember when we fixed X?" it doesn't hallucinate from the last 10K tokens of context. It queries its own history on disk and returns the actual event.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;p&gt;After testing against 107 real Claude Code sessions (53,668 events, 665 git operations, 376 problem→fix episodes, 299 conversation segments across 37 projects):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Semantic recall across 100+ sessions: ~126ms&lt;/li&gt;
&lt;li&gt;Storage footprint: ~1GB for a heavy power user, 200–400MB typical&lt;/li&gt;
&lt;li&gt;API calls per query: zero&lt;/li&gt;
&lt;li&gt;Summarization per query: zero&lt;/li&gt;
&lt;li&gt;Network requests: zero&lt;/li&gt;
&lt;li&gt;Works offline: yes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;170 unit tests. Security-audited, zero critical findings. Published on PyPI as &lt;code&gt;longhand&lt;/code&gt;. Registered in the official MCP Registry.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this unlocks
&lt;/h2&gt;

&lt;p&gt;The interesting part isn't the speed. It's what becomes possible once memory lives on your disk instead of in a vendor's context window.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cross-model portability.&lt;/strong&gt; Your history isn't locked to any model version. When Claude Opus 5 ships tomorrow, the same Longhand database works unchanged. Switch to a different model entirely? The data is yours.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Privacy by default.&lt;/strong&gt; Nothing leaves your machine. For regulated workflows, client work under NDA, or anyone who just doesn't want their session history flowing through someone else's servers, this is the only architecture that actually fits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Forensic replay.&lt;/strong&gt; Not just "what did we discuss" but "what was the exact state of &lt;code&gt;auth.ts&lt;/code&gt; on line 42 at 3:17pm last Tuesday?" — answerable deterministically, because every edit is in the record.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Offline work.&lt;/strong&gt; Airplane, remote location, air-gapped environment. Your memory works. Because it's a SQLite file.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Longhand doesn't try to do
&lt;/h2&gt;

&lt;p&gt;It's not a general-purpose AI memory system. It's specific to Claude Code's JSONL format.&lt;/p&gt;

&lt;p&gt;It won't help you with ChatGPT, Cursor, or any other client that doesn't write per-session logs to disk. (Though the architectural pattern — verbatim capture, local indexing, semantic recall — generalizes cleanly to anything that produces a rich session log.)&lt;/p&gt;

&lt;p&gt;It's also not trying to replace the context window. The window is still useful for the &lt;em&gt;current&lt;/em&gt; conversation. Longhand handles the rest — the 107 sessions that came before.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;longhand
longhand setup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The setup command backfills your existing Claude Code history, installs the auto-ingest hook, and registers as an MCP server. Takes about two minutes on a laptop with a year of sessions. Safe to re-run.&lt;/p&gt;

&lt;p&gt;Then try it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;longhand recall &lt;span class="s2"&gt;"that webhook fix from last week"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why I'm sharing this
&lt;/h2&gt;

&lt;p&gt;The memory crisis in AI was an artificial constraint — a default that everyone inherited without questioning. I wanted to see what fell out if you rejected the constraint entirely and asked: what if the disk carries the memory, and the model just queries it?&lt;/p&gt;

&lt;p&gt;What fell out is Longhand. 336 unique developers have cloned it in the last 14 days. 733 PyPI installs in the same window. 193 weekly visitors on PulseMCP. The curve is bending up, not flattening.&lt;/p&gt;

&lt;p&gt;If that resonates, the repo is here: &lt;a href="https://github.com/Wynelson94/longhand" rel="noopener noreferrer"&gt;https://github.com/Wynelson94/longhand&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;MIT licensed. Python 3.10+. 170 tests. Zero API calls. Yours.&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>ai</category>
      <category>python</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
