fix: address PR review comments and add file read gate docs

Fix indentation bugs flagged in PR review (SettingsDefaultsManager,
MigrationRunner), add current date/time to file read gate timeline
so the model can judge observation recency, and add documentation
for the file read gate feature.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alex Newman
2026-04-07 13:09:46 -07:00
parent f4570f2a0a
commit c21e49d9fa
7 changed files with 197 additions and 5 deletions

View File

@@ -70,6 +70,7 @@
"pages": [
"context-engineering",
"progressive-disclosure",
"file-read-gate",
"smart-explore-benchmark"
]
},

View File

@@ -0,0 +1,180 @@
---
title: "File Read Gate"
description: "How claude-mem intercepts file reads to save tokens using observation history"
---
# File Read Gate
## What It Is
The File Read Gate is a **PreToolUse hook** that intercepts Claude's `Read` tool calls. When Claude tries to read a file that has prior observations in the database, the gate blocks the read and instead shows a compact timeline of past work on that file. Claude then decides the cheapest path to get the context it needs.
This is a concrete implementation of [progressive disclosure](/progressive-disclosure) -- show what exists first, let the agent decide what to fetch.
---
## How It Works
```
Claude calls Read("src/services/worker-service.ts")
PreToolUse hook fires
File size < 1,500 bytes? ──→ Allow read (timeline costs more than file)
↓ No
Project excluded? ──→ Allow read
↓ No
Query worker: GET /api/observations/by-file
No observations found? ──→ Allow read
↓ Has observations
Deduplicate (1 per session)
Rank by specificity
Limit to 15
DENY read with timeline
```
When the gate fires, Claude sees a message like this:
```
Current: 2026-04-07 3:25pm PDT
Read blocked: This file has prior observations. Choose the cheapest path:
- Already know enough? The timeline below may be all you need (semantic priming).
- Need details? get_observations([IDs]) -- ~300 tokens each.
- Need current code? smart_outline("path") for structure (~1-2k tokens),
smart_unfold("path", "<symbol>") for a specific function (~400-2k tokens).
- Need to edit? Use smart tools for line numbers, then sed via Bash.
### Apr 5, 2026
42301 2:15pm Fixed database connection pooling
42298 1:50pm Refactored worker startup sequence
### Mar 28, 2026
41890 4:30pm Added health check endpoint
```
---
## The Decision Tree
Claude has four options after seeing the timeline, ordered from cheapest to most expensive:
| Option | Token Cost | When to Use |
|--------|-----------|-------------|
| **Semantic priming** | 0 extra | Timeline titles tell Claude enough to proceed |
| **get_observations([IDs])** | ~300 each | Need specific details from past work |
| **smart_outline / smart_unfold** | ~1-2k | Need current code structure or a specific function |
| **Full file read** | 5k-50k | File has changed significantly since observations |
In practice, most file reads resolve at the semantic priming or get_observations level, saving thousands of tokens per interaction.
---
## Current Date/Time for Temporal Reasoning
The timeline includes the current date and time as its first line:
```
Current: 2026-04-07 3:25pm PDT
```
This lets Claude reason about how recent the observations are relative to now. For example:
- **Observations from today** -- likely still accurate, semantic priming is safe
- **Observations from last week** -- probably accurate, get_observations for details
- **Observations from months ago** -- file may have changed, consider smart_outline or full read
The timestamp format matches the session start context header (`YYYY-MM-DD time timezone`), so Claude sees consistent temporal markers throughout its session.
---
## Token Economics
A typical source file costs **5,000-50,000 tokens** to read in full. The File Read Gate replaces that with:
| Component | Tokens |
|-----------|--------|
| Timeline header + instructions | ~120 |
| 15 observation entries | ~250 |
| **Total timeline** | **~370** |
If Claude needs more detail, it fetches individual observations at ~300 tokens each. Even fetching 3 observations totals ~1,270 tokens -- still a **75-97% savings** over reading the full file.
### Real-World Example
Without the gate (reading `worker-service.ts`):
```
Read: 18,000 tokens
```
With the gate:
```
Timeline: 370 tokens
+ 2 observations: 600 tokens
Total: 970 tokens (95% savings)
```
---
## Specificity Ranking
Not all observations about a file are equally relevant. The gate scores each observation by how specifically it relates to the target file:
| Signal | Score Bonus |
|--------|------------|
| File was **modified** (not just read) | +2 |
| Observation covers **3 or fewer** total files | +2 |
| Observation covers **4-8** total files | +1 |
| Observation covers **9+** files (survey-like) | +0 |
Higher-scoring observations appear first in the timeline. An observation where the file was the primary modification target ranks above one where the file was incidentally read alongside 20 others.
---
## Configuration
### Small File Bypass
Files smaller than **1,500 bytes** always pass through the gate without interception. At that size, the timeline (~370 tokens) would cost more than reading the file directly. This threshold is hardcoded in `src/cli/handlers/file-context.ts`.
### Project Exclusions
Projects matching patterns in `CLAUDE_MEM_EXCLUDED_PROJECTS` skip the gate entirely. Configure this in `~/.claude-mem/settings.json`:
```json
{
"CLAUDE_MEM_EXCLUDED_PROJECTS": "/tmp/*,/scratch/*"
}
```
### How to Disable the Gate
The File Read Gate is implemented as a PreToolUse hook on the `Read` tool matcher. To disable it, remove the `Read` matcher entry from the hooks configuration:
1. Open your Claude Code settings:
```
~/.claude/settings.json
```
2. Find the claude-mem hooks section under `hooks.PreToolUse` and remove the entry with the `Read` matcher.
Alternatively, if you want to keep the gate installed but bypass it for a specific read, Claude can ask you to allow the read -- the gate's deny decision is presented to the user, who can override it.
<Note>
Disabling the gate means Claude will read full files every time, which increases token usage but ensures it always sees the latest code. This is a reasonable choice for small projects or when observations are sparse.
</Note>
---
## How It Fits Together
The File Read Gate is one piece of claude-mem's layered context strategy:
1. **Session Start**: Inject timeline of recent observations (layer 1 -- metadata)
2. **File Read Gate**: Intercept reads with observation history (layer 1 -- metadata)
3. **get_observations**: Fetch specific observation details on demand (layer 2 -- details)
4. **smart_outline / smart_unfold**: Read current code structure efficiently (layer 3 -- source)
5. **Full file read**: Last resort when everything else is insufficient
Each layer is progressively more expensive. The gate ensures Claude starts at the cheapest layer and escalates only when needed.

View File

@@ -942,7 +942,7 @@ View Observations Live @ http://localhost:${i}`:void 0;return{hookSpecificOutput
`+String.fromCodePoint(128172)+` Community https://discord.gg/J4wttp9vDu
`+String.fromCodePoint(128250)+` Watch live in browser http://localhost:${r}/
`)}catch{}return{exitCode:it.SUCCESS}}}});function Nxe(t){return t.toLowerCase().replace(" am","a").replace(" pm","p")}function Mxe(t){return new Date(t).toLocaleString("en-US",{hour:"numeric",minute:"2-digit",hour12:!0})}function Dxe(t){return new Date(t).toLocaleString("en-US",{month:"short",day:"numeric",year:"numeric"})}function jxe(t,e,r){let n=new Set,i=[];for(let o of t){let a=o.memory_session_id??`no-session-${o.id}`;n.has(a)||(n.add(a),i.push(o))}let s=i.map(o=>{let a=Ca(o.files_read),c=Ca(o.files_modified),u=a.length+c.length,l=e.replace(/\\/g,"/"),d=c.some(f=>f.replace(/\\/g,"/")===l),p=0;return d&&(p+=2),u<=3?p+=2:u<=8&&(p+=1),{obs:o,specificityScore:p}});return s.sort((o,a)=>a.specificityScore-o.specificityScore),s.slice(0,r).map(o=>o.obs)}function zxe(t,e){let r=e.replace(/\\/g,"\\\\").replace(/"/g,'\\"').replace(/\n/g,"\\n"),n=new Map;for(let o of t){let a=Dxe(o.created_at_epoch);n.has(a)||n.set(a,[]),n.get(a).push(o)}let i=Array.from(n.entries()).sort((o,a)=>{let c=Math.min(...o[1].map(l=>l.created_at_epoch)),u=Math.min(...a[1].map(l=>l.created_at_epoch));return c-u}),s=["Read blocked: This file has prior observations. Choose the cheapest path:","- **Already know enough?** The timeline below may be all you need (semantic priming).","- **Need details?** get_observations([IDs]) \u2014 ~300 tokens each.",`- **Need current code?** smart_outline("${r}") for structure (~1-2k tokens), smart_unfold("${r}", "<symbol>") for a specific function (~400-2k tokens).`,"- **Need to edit?** Use smart tools for line numbers, then sed via Bash (Edit requires Read, but you already have the context)."];for(let[o,a]of i){let c=[...a].sort((u,l)=>u.created_at_epoch-l.created_at_epoch);s.push(`### ${o}`);for(let u of c){let l=u.title||"Untitled",d=Axe[u.type]||"\u2753",p=Nxe(Mxe(u.created_at_epoch));s.push(`${u.id} ${p} ${d} ${l}`)}}return s.join(`
`)}catch{}return{exitCode:it.SUCCESS}}}});function Nxe(t){return t.toLowerCase().replace(" am","a").replace(" pm","p")}function Mxe(t){return new Date(t).toLocaleString("en-US",{hour:"numeric",minute:"2-digit",hour12:!0})}function Dxe(t){return new Date(t).toLocaleString("en-US",{month:"short",day:"numeric",year:"numeric"})}function jxe(t,e,r){let n=new Set,i=[];for(let o of t){let a=o.memory_session_id??`no-session-${o.id}`;n.has(a)||(n.add(a),i.push(o))}let s=i.map(o=>{let a=Ca(o.files_read),c=Ca(o.files_modified),u=a.length+c.length,l=e.replace(/\\/g,"/"),d=c.some(f=>f.replace(/\\/g,"/")===l),p=0;return d&&(p+=2),u<=3?p+=2:u<=8&&(p+=1),{obs:o,specificityScore:p}});return s.sort((o,a)=>a.specificityScore-o.specificityScore),s.slice(0,r).map(o=>o.obs)}function zxe(t,e){let r=e.replace(/\\/g,"\\\\").replace(/"/g,'\\"').replace(/\n/g,"\\n"),n=new Map;for(let l of t){let d=Dxe(l.created_at_epoch);n.has(d)||n.set(d,[]),n.get(d).push(l)}let i=Array.from(n.entries()).sort((l,d)=>{let p=Math.min(...l[1].map(m=>m.created_at_epoch)),f=Math.min(...d[1].map(m=>m.created_at_epoch));return p-f}),s=new Date,o=s.toLocaleDateString("en-CA"),a=s.toLocaleTimeString("en-US",{hour:"numeric",minute:"2-digit",hour12:!0}).toLowerCase().replace(" ",""),c=s.toLocaleTimeString("en-US",{timeZoneName:"short"}).split(" ").pop(),u=[`Current: ${o} ${a} ${c}`,"Read blocked: This file has prior observations. Choose the cheapest path:","- **Already know enough?** The timeline below may be all you need (semantic priming).","- **Need details?** get_observations([IDs]) \u2014 ~300 tokens each.",`- **Need current code?** smart_outline("${r}") for structure (~1-2k tokens), smart_unfold("${r}", "<symbol>") for a specific function (~400-2k tokens).`,"- **Need to edit?** Use smart tools for line numbers, then sed via Bash (Edit requires Read, but you already have the context)."];for(let[l,d]of i){let p=[...d].sort((f,m)=>f.created_at_epoch-m.created_at_epoch);u.push(`### ${l}`);for(let f of p){let m=f.title||"Untitled",h=Axe[f.type]||"\u2753",g=Nxe(Mxe(f.created_at_epoch));u.push(`${f.id} ${g} ${h} ${m}`)}}return u.join(`
`)}var W5,Va,Oxe,Cxe,Pxe,Axe,LR,UR=Ee(()=>{"use strict";fr();te();Fs();W5=require("fs"),Va=Pe(require("path"),1);A_();tr();It();Fa();Oxe=1500,Cxe=40,Pxe=15,Axe={decision:"\u2696\uFE0F",bugfix:"\u{1F534}",feature:"\u{1F7E3}",refactor:"\u{1F504}",discovery:"\u{1F535}",change:"\u2705"};LR={async execute(t){let r=t.toolInput?.file_path;if(!r)return{continue:!0,suppressOutput:!0};try{let s=Va.default.isAbsolute(r)?r:Va.default.resolve(t.cwd||process.cwd(),r);if((0,W5.statSync)(s).size<Oxe)return{continue:!0,suppressOutput:!0}}catch(s){if(s.code==="ENOENT")return{continue:!0,suppressOutput:!0}}let n=ye.loadFromFile(vt);if(t.cwd&&gl(t.cwd,n.CLAUDE_MEM_EXCLUDED_PROJECTS))return _.debug("HOOK","Project excluded from tracking, skipping file context",{cwd:t.cwd}),{continue:!0,suppressOutput:!0};if(!await sr())return{continue:!0,suppressOutput:!0};try{let s=il(t.cwd),o=t.cwd||process.cwd(),a=Va.default.isAbsolute(r)?r:Va.default.resolve(o,r),c=Va.default.relative(o,a).split(Va.default.sep).join("/"),u=new URLSearchParams({path:c});s.allProjects.length>0&&u.set("projects",s.allProjects.join(",")),u.set("limit",String(Cxe));let l=await Qe(`/api/observations/by-file?${u.toString()}`,{method:"GET"});if(!l.ok)return _.warn("HOOK","File context query failed, skipping",{status:l.status,filePath:r}),{continue:!0,suppressOutput:!0};let d=await l.json();if(!d.observations||d.observations.length===0)return{continue:!0,suppressOutput:!0};let p=jxe(d.observations,c,Pxe);return p.length===0?{continue:!0,suppressOutput:!0}:{hookSpecificOutput:{hookEventName:"PreToolUse",additionalContext:"",permissionDecision:"deny",permissionDecisionReason:zxe(p,r)}}}catch(s){return _.warn("HOOK","File context fetch error, skipping",{error:s instanceof Error?s.message:String(s)}),{continue:!0,suppressOutput:!0}}}}});function V5(t){let e=Lxe[t];return e||(_.warn("HOOK",`Unknown event type: ${t}, returning no-op`),{async execute(){return{continue:!0,suppressOutput:!0,exitCode:it.SUCCESS}}})}var Lxe,K5=Ee(()=>{"use strict";On();te();NR();N_();M_();DR();zR();D_();UR();j_();NR();N_();M_();DR();zR();D_();UR();j_();Lxe={context:AR,"session-init":um,observation:lm,summarize:MR,"session-complete":pm,"user-message":jR,"file-edit":dm,"file-context":LR}});var X5={};Ln(X5,{hookCommand:()=>Uxe,isWorkerUnavailableError:()=>J5});function J5(t){let e=t instanceof Error?t.message:String(t),r=e.toLowerCase();return["econnrefused","econnreset","epipe","etimedout","enotfound","econnaborted","enetunreach","ehostunreach","fetch failed","unable to connect","socket hang up"].some(i=>r.includes(i))||r.includes("timed out")||r.includes("timeout")||/failed:\s*5\d{2}/.test(e)||/status[:\s]+5\d{2}/.test(e)||/failed:\s*429/.test(e)||/status[:\s]+429/.test(e)?!0:(/failed:\s*4\d{2}/.test(e)||/status[:\s]+4\d{2}/.test(e)||t instanceof TypeError||t instanceof ReferenceError||t instanceof SyntaxError,!1)}async function Uxe(t,e,r={}){let n=process.stderr.write.bind(process.stderr);process.stderr.write=(()=>!0);try{let i=q5(t),s=V5(e),o=await C5(),a=i.normalizeInput(o);a.platform=t;let c=await s.execute(a),u=i.formatOutput(c);console.log(JSON.stringify(u));let l=c.exitCode??it.SUCCESS;return r.skipExit||process.exit(l),l}catch(i){return J5(i)?(_.warn("HOOK",`Worker unavailable, skipping hook: ${i instanceof Error?i.message:i}`),r.skipExit||process.exit(it.SUCCESS),it.SUCCESS):(_.error("HOOK",`Hook error: ${i instanceof Error?i.message:i}`,{},i instanceof Error?i:void 0),r.skipExit||process.exit(it.BLOCKING_ERROR),it.BLOCKING_ERROR)}finally{process.stderr.write=n}}var Y5=Ee(()=>{"use strict";P5();H5();K5();On();te()});var qR={};Ln(qR,{cleanClaudeMd:()=>Qxe,generateClaudeMd:()=>Yxe});function Hxe(t){return qxe[t]||"\u{1F4DD}"}function Zxe(t){let e=(t.title?.length||0)+(t.subtitle?.length||0)+(t.narrative?.length||0)+(t.facts?.length||0);return Math.ceil(e/4)}function Bxe(t){let e=new Set;try{let n=(0,t3.execSync)("git ls-files",{cwd:t,encoding:"utf-8",maxBuffer:52428800}).trim().split(`
`).filter(i=>i);for(let i of n){let s=lr.default.join(t,i),o=lr.default.dirname(s);for(;o.length>t.length&&o.startsWith(t);)e.add(o),o=lr.default.dirname(o)}}catch(r){_.warn("CLAUDE_MD","git ls-files failed, falling back to directory walk",{error:String(r)}),r3(t,e)}return e}function r3(t,e,r=0){if(r>10)return;let n=["node_modules",".git",".next","dist","build",".cache","__pycache__",".venv","venv",".idea",".vscode","coverage",".claude-mem",".open-next",".turbo"];try{let i=(0,dr.readdirSync)(t,{withFileTypes:!0});for(let s of i){if(!s.isDirectory()||n.includes(s.name)||s.name.startsWith(".")&&s.name!==".claude")continue;let o=lr.default.join(t,s.name);e.add(o),r3(o,e,r+1)}}catch{}}function Gxe(t,e){let r=n=>{if(!n)return!1;try{let i=JSON.parse(n);if(Array.isArray(i))return i.some(s=>Ra(s,e))}catch{}return!1};return r(t.files_modified)||r(t.files_read)}function Wxe(t,e,r,n){let i=n*3,s=`
SELECT o.*, o.discovery_tokens

View File

@@ -944,7 +944,7 @@ View Observations Live @ http://localhost:${i}`:void 0;return{hookSpecificOutput
`+String.fromCodePoint(128172)+` Community https://discord.gg/J4wttp9vDu
`+String.fromCodePoint(128250)+` Watch live in browser http://localhost:${r}/
`)}catch{}return{exitCode:nt.SUCCESS}}}});function WSe(t){return t.toLowerCase().replace(" am","a").replace(" pm","p")}function VSe(t){return new Date(t).toLocaleString("en-US",{hour:"numeric",minute:"2-digit",hour12:!0})}function KSe(t){return new Date(t).toLocaleString("en-US",{month:"short",day:"numeric",year:"numeric"})}function JSe(t,e,r){let n=new Set,i=[];for(let o of t){let a=o.memory_session_id??`no-session-${o.id}`;n.has(a)||(n.add(a),i.push(o))}let s=i.map(o=>{let a=ka(o.files_read),c=ka(o.files_modified),u=a.length+c.length,l=e.replace(/\\/g,"/"),d=c.some(f=>f.replace(/\\/g,"/")===l),p=0;return d&&(p+=2),u<=3?p+=2:u<=8&&(p+=1),{obs:o,specificityScore:p}});return s.sort((o,a)=>a.specificityScore-o.specificityScore),s.slice(0,r).map(o=>o.obs)}function XSe(t,e){let r=e.replace(/\\/g,"\\\\").replace(/"/g,'\\"').replace(/\n/g,"\\n"),n=new Map;for(let o of t){let a=KSe(o.created_at_epoch);n.has(a)||n.set(a,[]),n.get(a).push(o)}let i=Array.from(n.entries()).sort((o,a)=>{let c=Math.min(...o[1].map(l=>l.created_at_epoch)),u=Math.min(...a[1].map(l=>l.created_at_epoch));return c-u}),s=["Read blocked: This file has prior observations. Choose the cheapest path:","- **Already know enough?** The timeline below may be all you need (semantic priming).","- **Need details?** get_observations([IDs]) \u2014 ~300 tokens each.",`- **Need current code?** smart_outline("${r}") for structure (~1-2k tokens), smart_unfold("${r}", "<symbol>") for a specific function (~400-2k tokens).`,"- **Need to edit?** Use smart tools for line numbers, then sed via Bash (Edit requires Read, but you already have the context)."];for(let[o,a]of i){let c=[...a].sort((u,l)=>u.created_at_epoch-l.created_at_epoch);s.push(`### ${o}`);for(let u of c){let l=u.title||"Untitled",d=GSe[u.type]||"\u2753",p=WSe(VSe(u.created_at_epoch));s.push(`${u.id} ${p} ${d} ${l}`)}}return s.join(`
`)}catch{}return{exitCode:nt.SUCCESS}}}});function WSe(t){return t.toLowerCase().replace(" am","a").replace(" pm","p")}function VSe(t){return new Date(t).toLocaleString("en-US",{hour:"numeric",minute:"2-digit",hour12:!0})}function KSe(t){return new Date(t).toLocaleString("en-US",{month:"short",day:"numeric",year:"numeric"})}function JSe(t,e,r){let n=new Set,i=[];for(let o of t){let a=o.memory_session_id??`no-session-${o.id}`;n.has(a)||(n.add(a),i.push(o))}let s=i.map(o=>{let a=ka(o.files_read),c=ka(o.files_modified),u=a.length+c.length,l=e.replace(/\\/g,"/"),d=c.some(f=>f.replace(/\\/g,"/")===l),p=0;return d&&(p+=2),u<=3?p+=2:u<=8&&(p+=1),{obs:o,specificityScore:p}});return s.sort((o,a)=>a.specificityScore-o.specificityScore),s.slice(0,r).map(o=>o.obs)}function XSe(t,e){let r=e.replace(/\\/g,"\\\\").replace(/"/g,'\\"').replace(/\n/g,"\\n"),n=new Map;for(let l of t){let d=KSe(l.created_at_epoch);n.has(d)||n.set(d,[]),n.get(d).push(l)}let i=Array.from(n.entries()).sort((l,d)=>{let p=Math.min(...l[1].map(m=>m.created_at_epoch)),f=Math.min(...d[1].map(m=>m.created_at_epoch));return p-f}),s=new Date,o=s.toLocaleDateString("en-CA"),a=s.toLocaleTimeString("en-US",{hour:"numeric",minute:"2-digit",hour12:!0}).toLowerCase().replace(" ",""),c=s.toLocaleTimeString("en-US",{timeZoneName:"short"}).split(" ").pop(),u=[`Current: ${o} ${a} ${c}`,"Read blocked: This file has prior observations. Choose the cheapest path:","- **Already know enough?** The timeline below may be all you need (semantic priming).","- **Need details?** get_observations([IDs]) \u2014 ~300 tokens each.",`- **Need current code?** smart_outline("${r}") for structure (~1-2k tokens), smart_unfold("${r}", "<symbol>") for a specific function (~400-2k tokens).`,"- **Need to edit?** Use smart tools for line numbers, then sed via Bash (Edit requires Read, but you already have the context)."];for(let[l,d]of i){let p=[...d].sort((f,m)=>f.created_at_epoch-m.created_at_epoch);u.push(`### ${l}`);for(let f of p){let m=f.title||"Untitled",h=GSe[f.type]||"\u2753",g=WSe(VSe(f.created_at_epoch));u.push(`${f.id} ${g} ${h} ${m}`)}}return u.join(`
`)}var _5,qa,HSe,ZSe,BSe,GSe,dR,pR=we(()=>{"use strict";Ir();re();zs();_5=require("fs"),qa=Pe(require("path"),1);f_();tr();It();Ma();HSe=1500,ZSe=40,BSe=15,GSe={decision:"\u2696\uFE0F",bugfix:"\u{1F534}",feature:"\u{1F7E3}",refactor:"\u{1F504}",discovery:"\u{1F535}",change:"\u2705"};dR={async execute(t){let r=t.toolInput?.file_path;if(!r)return{continue:!0,suppressOutput:!0};try{let s=qa.default.isAbsolute(r)?r:qa.default.resolve(t.cwd||process.cwd(),r);if((0,_5.statSync)(s).size<HSe)return{continue:!0,suppressOutput:!0}}catch(s){if(s.code==="ENOENT")return{continue:!0,suppressOutput:!0}}let n=ve.loadFromFile(vt);if(t.cwd&&cl(t.cwd,n.CLAUDE_MEM_EXCLUDED_PROJECTS))return _.debug("HOOK","Project excluded from tracking, skipping file context",{cwd:t.cwd}),{continue:!0,suppressOutput:!0};if(!await sr())return{continue:!0,suppressOutput:!0};try{let s=Ju(t.cwd),o=t.cwd||process.cwd(),a=qa.default.isAbsolute(r)?r:qa.default.resolve(o,r),c=qa.default.relative(o,a).split(qa.default.sep).join("/"),u=new URLSearchParams({path:c});s.allProjects.length>0&&u.set("projects",s.allProjects.join(",")),u.set("limit",String(ZSe));let l=await at(`/api/observations/by-file?${u.toString()}`,{method:"GET"});if(!l.ok)return _.warn("HOOK","File context query failed, skipping",{status:l.status,filePath:r}),{continue:!0,suppressOutput:!0};let d=await l.json();if(!d.observations||d.observations.length===0)return{continue:!0,suppressOutput:!0};let p=JSe(d.observations,c,BSe);return p.length===0?{continue:!0,suppressOutput:!0}:{hookSpecificOutput:{hookEventName:"PreToolUse",additionalContext:"",permissionDecision:"deny",permissionDecisionReason:XSe(p,r)}}}catch(s){return _.warn("HOOK","File context fetch error, skipping",{error:s instanceof Error?s.message:String(s)}),{continue:!0,suppressOutput:!0}}}}});function b5(t){let e=YSe[t];return e||(_.warn("HOOK",`Unknown event type: ${t}, returning no-op`),{async execute(){return{continue:!0,suppressOutput:!0,exitCode:nt.SUCCESS}}})}var YSe,S5=we(()=>{"use strict";On();re();oR();m_();h_();cR();lR();g_();pR();v_();oR();m_();h_();cR();lR();g_();pR();v_();YSe={context:sR,"session-init":Kf,observation:Jf,summarize:aR,"session-complete":Yf,"user-message":uR,"file-edit":Xf,"file-context":dR}});var w5={};wn(w5,{hookCommand:()=>QSe,isWorkerUnavailableError:()=>x5});function x5(t){let e=t instanceof Error?t.message:String(t),r=e.toLowerCase();return["econnrefused","econnreset","epipe","etimedout","enotfound","econnaborted","enetunreach","ehostunreach","fetch failed","unable to connect","socket hang up"].some(i=>r.includes(i))||r.includes("timed out")||r.includes("timeout")||/failed:\s*5\d{2}/.test(e)||/status[:\s]+5\d{2}/.test(e)||/failed:\s*429/.test(e)||/status[:\s]+429/.test(e)?!0:(/failed:\s*4\d{2}/.test(e)||/status[:\s]+4\d{2}/.test(e)||t instanceof TypeError||t instanceof ReferenceError||t instanceof SyntaxError,!1)}async function QSe(t,e,r={}){let n=process.stderr.write.bind(process.stderr);process.stderr.write=(()=>!0);try{let i=m5(t),s=b5(e),o=await n5(),a=i.normalizeInput(o);a.platform=t;let c=await s.execute(a),u=i.formatOutput(c);console.log(JSON.stringify(u));let l=c.exitCode??nt.SUCCESS;return r.skipExit||process.exit(l),l}catch(i){return x5(i)?(_.warn("HOOK",`Worker unavailable, skipping hook: ${i instanceof Error?i.message:i}`),r.skipExit||process.exit(nt.SUCCESS),nt.SUCCESS):(_.error("HOOK",`Hook error: ${i instanceof Error?i.message:i}`,{},i instanceof Error?i:void 0),r.skipExit||process.exit(nt.BLOCKING_ERROR),nt.BLOCKING_ERROR)}finally{process.stderr.write=n}}var E5=we(()=>{"use strict";i5();h5();S5();On();re()});var mR={};wn(mR,{cleanClaudeMd:()=>pxe,generateClaudeMd:()=>dxe});function rxe(t){return txe[t]||"\u{1F4DD}"}function nxe(t){let e=(t.title?.length||0)+(t.subtitle?.length||0)+(t.narrative?.length||0)+(t.facts?.length||0);return Math.ceil(e/4)}function ixe(t){let e=new Set;try{let n=(0,$5.execSync)("git ls-files",{cwd:t,encoding:"utf-8",maxBuffer:52428800}).trim().split(`
`).filter(i=>i);for(let i of n){let s=lr.default.join(t,i),o=lr.default.dirname(s);for(;o.length>t.length&&o.startsWith(t);)e.add(o),o=lr.default.dirname(o)}}catch(r){_.warn("CLAUDE_MD","git ls-files failed, falling back to directory walk",{error:String(r)}),I5(t,e)}return e}function I5(t,e,r=0){if(r>10)return;let n=["node_modules",".git",".next","dist","build",".cache","__pycache__",".venv","venv",".idea",".vscode","coverage",".claude-mem",".open-next",".turbo"];try{let i=(0,dr.readdirSync)(t,{withFileTypes:!0});for(let s of i){if(!s.isDirectory()||n.includes(s.name)||s.name.startsWith(".")&&s.name!==".claude")continue;let o=lr.default.join(t,s.name);e.add(o),I5(o,e,r+1)}}catch{}}function sxe(t,e){let r=n=>{if(!n)return!1;try{let i=JSON.parse(n);if(Array.isArray(i))return i.some(s=>wa(s,e))}catch{}return!1};return r(t.files_modified)||r(t.files_read)}function oxe(t,e,r,n){let i=n*3,s=`
SELECT o.*, o.discovery_tokens

View File

@@ -126,7 +126,18 @@ function formatFileTimeline(observations: ObservationRow[], filePath: string): s
return aEpoch - bEpoch;
});
// Include current date/time so the model can judge recency of observations
const now = new Date();
const currentDate = now.toLocaleDateString('en-CA'); // YYYY-MM-DD
const currentTime = now.toLocaleTimeString('en-US', {
hour: 'numeric',
minute: '2-digit',
hour12: true
}).toLowerCase().replace(' ', '');
const currentTimezone = now.toLocaleTimeString('en-US', { timeZoneName: 'short' }).split(' ').pop();
const lines: string[] = [
`Current: ${currentDate} ${currentTime} ${currentTimezone}`,
`Read blocked: This file has prior observations. Choose the cheapest path:`,
`- **Already know enough?** The timeline below may be all you need (semantic priming).`,
`- **Need details?** get_observations([IDs]) — ~300 tokens each.`,

View File

@@ -35,7 +35,7 @@ export class MigrationRunner {
this.addOnUpdateCascadeToForeignKeys();
this.addObservationContentHashColumn();
this.addSessionCustomTitleColumn();
this.createObservationFeedbackTable();
this.createObservationFeedbackTable();
this.addSessionPlatformSourceColumn();
}

View File

@@ -51,7 +51,7 @@ export interface SettingsDefaults {
CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE: string;
CLAUDE_MEM_CONTEXT_SHOW_TERMINAL_OUTPUT: string;
CLAUDE_MEM_FOLDER_CLAUDEMD_ENABLED: string;
CLAUDE_MEM_FOLDER_USE_LOCAL_MD: string; // 'true' | 'false' - write to CLAUDE.local.md instead of CLAUDE.md
CLAUDE_MEM_FOLDER_USE_LOCAL_MD: string; // 'true' | 'false' - write to CLAUDE.local.md instead of CLAUDE.md
CLAUDE_MEM_TRANSCRIPTS_ENABLED: string; // 'true' | 'false' - enable transcript watcher ingestion for Codex and other transcript-based clients
CLAUDE_MEM_TRANSCRIPTS_CONFIG_PATH: string; // Path to transcript watcher config JSON
// Process Management
@@ -122,7 +122,7 @@ export class SettingsDefaultsManager {
CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE: 'false',
CLAUDE_MEM_CONTEXT_SHOW_TERMINAL_OUTPUT: 'true',
CLAUDE_MEM_FOLDER_CLAUDEMD_ENABLED: 'false',
CLAUDE_MEM_FOLDER_USE_LOCAL_MD: 'false', // When true, writes to CLAUDE.local.md instead of CLAUDE.md
CLAUDE_MEM_FOLDER_USE_LOCAL_MD: 'false', // When true, writes to CLAUDE.local.md instead of CLAUDE.md
CLAUDE_MEM_TRANSCRIPTS_ENABLED: 'true',
CLAUDE_MEM_TRANSCRIPTS_CONFIG_PATH: join(homedir(), '.claude-mem', 'transcript-watch.json'),
// Process Management