@clauderecallhq/cli 0.72.0 → 0.74.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -140,7 +140,7 @@ Local **768-dimension embeddings** via `bge-base-en-v1.5` ONNX. Three-lane RRF f
140
140
 
141
141
  The embedder model (~110MB) auto-downloads on `npm install`. Skip with `RECALL_SKIP_MODEL_DOWNLOAD=1`.
142
142
 
143
- **One-click vectorization from the web UI.** Each project header has a `🧠 Vectorize` button. Click → pre-flight dialog shows scope (X of Y eligible sessions), three-✓ cost guarantees (free, on-device CPU only, no network), depth selector (Quick / Standard / Full / Custom chunks-per-session), and a **depth-aware ETA** computed server-side per cap level so Standard and Full give honest, different numbers. While running, the dialog flips to monitor mode with a live progress bar, ETA tick countdown, observed-throughput (drained-in-this-session) feeding the estimate, and visible **warm-up** (yellow) + **wind-down** (red) progress bars with timing context for the parts that have no deterministic ETA (embedder load is 5-60s by hardware; worker batch wind-down is 5-15s). Stop is a **global kill switch** — clears every project's queue and halts the worker — followed by a green "Stopped" confirmation badge. The vector worker **does not auto-resume on daemon restart** by default; enable `semantic.autoResumeWorker: true` in `~/.recall/config.json` if you want background draining without a click. Free tier doesn't see the button — endpoint is Pro-gated.
143
+ **One-click vectorization from the web UI.** Each project header has a `🧠 Vectorize` button. Click → pre-flight dialog shows scope (X of Y eligible sessions), three-✓ cost guarantees (free, on-device CPU only, no network), depth selector (Quick / Standard / Full / Custom chunks-per-session), and a **depth-aware ETA** computed server-side per cap level so Standard and Full give honest, different numbers. **Counters are per-repository:** the Queued, Throughput, and ETA tiles only count chunks from the project you opened the dialog from, with a separate small disclosure line surfacing how many chunks are queued from other projects ahead of yours (the worker drains FIFO globally). While running, the dialog flips to monitor mode with a live progress bar, ETA tick countdown, observed-throughput (drained-in-this-session) feeding the estimate, and visible **warm-up** (yellow) + **wind-down** (red) progress bars with timing context for the parts that have no deterministic ETA (embedder load is 5-60s by hardware; worker batch wind-down is 5-15s). **Three explicit confirmation states close every run:** green `✓ Vectorization complete — N chunks embedded in Xs` banner when the queue drains naturally, green `✓ Already vectorized — N{project} is already done` pre-flight callout (replacing the gray "Nothing to do" text) when there's nothing to vectorize, and the existing green `✓ Stopped — worker halted and queue cleared` badge for Stop. Stop itself is a **global kill switch** — clears every project's queue and halts the worker. The vector worker **does not auto-resume on daemon restart** by default; enable `semantic.autoResumeWorker: true` in `~/.recall/config.json` if you want background draining without a click. Free tier doesn't see the button — endpoint is Pro-gated.
144
144
 
145
145
  **Anonymous install ping (opt-in, off by default).** On first run we ask once whether you want to send a single anonymous message per month so we can count real installs vs bots. No PII, no fingerprint, no cross-month linkage. Full payload at [clauderecall.com/telemetry](https://clauderecall.com/telemetry). Toggle any time with `recall telemetry on` / `off` / `view`.
146
146
 
@@ -1772,7 +1772,7 @@ ${o}
1772
1772
  (SELECT COUNT(*) FROM messages) AS messages,
1773
1773
  (SELECT MIN(started_at) FROM sessions WHERE started_at IS NOT NULL) AS earliest,
1774
1774
  (SELECT MAX(started_at) FROM sessions WHERE started_at IS NOT NULL) AS latest`).get()}var Jy=/^(127\.0\.0\.1|localhost|\[::1\])(:\d+)?$/i,Gy=/^https?:\/\/(127\.0\.0\.1|localhost|\[::1\])(:\d+)?$/i;async function je(e,t){if((await Ke()).tier!=="pro")return e.json({error:"pro_required",message:"This feature requires a Claude Recall Pro license.",upgrade_url:"https://clauderecall.com/pricing",activate_command:"recall activate <license-key>"},402);await t()}var Xn=new Map,Up=0,Fp=0,$p=null,Yy=6e4;function zy(){Up+=1;let e=Date.now();e-Fp<Yy||(Fp=e,console.warn("[auth] /api/terminal/* request rejected without a valid X-Recall-Token. The VS Code / Cursor extension is likely outdated relative to the daemon \u2014 tab names will not flow through, and session titles will fall back to the heuristic first-message snippet. Reinstall: `code --install-extension extensions/vscode/clauderecall-vscode-*.vsix` and restart the extension host. Run `recall doctor` for a full pipeline view."))}function Ky(){$p=new Date().toISOString()}function Vy(){let e=ac();return{silentTerminalRejections:Up,lastTerminalSyncAt:$p,autoExtract:{circuitBroken:e.broken,brokenAt:e.brokenAt,reason:e.reason,consecutiveZeroTokenRuns:e.consecutiveZeroTokenRuns}}}function Jn(e,t){if(t)return"";let s=e;return` AND COALESCE(${s}.auto_title, '') NOT LIKE '[meta]%' AND COALESCE(${s}.auto_title, '') NOT LIKE '[output-index]%' AND COALESCE(${s}.auto_title, '') NOT LIKE '[skill]%' AND COALESCE(${s}.auto_title, '') NOT LIKE 'You are summarizing a Claude Code session%' AND COALESCE(${s}.auto_title, '') NOT LIKE 'You are extracting a structured Output Index%' AND COALESCE(${s}.title_quality, '') != 'programmatic'`}function Zy(e){let t=new Oy;if(t.use("*",Wy({maxSize:1*1024*1024})),t.use("*",async(i,l)=>{let p=i.req.raw.headers.get("host")??"";if(!Jy.test(p))return i.text("Forbidden: invalid Host header",403);let g=i.req.raw.headers.get("origin");if(g&&!Gy.test(g))return i.text("Forbidden: cross-origin request rejected",403);await l()}),e){let i=Buffer.from(e,"utf8");t.use("/api/*",async(l,p)=>{if(l.req.method==="GET"&&l.req.path==="/api/health")return p();let g=l.req.raw.headers.get("x-recall-token")??"";!g&&l.req.method==="GET"&&(g=new URL(l.req.url).searchParams.get("token")??"");let _=!1;if(g.length===e.length)try{_=My(Buffer.from(g,"utf8"),i)}catch{_=!1}return _?p():(l.req.path.startsWith("/api/terminal/")&&zy(),l.json({error:"unauthorized"},401))})}t.use("*",async(i,l)=>{await l(),i.res.headers.set("Content-Security-Policy","default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https://clauderecall.com; font-src 'self' data:; connect-src 'self' data: https://clauderecall.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"),i.res.headers.set("X-Content-Type-Options","nosniff"),i.res.headers.set("X-Frame-Options","DENY"),i.res.headers.set("Referrer-Policy","no-referrer"),i.res.headers.set("Cross-Origin-Resource-Policy","same-origin")}),t.get("/api/health",i=>i.json({status:"ok",version:Mp,uptimeSeconds:Math.round(process.uptime()),pipeline:Vy()})),t.get("/api/stats",i=>i.json(Dp())),t.get("/api/stats/session/:id",i=>{let l=zd(i.req.param("id"));return l?i.json(l):i.json({error:"session not found"},404)}),t.get("/api/stats/project/:name",i=>{let l=Kd(i.req.param("name"));return l?i.json(l):i.json({error:"project not found"},404)}),t.get("/api/stats/overview",i=>{let l=i.req.query("range"),p=l==="7d"?"7d":l==="30d"?"30d":"all";return i.json(Vd(p))}),t.post("/api/stats/backfill",async i=>{let l=await i.req.json().catch(()=>({})),p=l.limit?Math.max(1,Math.min(1e5,Number(l.limit))):5e3;if(p>5e3){let _=Jd({limit:p});return i.json({mode:"background",started:_,alreadyRunning:!_&&Gd(),limit:p,lastRun:Jo()})}let g=Xd({limit:p});return i.json({mode:"sync",started:!1,alreadyRunning:!1,limit:p,result:g,lastRun:Jo()})}),t.get("/api/stats/health",i=>i.json(Zd())),t.get("/api/stats/health/:projectId",i=>{let l=Number(i.req.param("projectId")),p=zo(l);return p?i.json(p):i.json({error:"project not found"},404)}),t.get("/api/config/verification",i=>i.json({enabled:si()})),t.put("/api/config/verification",async i=>{let l=await i.req.json();return typeof l.enabled=="boolean"&&Ip(l.enabled),i.json({enabled:si()})}),t.get("/api/sessions/:id/verification",i=>{let l=i.req.param("id"),p=xp(l);return i.json(p)}),t.get("/api/sessions/:id/share-stats",i=>{let l=i.req.param("id"),g=f().prepare(`SELECT tool_names, raw_json FROM messages
1775
- WHERE session_id = ? AND tool_names IS NOT NULL AND tool_names != ''`).all(l),_=g.length,E=new Set;for(let y of g){if(!/Read|Write|Edit/.test(y.tool_names))continue;let k=y.raw_json.match(/"(?:file_path|path)":\s*"([^"]+)"/g);if(k)for(let N of k){let A=N.match(/":\s*"([^"]+)"/);A&&E.add(A[1])}}return i.json({filesReferenced:E.size,toolCallCount:_})}),t.get("/api/sessions/:id/commits",async i=>{let l=i.req.param("id"),p=Vo(l);if(p.length>0||i.req.query("refresh")!=="1")return i.json({commits:p});let g=await Ko(l);return i.json({commits:Vo(l),status:g.status})}),t.get("/api/commits/:sha/session",i=>{let l=i.req.param("sha");return/^[0-9a-fA-F]{4,40}$/.test(l)?i.json({sessions:Bn(l)}):i.json({error:"invalid sha format"},400)}),t.get("/api/license/status",async i=>{let l=await Ke();return i.json(l)}),t.post("/api/feedback",async i=>{let l;try{l=await i.req.json()}catch{return i.json({error:"invalid json"},400)}if(!l||typeof l!="object")return i.json({error:"invalid body"},400);let p=process.env.RECALL_FEEDBACK_API??"https://clauderecall.com/api/feedback",g=p==="https://clauderecall.com/api/feedback",_=await Ke(),E=Pt(),y=g&&_.tier==="pro"&&E?E.license_jwt:null,k=(()=>{try{let L=Yn(pi(mi(import.meta.url)),"..","..","package.json");return JSON.parse(li(L,"utf8")).version}catch{return"unknown"}})(),N=l,A={score:N.score,comment:N.comment??null,surface:"web",version:typeof N.version=="string"?N.version:k,os:typeof N.os=="string"?N.os:process.platform,trigger_kind:typeof N.trigger_kind=="string"?N.trigger_kind:"manual",license_jwt:y};try{let L=await fetch(p,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(A)}),C=await L.json().catch(()=>({}));return i.json(C,L.status)}catch(L){let C=L instanceof Error?L.message:"network error";return i.json({error:"upstream_unreachable",detail:C},502)}}),t.get("/api/discover/today",je,async i=>{try{return i.json(await np())}catch(l){return console.error("[discover.today]",l),i.json({rediscovered:null,expensive:null,authored:null,availability:{semantic:!1,cost:!1,git:!1},generatedAt:new Date().toISOString(),error:l.message},500)}}),t.get("/api/macro-repos",i=>i.json({macro_repos:So(),orphan_projects:Tu()})),t.get("/api/bug-synthesis",i=>{let l=i.req.query("scope"),p=i.req.query("target_id"),g=i.req.query("limit"),_=l==="cluster"||l==="project"?l:void 0,E=g?Math.max(1,Number(g)):50,y=Ou({scope:_,target_id:p??void 0,limit:E});return i.json({results:y})}),t.get("/api/bug-synthesis/counts",i=>{let l=i.req.query("scope");if(l!=="cluster"&&l!=="project")return i.json({error:'scope must be "cluster" or "project"'},400);let p=Lu(l);return i.json({counts:Array.from(p.entries()).map(([g,_])=>({target_id:g,count:_}))})}),t.get("/api/bug-synthesis/:id",i=>{let l=Number(i.req.param("id"));if(!Number.isFinite(l))return i.json({error:"invalid id"},400);let p=To(l);return p?i.json({result:p}):i.json({error:"not found"},404)}),t.delete("/api/bug-synthesis/:id",i=>{let l=Number(i.req.param("id"));return Number.isFinite(l)?(Cu(l),i.json({ok:!0})):i.json({error:"invalid id"},400)});let s=M.object({name:M.string().min(1).max(100),description:M.string().max(500).nullable().optional()});t.post("/api/macro-repos",async i=>{let l=await i.req.json().catch(()=>null),p=s.safeParse(l);if(!p.success)return i.json({error:"invalid request body",details:p.error.format()},400);try{let g=yu({name:p.data.name,description:p.data.description??null});return i.json({macro_repo:g},201)}catch(g){let _=g instanceof Error?g.message:String(g);return _.includes("UNIQUE constraint")?i.json({error:`a macro repo named "${p.data.name}" already exists`},409):i.json({error:_},400)}});let n=M.object({name:M.string().min(1).max(100).optional(),description:M.string().max(500).nullable().optional()});t.patch("/api/macro-repos/:id",async i=>{let l=Number(i.req.param("id"));if(!Number.isFinite(l))return i.json({error:"invalid id"},400);let p=await i.req.json().catch(()=>null),g=n.safeParse(p);if(!g.success)return i.json({error:"invalid request body"},400);try{let _=wu(l,g.data);return i.json({macro_repo:_})}catch(_){let E=_ instanceof Error?_.message:String(_);return E.includes("not found")?i.json({error:E},404):E.includes("UNIQUE constraint")?i.json({error:"another macro repo already has that name"},409):i.json({error:E},400)}}),t.delete("/api/macro-repos/:id",i=>{let l=Number(i.req.param("id"));return Number.isFinite(l)?(Ru(l),i.json({ok:!0})):i.json({error:"invalid id"},400)});let r=M.object({project_id:M.number().int().positive()});t.post("/api/macro-repos/:id/members",async i=>{let l=Number(i.req.param("id"));if(!Number.isFinite(l))return i.json({error:"invalid id"},400);let p=await i.req.json().catch(()=>null),g=r.safeParse(p);if(!g.success)return i.json({error:"invalid request body"},400);try{return ku(l,g.data.project_id),i.json({macro_repo:kt(l)})}catch(_){let E=_ instanceof Error?_.message:String(_);return i.json({error:E},E.includes("not found")?404:400)}}),t.delete("/api/macro-repos/:id/members/:projectId",i=>{let l=Number(i.req.param("id")),p=Number(i.req.param("projectId"));return!Number.isFinite(l)||!Number.isFinite(p)?i.json({error:"invalid id"},400):(Au(l,p),i.json({macro_repo:kt(l)}))}),t.get("/api/projects",i=>{let l=f(),p=i.req.query("system")==="1"||i.req.query("system")==="true",g=Jn("s",p),_=l.prepare(`SELECT p.id, p.name, p.decoded_path,
1775
+ WHERE session_id = ? AND tool_names IS NOT NULL AND tool_names != ''`).all(l),_=g.length,E=new Set;for(let y of g){if(!/Read|Write|Edit/.test(y.tool_names))continue;let k=y.raw_json.match(/"(?:file_path|path)":\s*"([^"]+)"/g);if(k)for(let N of k){let A=N.match(/":\s*"([^"]+)"/);A&&E.add(A[1])}}return i.json({filesReferenced:E.size,toolCallCount:_})}),t.get("/api/sessions/:id/commits",async i=>{let l=i.req.param("id"),p=Vo(l);if(p.length>0||i.req.query("refresh")!=="1")return i.json({commits:p});let g=await Ko(l);return i.json({commits:Vo(l),status:g.status})}),t.get("/api/commits/:sha/session",i=>{let l=i.req.param("sha");return/^[0-9a-fA-F]{4,40}$/.test(l)?i.json({sessions:Bn(l)}):i.json({error:"invalid sha format"},400)}),t.get("/api/license/status",async i=>{let l=await Ke();return i.json(l)}),t.post("/api/feedback",async i=>{let l;try{l=await i.req.json()}catch{return i.json({error:"invalid json"},400)}if(!l||typeof l!="object")return i.json({error:"invalid body"},400);let p=process.env.RECALL_FEEDBACK_API??"https://clauderecall.com/api/feedback",g=p==="https://clauderecall.com/api/feedback",_=await Ke(),E=Pt(),y=g&&_.tier==="pro"&&E?E.license_jwt:null,k=(()=>{try{let O=Yn(pi(mi(import.meta.url)),"..","..","package.json");return JSON.parse(li(O,"utf8")).version}catch{return"unknown"}})(),N=l,A={score:N.score,comment:N.comment??null,surface:"web",version:typeof N.version=="string"?N.version:k,os:typeof N.os=="string"?N.os:process.platform,trigger_kind:typeof N.trigger_kind=="string"?N.trigger_kind:"manual",license_jwt:y};try{let O=await fetch(p,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(A)}),C=await O.json().catch(()=>({}));return i.json(C,O.status)}catch(O){let C=O instanceof Error?O.message:"network error";return i.json({error:"upstream_unreachable",detail:C},502)}}),t.get("/api/discover/today",je,async i=>{try{return i.json(await np())}catch(l){return console.error("[discover.today]",l),i.json({rediscovered:null,expensive:null,authored:null,availability:{semantic:!1,cost:!1,git:!1},generatedAt:new Date().toISOString(),error:l.message},500)}}),t.get("/api/macro-repos",i=>i.json({macro_repos:So(),orphan_projects:Tu()})),t.get("/api/bug-synthesis",i=>{let l=i.req.query("scope"),p=i.req.query("target_id"),g=i.req.query("limit"),_=l==="cluster"||l==="project"?l:void 0,E=g?Math.max(1,Number(g)):50,y=Ou({scope:_,target_id:p??void 0,limit:E});return i.json({results:y})}),t.get("/api/bug-synthesis/counts",i=>{let l=i.req.query("scope");if(l!=="cluster"&&l!=="project")return i.json({error:'scope must be "cluster" or "project"'},400);let p=Lu(l);return i.json({counts:Array.from(p.entries()).map(([g,_])=>({target_id:g,count:_}))})}),t.get("/api/bug-synthesis/:id",i=>{let l=Number(i.req.param("id"));if(!Number.isFinite(l))return i.json({error:"invalid id"},400);let p=To(l);return p?i.json({result:p}):i.json({error:"not found"},404)}),t.delete("/api/bug-synthesis/:id",i=>{let l=Number(i.req.param("id"));return Number.isFinite(l)?(Cu(l),i.json({ok:!0})):i.json({error:"invalid id"},400)});let s=M.object({name:M.string().min(1).max(100),description:M.string().max(500).nullable().optional()});t.post("/api/macro-repos",async i=>{let l=await i.req.json().catch(()=>null),p=s.safeParse(l);if(!p.success)return i.json({error:"invalid request body",details:p.error.format()},400);try{let g=yu({name:p.data.name,description:p.data.description??null});return i.json({macro_repo:g},201)}catch(g){let _=g instanceof Error?g.message:String(g);return _.includes("UNIQUE constraint")?i.json({error:`a macro repo named "${p.data.name}" already exists`},409):i.json({error:_},400)}});let n=M.object({name:M.string().min(1).max(100).optional(),description:M.string().max(500).nullable().optional()});t.patch("/api/macro-repos/:id",async i=>{let l=Number(i.req.param("id"));if(!Number.isFinite(l))return i.json({error:"invalid id"},400);let p=await i.req.json().catch(()=>null),g=n.safeParse(p);if(!g.success)return i.json({error:"invalid request body"},400);try{let _=wu(l,g.data);return i.json({macro_repo:_})}catch(_){let E=_ instanceof Error?_.message:String(_);return E.includes("not found")?i.json({error:E},404):E.includes("UNIQUE constraint")?i.json({error:"another macro repo already has that name"},409):i.json({error:E},400)}}),t.delete("/api/macro-repos/:id",i=>{let l=Number(i.req.param("id"));return Number.isFinite(l)?(Ru(l),i.json({ok:!0})):i.json({error:"invalid id"},400)});let r=M.object({project_id:M.number().int().positive()});t.post("/api/macro-repos/:id/members",async i=>{let l=Number(i.req.param("id"));if(!Number.isFinite(l))return i.json({error:"invalid id"},400);let p=await i.req.json().catch(()=>null),g=r.safeParse(p);if(!g.success)return i.json({error:"invalid request body"},400);try{return ku(l,g.data.project_id),i.json({macro_repo:kt(l)})}catch(_){let E=_ instanceof Error?_.message:String(_);return i.json({error:E},E.includes("not found")?404:400)}}),t.delete("/api/macro-repos/:id/members/:projectId",i=>{let l=Number(i.req.param("id")),p=Number(i.req.param("projectId"));return!Number.isFinite(l)||!Number.isFinite(p)?i.json({error:"invalid id"},400):(Au(l,p),i.json({macro_repo:kt(l)}))}),t.get("/api/projects",i=>{let l=f(),p=i.req.query("system")==="1"||i.req.query("system")==="true",g=Jn("s",p),_=l.prepare(`SELECT p.id, p.name, p.decoded_path,
1776
1776
  COUNT(CASE WHEN s.id IS NOT NULL${g} THEN 1 END) AS session_count,
1777
1777
  COALESCE(SUM(CASE WHEN s.id IS NOT NULL${g} THEN s.message_count ELSE 0 END), 0) AS message_count,
1778
1778
  MAX(COALESCE(s.ended_at, s.started_at)) AS latest
@@ -1795,7 +1795,7 @@ ${o}
1795
1795
  FROM thread_edges
1796
1796
  WHERE session_id IN (${A})
1797
1797
  AND (parent_session_id IS NULL
1798
- OR parent_session_id IN (${A}))`).all(...y,...y)}let N=E.map(A=>{let L=A.alias??A.auto_title??A.first_user_message??A.id.slice(0,8),C=null,I=null;if(A.auto_title?.startsWith("/")){let O=A.auto_title.split(" \xB7 ");C=O[0],I=O.length>1?O.slice(1).join(" \xB7 "):null}return{id:A.id.slice(0,8),full_id:A.id,title:L,alias:A.alias,auto_title:A.auto_title,auto_title_source:A.auto_title_source,title_quality:A.title_quality,started_at:A.started_at,msgs:A.message_count,skill:C,brand:I}});return i.json({project:g,sessions:N,thread_edges:k})});let o=new Set(["citation","similar","skill_track","bug_pattern","wiki_link","temporal_proximity"]),a=new Set(["pending","approved","rejected"]),c=new Set(["L1","L2","L3","L4","user"]);t.get("/api/links",i=>{let l=i.req.query("source_id")??void 0,p=i.req.query("target_id")??void 0,g=i.req.query("type"),_=i.req.query("approved"),E=i.req.query("limit"),y;if(g){if(!o.has(g))return i.json({error:`invalid type: ${g}`},400);y=g}let k=_==="1"||_==="true",N=E?Number(E):void 0;if(N!==void 0&&(!Number.isFinite(N)||N<1))return i.json({error:"invalid limit"},400);try{let A=Os({sourceSessionId:l,targetSessionId:p,linkType:y,approvedOnly:k,limit:N});return i.json({links:A})}catch(A){return i.json({error:A.message},400)}}),t.get("/api/links/suggestions",i=>{let l=i.req.query("status"),p=i.req.query("source_id")??void 0,g=i.req.query("target_id")??void 0,_=i.req.query("inferred_by"),E=i.req.query("limit"),y;if(l){if(!a.has(l))return i.json({error:`invalid status: ${l}`},400);y=l}let k;if(_){if(!c.has(_))return i.json({error:`invalid inferred_by: ${_}`},400);k=_}let N=E?Number(E):void 0;if(N!==void 0&&(!Number.isFinite(N)||N<1))return i.json({error:"invalid limit"},400);try{let A=lt({status:y,sourceSessionId:p,targetSessionId:g,inferredBy:k,limit:N}),L=new Set;for(let O of A)L.add(O.source_session_id),L.add(O.target_session_id);let C=new Map;if(L.size>0){let O=Array.from(L),F=O.map(()=>"?").join(","),K=f().prepare(`SELECT s.id,
1798
+ OR parent_session_id IN (${A}))`).all(...y,...y)}let N=E.map(A=>{let O=A.alias??A.auto_title??A.first_user_message??A.id.slice(0,8),C=null,I=null;if(A.auto_title?.startsWith("/")){let L=A.auto_title.split(" \xB7 ");C=L[0],I=L.length>1?L.slice(1).join(" \xB7 "):null}return{id:A.id.slice(0,8),full_id:A.id,title:O,alias:A.alias,auto_title:A.auto_title,auto_title_source:A.auto_title_source,title_quality:A.title_quality,started_at:A.started_at,msgs:A.message_count,skill:C,brand:I}});return i.json({project:g,sessions:N,thread_edges:k})});let o=new Set(["citation","similar","skill_track","bug_pattern","wiki_link","temporal_proximity"]),a=new Set(["pending","approved","rejected"]),c=new Set(["L1","L2","L3","L4","user"]);t.get("/api/links",i=>{let l=i.req.query("source_id")??void 0,p=i.req.query("target_id")??void 0,g=i.req.query("type"),_=i.req.query("approved"),E=i.req.query("limit"),y;if(g){if(!o.has(g))return i.json({error:`invalid type: ${g}`},400);y=g}let k=_==="1"||_==="true",N=E?Number(E):void 0;if(N!==void 0&&(!Number.isFinite(N)||N<1))return i.json({error:"invalid limit"},400);try{let A=Os({sourceSessionId:l,targetSessionId:p,linkType:y,approvedOnly:k,limit:N});return i.json({links:A})}catch(A){return i.json({error:A.message},400)}}),t.get("/api/links/suggestions",i=>{let l=i.req.query("status"),p=i.req.query("source_id")??void 0,g=i.req.query("target_id")??void 0,_=i.req.query("inferred_by"),E=i.req.query("limit"),y;if(l){if(!a.has(l))return i.json({error:`invalid status: ${l}`},400);y=l}let k;if(_){if(!c.has(_))return i.json({error:`invalid inferred_by: ${_}`},400);k=_}let N=E?Number(E):void 0;if(N!==void 0&&(!Number.isFinite(N)||N<1))return i.json({error:"invalid limit"},400);try{let A=lt({status:y,sourceSessionId:p,targetSessionId:g,inferredBy:k,limit:N}),O=new Set;for(let L of A)O.add(L.source_session_id),O.add(L.target_session_id);let C=new Map;if(O.size>0){let L=Array.from(O),F=L.map(()=>"?").join(","),K=f().prepare(`SELECT s.id,
1799
1799
  NULLIF(sa.alias, '') AS alias,
1800
1800
  s.auto_title,
1801
1801
  s.first_user_message,
@@ -1803,7 +1803,7 @@ ${o}
1803
1803
  FROM sessions s
1804
1804
  LEFT JOIN session_aliases sa ON sa.session_id = s.id
1805
1805
  LEFT JOIN projects p ON p.id = s.project_id
1806
- WHERE s.id IN (${F})`).all(...O);for(let G of K){let P=G.first_user_message?G.first_user_message.slice(0,80):null,X=G.alias??G.auto_title??P??G.id.slice(0,8);C.set(G.id,{title:X,project:G.project})}}let I=A.map(O=>{let F=C.get(O.source_session_id),W=C.get(O.target_session_id);return{...O,source_title:F?.title??O.source_session_id.slice(0,8),source_project:F?.project??null,target_title:W?.title??O.target_session_id.slice(0,8),target_project:W?.project??null}});return i.json({suggestions:I})}catch(A){return i.json({error:A.message},400)}}),t.get("/api/output-index/:sessionId",i=>{let l=i.req.param("sessionId");if(!l)return i.json({error:"sessionId required"},400);let p=Ze(l);return p?i.json(p):i.json({error:`no output index for session ${l}`},404)});let u=new Set(["pagerank","embedding-rerank","hybrid"]);t.get("/api/neighborhood/:sessionId",i=>{let l=i.req.param("sessionId");if(!l)return i.json({error:"sessionId required"},400);let p=i.req.query("budget"),g=p!==void 0?Number(p):4e3;if(!Number.isFinite(g)||g<100)return i.json({error:"budget must be a number \u2265 100"},400);let _=i.req.query("scoring")??"hybrid";if(!u.has(_))return i.json({error:`invalid scoring: ${_}; valid: pagerank, embedding-rerank, hybrid`},400);let E=_,y=i.req.query("max_depth"),k=y!==void 0?Number(y):2;if(!Number.isFinite(k)||k<1)return i.json({error:"max_depth must be a number \u2265 1"},400);let N,A=i.req.query("edge_types");if(A){let F=A.split(",").map(W=>W.trim()).filter(Boolean);for(let W of F)if(!o.has(W))return i.json({error:`invalid edge_type: ${W}`},400);N=F}let L=i.req.query("include_wiki_links"),C=L===void 0?!0:!(L==="0"||L==="false"),I=i.req.query("include_suggestions"),O=I==="1"||I==="true";try{let F=mn(l,{budget:g,scoring:E,maxDepth:k,edgeTypes:N,includeWikiLinks:C,includeSuggestions:O});return i.json(F)}catch(F){let W=F instanceof Error?F.message:"unknown error",K=/not found/.test(W)?404:500;return i.json({error:W},K)}}),t.get("/api/bug-patterns",i=>{let l=i.req.query("min_count"),p=i.req.query("status"),g=i.req.query("project")??void 0,_=i.req.query("limit"),E=i.req.query("offset"),y=l?Number(l):void 0;if(y!==void 0&&(!Number.isFinite(y)||y<1))return i.json({error:"min_count must be a positive integer"},400);let k;if(p==="open")k=!1;else if(p==="resolved")k=!0;else if(p&&p!=="all")return i.json({error:`invalid status: ${p}; valid: open, resolved, all`},400);let N=_?Number(_):void 0;if(N!==void 0&&(!Number.isFinite(N)||N<1))return i.json({error:"invalid limit"},400);let A=E?Number(E):void 0;if(A!==void 0&&(!Number.isFinite(A)||A<0))return i.json({error:"invalid offset"},400);try{let L=Xi({minOccurrenceCount:y,hasResolved:k,project:g,limit:N,offset:A});return i.json(L)}catch(L){return i.json({error:L.message},400)}}),t.get("/api/bug-patterns/setup-status",i=>{let l=f(),g=l.prepare(`SELECT p.name AS project,
1806
+ WHERE s.id IN (${F})`).all(...L);for(let G of K){let P=G.first_user_message?G.first_user_message.slice(0,80):null,X=G.alias??G.auto_title??P??G.id.slice(0,8);C.set(G.id,{title:X,project:G.project})}}let I=A.map(L=>{let F=C.get(L.source_session_id),W=C.get(L.target_session_id);return{...L,source_title:F?.title??L.source_session_id.slice(0,8),source_project:F?.project??null,target_title:W?.title??L.target_session_id.slice(0,8),target_project:W?.project??null}});return i.json({suggestions:I})}catch(A){return i.json({error:A.message},400)}}),t.get("/api/output-index/:sessionId",i=>{let l=i.req.param("sessionId");if(!l)return i.json({error:"sessionId required"},400);let p=Ze(l);return p?i.json(p):i.json({error:`no output index for session ${l}`},404)});let u=new Set(["pagerank","embedding-rerank","hybrid"]);t.get("/api/neighborhood/:sessionId",i=>{let l=i.req.param("sessionId");if(!l)return i.json({error:"sessionId required"},400);let p=i.req.query("budget"),g=p!==void 0?Number(p):4e3;if(!Number.isFinite(g)||g<100)return i.json({error:"budget must be a number \u2265 100"},400);let _=i.req.query("scoring")??"hybrid";if(!u.has(_))return i.json({error:`invalid scoring: ${_}; valid: pagerank, embedding-rerank, hybrid`},400);let E=_,y=i.req.query("max_depth"),k=y!==void 0?Number(y):2;if(!Number.isFinite(k)||k<1)return i.json({error:"max_depth must be a number \u2265 1"},400);let N,A=i.req.query("edge_types");if(A){let F=A.split(",").map(W=>W.trim()).filter(Boolean);for(let W of F)if(!o.has(W))return i.json({error:`invalid edge_type: ${W}`},400);N=F}let O=i.req.query("include_wiki_links"),C=O===void 0?!0:!(O==="0"||O==="false"),I=i.req.query("include_suggestions"),L=I==="1"||I==="true";try{let F=mn(l,{budget:g,scoring:E,maxDepth:k,edgeTypes:N,includeWikiLinks:C,includeSuggestions:L});return i.json(F)}catch(F){let W=F instanceof Error?F.message:"unknown error",K=/not found/.test(W)?404:500;return i.json({error:W},K)}}),t.get("/api/bug-patterns",i=>{let l=i.req.query("min_count"),p=i.req.query("status"),g=i.req.query("project")??void 0,_=i.req.query("limit"),E=i.req.query("offset"),y=l?Number(l):void 0;if(y!==void 0&&(!Number.isFinite(y)||y<1))return i.json({error:"min_count must be a positive integer"},400);let k;if(p==="open")k=!1;else if(p==="resolved")k=!0;else if(p&&p!=="all")return i.json({error:`invalid status: ${p}; valid: open, resolved, all`},400);let N=_?Number(_):void 0;if(N!==void 0&&(!Number.isFinite(N)||N<1))return i.json({error:"invalid limit"},400);let A=E?Number(E):void 0;if(A!==void 0&&(!Number.isFinite(A)||A<0))return i.json({error:"invalid offset"},400);try{let O=Xi({minOccurrenceCount:y,hasResolved:k,project:g,limit:N,offset:A});return i.json(O)}catch(O){return i.json({error:O.message},400)}}),t.get("/api/bug-patterns/setup-status",i=>{let l=f(),g=l.prepare(`SELECT p.name AS project,
1807
1807
  COUNT(s.id) AS total_sessions,
1808
1808
  SUM(CASE WHEN oi.session_id IS NOT NULL THEN 1 ELSE 0 END) AS extracted_sessions,
1809
1809
  MAX(oi.extracted_at) AS last_extracted_at
@@ -1811,20 +1811,20 @@ ${o}
1811
1811
  LEFT JOIN sessions s ON s.project_id = p.id
1812
1812
  LEFT JOIN session_output_index oi ON oi.session_id = s.id
1813
1813
  GROUP BY p.id
1814
- ORDER BY total_sessions DESC`).all().map(y=>({project:y.project,total_sessions:y.total_sessions??0,extracted_sessions:y.extracted_sessions??0,remaining_sessions:(y.total_sessions??0)-(y.extracted_sessions??0),last_extracted_at:y.last_extracted_at})),_=g.reduce((y,k)=>(y.total_sessions+=k.total_sessions,y.extracted_sessions+=k.extracted_sessions,y.remaining_sessions+=k.remaining_sessions,y),{total_sessions:0,extracted_sessions:0,remaining_sessions:0}),E=l.prepare("SELECT COUNT(*) AS n FROM bug_pattern_clusters").get();return i.json({projects:g,totals:{..._,cluster_count:E.n}})});let d=M.object({project:M.string().min(1),model:M.string().regex(/^[a-zA-Z0-9._-]{1,100}$/).optional(),limit:M.number().int().positive().optional(),force:M.boolean().optional()});t.post("/api/extract-outputs/preflight",async i=>{let l=Ce(i);if(l)return l;let p=$r();if(p>0&&p<1*1024**3)return i.json({error:"insufficient-disk-space",message:`${(p/1024**3).toFixed(2)} GB free \u2014 extract-outputs needs at least 1 GB headroom. Free up disk and retry.`,freeBytes:p},507);let g=await i.req.json().catch(()=>null),_=d.safeParse(g);if(!_.success)return i.json({error:"invalid request body",details:_.error.format()},400);let E={project:_.data.project,model:_.data.model??st,limit:_.data.limit??200,force:_.data.force??!1},y=xo();if(E.limit>y.sessionCeiling)return ne({kind:"run-rejected",job_id:null,project:E.project,model:E.model,limit:E.limit,origin:i.req.header("origin")??null,reason:`limit ${E.limit} exceeds session ceiling ${y.sessionCeiling}`}),i.json({error:`requested limit ${E.limit} exceeds session ceiling ${y.sessionCeiling}. Lower the limit or edit launcher.sessionCeiling in ~/.recall/config.json.`},400);let k=is(E.project);if(!k)return i.json({error:`project "${E.project}" not found`},404);let A=gt({projectId:k.id,limit:E.limit,force:E.force}).eligible.length,L=No(A),C=We(A,E.model),I=At(),O=L.estimated_input_tokens_max+L.estimated_output_tokens_max>I.remaining_tokens_24h;if(ne({kind:"preflight",job_id:null,project:E.project,model:E.model,limit:E.limit,origin:i.req.header("origin")??null,sessions_eligible:A}),A===0)return i.json({eligible_session_count:0,...L,plan_window_estimate:C,budget:I,would_exceed_budget:!1,preflight_token:null,expires_at:null,message:"No eligible sessions to extract. Pass force=true to re-extract sessions already at the current extractor version."});let{token:F,expiresAt:W}=wo(E);return i.json({preflight_token:F,expires_at:new Date(W).toISOString(),eligible_session_count:A,...L,plan_window_estimate:C,budget:I,would_exceed_budget:O})});let m=M.object({preflight_token:M.string().length(64)});t.post("/api/extract-outputs/run",async i=>{let l=Ce(i);if(l)return l;let p=await i.req.json().catch(()=>null),g=m.safeParse(p);if(!g.success)return i.json({error:"invalid request body"},400);let _=Ro(g.data.preflight_token);if(!_)return ne({kind:"run-rejected",job_id:null,project:null,model:null,limit:null,origin:i.req.header("origin")??null,reason:"preflight token invalid, expired, or already used"}),i.json({error:"preflight token invalid, expired, or already used"},400);let E=At(),k=(()=>{let C=is(_.project);return C?gt({projectId:C.id,limit:_.limit,force:_.force}):null})()?.eligible.length??0,N=No(k),A=N.estimated_input_tokens_max+N.estimated_output_tokens_max;if(A>E.remaining_tokens_24h)return ne({kind:"run-rejected",job_id:null,project:_.project,model:_.model,limit:_.limit,origin:i.req.header("origin")??null,reason:`projected spend ${A} exceeds remaining 24h budget ${E.remaining_tokens_24h}`}),i.json({error:"daily token budget would be exceeded. Wait for the rolling 24h window to clear, or raise launcher.dailyTokenBudget in ~/.recall/config.json.",budget:E,projected_spend:A},429);let L=Hu({project:_.project,model:_.model,limit:_.limit,force:_.force,origin:i.req.header("origin")??null});return"error"in L?i.json({error:L.error},400):i.json({jobId:L.jobId,reused:L.reused},L.reused?409:200)}),t.get("/api/extract-outputs/jobs/:jobId/stream",i=>{let l=i.req.param("jobId");if(!Co(l))return i.json({error:"job not found"},404);let g=Number(i.req.header("Last-Event-ID")??0);return Ye(i,async _=>{let E=!1,y=setInterval(()=>{E||_.writeSSE({event:"heartbeat",data:""}).catch(()=>{E=!0})},15e3);try{for await(let k of Bu(l,g))if(E||(await _.writeSSE({id:String(k.id),event:k.kind,data:JSON.stringify(k.data)}),k.kind==="done"))break}finally{E=!0,clearInterval(y)}})}),t.get("/api/extract-outputs/jobs/:jobId",i=>{let l=Co(i.req.param("jobId"));return l?i.json(l):i.json({error:"job not found"},404)}),t.delete("/api/extract-outputs/jobs/:jobId",i=>{let l=Ce(i);return l||(Wu(i.req.param("jobId"))?i.json({ok:!0}):i.json({error:"job not found or already done"},404))}),t.get("/api/extract-outputs/budget",i=>i.json(At()));let h=M.object({scope:M.enum(["cluster","project"]),target_id:M.string().min(1),mode:M.enum(["synopsis","priorities","root_cause"]),model:M.string().regex(/^[a-zA-Z0-9._-]{1,100}$/).optional()});t.post("/api/bug-patterns/synthesize/preflight",async i=>{{let W=$r();if(W>0&&W<1*1024**3)return i.json({error:"insufficient-disk-space",message:`${(W/1024**3).toFixed(2)} GB free \u2014 bug synthesis needs at least 1 GB headroom. Free up disk and retry.`,freeBytes:W},507)}let l=Ce(i);if(l)return l;let p=await i.req.json().catch(()=>null),g=h.safeParse(p);if(!g.success)return i.json({error:"invalid request body",details:g.error.format()},400);let _={scope:g.data.scope,target_id:g.data.target_id,mode:g.data.mode,model:g.data.model??qu},E=xn(_);if(!E)return ne({kind:"synth-rejected",job_id:null,project:_.scope==="project"?_.target_id:null,model:_.model,limit:null,origin:i.req.header("origin")??null,reason:"target not found"}),i.json({error:_.scope==="cluster"?`cluster "${_.target_id}" not found in any extracted findings`:`project "${_.target_id}" has no extracted findings to synthesize`},404);let y=Oo({scope:_.scope,mode:_.mode,member_session_count:E.context_summary.session_count,cluster_count:E.context_summary.cluster_count}),k=y.estimated_input_tokens_max+y.estimated_output_tokens_max,N=$u(k),A=We(N,_.model),L=At(),I=y.estimated_input_tokens_max+y.estimated_output_tokens_max>L.remaining_tokens_24h;ne({kind:"synth-preflight",job_id:null,project:_.scope==="project"?_.target_id:null,model:_.model,limit:null,origin:i.req.header("origin")??null,reason:`${_.scope}/${_.mode}/${_.target_id}`});let{token:O,expiresAt:F}=wo({project:_.target_id,model:_.model,limit:1,force:!1});return Xn.set(O,_),setTimeout(()=>Xn.delete(O),9e4).unref?.(),i.json({preflight_token:O,expires_at:new Date(F).toISOString(),estimated_input_tokens_max:y.estimated_input_tokens_max,estimated_output_tokens_max:y.estimated_output_tokens_max,plan_window_estimate:A,budget:L,would_exceed_budget:I,context_summary:E.context_summary})});let b=M.object({preflight_token:M.string().length(64)});t.post("/api/bug-patterns/synthesize/run",async i=>{let l=Ce(i);if(l)return l;let p=await i.req.json().catch(()=>null),g=b.safeParse(p);if(!g.success)return i.json({error:"invalid request body"},400);let _=Ro(g.data.preflight_token),E=Xn.get(g.data.preflight_token)??null;if(Xn.delete(g.data.preflight_token),!E||!_)return ne({kind:"synth-rejected",job_id:null,project:null,model:null,limit:null,origin:i.req.header("origin")??null,reason:"preflight token invalid, expired, or already used"}),i.json({error:"preflight token invalid, expired, or already used"},400);let y=At(),k=xn(E);if(!k)return i.json({error:E.scope==="cluster"?`cluster "${E.target_id}" no longer exists`:`project "${E.target_id}" has no findings`},404);let N=Oo({scope:E.scope,mode:E.mode,member_session_count:k.context_summary.session_count,cluster_count:k.context_summary.cluster_count}),A=N.estimated_input_tokens_max+N.estimated_output_tokens_max;if(A>y.remaining_tokens_24h)return ne({kind:"synth-rejected",job_id:null,project:E.scope==="project"?E.target_id:null,model:E.model,limit:null,origin:i.req.header("origin")??null,reason:`projected spend ${A} exceeds remaining 24h budget ${y.remaining_tokens_24h}`}),i.json({error:"daily token budget would be exceeded. Wait for the rolling 24h window to clear, or raise launcher.dailyTokenBudget in ~/.recall/config.json.",budget:y,projected_spend:A},429);let L=Yu({intent:E,origin:i.req.header("origin")??null});return"error"in L?i.json({error:L.error},400):i.json({jobId:L.jobId,reused:L.reused},L.reused?409:200)}),t.get("/api/bug-patterns/synthesize/jobs/:jobId/stream",i=>{let l=i.req.param("jobId");if(!Io(l))return i.json({error:"job not found"},404);let g=Number(i.req.header("Last-Event-ID")??0);return Ye(i,async _=>{let E=!1,y=setInterval(()=>{E||_.writeSSE({event:"heartbeat",data:""}).catch(()=>{E=!0})},15e3);try{for await(let k of zu(l,g))if(E||(await _.writeSSE({id:String(k.id),event:k.kind,data:JSON.stringify(k.data)}),k.kind==="done"))break}finally{E=!0,clearInterval(y)}})}),t.get("/api/bug-patterns/synthesize/jobs/:jobId",i=>{let l=Io(i.req.param("jobId"));return l?i.json(l):i.json({error:"job not found"},404)}),t.delete("/api/bug-patterns/synthesize/jobs/:jobId",i=>{let l=Ce(i);return l||(Ku(i.req.param("jobId"))?i.json({ok:!0}):i.json({error:"job not found or already done"},404))}),t.get("/api/bug-signatures",i=>{let l=i.req.query("project")??null,p=Math.min(Math.max(1,Number(i.req.query("limit")??100)),500),g=f(),_=["oi.bug_signatures IS NOT NULL"],E=[];l&&(_.push("p.name = ?"),E.push(l));let k=g.prepare(`SELECT s.id AS session_id, p.name AS project, s.auto_title,
1814
+ ORDER BY total_sessions DESC`).all().map(y=>({project:y.project,total_sessions:y.total_sessions??0,extracted_sessions:y.extracted_sessions??0,remaining_sessions:(y.total_sessions??0)-(y.extracted_sessions??0),last_extracted_at:y.last_extracted_at})),_=g.reduce((y,k)=>(y.total_sessions+=k.total_sessions,y.extracted_sessions+=k.extracted_sessions,y.remaining_sessions+=k.remaining_sessions,y),{total_sessions:0,extracted_sessions:0,remaining_sessions:0}),E=l.prepare("SELECT COUNT(*) AS n FROM bug_pattern_clusters").get();return i.json({projects:g,totals:{..._,cluster_count:E.n}})});let d=M.object({project:M.string().min(1),model:M.string().regex(/^[a-zA-Z0-9._-]{1,100}$/).optional(),limit:M.number().int().positive().optional(),force:M.boolean().optional()});t.post("/api/extract-outputs/preflight",async i=>{let l=Ce(i);if(l)return l;let p=$r();if(p>0&&p<1*1024**3)return i.json({error:"insufficient-disk-space",message:`${(p/1024**3).toFixed(2)} GB free \u2014 extract-outputs needs at least 1 GB headroom. Free up disk and retry.`,freeBytes:p},507);let g=await i.req.json().catch(()=>null),_=d.safeParse(g);if(!_.success)return i.json({error:"invalid request body",details:_.error.format()},400);let E={project:_.data.project,model:_.data.model??st,limit:_.data.limit??200,force:_.data.force??!1},y=xo();if(E.limit>y.sessionCeiling)return ne({kind:"run-rejected",job_id:null,project:E.project,model:E.model,limit:E.limit,origin:i.req.header("origin")??null,reason:`limit ${E.limit} exceeds session ceiling ${y.sessionCeiling}`}),i.json({error:`requested limit ${E.limit} exceeds session ceiling ${y.sessionCeiling}. Lower the limit or edit launcher.sessionCeiling in ~/.recall/config.json.`},400);let k=is(E.project);if(!k)return i.json({error:`project "${E.project}" not found`},404);let A=gt({projectId:k.id,limit:E.limit,force:E.force}).eligible.length,O=No(A),C=We(A,E.model),I=At(),L=O.estimated_input_tokens_max+O.estimated_output_tokens_max>I.remaining_tokens_24h;if(ne({kind:"preflight",job_id:null,project:E.project,model:E.model,limit:E.limit,origin:i.req.header("origin")??null,sessions_eligible:A}),A===0)return i.json({eligible_session_count:0,...O,plan_window_estimate:C,budget:I,would_exceed_budget:!1,preflight_token:null,expires_at:null,message:"No eligible sessions to extract. Pass force=true to re-extract sessions already at the current extractor version."});let{token:F,expiresAt:W}=wo(E);return i.json({preflight_token:F,expires_at:new Date(W).toISOString(),eligible_session_count:A,...O,plan_window_estimate:C,budget:I,would_exceed_budget:L})});let m=M.object({preflight_token:M.string().length(64)});t.post("/api/extract-outputs/run",async i=>{let l=Ce(i);if(l)return l;let p=await i.req.json().catch(()=>null),g=m.safeParse(p);if(!g.success)return i.json({error:"invalid request body"},400);let _=Ro(g.data.preflight_token);if(!_)return ne({kind:"run-rejected",job_id:null,project:null,model:null,limit:null,origin:i.req.header("origin")??null,reason:"preflight token invalid, expired, or already used"}),i.json({error:"preflight token invalid, expired, or already used"},400);let E=At(),k=(()=>{let C=is(_.project);return C?gt({projectId:C.id,limit:_.limit,force:_.force}):null})()?.eligible.length??0,N=No(k),A=N.estimated_input_tokens_max+N.estimated_output_tokens_max;if(A>E.remaining_tokens_24h)return ne({kind:"run-rejected",job_id:null,project:_.project,model:_.model,limit:_.limit,origin:i.req.header("origin")??null,reason:`projected spend ${A} exceeds remaining 24h budget ${E.remaining_tokens_24h}`}),i.json({error:"daily token budget would be exceeded. Wait for the rolling 24h window to clear, or raise launcher.dailyTokenBudget in ~/.recall/config.json.",budget:E,projected_spend:A},429);let O=Hu({project:_.project,model:_.model,limit:_.limit,force:_.force,origin:i.req.header("origin")??null});return"error"in O?i.json({error:O.error},400):i.json({jobId:O.jobId,reused:O.reused},O.reused?409:200)}),t.get("/api/extract-outputs/jobs/:jobId/stream",i=>{let l=i.req.param("jobId");if(!Co(l))return i.json({error:"job not found"},404);let g=Number(i.req.header("Last-Event-ID")??0);return Ye(i,async _=>{let E=!1,y=setInterval(()=>{E||_.writeSSE({event:"heartbeat",data:""}).catch(()=>{E=!0})},15e3);try{for await(let k of Bu(l,g))if(E||(await _.writeSSE({id:String(k.id),event:k.kind,data:JSON.stringify(k.data)}),k.kind==="done"))break}finally{E=!0,clearInterval(y)}})}),t.get("/api/extract-outputs/jobs/:jobId",i=>{let l=Co(i.req.param("jobId"));return l?i.json(l):i.json({error:"job not found"},404)}),t.delete("/api/extract-outputs/jobs/:jobId",i=>{let l=Ce(i);return l||(Wu(i.req.param("jobId"))?i.json({ok:!0}):i.json({error:"job not found or already done"},404))}),t.get("/api/extract-outputs/budget",i=>i.json(At()));let h=M.object({scope:M.enum(["cluster","project"]),target_id:M.string().min(1),mode:M.enum(["synopsis","priorities","root_cause"]),model:M.string().regex(/^[a-zA-Z0-9._-]{1,100}$/).optional()});t.post("/api/bug-patterns/synthesize/preflight",async i=>{{let W=$r();if(W>0&&W<1*1024**3)return i.json({error:"insufficient-disk-space",message:`${(W/1024**3).toFixed(2)} GB free \u2014 bug synthesis needs at least 1 GB headroom. Free up disk and retry.`,freeBytes:W},507)}let l=Ce(i);if(l)return l;let p=await i.req.json().catch(()=>null),g=h.safeParse(p);if(!g.success)return i.json({error:"invalid request body",details:g.error.format()},400);let _={scope:g.data.scope,target_id:g.data.target_id,mode:g.data.mode,model:g.data.model??qu},E=xn(_);if(!E)return ne({kind:"synth-rejected",job_id:null,project:_.scope==="project"?_.target_id:null,model:_.model,limit:null,origin:i.req.header("origin")??null,reason:"target not found"}),i.json({error:_.scope==="cluster"?`cluster "${_.target_id}" not found in any extracted findings`:`project "${_.target_id}" has no extracted findings to synthesize`},404);let y=Oo({scope:_.scope,mode:_.mode,member_session_count:E.context_summary.session_count,cluster_count:E.context_summary.cluster_count}),k=y.estimated_input_tokens_max+y.estimated_output_tokens_max,N=$u(k),A=We(N,_.model),O=At(),I=y.estimated_input_tokens_max+y.estimated_output_tokens_max>O.remaining_tokens_24h;ne({kind:"synth-preflight",job_id:null,project:_.scope==="project"?_.target_id:null,model:_.model,limit:null,origin:i.req.header("origin")??null,reason:`${_.scope}/${_.mode}/${_.target_id}`});let{token:L,expiresAt:F}=wo({project:_.target_id,model:_.model,limit:1,force:!1});return Xn.set(L,_),setTimeout(()=>Xn.delete(L),9e4).unref?.(),i.json({preflight_token:L,expires_at:new Date(F).toISOString(),estimated_input_tokens_max:y.estimated_input_tokens_max,estimated_output_tokens_max:y.estimated_output_tokens_max,plan_window_estimate:A,budget:O,would_exceed_budget:I,context_summary:E.context_summary})});let b=M.object({preflight_token:M.string().length(64)});t.post("/api/bug-patterns/synthesize/run",async i=>{let l=Ce(i);if(l)return l;let p=await i.req.json().catch(()=>null),g=b.safeParse(p);if(!g.success)return i.json({error:"invalid request body"},400);let _=Ro(g.data.preflight_token),E=Xn.get(g.data.preflight_token)??null;if(Xn.delete(g.data.preflight_token),!E||!_)return ne({kind:"synth-rejected",job_id:null,project:null,model:null,limit:null,origin:i.req.header("origin")??null,reason:"preflight token invalid, expired, or already used"}),i.json({error:"preflight token invalid, expired, or already used"},400);let y=At(),k=xn(E);if(!k)return i.json({error:E.scope==="cluster"?`cluster "${E.target_id}" no longer exists`:`project "${E.target_id}" has no findings`},404);let N=Oo({scope:E.scope,mode:E.mode,member_session_count:k.context_summary.session_count,cluster_count:k.context_summary.cluster_count}),A=N.estimated_input_tokens_max+N.estimated_output_tokens_max;if(A>y.remaining_tokens_24h)return ne({kind:"synth-rejected",job_id:null,project:E.scope==="project"?E.target_id:null,model:E.model,limit:null,origin:i.req.header("origin")??null,reason:`projected spend ${A} exceeds remaining 24h budget ${y.remaining_tokens_24h}`}),i.json({error:"daily token budget would be exceeded. Wait for the rolling 24h window to clear, or raise launcher.dailyTokenBudget in ~/.recall/config.json.",budget:y,projected_spend:A},429);let O=Yu({intent:E,origin:i.req.header("origin")??null});return"error"in O?i.json({error:O.error},400):i.json({jobId:O.jobId,reused:O.reused},O.reused?409:200)}),t.get("/api/bug-patterns/synthesize/jobs/:jobId/stream",i=>{let l=i.req.param("jobId");if(!Io(l))return i.json({error:"job not found"},404);let g=Number(i.req.header("Last-Event-ID")??0);return Ye(i,async _=>{let E=!1,y=setInterval(()=>{E||_.writeSSE({event:"heartbeat",data:""}).catch(()=>{E=!0})},15e3);try{for await(let k of zu(l,g))if(E||(await _.writeSSE({id:String(k.id),event:k.kind,data:JSON.stringify(k.data)}),k.kind==="done"))break}finally{E=!0,clearInterval(y)}})}),t.get("/api/bug-patterns/synthesize/jobs/:jobId",i=>{let l=Io(i.req.param("jobId"));return l?i.json(l):i.json({error:"job not found"},404)}),t.delete("/api/bug-patterns/synthesize/jobs/:jobId",i=>{let l=Ce(i);return l||(Ku(i.req.param("jobId"))?i.json({ok:!0}):i.json({error:"job not found or already done"},404))}),t.get("/api/bug-signatures",i=>{let l=i.req.query("project")??null,p=Math.min(Math.max(1,Number(i.req.query("limit")??100)),500),g=f(),_=["oi.bug_signatures IS NOT NULL"],E=[];l&&(_.push("p.name = ?"),E.push(l));let k=g.prepare(`SELECT s.id AS session_id, p.name AS project, s.auto_title,
1815
1815
  s.started_at, oi.extracted_at, oi.bug_signatures
1816
1816
  FROM session_output_index oi
1817
1817
  JOIN sessions s ON s.id = oi.session_id
1818
1818
  JOIN projects p ON p.id = s.project_id
1819
1819
  WHERE ${_.join(" AND ")}
1820
1820
  ORDER BY oi.extracted_at DESC
1821
- LIMIT ?`).all(...E,p).map(I=>{let O=[];try{let F=JSON.parse(I.bug_signatures);Array.isArray(F)&&(O=F)}catch{O=[]}return{session_id:I.session_id,project:I.project,auto_title:I.auto_title,started_at:I.started_at,extracted_at:I.extracted_at,rawSignatures:O}}),N=k.flatMap(I=>I.rawSignatures.map(O=>O.message_hash).filter(O=>typeof O=="string")),A=Eo(N),L=k.map(I=>({session_id:I.session_id,project:I.project,auto_title:I.auto_title,started_at:I.started_at,extracted_at:I.extracted_at,signatures:I.rawSignatures.map(O=>{let F=O.message_hash?A.get(O.message_hash)??null:null;return{...O,resolved:bo(F),resolution:F}}),signature_count:I.rawSignatures.length})),C=L.reduce((I,O)=>(I.sessions_total+=1,O.signature_count>0?(I.sessions_with_findings+=1,I.total_findings+=O.signature_count):I.sessions_empty+=1,I),{sessions_total:0,sessions_with_findings:0,sessions_empty:0,total_findings:0});return i.json({sessions:L,totals:C})}),t.post("/api/bug-signatures/:hash/resolve",async i=>{let l=i.req.param("hash");if(!l||l.length<4)return i.json({error:"invalid message hash"},400);let p=await i.req.json().catch(()=>({})),g=bu({messageHash:l,resolvedInSessionId:p.resolved_in_session_id??null,fixSummary:p.fix_summary??null});return i.json({resolution:g})}),t.post("/api/bug-signatures/:hash/unresolve",i=>{let l=i.req.param("hash");return!l||l.length<4?i.json({error:"invalid message hash"},400):(Su(l),i.json({ok:!0}))}),t.get("/api/bug-patterns/graph",i=>{let l=i.req.query("project")??null,p=i.req.query("include_resolved")!=="0",g=f(),_=["oi.bug_signatures IS NOT NULL","oi.bug_signatures != '[]'"],E=[];l&&(_.push("p.name = ?"),E.push(l));let y=g.prepare(`SELECT s.id AS session_id, p.name AS project, s.auto_title,
1821
+ LIMIT ?`).all(...E,p).map(I=>{let L=[];try{let F=JSON.parse(I.bug_signatures);Array.isArray(F)&&(L=F)}catch{L=[]}return{session_id:I.session_id,project:I.project,auto_title:I.auto_title,started_at:I.started_at,extracted_at:I.extracted_at,rawSignatures:L}}),N=k.flatMap(I=>I.rawSignatures.map(L=>L.message_hash).filter(L=>typeof L=="string")),A=Eo(N),O=k.map(I=>({session_id:I.session_id,project:I.project,auto_title:I.auto_title,started_at:I.started_at,extracted_at:I.extracted_at,signatures:I.rawSignatures.map(L=>{let F=L.message_hash?A.get(L.message_hash)??null:null;return{...L,resolved:bo(F),resolution:F}}),signature_count:I.rawSignatures.length})),C=O.reduce((I,L)=>(I.sessions_total+=1,L.signature_count>0?(I.sessions_with_findings+=1,I.total_findings+=L.signature_count):I.sessions_empty+=1,I),{sessions_total:0,sessions_with_findings:0,sessions_empty:0,total_findings:0});return i.json({sessions:O,totals:C})}),t.post("/api/bug-signatures/:hash/resolve",async i=>{let l=i.req.param("hash");if(!l||l.length<4)return i.json({error:"invalid message hash"},400);let p=await i.req.json().catch(()=>({})),g=bu({messageHash:l,resolvedInSessionId:p.resolved_in_session_id??null,fixSummary:p.fix_summary??null});return i.json({resolution:g})}),t.post("/api/bug-signatures/:hash/unresolve",i=>{let l=i.req.param("hash");return!l||l.length<4?i.json({error:"invalid message hash"},400):(Su(l),i.json({ok:!0}))}),t.get("/api/bug-patterns/graph",i=>{let l=i.req.query("project")??null,p=i.req.query("include_resolved")!=="0",g=f(),_=["oi.bug_signatures IS NOT NULL","oi.bug_signatures != '[]'"],E=[];l&&(_.push("p.name = ?"),E.push(l));let y=g.prepare(`SELECT s.id AS session_id, p.name AS project, s.auto_title,
1822
1822
  s.started_at, oi.extracted_at, oi.bug_signatures
1823
1823
  FROM session_output_index oi
1824
1824
  JOIN sessions s ON s.id = oi.session_id
1825
1825
  JOIN projects p ON p.id = s.project_id
1826
1826
  WHERE ${_.join(" AND ")}
1827
- ORDER BY oi.extracted_at DESC`).all(...E),k=[];for(let P of y){let X=[];try{let Y=JSON.parse(P.bug_signatures);Array.isArray(Y)&&(X=Y)}catch{continue}for(let Y of X)k.push({sig:Y,session_id:P.session_id,project:P.project,auto_title:P.auto_title})}let N=new Map;for(let P of k){let X=P.sig.message_hash??`nohash:${(P.sig.snippet??"").slice(0,64)}`,Y=N.get(X);Y?Y.push(P):N.set(X,[P])}let A=Array.from(N.keys()).filter(P=>!P.startsWith("nohash:")),L=Eo(A),C=[],I=new Map,O=[];for(let[P,X]of N){let Y=X[0],Q=Y.sig.message_hash??null,V=Q?L.get(Q)??null:null,J=bo(V);if(!p&&J)continue;let re=Array.from(new Set(X.map(me=>me.project))).sort(),ze=Array.from(new Set(X.map(me=>me.session_id))),Me={id:Q??P,message_hash:Q,error_type:Y.sig.error_type??null,snippet:(Y.sig.snippet??"").slice(0,200),file:Y.sig.file??null,occurrence_count:X.length,projects:re,resolved:J,fix_summary:V?.fix_summary??null,member_session_ids:ze};C.push(Me);for(let me of X)I.has(me.session_id)||I.set(me.session_id,{session_id:me.session_id,project:me.project,auto_title:me.auto_title}),O.push({cluster_id:Me.id,session_id:me.session_id})}let F=[],W=4,K=new Map;function G(P){let X=K.get(P)??0;return X>=W?!1:(K.set(P,X+1),!0)}for(let P=0;P<C.length;P+=1)for(let X=P+1;X<C.length;X+=1){let Y=C[P],Q=C[X],V=null;Y.error_type&&Y.error_type!=="unknown"&&Y.error_type===Q.error_type?V="same_error_type":Y.file&&Q.file&&Y.file===Q.file&&(V="same_file"),V&&(!G(Y.id)||!G(Q.id)||F.push({a:Y.id,b:Q.id,reason:V}))}return i.json({clusters:C,sessions:Array.from(I.values()),member_edges:O,related_edges:F,totals:{cluster_count:C.length,singleton_count:C.filter(P=>P.occurrence_count===1).length,recurring_count:C.filter(P=>P.occurrence_count>1).length,session_count:I.size,resolved_count:C.filter(P=>P.resolved).length}})}),t.get("/api/bug-patterns/:clusterId",i=>{let l=i.req.param("clusterId");if(!l)return i.json({error:"clusterId required"},400);let p=Ji(l);return p?i.json(p):i.json({error:`cluster ${l} not found`},404)}),t.post("/api/bug-patterns/:clusterId/resolve",async i=>{let l=i.req.param("clusterId");if(!l)return i.json({error:"clusterId required"},400);let p=await i.req.json().catch(()=>null);if(!p)return i.json({error:"body required"},400);let g=p.resolved_in_session_id,_=p.fix_summary;if(typeof g!="string"||g.length===0)return i.json({error:"resolved_in_session_id required"},400);if(typeof _!="string"||_.trim().length===0)return i.json({error:"fix_summary required"},400);try{let E=Gi(l,g,_);return i.json(E)}catch(E){let y=E instanceof Error?E.message:"unknown error",k=/not found/.test(y)?404:(/not a member/.test(y),400);return i.json({error:y},k)}}),t.post("/api/bug-patterns/:clusterId/split",async i=>{let l=i.req.param("clusterId");if(!l)return i.json({error:"clusterId required"},400);let p=await i.req.json().catch(()=>null);if(!p)return i.json({error:"body required"},400);let g=p.member_session_ids;if(!Array.isArray(g)||g.length===0)return i.json({error:"member_session_ids must be a non-empty array of strings"},400);let _=[];for(let E of g){if(typeof E!="string"||E.length===0)return i.json({error:"member_session_ids must contain only non-empty strings"},400);_.push(E)}try{let E=Yi(l,_);return i.json(E)}catch(E){let y=E instanceof Error?E.message:"unknown error",k=/not found/.test(y)?404:(/cannot split|none of the supplied/.test(y),400);return i.json({error:y},k)}}),t.post("/api/links",async i=>{let l=await i.req.json().catch(()=>null);if(!l)return i.json({error:"body required"},400);let p=l.source_session_id,g=l.target_session_id,_=l.link_type;if(typeof p!="string"||p.length===0)return i.json({error:"source_session_id required"},400);if(typeof g!="string"||g.length===0)return i.json({error:"target_session_id required"},400);if(typeof _!="string")return i.json({error:"link_type required"},400);if(!o.has(_))return i.json({error:`invalid link_type: ${_}`},400);if(_!=="wiki_link")return i.json({error:`link_type '${_}' is not user-writable; only wiki_link is exposed via this endpoint. Other types must go through the suggestions-queue review flow.`},403);if(p===g)return i.json({error:"a session cannot link to itself"},400);let E=f();if(!E.prepare("SELECT 1 FROM sessions WHERE id = ?").get(p))return i.json({error:`source session not found: ${p}`},404);if(!E.prepare("SELECT 1 FROM sessions WHERE id = ?").get(g))return i.json({error:`target session not found: ${g}`},404);let N=Os({sourceSessionId:g,targetSessionId:p,linkType:"wiki_link"});if(N.length>0)return i.json({link:N[0]});try{let A=ga({source_session_id:p,target_session_id:g,link_type:"wiki_link",confidence:1,source:"manual",evidence:l.evidence??{created_via:"context_menu"},approved:!0});return i.json({link:A})}catch(A){return i.json({error:A.message},400)}}),t.delete("/api/links/:id",i=>{let l=i.req.param("id"),p=Number(l);if(!Number.isInteger(p)||p<=0)return i.json({error:"id must be a positive integer"},400);let g=_a(p);return g.removed===0?i.json({error:`link ${p} not found`},404):i.json(g)}),t.get("/api/sessions/:id/links",i=>{let l=i.req.param("id");if(!l)return i.json({error:"sessionId required"},400);let p=f();if(!p.prepare("SELECT 1 FROM sessions WHERE id = ?").get(l))return i.json({error:`session not found: ${l}`},404);let _=i.req.query("type")??"wiki_link";if(!o.has(_))return i.json({error:`invalid type: ${_}`},400);let E=_,y=Bt(l).filter(I=>I.link_type===E),k=fl(l,y);if(k.length===0)return i.json({links:[]});let N=k.map(I=>I.otherSessionId),A=N.map(()=>"?").join(","),L=p.prepare(`SELECT s.id,
1827
+ ORDER BY oi.extracted_at DESC`).all(...E),k=[];for(let P of y){let X=[];try{let Y=JSON.parse(P.bug_signatures);Array.isArray(Y)&&(X=Y)}catch{continue}for(let Y of X)k.push({sig:Y,session_id:P.session_id,project:P.project,auto_title:P.auto_title})}let N=new Map;for(let P of k){let X=P.sig.message_hash??`nohash:${(P.sig.snippet??"").slice(0,64)}`,Y=N.get(X);Y?Y.push(P):N.set(X,[P])}let A=Array.from(N.keys()).filter(P=>!P.startsWith("nohash:")),O=Eo(A),C=[],I=new Map,L=[];for(let[P,X]of N){let Y=X[0],Q=Y.sig.message_hash??null,V=Q?O.get(Q)??null:null,J=bo(V);if(!p&&J)continue;let re=Array.from(new Set(X.map(me=>me.project))).sort(),ze=Array.from(new Set(X.map(me=>me.session_id))),Me={id:Q??P,message_hash:Q,error_type:Y.sig.error_type??null,snippet:(Y.sig.snippet??"").slice(0,200),file:Y.sig.file??null,occurrence_count:X.length,projects:re,resolved:J,fix_summary:V?.fix_summary??null,member_session_ids:ze};C.push(Me);for(let me of X)I.has(me.session_id)||I.set(me.session_id,{session_id:me.session_id,project:me.project,auto_title:me.auto_title}),L.push({cluster_id:Me.id,session_id:me.session_id})}let F=[],W=4,K=new Map;function G(P){let X=K.get(P)??0;return X>=W?!1:(K.set(P,X+1),!0)}for(let P=0;P<C.length;P+=1)for(let X=P+1;X<C.length;X+=1){let Y=C[P],Q=C[X],V=null;Y.error_type&&Y.error_type!=="unknown"&&Y.error_type===Q.error_type?V="same_error_type":Y.file&&Q.file&&Y.file===Q.file&&(V="same_file"),V&&(!G(Y.id)||!G(Q.id)||F.push({a:Y.id,b:Q.id,reason:V}))}return i.json({clusters:C,sessions:Array.from(I.values()),member_edges:L,related_edges:F,totals:{cluster_count:C.length,singleton_count:C.filter(P=>P.occurrence_count===1).length,recurring_count:C.filter(P=>P.occurrence_count>1).length,session_count:I.size,resolved_count:C.filter(P=>P.resolved).length}})}),t.get("/api/bug-patterns/:clusterId",i=>{let l=i.req.param("clusterId");if(!l)return i.json({error:"clusterId required"},400);let p=Ji(l);return p?i.json(p):i.json({error:`cluster ${l} not found`},404)}),t.post("/api/bug-patterns/:clusterId/resolve",async i=>{let l=i.req.param("clusterId");if(!l)return i.json({error:"clusterId required"},400);let p=await i.req.json().catch(()=>null);if(!p)return i.json({error:"body required"},400);let g=p.resolved_in_session_id,_=p.fix_summary;if(typeof g!="string"||g.length===0)return i.json({error:"resolved_in_session_id required"},400);if(typeof _!="string"||_.trim().length===0)return i.json({error:"fix_summary required"},400);try{let E=Gi(l,g,_);return i.json(E)}catch(E){let y=E instanceof Error?E.message:"unknown error",k=/not found/.test(y)?404:(/not a member/.test(y),400);return i.json({error:y},k)}}),t.post("/api/bug-patterns/:clusterId/split",async i=>{let l=i.req.param("clusterId");if(!l)return i.json({error:"clusterId required"},400);let p=await i.req.json().catch(()=>null);if(!p)return i.json({error:"body required"},400);let g=p.member_session_ids;if(!Array.isArray(g)||g.length===0)return i.json({error:"member_session_ids must be a non-empty array of strings"},400);let _=[];for(let E of g){if(typeof E!="string"||E.length===0)return i.json({error:"member_session_ids must contain only non-empty strings"},400);_.push(E)}try{let E=Yi(l,_);return i.json(E)}catch(E){let y=E instanceof Error?E.message:"unknown error",k=/not found/.test(y)?404:(/cannot split|none of the supplied/.test(y),400);return i.json({error:y},k)}}),t.post("/api/links",async i=>{let l=await i.req.json().catch(()=>null);if(!l)return i.json({error:"body required"},400);let p=l.source_session_id,g=l.target_session_id,_=l.link_type;if(typeof p!="string"||p.length===0)return i.json({error:"source_session_id required"},400);if(typeof g!="string"||g.length===0)return i.json({error:"target_session_id required"},400);if(typeof _!="string")return i.json({error:"link_type required"},400);if(!o.has(_))return i.json({error:`invalid link_type: ${_}`},400);if(_!=="wiki_link")return i.json({error:`link_type '${_}' is not user-writable; only wiki_link is exposed via this endpoint. Other types must go through the suggestions-queue review flow.`},403);if(p===g)return i.json({error:"a session cannot link to itself"},400);let E=f();if(!E.prepare("SELECT 1 FROM sessions WHERE id = ?").get(p))return i.json({error:`source session not found: ${p}`},404);if(!E.prepare("SELECT 1 FROM sessions WHERE id = ?").get(g))return i.json({error:`target session not found: ${g}`},404);let N=Os({sourceSessionId:g,targetSessionId:p,linkType:"wiki_link"});if(N.length>0)return i.json({link:N[0]});try{let A=ga({source_session_id:p,target_session_id:g,link_type:"wiki_link",confidence:1,source:"manual",evidence:l.evidence??{created_via:"context_menu"},approved:!0});return i.json({link:A})}catch(A){return i.json({error:A.message},400)}}),t.delete("/api/links/:id",i=>{let l=i.req.param("id"),p=Number(l);if(!Number.isInteger(p)||p<=0)return i.json({error:"id must be a positive integer"},400);let g=_a(p);return g.removed===0?i.json({error:`link ${p} not found`},404):i.json(g)}),t.get("/api/sessions/:id/links",i=>{let l=i.req.param("id");if(!l)return i.json({error:"sessionId required"},400);let p=f();if(!p.prepare("SELECT 1 FROM sessions WHERE id = ?").get(l))return i.json({error:`session not found: ${l}`},404);let _=i.req.query("type")??"wiki_link";if(!o.has(_))return i.json({error:`invalid type: ${_}`},400);let E=_,y=Bt(l).filter(I=>I.link_type===E),k=fl(l,y);if(k.length===0)return i.json({links:[]});let N=k.map(I=>I.otherSessionId),A=N.map(()=>"?").join(","),O=p.prepare(`SELECT s.id,
1828
1828
  NULLIF(sa.alias, '') AS alias,
1829
1829
  s.auto_title,
1830
1830
  s.first_user_message,
@@ -1832,7 +1832,7 @@ ${o}
1832
1832
  FROM sessions s
1833
1833
  LEFT JOIN session_aliases sa ON sa.session_id = s.id
1834
1834
  LEFT JOIN projects p ON p.id = s.project_id
1835
- WHERE s.id IN (${A})`).all(...N),C=new Map(L.map(I=>[I.id,I]));return i.json({links:k.map(I=>{let O=C.get(I.otherSessionId),F=O?.alias?.trim()||O?.auto_title?.trim()||(O?.first_user_message?O.first_user_message.slice(0,80):"")||I.otherSessionId.slice(0,8);return{linkId:I.linkId,otherSessionId:I.otherSessionId,direction:I.direction,updatedAt:I.updatedAt,title:F,project:O?.project??null}})})}),t.patch("/api/links/suggestions/:id",async i=>{let l=i.req.param("id"),p=Number(l);if(!Number.isInteger(p)||p<=0)return i.json({error:"id must be a positive integer"},400);let g=await i.req.json().catch(()=>null);if(!g||typeof g.status!="string")return i.json({error:"status required (approved|rejected)"},400);if(g.status!=="approved"&&g.status!=="rejected")return i.json({error:`invalid status: ${g.status}`},400);try{let _=ur(p,g.status);return i.json(_)}catch(_){let E=_.message,y=/already decided/.test(E)?409:/not found/.test(E)?404:400;return i.json({error:E},y)}}),t.post("/api/links/suggestions/bulk-decide",async i=>{let l=await i.req.json().catch(()=>null);if(!l)return i.json({error:"body required"},400);let p=Array.isArray(l.ids)?l.ids:null;if(!p||p.length===0)return i.json({error:"ids must be a non-empty array"},400);if(p.length>1e3)return i.json({error:"bulk-decide capped at 1000 ids per call"},400);if(l.status!=="approved"&&l.status!=="rejected")return i.json({error:`invalid status: ${l.status}`},400);let g=l.status,_=0,E=0,y=[];for(let k of p){let N=Number(k);if(!Number.isInteger(N)||N<=0){y.push({id:Number.isFinite(Number(k))?Number(k):-1,error:"invalid id"});continue}try{ur(N,g),_+=1}catch(A){let L=A.message;/already decided/.test(L)?E+=1:y.push({id:N,error:L})}}return i.json({decided:_,skipped:E,errors:y})}),t.get("/api/sessions",i=>{let l=f(),p=i.req.query("project"),g=i.req.query("since"),_=i.req.query("until"),E=i.req.queries("tag")??[],y=i.req.query("collection"),k=Math.max(1,Math.min(500,Number(i.req.query("limit")??100))),N=i.req.query("cursor"),A=null;if(N)try{let K=Buffer.from(N,"base64url").toString("utf8"),G=JSON.parse(K);typeof G.ts=="string"&&typeof G.id=="string"&&(A={ts:G.ts,id:G.id})}catch{}let L=i.req.query("system")==="1"||i.req.query("system")==="true",C={limit:k},I="s.message_count > 2"+Jn("s",L);if(p&&(I+=" AND (p.name LIKE @proj ESCAPE '\\' OR p.decoded_path LIKE @proj ESCAPE '\\')",C.proj=`%${Ts(p)}%`),g&&(I+=" AND s.started_at >= @since",C.since=g),_&&(I+=" AND s.started_at <= @until",/^\d{4}-\d{2}-\d{2}$/.test(_)?C.until=`${_}T23:59:59.999Z`:C.until=_),E.length>0&&E.map(G=>Qe(G)).filter(Boolean).forEach((G,P)=>{I+=` AND s.id IN (SELECT session_id FROM session_tags WHERE tag = @tag_${P})`,C[`tag_${P}`]=G}),y){let K=Wr(y);if(K.length===0)return i.json({items:[],nextCursor:null});let G=K.map((P,X)=>`@col_${X}`).join(",");I+=` AND s.id IN (SELECT session_id FROM collection_sessions WHERE collection_id IN (${G}))`,K.forEach((P,X)=>{C[`col_${X}`]=P})}A&&(I+=" AND (COALESCE(s.ended_at, s.started_at, '') < @cursor_ts OR (COALESCE(s.ended_at, s.started_at, '') = @cursor_ts AND s.id < @cursor_id))",C.cursor_ts=A.ts,C.cursor_id=A.id);let O=l.prepare(`SELECT s.id, p.name AS project, s.started_at, s.ended_at,
1835
+ WHERE s.id IN (${A})`).all(...N),C=new Map(O.map(I=>[I.id,I]));return i.json({links:k.map(I=>{let L=C.get(I.otherSessionId),F=L?.alias?.trim()||L?.auto_title?.trim()||(L?.first_user_message?L.first_user_message.slice(0,80):"")||I.otherSessionId.slice(0,8);return{linkId:I.linkId,otherSessionId:I.otherSessionId,direction:I.direction,updatedAt:I.updatedAt,title:F,project:L?.project??null}})})}),t.patch("/api/links/suggestions/:id",async i=>{let l=i.req.param("id"),p=Number(l);if(!Number.isInteger(p)||p<=0)return i.json({error:"id must be a positive integer"},400);let g=await i.req.json().catch(()=>null);if(!g||typeof g.status!="string")return i.json({error:"status required (approved|rejected)"},400);if(g.status!=="approved"&&g.status!=="rejected")return i.json({error:`invalid status: ${g.status}`},400);try{let _=ur(p,g.status);return i.json(_)}catch(_){let E=_.message,y=/already decided/.test(E)?409:/not found/.test(E)?404:400;return i.json({error:E},y)}}),t.post("/api/links/suggestions/bulk-decide",async i=>{let l=await i.req.json().catch(()=>null);if(!l)return i.json({error:"body required"},400);let p=Array.isArray(l.ids)?l.ids:null;if(!p||p.length===0)return i.json({error:"ids must be a non-empty array"},400);if(p.length>1e3)return i.json({error:"bulk-decide capped at 1000 ids per call"},400);if(l.status!=="approved"&&l.status!=="rejected")return i.json({error:`invalid status: ${l.status}`},400);let g=l.status,_=0,E=0,y=[];for(let k of p){let N=Number(k);if(!Number.isInteger(N)||N<=0){y.push({id:Number.isFinite(Number(k))?Number(k):-1,error:"invalid id"});continue}try{ur(N,g),_+=1}catch(A){let O=A.message;/already decided/.test(O)?E+=1:y.push({id:N,error:O})}}return i.json({decided:_,skipped:E,errors:y})}),t.get("/api/sessions",i=>{let l=f(),p=i.req.query("project"),g=i.req.query("since"),_=i.req.query("until"),E=i.req.queries("tag")??[],y=i.req.query("collection"),k=Math.max(1,Math.min(500,Number(i.req.query("limit")??100))),N=i.req.query("cursor"),A=null;if(N)try{let K=Buffer.from(N,"base64url").toString("utf8"),G=JSON.parse(K);typeof G.ts=="string"&&typeof G.id=="string"&&(A={ts:G.ts,id:G.id})}catch{}let O=i.req.query("system")==="1"||i.req.query("system")==="true",C={limit:k},I="s.message_count > 2"+Jn("s",O);if(p&&(I+=" AND (p.name LIKE @proj ESCAPE '\\' OR p.decoded_path LIKE @proj ESCAPE '\\')",C.proj=`%${Ts(p)}%`),g&&(I+=" AND s.started_at >= @since",C.since=g),_&&(I+=" AND s.started_at <= @until",/^\d{4}-\d{2}-\d{2}$/.test(_)?C.until=`${_}T23:59:59.999Z`:C.until=_),E.length>0&&E.map(G=>Qe(G)).filter(Boolean).forEach((G,P)=>{I+=` AND s.id IN (SELECT session_id FROM session_tags WHERE tag = @tag_${P})`,C[`tag_${P}`]=G}),y){let K=Wr(y);if(K.length===0)return i.json({items:[],nextCursor:null});let G=K.map((P,X)=>`@col_${X}`).join(",");I+=` AND s.id IN (SELECT session_id FROM collection_sessions WHERE collection_id IN (${G}))`,K.forEach((P,X)=>{C[`col_${X}`]=P})}A&&(I+=" AND (COALESCE(s.ended_at, s.started_at, '') < @cursor_ts OR (COALESCE(s.ended_at, s.started_at, '') = @cursor_ts AND s.id < @cursor_id))",C.cursor_ts=A.ts,C.cursor_id=A.id);let L=l.prepare(`SELECT s.id, p.name AS project, s.started_at, s.ended_at,
1836
1836
  s.message_count, s.first_user_message, s.git_branch,
1837
1837
  s.auto_title, s.auto_title_source, s.verification_status,
1838
1838
  COALESCE(s.ended_at, s.started_at, '') AS _cursor_ts,
@@ -1853,7 +1853,7 @@ ${o}
1853
1853
  LEFT JOIN session_notes sn ON sn.session_id = s.id
1854
1854
  WHERE ${I}
1855
1855
  ORDER BY COALESCE(s.ended_at, s.started_at, '') DESC, s.id DESC
1856
- LIMIT @limit`).all(C),F=O.map(({tags_csv:K,_cursor_ts:G,...P})=>{let X=P.id,Y=v.getOrigin(X),Q=P.alias,V=Q==null?null:v.isSessionAutoLinked(X)?"auto":"manual",J=ts({auto_title:P.auto_title,auto_title_source:P.auto_title_source??null,has_alias:Q!=null&&V==="manual"});return{...P,tags:K?K.split(","):[],origin:Y?{editor:Y.editor,label:Y.label}:null,alias_source:V,title_quality:J}}),W=null;if(O.length===k&&O.length>0){let K=O[O.length-1],G=JSON.stringify({ts:K._cursor_ts??"",id:K.id});W=Buffer.from(G,"utf8").toString("base64url")}return i.json({items:F,nextCursor:W})}),t.get("/api/sessions/:id",i=>{let l=f(),p=i.req.param("id"),g=l.prepare(`SELECT s.*, p.name AS project_name, p.decoded_path,
1856
+ LIMIT @limit`).all(C),F=L.map(({tags_csv:K,_cursor_ts:G,...P})=>{let X=P.id,Y=v.getOrigin(X),Q=P.alias,V=Q==null?null:v.isSessionAutoLinked(X)?"auto":"manual",J=ts({auto_title:P.auto_title,auto_title_source:P.auto_title_source??null,has_alias:Q!=null&&V==="manual"});return{...P,tags:K?K.split(","):[],origin:Y?{editor:Y.editor,label:Y.label}:null,alias_source:V,title_quality:J}}),W=null;if(L.length===k&&L.length>0){let K=L[L.length-1],G=JSON.stringify({ts:K._cursor_ts??"",id:K.id});W=Buffer.from(G,"utf8").toString("base64url")}return i.json({items:F,nextCursor:W})}),t.get("/api/sessions/:id",i=>{let l=f(),p=i.req.param("id"),g=l.prepare(`SELECT s.*, p.name AS project_name, p.decoded_path,
1857
1857
  NULLIF(sa.alias, '') AS alias
1858
1858
  FROM sessions s
1859
1859
  JOIN projects p ON p.id = s.project_id
@@ -1873,7 +1873,7 @@ ${o}
1873
1873
  LEFT JOIN session_aliases sa ON sa.session_id = s.id
1874
1874
  WHERE s.message_count > 2
1875
1875
  ORDER BY COALESCE(s.ended_at, s.started_at, '') DESC
1876
- LIMIT 1`).get();return i.json({state:Fn(),mostRecentSession:p??null})}),t.put("/api/onboarding",async i=>{let l=await i.req.json().catch(()=>({})),p=Dn.partial().safeParse(l);return p.success?i.json(Fd(p.data)):i.json({error:"invalid onboarding state",issues:p.error.issues},400)}),t.post("/api/onboarding/reset",i=>i.json(Pd())),t.get("/api/config/mcp-install",i=>i.json({...qe(),claudeCliAvailable:pe()})),t.post("/api/config/mcp-install",i=>i.json({...Ld(),claudeCliAvailable:pe()})),t.delete("/api/config/mcp-install",i=>i.json({...Cd(),claudeCliAvailable:pe()}));let T=M.object({scope:M.object({untaggedOnly:M.boolean().optional(),project:M.string().optional(),collectionId:M.string().optional(),sessionIds:M.array(M.string()).optional(),limit:M.number().int().min(1).max(500).optional()}).default({}),model:M.string().regex(/^[a-zA-Z0-9._-]{1,100}$/).optional(),scanId:M.string().min(1).max(100).optional()});t.post("/api/tags/scan/claude-cli",async i=>{if(ii)return i.json({error:"a scan is already running"},409);if(!pe())return i.json({error:"claude CLI not found on PATH. Install Claude Code locally, then reload."},400);if(!qe().installed)return i.json({error:"Recall MCP is not installed in Claude Code yet, run the one-click install first."},400);let p=await i.req.json().catch(()=>({})),g=T.safeParse(p);if(!g.success)return i.json({error:"invalid scope",issues:g.error.issues},400);let _=g.data.scope,E=Fe(),y=g.data.model??E.model,k=f(),N=()=>k.prepare("SELECT COUNT(*) AS n FROM session_tags").get().n,A=N();ii=!0;let L;try{let C=g.data.scanId;L=await br(_,{model:y,scanId:C});let I=N(),O=Math.max(0,I-A);return C&&Is(C,{type:"done",result:{success:L.success,exitCode:L.exitCode,tagsAdded:O}}),i.json({success:L.success,exitCode:L.exitCode,tagsAdded:O,model:y,stdout:Se(L.stdout.slice(0,2e3)).redacted,stderrTail:Se(L.stderr.slice(-2e3)).redacted})}finally{ii=!1}}),t.get("/api/claude-cli/scan/:scanId/progress",i=>{let l=i.req.param("scanId");return Ye(i,async p=>{let g=[],_={resolve:()=>{}},E=new Promise(A=>{_.resolve=A}),y=Na(l,A=>{g.push(A);let L=_.resolve;E=new Promise(C=>{_.resolve=C}),L()}),k=!1,N=setInterval(()=>{k||p.writeSSE({event:"heartbeat",data:""}).catch(()=>{k=!0})},15e3);try{for(;!k;){g.length===0&&await E;let A=g.shift();if(A&&(await p.writeSSE({event:A.type,data:JSON.stringify(A)}),A.type==="done"))break}}finally{k=!0,clearInterval(N),y()}})}),t.get("/api/prompts",i=>i.json({prompts:fr.map(l=>({name:l.name,title:l.title,description:l.description})),claudeCliAvailable:pe()})),t.post("/api/prompts/run",async i=>{if(!pe())return i.json({error:"claude CLI not found on PATH. Install Claude Code locally, then reload."},400);if(!qe().installed)return i.json({error:"Recall MCP is not installed in Claude Code yet, run the one-click install first."},400);let p=await i.req.json().catch(()=>({})),_=M.object({name:M.string(),args:M.record(M.string(),M.unknown()).optional(),model:M.string().regex(/^[a-zA-Z0-9._-]{1,100}$/).optional()}).safeParse(p);if(!_.success)return i.json({error:"invalid request",issues:_.error.issues},400);let E=xa(_.data.name);if(!E)return i.json({error:`unknown prompt: ${_.data.name}`},404);let y=E.build(_.data.args??{}),k=Fe(),N=_.data.model??k.model,A=await et(y,E.allowedTools,{model:N});return i.json({success:A.success,exitCode:A.exitCode,promptName:E.name,model:N,stdout:A.stdout,stderrTail:A.stderr.slice(-4e3)})}),t.get("/api/autopilot/status",i=>i.json(fs())),t.get("/api/autopilot/events",i=>Ye(i,async l=>{await l.writeSSE({event:"state",data:JSON.stringify(fs())});let p=[],g=()=>{},_=new Promise(y=>g=y),E=xd(y=>{p.push(y);let k=g;_=new Promise(N=>g=N),k()});try{for(;;){if(p.length===0){let k=new Promise(A=>setTimeout(()=>A("tick"),3e4));if(await Promise.race([_.then(()=>"event"),k])==="tick"){await l.writeSSE({event:"heartbeat",data:"1"});continue}}let y=p.shift();y&&await l.writeSSE({event:"state",data:JSON.stringify(y)})}}finally{E()}})),t.post("/api/autopilot/kick",i=>(Mn(),i.json({ok:!0,snapshot:fs()})));let S=M.object({scope:M.object({untaggedOnly:M.boolean().optional(),project:M.string().optional(),collectionId:M.string().optional(),sessionIds:M.array(M.string()).optional(),limit:M.number().int().min(1).max(500).optional()}).default({})});t.post("/api/tags/scan",async i=>{let l=Fe();if(!l.enabled)return i.json({error:"auto-tagging is disabled"},403);if(l.backend!=="api")return i.json({error:"api-backend scan requires backend=api in config"},400);if(!l.apiKey)return i.json({error:"no api key configured"},400);let p=await i.req.json().catch(()=>({})),g=S.safeParse(p);if(!g.success)return i.json({error:"invalid scope",issues:g.error.issues},400);let _=mt(g.data.scope);if(_.length===0)return i.json({error:"no sessions match scope"},400);let E=Sd(_.length);return kd(E,{apiKey:l.apiKey,model:l.model,minTags:l.minTagsPerSession,maxTags:l.maxTagsPerSession,sessions:_}),i.json({scanId:E.id,total:E.total})}),t.get("/api/tags/scan/:id",i=>{let l=Ln(i.req.param("id"));if(!l)return i.json({error:"scan not found"},404);let{controller:p,listeners:g,..._}=l;return i.json(_)}),t.get("/api/tags/scan/:id/events",i=>{let l=Ln(i.req.param("id"));return l?Ye(i,async p=>{await p.writeSSE({event:"state",data:JSON.stringify({completed:l.completed,total:l.total,status:l.status})});for(let k of l.results)await p.writeSSE({event:"result",data:JSON.stringify(k)});let g=[],_={resolve:()=>{}},E=new Promise(k=>{_.resolve=k}),y=Td(l,k=>{g.push(k);let N=_.resolve;E=new Promise(A=>{_.resolve=A}),N()});try{for(;l.status==="running"||l.status==="pending";){g.length===0&&await E;let k=g.shift();if(k&&(await p.writeSSE({event:k.type,data:JSON.stringify(k)}),k.type==="done"||k.type==="status"&&(k.status==="cancelled"||k.status==="failed")))break}}finally{y()}}):i.json({error:"scan not found"},404)});let w=M.object({selection:M.array(M.object({sessionId:M.string(),tags:M.array(M.string()).min(1)}))});t.post("/api/tags/scan/:id/apply",async i=>{let l=Ln(i.req.param("id"));if(!l)return i.json({error:"scan not found"},404);let p=await i.req.json().catch(()=>({})),g=w.safeParse(p);if(!g.success)return i.json({error:"invalid selection"},400);let _=Ad(l,g.data.selection);return i.json(_)}),t.delete("/api/tags/scan/:id",i=>{let l=i.req.param("id");return yd(l),wd(l),i.json({ok:!0})}),t.put("/api/sessions/:id/alias",async i=>{let l=i.req.param("id"),p=await i.req.json().catch(()=>null);if(!p||typeof p.alias!="string")return i.json({error:"alias required"},400);try{let g=he(l,p.alias);if(p.pin===!0)v.unlinkSession(l);else{let _=f().prepare("SELECT cwd, started_at FROM sessions WHERE id = ?").get(l),E=_?.cwd?_.cwd.replace(/\/+$/,""):null,y=!1;if(E&&_?.started_at){let k=Date.parse(_.started_at),N=_.started_at,A=v.all().filter(L=>L.cwd&&L.cwd.replace(/\/+$/,"")===E&&Kt({sessionStartedAt:N,terminalOpenedAt:L.opened_at??null}).allowed);if(Number.isFinite(k)&&A.length>0){let C=A.map(I=>({t:I,gap:k-Date.parse(I.opened_at??"")})).filter(I=>Number.isFinite(I.gap)).sort((I,O)=>I.gap-O.gap)[0];C&&(v.linkSession(l,C.t.shell_pid),y=!0)}}y||v.unlinkSession(l)}return i.json(g)}catch(g){return i.json({error:g.message},400)}}),t.delete("/api/sessions/:id/alias",i=>{let l=i.req.param("id");return Xs(l),v.unlinkSession(l),i.json({ok:!0})}),t.get("/api/sessions/:id/alias",i=>{let l=i.req.param("id");return i.json({alias:Te(l)})}),t.get("/api/config/auto-title",i=>i.json(it())),t.put("/api/config/auto-title",async i=>{let l=await i.req.json().catch(()=>({})),p=Rn.partial().safeParse(l);return p.success?i.json(_u(p.data)):i.json({error:"invalid config",issues:p.error.issues},400)}),t.get("/api/sessions/:id/auto-title",i=>{let l=i.req.param("id"),p=Le(l);return p?i.json(p):i.json({error:"session not found"},404)}),t.post("/api/sessions/:id/auto-title",async i=>{let l=i.req.param("id");if(!it().agentEnabled)return i.json({error:"autoTitle.agentEnabled is false"},403);if(!f().prepare("SELECT 1 FROM sessions WHERE id = ?").get(l))return i.json({error:"session not found"},404);try{let E=await Dl(l);return Ee(l,E,"agent"),i.json(Le(l))}catch(E){return i.json({error:E.message,code:"agent-title-failed"},500)}}),t.post("/api/sessions/:id/auto-title/revert",i=>{let l=i.req.param("id"),p=Le(l);if(!p)return i.json({error:"session not found"},404);let g=p.auto_title_history;if(!g||g.length===0)return i.json({error:"no prior title to revert to",code:"no-history"},422);let _=g[g.length-1];return Ee(l,_.title,"agent"),i.json(Le(l))}),t.post("/api/sessions/:id/regenerate-title",async i=>{let l=i.req.param("id"),p=await i.req.json().catch(()=>({})),g=p.model??kn;try{let _=await ho(l,{model:g,force:p.force===!0,budget:typeof p.budget=="number"?p.budget:void 0,signal:i.req.raw.signal}),E=Le(l),y=E?.auto_title_history&&E.auto_title_history.length>0?E.auto_title_history[E.auto_title_history.length-1].title:null;return i.json({..._,previous_title:y})}catch(_){if(_ instanceof Rt)return i.json({error:_.message,code:"no-context-available",session_id:_.sessionId},422);let E=_ instanceof Error?_.message:"unknown error",y=/not found|unknown/i.test(E)?404:500;return i.json({error:E,code:"regenerate-failed"},y)}}),t.post("/api/sessions/regenerate-titles-batch",async i=>{let l=await i.req.json().catch(()=>null);if(!l)return i.json({error:"body required"},400);let p=l.project;if(typeof p!="string"||p.length===0)return i.json({error:"project (string) required"},400);let g=l.quality_filter;if(!Array.isArray(g)||g.length===0)return i.json({error:"quality_filter (non-empty array) required"},400);let _=new Set(["low_signal","recursive_meta","programmatic"]),E=[];for(let O of g){if(typeof O!="string")return i.json({error:`invalid quality_filter entry: ${O}`},400);if(!_.has(O))return i.json({error:`quality_filter must be a subset of ${[..._].join(",")}; got ${O}`},400);E.push(O)}let y=typeof l.model=="string"&&l.model.length>0?l.model:kn,k=typeof l.limit=="number"&&l.limit>0?Math.min(2e3,Math.floor(l.limit)):500,N=typeof l.budget=="number"&&l.budget>=100?Math.floor(l.budget):void 0,L=f().prepare(`SELECT s.id,
1876
+ LIMIT 1`).get();return i.json({state:Fn(),mostRecentSession:p??null})}),t.put("/api/onboarding",async i=>{let l=await i.req.json().catch(()=>({})),p=Dn.partial().safeParse(l);return p.success?i.json(Fd(p.data)):i.json({error:"invalid onboarding state",issues:p.error.issues},400)}),t.post("/api/onboarding/reset",i=>i.json(Pd())),t.get("/api/config/mcp-install",i=>i.json({...qe(),claudeCliAvailable:pe()})),t.post("/api/config/mcp-install",i=>i.json({...Ld(),claudeCliAvailable:pe()})),t.delete("/api/config/mcp-install",i=>i.json({...Cd(),claudeCliAvailable:pe()}));let T=M.object({scope:M.object({untaggedOnly:M.boolean().optional(),project:M.string().optional(),collectionId:M.string().optional(),sessionIds:M.array(M.string()).optional(),limit:M.number().int().min(1).max(500).optional()}).default({}),model:M.string().regex(/^[a-zA-Z0-9._-]{1,100}$/).optional(),scanId:M.string().min(1).max(100).optional()});t.post("/api/tags/scan/claude-cli",async i=>{if(ii)return i.json({error:"a scan is already running"},409);if(!pe())return i.json({error:"claude CLI not found on PATH. Install Claude Code locally, then reload."},400);if(!qe().installed)return i.json({error:"Recall MCP is not installed in Claude Code yet, run the one-click install first."},400);let p=await i.req.json().catch(()=>({})),g=T.safeParse(p);if(!g.success)return i.json({error:"invalid scope",issues:g.error.issues},400);let _=g.data.scope,E=Fe(),y=g.data.model??E.model,k=f(),N=()=>k.prepare("SELECT COUNT(*) AS n FROM session_tags").get().n,A=N();ii=!0;let O;try{let C=g.data.scanId;O=await br(_,{model:y,scanId:C});let I=N(),L=Math.max(0,I-A);return C&&Is(C,{type:"done",result:{success:O.success,exitCode:O.exitCode,tagsAdded:L}}),i.json({success:O.success,exitCode:O.exitCode,tagsAdded:L,model:y,stdout:Se(O.stdout.slice(0,2e3)).redacted,stderrTail:Se(O.stderr.slice(-2e3)).redacted})}finally{ii=!1}}),t.get("/api/claude-cli/scan/:scanId/progress",i=>{let l=i.req.param("scanId");return Ye(i,async p=>{let g=[],_={resolve:()=>{}},E=new Promise(A=>{_.resolve=A}),y=Na(l,A=>{g.push(A);let O=_.resolve;E=new Promise(C=>{_.resolve=C}),O()}),k=!1,N=setInterval(()=>{k||p.writeSSE({event:"heartbeat",data:""}).catch(()=>{k=!0})},15e3);try{for(;!k;){g.length===0&&await E;let A=g.shift();if(A&&(await p.writeSSE({event:A.type,data:JSON.stringify(A)}),A.type==="done"))break}}finally{k=!0,clearInterval(N),y()}})}),t.get("/api/prompts",i=>i.json({prompts:fr.map(l=>({name:l.name,title:l.title,description:l.description})),claudeCliAvailable:pe()})),t.post("/api/prompts/run",async i=>{if(!pe())return i.json({error:"claude CLI not found on PATH. Install Claude Code locally, then reload."},400);if(!qe().installed)return i.json({error:"Recall MCP is not installed in Claude Code yet, run the one-click install first."},400);let p=await i.req.json().catch(()=>({})),_=M.object({name:M.string(),args:M.record(M.string(),M.unknown()).optional(),model:M.string().regex(/^[a-zA-Z0-9._-]{1,100}$/).optional()}).safeParse(p);if(!_.success)return i.json({error:"invalid request",issues:_.error.issues},400);let E=xa(_.data.name);if(!E)return i.json({error:`unknown prompt: ${_.data.name}`},404);let y=E.build(_.data.args??{}),k=Fe(),N=_.data.model??k.model,A=await et(y,E.allowedTools,{model:N});return i.json({success:A.success,exitCode:A.exitCode,promptName:E.name,model:N,stdout:A.stdout,stderrTail:A.stderr.slice(-4e3)})}),t.get("/api/autopilot/status",i=>i.json(fs())),t.get("/api/autopilot/events",i=>Ye(i,async l=>{await l.writeSSE({event:"state",data:JSON.stringify(fs())});let p=[],g=()=>{},_=new Promise(y=>g=y),E=xd(y=>{p.push(y);let k=g;_=new Promise(N=>g=N),k()});try{for(;;){if(p.length===0){let k=new Promise(A=>setTimeout(()=>A("tick"),3e4));if(await Promise.race([_.then(()=>"event"),k])==="tick"){await l.writeSSE({event:"heartbeat",data:"1"});continue}}let y=p.shift();y&&await l.writeSSE({event:"state",data:JSON.stringify(y)})}}finally{E()}})),t.post("/api/autopilot/kick",i=>(Mn(),i.json({ok:!0,snapshot:fs()})));let S=M.object({scope:M.object({untaggedOnly:M.boolean().optional(),project:M.string().optional(),collectionId:M.string().optional(),sessionIds:M.array(M.string()).optional(),limit:M.number().int().min(1).max(500).optional()}).default({})});t.post("/api/tags/scan",async i=>{let l=Fe();if(!l.enabled)return i.json({error:"auto-tagging is disabled"},403);if(l.backend!=="api")return i.json({error:"api-backend scan requires backend=api in config"},400);if(!l.apiKey)return i.json({error:"no api key configured"},400);let p=await i.req.json().catch(()=>({})),g=S.safeParse(p);if(!g.success)return i.json({error:"invalid scope",issues:g.error.issues},400);let _=mt(g.data.scope);if(_.length===0)return i.json({error:"no sessions match scope"},400);let E=Sd(_.length);return kd(E,{apiKey:l.apiKey,model:l.model,minTags:l.minTagsPerSession,maxTags:l.maxTagsPerSession,sessions:_}),i.json({scanId:E.id,total:E.total})}),t.get("/api/tags/scan/:id",i=>{let l=Ln(i.req.param("id"));if(!l)return i.json({error:"scan not found"},404);let{controller:p,listeners:g,..._}=l;return i.json(_)}),t.get("/api/tags/scan/:id/events",i=>{let l=Ln(i.req.param("id"));return l?Ye(i,async p=>{await p.writeSSE({event:"state",data:JSON.stringify({completed:l.completed,total:l.total,status:l.status})});for(let k of l.results)await p.writeSSE({event:"result",data:JSON.stringify(k)});let g=[],_={resolve:()=>{}},E=new Promise(k=>{_.resolve=k}),y=Td(l,k=>{g.push(k);let N=_.resolve;E=new Promise(A=>{_.resolve=A}),N()});try{for(;l.status==="running"||l.status==="pending";){g.length===0&&await E;let k=g.shift();if(k&&(await p.writeSSE({event:k.type,data:JSON.stringify(k)}),k.type==="done"||k.type==="status"&&(k.status==="cancelled"||k.status==="failed")))break}}finally{y()}}):i.json({error:"scan not found"},404)});let w=M.object({selection:M.array(M.object({sessionId:M.string(),tags:M.array(M.string()).min(1)}))});t.post("/api/tags/scan/:id/apply",async i=>{let l=Ln(i.req.param("id"));if(!l)return i.json({error:"scan not found"},404);let p=await i.req.json().catch(()=>({})),g=w.safeParse(p);if(!g.success)return i.json({error:"invalid selection"},400);let _=Ad(l,g.data.selection);return i.json(_)}),t.delete("/api/tags/scan/:id",i=>{let l=i.req.param("id");return yd(l),wd(l),i.json({ok:!0})}),t.put("/api/sessions/:id/alias",async i=>{let l=i.req.param("id"),p=await i.req.json().catch(()=>null);if(!p||typeof p.alias!="string")return i.json({error:"alias required"},400);try{let g=he(l,p.alias);if(p.pin===!0)v.unlinkSession(l);else{let _=f().prepare("SELECT cwd, started_at FROM sessions WHERE id = ?").get(l),E=_?.cwd?_.cwd.replace(/\/+$/,""):null,y=!1;if(E&&_?.started_at){let k=Date.parse(_.started_at),N=_.started_at,A=v.all().filter(O=>O.cwd&&O.cwd.replace(/\/+$/,"")===E&&Kt({sessionStartedAt:N,terminalOpenedAt:O.opened_at??null}).allowed);if(Number.isFinite(k)&&A.length>0){let C=A.map(I=>({t:I,gap:k-Date.parse(I.opened_at??"")})).filter(I=>Number.isFinite(I.gap)).sort((I,L)=>I.gap-L.gap)[0];C&&(v.linkSession(l,C.t.shell_pid),y=!0)}}y||v.unlinkSession(l)}return i.json(g)}catch(g){return i.json({error:g.message},400)}}),t.delete("/api/sessions/:id/alias",i=>{let l=i.req.param("id");return Xs(l),v.unlinkSession(l),i.json({ok:!0})}),t.get("/api/sessions/:id/alias",i=>{let l=i.req.param("id");return i.json({alias:Te(l)})}),t.get("/api/config/auto-title",i=>i.json(it())),t.put("/api/config/auto-title",async i=>{let l=await i.req.json().catch(()=>({})),p=Rn.partial().safeParse(l);return p.success?i.json(_u(p.data)):i.json({error:"invalid config",issues:p.error.issues},400)}),t.get("/api/sessions/:id/auto-title",i=>{let l=i.req.param("id"),p=Le(l);return p?i.json(p):i.json({error:"session not found"},404)}),t.post("/api/sessions/:id/auto-title",async i=>{let l=i.req.param("id");if(!it().agentEnabled)return i.json({error:"autoTitle.agentEnabled is false"},403);if(!f().prepare("SELECT 1 FROM sessions WHERE id = ?").get(l))return i.json({error:"session not found"},404);try{let E=await Dl(l);return Ee(l,E,"agent"),i.json(Le(l))}catch(E){return i.json({error:E.message,code:"agent-title-failed"},500)}}),t.post("/api/sessions/:id/auto-title/revert",i=>{let l=i.req.param("id"),p=Le(l);if(!p)return i.json({error:"session not found"},404);let g=p.auto_title_history;if(!g||g.length===0)return i.json({error:"no prior title to revert to",code:"no-history"},422);let _=g[g.length-1];return Ee(l,_.title,"agent"),i.json(Le(l))}),t.post("/api/sessions/:id/regenerate-title",async i=>{let l=i.req.param("id"),p=await i.req.json().catch(()=>({})),g=p.model??kn;try{let _=await ho(l,{model:g,force:p.force===!0,budget:typeof p.budget=="number"?p.budget:void 0,signal:i.req.raw.signal}),E=Le(l),y=E?.auto_title_history&&E.auto_title_history.length>0?E.auto_title_history[E.auto_title_history.length-1].title:null;return i.json({..._,previous_title:y})}catch(_){if(_ instanceof Rt)return i.json({error:_.message,code:"no-context-available",session_id:_.sessionId},422);let E=_ instanceof Error?_.message:"unknown error",y=/not found|unknown/i.test(E)?404:500;return i.json({error:E,code:"regenerate-failed"},y)}}),t.post("/api/sessions/regenerate-titles-batch",async i=>{let l=await i.req.json().catch(()=>null);if(!l)return i.json({error:"body required"},400);let p=l.project;if(typeof p!="string"||p.length===0)return i.json({error:"project (string) required"},400);let g=l.quality_filter;if(!Array.isArray(g)||g.length===0)return i.json({error:"quality_filter (non-empty array) required"},400);let _=new Set(["low_signal","recursive_meta","programmatic"]),E=[];for(let L of g){if(typeof L!="string")return i.json({error:`invalid quality_filter entry: ${L}`},400);if(!_.has(L))return i.json({error:`quality_filter must be a subset of ${[..._].join(",")}; got ${L}`},400);E.push(L)}let y=typeof l.model=="string"&&l.model.length>0?l.model:kn,k=typeof l.limit=="number"&&l.limit>0?Math.min(2e3,Math.floor(l.limit)):500,N=typeof l.budget=="number"&&l.budget>=100?Math.floor(l.budget):void 0,O=f().prepare(`SELECT s.id,
1877
1877
  s.auto_title,
1878
1878
  s.auto_title_source,
1879
1879
  NULLIF(sa.alias, '') AS alias
@@ -1882,14 +1882,14 @@ ${o}
1882
1882
  LEFT JOIN session_aliases sa ON sa.session_id = s.id
1883
1883
  WHERE p.name LIKE @proj ESCAPE '\\' OR p.decoded_path LIKE @proj ESCAPE '\\'
1884
1884
  ORDER BY COALESCE(s.ended_at, s.started_at, '') DESC
1885
- LIMIT @limit`).all({proj:`%${Ts(p)}%`,limit:k}),C=new Set(E),I=L.filter(O=>{let F=O.alias==null?null:v.isSessionAutoLinked(O.id)?"auto":"manual",W=ts({auto_title:O.auto_title,auto_title_source:O.auto_title_source??null,has_alias:O.alias!=null&&F==="manual"});return C.has(W)});return Ye(i,async O=>{let F=I.length,W=[],K=[],G=[],P=0,X=async(Q,V)=>{P+=1;try{await O.writeSSE({id:String(P),event:Q,data:JSON.stringify(V)})}catch{}};await X("start",{total:F,model:y});let Y=0;for(let Q of I){if(i.req.raw.signal.aborted)break;Y+=1;try{let V=await ho(Q.id,{model:y,budget:N,signal:i.req.raw.signal});V.written?(W.push(Q.id),await X("progress",{sessionId:Q.id,title:V.title,evidence:V.evidence,confidence:V.confidence,current:Y,total:F})):(K.push({sessionId:Q.id,reason:V.skipped??"unknown"}),await X("skipped",{sessionId:Q.id,reason:V.skipped??"unknown",current:Y,total:F}))}catch(V){let J=V instanceof Error?V.message:String(V),re=V instanceof Rt?"no-context-available":"failed";G.push({sessionId:Q.id,error:J}),await X("error",{sessionId:Q.id,error:J,code:re,current:Y,total:F})}}await X("done",{generated:W,skipped:K,failed:G,cancelled:i.req.raw.signal.aborted})})}),t.get("/api/sessions/:id/notes",i=>{let l=i.req.param("id"),p=Js(l);return p?i.json(p):i.body(null,204)}),t.put("/api/sessions/:id/notes",async i=>{let l=i.req.param("id"),p=await i.req.json().catch(()=>null);if(!p||typeof p.content!="string")return i.json({error:"content required (string)"},400);try{let g=fc(l,p.content);return i.json(g)}catch(g){return console.error("[notes] failed to save note for session",l,g),i.json({error:"failed to save note"},500)}}),t.post("/api/sessions/:id/generate-note",async i=>{let l=i.req.param("id");if(!it().agentEnabled)return i.json({error:"autoTitle.agentEnabled is false"},403);try{let g=await hc(l),_=Ec(l,g);return i.json(_)}catch(g){let _=g.message,E=/no messages available/i.test(_)?404:500;return i.json({error:_},E)}}),t.get("/api/semantic/status",i=>i.json(yr())),t.put("/api/semantic/config",async i=>{let l=await i.req.json().catch(()=>({})),p=js.partial().safeParse(l);return p.success?(Ms(p.data),i.json(yr())):i.json({error:"invalid semantic config",issues:p.error.issues},400)}),t.get("/api/semantic/config",i=>i.json(ae())),t.post("/api/semantic/backfill",je,async i=>{if(ci)return i.json({error:"a scan is already running"},409);if(!ae().enabled)return i.json({error:"semantic search is disabled"},400);let p=await i.req.json().catch(()=>({})),g=Math.max(1,Math.min(5e3,Number(p.limit??200)));return ci=!0,Ps({limit:g,force:!!p.force}).catch(_=>console.error("[semantic.backfill] error:",_)).finally(()=>{ci=!1}),i.json({ok:!0,limit:g})}),t.post("/api/semantic/sessions/:id",je,async i=>{if(!ae().enabled)return i.json({error:"semantic search is disabled"},400);let p=i.req.param("id");if(!p)return i.json({error:"session id required"},400);let g=await Fs(p);return i.json(g)}),t.get("/api/semantic/vector-status",i=>{let l=_e(),p=ve(),g=Ge(),_=ti(),E=_?.chunksPerSec??0,y=E>0?Math.ceil(p.queueDepth/E):0;return i.json({embedder:l,worker:p,modelInstalled:g,throughput:{chunksPerSec:E,samples:_?.samples??0,source:_?"local-measured":"no-samples-yet"},etaSeconds:y})}),t.post("/api/semantic/install",je,async i=>{if(Ge())return i.json({ok:!0,status:"already_installed"});if(ai)return i.json({error:"a scan is already running"},409);ai=!0;try{return await Ap(),await $e(),Ss(),i.json({ok:!0,status:"installed"})}catch(l){let p=l instanceof Error?l.message:"unknown error";return i.json({ok:!1,error:p},500)}finally{ai=!1}}),t.get("/api/semantic/reindex-preview",je,async i=>{let l=(i.req.query("project")??"").trim();if(!l)return i.json({error:"project name required"},400);let p=f(),g=p.prepare("SELECT id, name FROM projects WHERE name = ? OR decoded_path = ? LIMIT 1").get(l,l);if(!g)return i.json({error:`project not found: ${l}`},404);let _=p.prepare(`SELECT
1885
+ LIMIT @limit`).all({proj:`%${Ts(p)}%`,limit:k}),C=new Set(E),I=O.filter(L=>{let F=L.alias==null?null:v.isSessionAutoLinked(L.id)?"auto":"manual",W=ts({auto_title:L.auto_title,auto_title_source:L.auto_title_source??null,has_alias:L.alias!=null&&F==="manual"});return C.has(W)});return Ye(i,async L=>{let F=I.length,W=[],K=[],G=[],P=0,X=async(Q,V)=>{P+=1;try{await L.writeSSE({id:String(P),event:Q,data:JSON.stringify(V)})}catch{}};await X("start",{total:F,model:y});let Y=0;for(let Q of I){if(i.req.raw.signal.aborted)break;Y+=1;try{let V=await ho(Q.id,{model:y,budget:N,signal:i.req.raw.signal});V.written?(W.push(Q.id),await X("progress",{sessionId:Q.id,title:V.title,evidence:V.evidence,confidence:V.confidence,current:Y,total:F})):(K.push({sessionId:Q.id,reason:V.skipped??"unknown"}),await X("skipped",{sessionId:Q.id,reason:V.skipped??"unknown",current:Y,total:F}))}catch(V){let J=V instanceof Error?V.message:String(V),re=V instanceof Rt?"no-context-available":"failed";G.push({sessionId:Q.id,error:J}),await X("error",{sessionId:Q.id,error:J,code:re,current:Y,total:F})}}await X("done",{generated:W,skipped:K,failed:G,cancelled:i.req.raw.signal.aborted})})}),t.get("/api/sessions/:id/notes",i=>{let l=i.req.param("id"),p=Js(l);return p?i.json(p):i.body(null,204)}),t.put("/api/sessions/:id/notes",async i=>{let l=i.req.param("id"),p=await i.req.json().catch(()=>null);if(!p||typeof p.content!="string")return i.json({error:"content required (string)"},400);try{let g=fc(l,p.content);return i.json(g)}catch(g){return console.error("[notes] failed to save note for session",l,g),i.json({error:"failed to save note"},500)}}),t.post("/api/sessions/:id/generate-note",async i=>{let l=i.req.param("id");if(!it().agentEnabled)return i.json({error:"autoTitle.agentEnabled is false"},403);try{let g=await hc(l),_=Ec(l,g);return i.json(_)}catch(g){let _=g.message,E=/no messages available/i.test(_)?404:500;return i.json({error:_},E)}}),t.get("/api/semantic/status",i=>i.json(yr())),t.put("/api/semantic/config",async i=>{let l=await i.req.json().catch(()=>({})),p=js.partial().safeParse(l);return p.success?(Ms(p.data),i.json(yr())):i.json({error:"invalid semantic config",issues:p.error.issues},400)}),t.get("/api/semantic/config",i=>i.json(ae())),t.post("/api/semantic/backfill",je,async i=>{if(ci)return i.json({error:"a scan is already running"},409);if(!ae().enabled)return i.json({error:"semantic search is disabled"},400);let p=await i.req.json().catch(()=>({})),g=Math.max(1,Math.min(5e3,Number(p.limit??200)));return ci=!0,Ps({limit:g,force:!!p.force}).catch(_=>console.error("[semantic.backfill] error:",_)).finally(()=>{ci=!1}),i.json({ok:!0,limit:g})}),t.post("/api/semantic/sessions/:id",je,async i=>{if(!ae().enabled)return i.json({error:"semantic search is disabled"},400);let p=i.req.param("id");if(!p)return i.json({error:"session id required"},400);let g=await Fs(p);return i.json(g)}),t.get("/api/semantic/vector-status",i=>{let l=_e(),p=ve(),g=Ge(),_=(i.req.query("project")??"").trim(),E=_.length>512?"":_,y=null,k=0;if(E){let C=f(),I=C.prepare("SELECT id FROM projects WHERE name = ? OR decoded_path = ? LIMIT 1").get(E,E);I?y=C.prepare("SELECT COUNT(*) AS n FROM chunk_queue WHERE session_id IN (SELECT id FROM sessions WHERE project_id = ?)").get(I.id).n:y=0}let N=ti(),A=N?.chunksPerSec??0,O=A>0?Math.ceil(p.queueDepth/A):0;return A>0&&y!==null&&(k=Math.ceil(y/A)),i.json({embedder:l,worker:p,modelInstalled:g,project:E||null,queueDepthForProject:y,etaForProject:k,throughput:{chunksPerSec:A,samples:N?.samples??0,source:N?"local-measured":"no-samples-yet"},etaSeconds:O})}),t.post("/api/semantic/install",je,async i=>{if(Ge())return i.json({ok:!0,status:"already_installed"});if(ai)return i.json({error:"a scan is already running"},409);ai=!0;try{return await Ap(),await $e(),Ss(),i.json({ok:!0,status:"installed"})}catch(l){let p=l instanceof Error?l.message:"unknown error";return i.json({ok:!1,error:p},500)}finally{ai=!1}}),t.get("/api/semantic/reindex-preview",je,async i=>{let l=(i.req.query("project")??"").trim();if(!l)return i.json({error:"project name required"},400);let p=f(),g=p.prepare("SELECT id, name FROM projects WHERE name = ? OR decoded_path = ? LIMIT 1").get(l,l);if(!g)return i.json({error:`project not found: ${l}`},404);let _=p.prepare(`SELECT
1886
1886
  COUNT(*) AS total,
1887
1887
  SUM(CASE WHEN s.message_count >= 3 THEN 1 ELSE 0 END) AS eligible,
1888
1888
  SUM(CASE WHEN s.message_count >= 3 AND EXISTS
1889
1889
  (SELECT 1 FROM chunk_meta cm WHERE cm.session_id = s.id) THEN 1 ELSE 0 END)
1890
1890
  AS indexed
1891
1891
  FROM sessions s WHERE s.project_id = ?`).get(g.id),E=_.eligible??0,y=_.indexed??0,k=Math.max(0,E-y),N=p.prepare(`SELECT COUNT(*) AS n FROM chunk_queue
1892
- WHERE session_id NOT IN (SELECT id FROM sessions WHERE project_id = ?)`).get(g.id).n,A=ti(),L=2,C=30,I=L,O=C,F=L*C,W="fallback-baseline",K=0;if(A)I=A.sessionsPerSec,O=A.avgChunksPerSession,F=A.chunksPerSec,W="local-measured",K=A.samples;else if(Ge()){if(!_e().loaded)try{await $e()}catch{}try{let Y=["[user] benchmark probe one \u2014 typical session opening turn","[assistant] benchmark probe two \u2014 typical assistant response with code reference","[user] benchmark probe three \u2014 typical follow-up clarification"],Q=Date.now();await ct(Y);let V=Date.now()-Q;V>0&&(F=Y.length*1e3/V,I=F/C,W="live-benchmark")}catch{}}let G=Y=>p.prepare(`SELECT
1892
+ WHERE session_id NOT IN (SELECT id FROM sessions WHERE project_id = ?)`).get(g.id).n,A=ti(),O=2,C=30,I=O,L=C,F=O*C,W="fallback-baseline",K=0;if(A)I=A.sessionsPerSec,L=A.avgChunksPerSession,F=A.chunksPerSec,W="local-measured",K=A.samples;else if(Ge()){if(!_e().loaded)try{await $e()}catch{}try{let Y=["[user] benchmark probe one \u2014 typical session opening turn","[assistant] benchmark probe two \u2014 typical assistant response with code reference","[user] benchmark probe three \u2014 typical follow-up clarification"],Q=Date.now();await ct(Y);let V=Date.now()-Q;V>0&&(F=Y.length*1e3/V,I=F/C,W="live-benchmark")}catch{}}let G=Y=>p.prepare(`SELECT
1893
1893
  COALESCE(SUM(CASE WHEN ec > 5 THEN 5 ELSE ec END), 0) AS total_quick,
1894
1894
  COALESCE(SUM(CASE WHEN ec > 80 THEN 80 ELSE ec END), 0) AS total_standard,
1895
1895
  COALESCE(SUM(CASE WHEN ec > 200 THEN 200 ELSE ec END), 0) AS total_full,
@@ -1902,9 +1902,9 @@ ${o}
1902
1902
  END AS ec
1903
1903
  FROM sessions s
1904
1904
  WHERE s.project_id = ? AND s.message_count >= 3 ${Y}
1905
- )`).get(g.id),P=G("AND NOT EXISTS (SELECT 1 FROM chunk_meta cm WHERE cm.session_id = s.id)"),X=G("");return i.json({project:g.name,total:_.total??0,eligible:E,indexed:y,pendingNew:k,pendingForce:E,modelInstalled:Ge(),modelName:"BAAI/bge-base-en-v1.5",embedderLoaded:_e().loaded,workerRunning:ve().running,queueDepthOther:N,sessionsPerSec:I,avgChunksPerSession:O,chunksPerSec:F,throughputSource:W,throughputSamples:K,estimatedChunksByDepth:{new:{quick:P.total_quick,standard:P.total_standard,full:P.total_full,uncapped:P.total_uncapped},force:{quick:X.total_quick,standard:X.total_standard,full:X.total_full,uncapped:X.total_uncapped}}})}),t.post("/api/semantic/cancel-reindex",je,async i=>{let l={};try{l=await i.req.json()}catch{}let p=(l.project??"").trim(),g=f(),_=0,E=null;if(p){let y=g.prepare("SELECT id FROM projects WHERE name = ? OR decoded_path = ? LIMIT 1").get(p,p);if(!y)return i.json({error:`project not found: ${p}`},404);let k=g.prepare("SELECT COUNT(*) AS n FROM chunk_queue WHERE session_id IN (SELECT id FROM sessions WHERE project_id = ?)").get(y.id).n;g.prepare("DELETE FROM chunk_queue WHERE session_id IN (SELECT id FROM sessions WHERE project_id = ?)").run(y.id),_=k,E=y.id}else{let y=g.prepare("SELECT COUNT(*) AS n FROM chunk_queue").get().n;g.prepare("DELETE FROM chunk_queue").run(),_=y}return E!==null?hp(E):Ep(),ve().queueDepth===0&&Sp(),i.json({cleared:_,project:p||null,queueDepth:ve().queueDepth})}),t.post("/api/semantic/reindex-project",je,async i=>{if(!Ge())return i.json({error:"embedder not installed \u2014 run `recall semantic install` first"},503);let l={};try{l=await i.req.json()}catch{}let p=(l.project??"").trim();if(!p)return i.json({error:"project name required"},400);let g=!!l.force,_=Math.max(0,Math.floor(Number(l.maxChunks??0)));_p(_);let E=f(),y=E.prepare("SELECT id FROM projects WHERE name = ? OR decoded_path = ? LIMIT 1").get(p,p);if(!y)return i.json({error:`project not found: ${p}`},404);let k=g?"SELECT id FROM sessions WHERE project_id = ? AND message_count >= 3":`SELECT s.id FROM sessions s
1905
+ )`).get(g.id),P=G("AND NOT EXISTS (SELECT 1 FROM chunk_meta cm WHERE cm.session_id = s.id)"),X=G("");return i.json({project:g.name,total:_.total??0,eligible:E,indexed:y,pendingNew:k,pendingForce:E,modelInstalled:Ge(),modelName:"BAAI/bge-base-en-v1.5",embedderLoaded:_e().loaded,workerRunning:ve().running,queueDepthOther:N,sessionsPerSec:I,avgChunksPerSession:L,chunksPerSec:F,throughputSource:W,throughputSamples:K,estimatedChunksByDepth:{new:{quick:P.total_quick,standard:P.total_standard,full:P.total_full,uncapped:P.total_uncapped},force:{quick:X.total_quick,standard:X.total_standard,full:X.total_full,uncapped:X.total_uncapped}}})}),t.post("/api/semantic/cancel-reindex",je,async i=>{let l={};try{l=await i.req.json()}catch{}let p=(l.project??"").trim(),g=f(),_=0,E=null;if(p){let y=g.prepare("SELECT id FROM projects WHERE name = ? OR decoded_path = ? LIMIT 1").get(p,p);if(!y)return i.json({error:`project not found: ${p}`},404);let k=g.prepare("SELECT COUNT(*) AS n FROM chunk_queue WHERE session_id IN (SELECT id FROM sessions WHERE project_id = ?)").get(y.id).n;g.prepare("DELETE FROM chunk_queue WHERE session_id IN (SELECT id FROM sessions WHERE project_id = ?)").run(y.id),_=k,E=y.id}else{let y=g.prepare("SELECT COUNT(*) AS n FROM chunk_queue").get().n;g.prepare("DELETE FROM chunk_queue").run(),_=y}return E!==null?hp(E):Ep(),ve().queueDepth===0&&Sp(),i.json({cleared:_,project:p||null,queueDepth:ve().queueDepth})}),t.post("/api/semantic/reindex-project",je,async i=>{if(!Ge())return i.json({error:"embedder not installed \u2014 run `recall semantic install` first"},503);let l={};try{l=await i.req.json()}catch{}let p=(l.project??"").trim();if(!p)return i.json({error:"project name required"},400);let g=!!l.force,_=Math.max(0,Math.floor(Number(l.maxChunks??0)));_p(_);let E=f(),y=E.prepare("SELECT id FROM projects WHERE name = ? OR decoded_path = ? LIMIT 1").get(p,p);if(!y)return i.json({error:`project not found: ${p}`},404);let k=g?"SELECT id FROM sessions WHERE project_id = ? AND message_count >= 3":`SELECT s.id FROM sessions s
1906
1906
  WHERE s.project_id = ? AND s.message_count >= 3
1907
- AND NOT EXISTS (SELECT 1 FROM chunk_meta cm WHERE cm.session_id = s.id)`,N=E.prepare(k).all(y.id);if(N.length===0)return i.json({enqueued:0,queueDepth:ve().queueDepth,message:"nothing to do \u2014 every session in this project is already vectorized (use force:true to re-embed)"});let A=E.prepare("INSERT INTO chunk_queue(session_id, action) VALUES (?, 'embed')");if(E.transaction(()=>{for(let C of N)A.run(C.id)})(),!_e().loaded)try{await $e()}catch(C){let I=C instanceof Error?C.message:"unknown error";return i.json({error:`embedder load failed: ${I}`},500)}return ve().running||Ss(),i.json({enqueued:N.length,queueDepth:ve().queueDepth,project:p,appliedMaxChunks:_})}),t.get("/api/sessions/:id/similar",je,async i=>{if(!_e().loaded)return i.json({error:"vector model not loaded"},503);let l=i.req.param("id"),p=Math.max(1,Math.min(50,Number(i.req.query("limit")??10)));try{let g=await op(l,p);return i.json({sessionId:l,similar:g})}catch(g){let _=g instanceof Error?g.message:"unknown error";return i.json({error:_},500)}}),t.get("/api/search",je,async i=>{let l=f(),p=i.req.query("q")?.trim();if(!p)return i.json({query:"",hits:[],tags:[]});if(p.length>500)return i.json({error:"query too long (max 500 chars)"},400);let g=i.req.query("project"),_=p.split(/\s+/).filter(J=>J.length>0),E=_.filter(J=>J.startsWith("#")).map(J=>Qe(J)).filter(Boolean),y=_.filter(J=>!J.startsWith("#")),k=y.length>20,A=(k?y.slice(0,20):y).map(J=>`"${J.replace(/"/g,"")}"`),L=A.join(" "),C=Math.max(1,Math.min(200,Number(i.req.query("limit")??30))),I=i.req.query("system")==="1"||i.req.query("system")==="true",O=Jn("s",I);if(A.length===0&&E.length>0){let J=`
1907
+ AND NOT EXISTS (SELECT 1 FROM chunk_meta cm WHERE cm.session_id = s.id)`,N=E.prepare(k).all(y.id);if(N.length===0)return i.json({enqueued:0,queueDepth:ve().queueDepth,message:"nothing to do \u2014 every session in this project is already vectorized (use force:true to re-embed)"});let A=E.prepare("INSERT INTO chunk_queue(session_id, action) VALUES (?, 'embed')");if(E.transaction(()=>{for(let C of N)A.run(C.id)})(),!_e().loaded)try{await $e()}catch(C){let I=C instanceof Error?C.message:"unknown error";return i.json({error:`embedder load failed: ${I}`},500)}return ve().running||Ss(),i.json({enqueued:N.length,queueDepth:ve().queueDepth,project:p,appliedMaxChunks:_})}),t.get("/api/sessions/:id/similar",je,async i=>{if(!_e().loaded)return i.json({error:"vector model not loaded"},503);let l=i.req.param("id"),p=Math.max(1,Math.min(50,Number(i.req.query("limit")??10)));try{let g=await op(l,p);return i.json({sessionId:l,similar:g})}catch(g){let _=g instanceof Error?g.message:"unknown error";return i.json({error:_},500)}}),t.get("/api/search",je,async i=>{let l=f(),p=i.req.query("q")?.trim();if(!p)return i.json({query:"",hits:[],tags:[]});if(p.length>500)return i.json({error:"query too long (max 500 chars)"},400);let g=i.req.query("project"),_=p.split(/\s+/).filter(J=>J.length>0),E=_.filter(J=>J.startsWith("#")).map(J=>Qe(J)).filter(Boolean),y=_.filter(J=>!J.startsWith("#")),k=y.length>20,A=(k?y.slice(0,20):y).map(J=>`"${J.replace(/"/g,"")}"`),O=A.join(" "),C=Math.max(1,Math.min(200,Number(i.req.query("limit")??30))),I=i.req.query("system")==="1"||i.req.query("system")==="true",L=Jn("s",I);if(A.length===0&&E.length>0){let J=`
1908
1908
  SELECT s.id AS session_id,
1909
1909
  s.id AS message_uuid,
1910
1910
  p.name AS project,
@@ -1916,7 +1916,7 @@ ${o}
1916
1916
  FROM sessions s
1917
1917
  JOIN projects p ON p.id = s.project_id
1918
1918
  LEFT JOIN session_aliases sa ON sa.session_id = s.id
1919
- WHERE 1=1${O}
1919
+ WHERE 1=1${L}
1920
1920
  `,re={limit:C};g&&(J+=" AND (p.name LIKE @proj ESCAPE '\\' OR p.decoded_path LIKE @proj ESCAPE '\\')",re.proj=`%${Ts(g)}%`),E.forEach((Me,me)=>{J+=` AND s.id IN (SELECT session_id FROM session_tags WHERE tag = @tag_${me})`,re[`tag_${me}`]=Me}),J+=" ORDER BY COALESCE(s.started_at, '') DESC LIMIT @limit";let ze=l.prepare(J).all(re);return i.json({query:p,hits:ze,tags:E,truncated:k})}let F=`
1921
1921
  SELECT m.session_id AS session_id,
1922
1922
  m.uuid AS message_uuid,
@@ -1931,8 +1931,8 @@ ${o}
1931
1931
  JOIN sessions s ON s.id = m.session_id
1932
1932
  JOIN projects p ON p.id = s.project_id
1933
1933
  LEFT JOIN session_aliases sa ON sa.session_id = s.id
1934
- WHERE messages_fts MATCH @fts${O}
1935
- `,W={fts:L,limit:C};g&&(F+=" AND (p.name LIKE @proj ESCAPE '\\' OR p.decoded_path LIKE @proj ESCAPE '\\')",W.proj=`%${Ts(g)}%`),E.forEach((J,re)=>{F+=` AND s.id IN (SELECT session_id FROM session_tags WHERE tag = @tag_${re})`,W[`tag_${re}`]=J}),F+=" ORDER BY bm25(messages_fts) LIMIT @limit";let G=l.prepare(F).all(W).map(J=>({...J,matched_via:"fts"}));if(i.req.query("mode")!=="semantic")return i.json({query:p,hits:G,tags:E,truncated:k});let X=[];try{let J=`
1934
+ WHERE messages_fts MATCH @fts${L}
1935
+ `,W={fts:O,limit:C};g&&(F+=" AND (p.name LIKE @proj ESCAPE '\\' OR p.decoded_path LIKE @proj ESCAPE '\\')",W.proj=`%${Ts(g)}%`),E.forEach((J,re)=>{F+=` AND s.id IN (SELECT session_id FROM session_tags WHERE tag = @tag_${re})`,W[`tag_${re}`]=J}),F+=" ORDER BY bm25(messages_fts) LIMIT @limit";let G=l.prepare(F).all(W).map(J=>({...J,matched_via:"fts"}));if(i.req.query("mode")!=="semantic")return i.json({query:p,hits:G,tags:E,truncated:k});let X=[];try{let J=`
1936
1936
  SELECT s.id AS session_id,
1937
1937
  s.id AS message_uuid,
1938
1938
  p.name AS project,
@@ -1947,16 +1947,16 @@ ${o}
1947
1947
  JOIN sessions s ON s.id = ss.session_id
1948
1948
  JOIN projects p ON p.id = s.project_id
1949
1949
  LEFT JOIN session_aliases sa ON sa.session_id = s.id
1950
- WHERE sessions_fts MATCH @fts${O}
1951
- `,re={fts:L,limit:C};g&&(J+=" AND (p.name LIKE @proj ESCAPE '\\' OR p.decoded_path LIKE @proj ESCAPE '\\')",re.proj=`%${Ts(g)}%`),E.forEach((ze,Me)=>{J+=` AND s.id IN (SELECT session_id FROM session_tags WHERE tag = @tag_${Me})`,re[`tag_${Me}`]=ze}),J+=" ORDER BY rank LIMIT @limit",X=l.prepare(J).all(re)}catch(J){console.error("[search.semantic] failed:",J)}if(_e().loaded)try{let J=await rp(p,C),re=G.map(le=>({id:String(le.session_id),data:le,lane:"bm25"})),ze=X.map(le=>({id:String(le.session_id),data:le,lane:"summary"})),Me=J.map(le=>({id:le.sessionId,data:{session_id:le.sessionId,snippet:le.text,matched_via:"vector"},lane:"vector"})),sm=ip([re,ze,Me]).slice(0,C).map(le=>({...le.data,session_id:le.id,rrf_score:le.score,lanes:le.lanes,matched_via:le.lanes.length>1?"fused":le.lanes[0]}));return i.json({query:p,hits:sm,tags:E,mode:"semantic",fusion:"rrf",truncated:k})}catch(J){console.error("[search.vector] failed, falling back:",J)}let Y=new Set(G.map(J=>String(J.session_id))),Q=X.filter(J=>!Y.has(String(J.session_id))).map(({rank:J,...re})=>({...re,matched_via:"semantic"})),V=[...G,...Q].slice(0,C);return i.json({query:p,hits:V,tags:E,mode:"semantic",truncated:k})}),t.get("/api/sessions/:id/context",je,i=>{let l=f(),p=i.req.param("id"),g=i.req.query("mode")==="full"?"full":"condensed",_=i.req.query("subagents")==="1",E=i.req.query("prelude")??null,y=l.prepare(`SELECT s.id, p.name AS project_name, p.decoded_path,
1950
+ WHERE sessions_fts MATCH @fts${L}
1951
+ `,re={fts:O,limit:C};g&&(J+=" AND (p.name LIKE @proj ESCAPE '\\' OR p.decoded_path LIKE @proj ESCAPE '\\')",re.proj=`%${Ts(g)}%`),E.forEach((ze,Me)=>{J+=` AND s.id IN (SELECT session_id FROM session_tags WHERE tag = @tag_${Me})`,re[`tag_${Me}`]=ze}),J+=" ORDER BY rank LIMIT @limit",X=l.prepare(J).all(re)}catch(J){console.error("[search.semantic] failed:",J)}if(_e().loaded)try{let J=await rp(p,C),re=G.map(le=>({id:String(le.session_id),data:le,lane:"bm25"})),ze=X.map(le=>({id:String(le.session_id),data:le,lane:"summary"})),Me=J.map(le=>({id:le.sessionId,data:{session_id:le.sessionId,snippet:le.text,matched_via:"vector"},lane:"vector"})),sm=ip([re,ze,Me]).slice(0,C).map(le=>({...le.data,session_id:le.id,rrf_score:le.score,lanes:le.lanes,matched_via:le.lanes.length>1?"fused":le.lanes[0]}));return i.json({query:p,hits:sm,tags:E,mode:"semantic",fusion:"rrf",truncated:k})}catch(J){console.error("[search.vector] failed, falling back:",J)}let Y=new Set(G.map(J=>String(J.session_id))),Q=X.filter(J=>!Y.has(String(J.session_id))).map(({rank:J,...re})=>({...re,matched_via:"semantic"})),V=[...G,...Q].slice(0,C);return i.json({query:p,hits:V,tags:E,mode:"semantic",truncated:k})}),t.get("/api/sessions/:id/context",je,i=>{let l=f(),p=i.req.param("id"),g=i.req.query("mode")==="full"?"full":"condensed",_=i.req.query("subagents")==="1",E=i.req.query("prelude")??null,y=l.prepare(`SELECT s.id, p.name AS project_name, p.decoded_path,
1952
1952
  s.started_at, s.ended_at, s.message_count, s.git_branch
1953
1953
  FROM sessions s JOIN projects p ON p.id = s.project_id
1954
1954
  WHERE s.id = ?`).get(p);if(!y)return i.json({error:"not found"},404);let k=l.prepare(`SELECT uuid, type, role, timestamp, is_sidechain, content_text, tool_names
1955
1955
  FROM messages
1956
1956
  WHERE session_id = ?
1957
- ORDER BY COALESCE(timestamp, ''), rowid`).all(p),N=uc(y,k,{mode:g,includeSidechain:_,prelude:E});return i.text(N)}),t.get("/api/collections",i=>{let l=i.req.query("archived")==="1";return i.json({collections:Sc(l)})}),t.get("/api/collections/:id",i=>{let l=i.req.param("id"),p=He(l);if(!p)return i.json({error:"not found"},404);let g=Tc(l,!0);return i.json({collection:p,members:g})}),t.post("/api/collections",async i=>{let l=await i.req.json().catch(()=>null);if(!l||typeof l.name!="string")return i.json({error:"name required"},400);try{let p=Vt({name:l.name,description:l.description??null,icon:l.icon??null,color:l.color??null,parent_id:l.parent_id??null,sort_key:l.sort_key});return i.json(p,201)}catch(p){return i.json({error:p.message},400)}}),t.patch("/api/collections/:id",async i=>{let l=i.req.param("id"),p=await i.req.json().catch(()=>null);if(!p)return i.json({error:"body required"},400);try{let g=wc(l,p);return i.json(g)}catch(g){return i.json({error:g.message},400)}}),t.post("/api/collections/:id/archive",i=>{let l=i.req.param("id");try{let p=Rc(l);return i.json(p)}catch(p){return i.json({error:p.message},404)}}),t.post("/api/collections/:id/restore",i=>{let l=i.req.param("id");try{let p=kc(l);return i.json(p)}catch(p){return i.json({error:p.message},404)}}),t.post("/api/collections/:id/members",async i=>{let l=i.req.param("id"),p=await i.req.json().catch(()=>null);if(!p||typeof p.session_id!="string")return i.json({error:"session_id required"},400);try{let g=Zt(l,p.session_id,p.note??null);return i.json(g)}catch(g){return i.json({error:g.message},400)}}),t.delete("/api/collections/:id/members/:sid",i=>{let l=i.req.param("id"),p=i.req.param("sid");try{let g=Ac(l,p);return i.json(g)}catch(g){return i.json({error:g.message},400)}}),t.get("/api/sessions/:id/collections",i=>{let l=i.req.param("id");return i.json({collections:yc(l)})});let R=["cwd-prefix","project-id","tag","plan-file","git-branch-prefix"];t.get("/api/auto-collections/rules",i=>i.json({rules:Cc()})),t.post("/api/auto-collections/rules",async i=>{let l=await i.req.json().catch(()=>null);if(!l||typeof l.name!="string"||typeof l.pattern!="string"||!l.type||!R.includes(l.type))return i.json({error:"name, type, pattern required (type must be a known matcher)"},400);try{let p=zr({name:l.name,type:l.type,pattern:l.pattern,collection_id:l.collection_id,parent_collection_id:l.parent_collection_id,priority:l.priority,enabled:l.enabled});return i.json(p,201)}catch(p){return i.json({error:p.message},400)}}),t.patch("/api/auto-collections/rules/:id",async i=>{let l=i.req.param("id"),p=await i.req.json().catch(()=>null);if(!p)return i.json({error:"body required"},400);try{let g=Ic(l,p);return i.json(g)}catch(g){return i.json({error:g.message},400)}}),t.delete("/api/auto-collections/rules/:id",i=>{let l=i.req.param("id");try{let p=vc(l);return i.json(p)}catch(p){return i.json({error:p.message},400)}}),t.get("/api/auto-collections/suggestions",i=>{let l=i.req.query("dismissed")==="1";return i.json({suggestions:en({includeDismissed:l})})}),t.post("/api/auto-collections/suggestions/:id/accept",i=>{let l=i.req.param("id");try{let p=Mc(l);return i.json({rule:p})}catch(p){return i.json({error:p.message},400)}}),t.post("/api/auto-collections/suggestions/:id/dismiss",i=>{let l=i.req.param("id");try{return jc(l),i.json({ok:!0})}catch(p){return i.json({error:p.message},400)}}),t.post("/api/auto-collections/detect",i=>{let l=tn();return i.json({suggestions:l})}),t.get("/api/auto-collections/suggestions/:id/preview",i=>{let l=i.req.param("id"),p=Math.max(1,Math.min(20,Number(i.req.query("limit"))||3)),_=en({includeDismissed:!1}).find(y=>y.id===l);if(!_)return i.json({error:"suggestion not found"},404);let E=Oc(_.type,_.pattern,p);return i.json({sessions:E})}),t.get("/api/auto-collections/parents",i=>{let l=Array.from(Dc());return i.json({auto_collection_ids:l})}),t.get("/api/threads",i=>{let l=i.req.query("archived")==="1";return i.json({threads:Qr({includeArchived:l})})}),t.get("/api/threads/:id",i=>{let l=i.req.param("id"),p=oe(l);if(!p)return i.json({error:"thread not found"},404);let g=p.edges.map(_=>({..._,alias_source:_.alias==null?null:v.isSessionAutoLinked(_.session_id)?"auto":"manual"}));return i.json({thread:{...p,edges:g}})}),t.post("/api/threads",async i=>{let l=await i.req.json().catch(()=>({}));if(!l.name)return i.json({error:"name required"},400);try{let p=nn({name:l.name,summary:l.summary??null,originSessionId:l.originSessionId});return i.json({thread:p})}catch(p){return i.json({error:p.message},400)}}),t.patch("/api/threads/:id",async i=>{let l=i.req.param("id"),p=await i.req.json().catch(()=>({}));try{p.name&&Jc(l,p.name),p.close&&Gc(l),p.reopen&&Yc(l),p.archive&&zc(l),"folder_id"in p&&_l(l,p.folder_id??null);let g=oe(l);return g?i.json({thread:g}):i.json({error:"thread not found"},404)}catch(g){return i.json({error:g.message},400)}}),t.get("/api/thread-folders",i=>i.json({folders:no()})),t.post("/api/thread-folders",async i=>{let l=await i.req.json().catch(()=>({}));if(!l.name||typeof l.name!="string")return i.json({error:"name required"},400);try{let p=ll({name:l.name,parentFolderId:l.parent_folder_id??null,projectScope:l.project_scope??null});return i.json({folder:p})}catch(p){return i.json({error:p.message},400)}}),t.patch("/api/thread-folders/:id",async i=>{let l=i.req.param("id"),p=await i.req.json().catch(()=>({}));try{let g;return p.name&&(g=dl(l,p.name)),"parent_folder_id"in p&&(g=pl(l,p.parent_folder_id??null)),g?i.json({folder:g}):i.json({error:"no patch fields"},400)}catch(g){return i.json({error:g.message},400)}}),t.delete("/api/thread-folders/:id",i=>{let l=i.req.param("id");try{return gl(l),i.json({ok:!0})}catch(p){return i.json({error:p.message},400)}}),t.post("/api/thread-folders/reorder",async i=>{let l=await i.req.json().catch(()=>({})),p=l.ordered_ids;if(!Array.isArray(p))return i.json({error:"ordered_ids must be an array"},400);try{return ml(l.parent_folder_id??null,l.project_scope??null,p),i.json({ok:!0})}catch(g){return i.json({error:g.message},400)}}),t.post("/api/threads/:id/sessions",async i=>{let l=i.req.param("id"),p=await i.req.json().catch(()=>({}));if(!p.sessionId)return i.json({error:"sessionId required"},400);try{let g=rn({threadId:l,sessionId:p.sessionId,parentSessionId:p.parentSessionId??null,role:p.role});return i.json({edge:g})}catch(g){return i.json({error:g.message},400)}}),t.delete("/api/threads/:id/sessions/:sessionId",i=>{let l=i.req.param("id"),p=i.req.param("sessionId"),g=Xc(l,p);return i.json(g)}),t.patch("/api/threads/:id/sessions/:sessionId",async i=>{let l=i.req.param("id"),p=i.req.param("sessionId"),g=await i.req.json().catch(()=>({}));try{let _=Qt(l,p,g.parentSessionId??null);return i.json({edge:_})}catch(_){return i.json({error:_.message},400)}}),t.post("/api/threads/:id/merge",async i=>{let l=i.req.param("id"),p=await i.req.json().catch(()=>({}));if(!p.sourceId)return i.json({error:"sourceId required"},400);try{let g=Kc(p.sourceId,l);return i.json({thread:g})}catch(g){return i.json({error:g.message},400)}}),t.post("/api/threads/:id/split",async i=>{let l=i.req.param("id"),p=await i.req.json().catch(()=>({}));if(!p.sessionIds?.length||!p.newThreadName)return i.json({error:"sessionIds and newThreadName required"},400);try{let g=Vc({threadId:l,sessionIds:p.sessionIds,newThreadName:p.newThreadName});return i.json({thread:g})}catch(g){return i.json({error:g.message},400)}}),t.get("/api/sessions/:id/threads",i=>{let l=i.req.param("id");return i.json({threads:qc(l)})});let j=M.object({enabled:M.boolean(),band_lo:M.number().min(0).max(1).optional(),band_hi:M.number().min(0).max(1).optional(),model:M.string().regex(/^[a-zA-Z0-9._-]{1,100}$/).optional()}).optional(),x=M.object({project:M.string().min(1),threshold:M.number().min(0).max(1).optional(),model:M.string().regex(/^[a-zA-Z0-9._-]{1,100}$/).optional(),llm_rescore:j});t.post("/api/threads/scan/preflight",async i=>{let l=Ce(i);if(l)return l;let p=await i.req.json().catch(()=>null),g=x.safeParse(p);if(!g.success)return i.json({error:"invalid request body",details:g.error.format()},400);let _=_d({project:g.data.project,threshold:g.data.threshold,model:g.data.model,llm_rescore:g.data.llm_rescore});return"error"in _?i.json({error:_.error},400):i.json(_)});let q=M.object({project:M.string().min(1),threshold:M.number().min(0).max(1).optional(),llm_names:M.boolean().optional(),model:M.string().regex(/^[a-zA-Z0-9._-]{1,100}$/).optional(),llm_rescore:j});t.post("/api/threads/scan/apply",async i=>{let l=Ce(i);if(l)return l;let p=await i.req.json().catch(()=>null),g=q.safeParse(p);if(!g.success)return i.json({error:"invalid request body",details:g.error.format()},400);let _=hd({project:g.data.project,threshold:g.data.threshold,llm_names:g.data.llm_names,model:g.data.model,llm_rescore:g.data.llm_rescore});return"error"in _?i.json({error:_.error},400):i.json({jobId:_.jobId,reused:_.reused},_.reused?409:200)}),t.get("/api/threads/scan/jobs/:jobId/stream",i=>{let l=i.req.param("jobId");if(!Fo(l))return i.json({error:"job not found"},404);let g=Number(i.req.header("Last-Event-ID")??0);return Ye(i,async _=>{let E=!1,y=setInterval(()=>{E||_.writeSSE({event:"heartbeat",data:""}).catch(()=>{E=!0})},15e3);try{for await(let k of Ed(l,g))if(E||(await _.writeSSE({id:String(k.id),event:k.kind,data:JSON.stringify(k.data)}),k.kind==="done"))break}finally{E=!0,clearInterval(y)}})}),t.get("/api/threads/scan/jobs/:jobId",i=>{let l=Fo(i.req.param("jobId"));return l?i.json(l):i.json({error:"job not found"},404)}),t.delete("/api/threads/scan/jobs/:jobId",i=>{let l=Ce(i);return l||(bd(i.req.param("jobId"))?i.json({ok:!0}):i.json({error:"job not found or already done"},404))});let D=M.object({project_id:M.number().int().positive(),mode:M.enum(["preflight","apply"]),window_hours:M.number().min(.5).max(168).optional(),score_threshold:M.number().min(0).max(1).optional(),use_live_pids:M.boolean().optional()});t.post("/api/threads/sync-active",async i=>{let l=await i.req.json().catch(()=>null),p=D.safeParse(l);if(!p.success)return i.json({error:"invalid request body",details:p.error.format()},400);try{let g=await il(p.data.project_id,{windowHours:p.data.window_hours,scoreThreshold:p.data.score_threshold,useLivePids:p.data.use_live_pids});if(p.data.mode==="preflight")return i.json({plan:g});let _=al(g);return i.json({plan:g,result:_})}catch(g){return i.json({error:g.message},400)}}),t.get("/api/threads/:id/titles/preflight",i=>{let l=i.req.param("id"),p=oe(l);if(!p)return i.json({error:"thread not found"},404);let g=f(),_=0;for(let E of p.edges)g.prepare("SELECT auto_title_source FROM sessions WHERE id = ?").get(E.session_id)?.auto_title_source==="agent"&&(_+=1);return i.json({total:p.edges.length,alreadyTitled:_,untitled:p.edges.length-_})}),t.post("/api/threads/:id/titles/generate",async i=>{let l=i.req.param("id"),p=await i.req.json().catch(()=>({})),g=oe(l);if(!g)return i.json({error:"thread not found"},404);if(g.edges.length===0)return i.json({error:"thread has no sessions"},400);let _=Fe(),E=p.model??_.model,y=ql({threadId:l,force:p.force??!1,model:E});return i.json({jobId:y})}),t.get("/api/jobs/:jobId/stream",i=>{let l=i.req.param("jobId");if(!uo(l))return i.json({error:"job not found"},404);let g=Number(i.req.header("Last-Event-ID")??0);return Ye(i,async _=>{let E=!1,y=setInterval(()=>{E||_.writeSSE({event:"heartbeat",data:""}).catch(()=>{E=!0})},15e3);try{for await(let k of Xl(l,g))if(E||(await _.writeSSE({id:String(k.id),event:k.kind,data:JSON.stringify(k.data)}),k.kind==="done"))break}finally{E=!0,clearInterval(y)}})}),t.get("/api/jobs/:jobId",i=>{let l=uo(i.req.param("jobId"));return l?i.json(l):i.json({error:"job not found"},404)}),t.delete("/api/jobs/:jobId",i=>Jl(i.req.param("jobId"))?i.json({ok:!0}):i.json({error:"job not found or already done"},404)),t.post("/api/terminal/opened",async i=>{let l=await i.req.json().catch(()=>null);if(!l||typeof l.shell_pid!="number"||typeof l.tab_name!="string")return i.json({error:"shell_pid and tab_name required"},400);let p=v.claimPidOwnership(l.shell_pid,l.extension_instance_id??null);if(p==="rejected")return i.json({ok:!0,rejected:!0,reason:"pid-owned-by-other-extension-instance",count:v.size()});let g=v.upsert({shell_pid:l.shell_pid,tab_name:l.tab_name,cwd:l.cwd??null,opened_at:l.opened_at??new Date().toISOString()});return i.json({ok:!0,ownership:p,count:v.size(),entry:g})}),t.post("/api/terminal/renamed",async i=>{let l=await i.req.json().catch(()=>null);if(!l||typeof l.shell_pid!="number"||typeof l.tab_name!="string")return i.json({error:"shell_pid and tab_name required"},400);let p=v.claimPidOwnership(l.shell_pid,l.extension_instance_id??null);if(p==="rejected")return i.json({ok:!0,rejected:!0,reason:"pid-owned-by-other-extension-instance"});let g=v.rename(l.shell_pid,l.tab_name);if(!g)return i.json({error:"unknown shell_pid"},404);let _=jp(l.shell_pid,l.tab_name);return i.json({ok:!0,ownership:p,entry:g,propagated:_})}),t.post("/api/terminal/closed",async i=>{let l=await i.req.json().catch(()=>null);if(!l||typeof l.shell_pid!="number")return i.json({error:"shell_pid required"},400);let p=v.remove(l.shell_pid);return i.json({ok:!0,removed:p,count:v.size()})}),t.post("/api/terminal/claude-started",async i=>{let l=await i.req.json().catch(()=>null);return!l||typeof l.shell_pid!="number"?i.json({error:"shell_pid required"},400):(v.pushPending({shell_pid:l.shell_pid,tab_name:typeof l.tab_name=="string"?l.tab_name:"",cwd:typeof l.cwd=="string"?l.cwd:null,started_at:typeof l.started_at=="string"?l.started_at:new Date().toISOString()}),i.json({ok:!0,pending:v.pendingSize()}))}),t.post("/api/terminal/output",async i=>{let l=await i.req.json().catch(()=>null);if(!l||typeof l.shell_pid!="number"||typeof l.text!="string")return i.json({error:"shell_pid and text required"},400);let p=l.text.length>8192?l.text.slice(-8192):l.text,g=typeof l.captured_at=="string"?l.captured_at:new Date().toISOString();return v.setOutputTail(l.shell_pid,p,g),i.json({ok:!0})}),t.post("/api/terminal/sync",async i=>{let l=await i.req.json().catch(()=>null);if(!l||!Array.isArray(l.terminals))return i.json({error:"terminals array required"},400);let p=new Map;for(let O of v.all())p.set(O.shell_pid,O.tab_name);let g=l.terminals.filter(O=>!!O&&typeof O.shell_pid=="number"&&typeof O.tab_name=="string").map(O=>({shell_pid:O.shell_pid,tab_name:O.tab_name,cwd:O.cwd??null,opened_at:O.opened_at??new Date().toISOString()})),_=l.extension_instance_id??null,E=[],y=g.filter(O=>{let F=v.claimPidOwnership(O.shell_pid,_);return E.push({shell_pid:O.shell_pid,ownership:F}),F!=="rejected"}),k=v.sync(y),N=0;for(let O of y){let F=p.get(O.shell_pid),W=v.get(O.shell_pid)?.tab_name??O.tab_name,G=!!W&&!de(W)&&!ce(W)?W:O.tab_name;F!==void 0&&F!==G&&(N+=jp(O.shell_pid,G))}let A=E.filter(O=>O.ownership==="rejected").length;A>0&&console.log(`[terminal/sync] dropped ${A} tab_name update(s), pid(s) owned by a different extension instance`);let L=await Py(),C={resolved:0,expired:0};try{C=nu()}catch{}let I={rebound:0,ghosts:0,ambiguous:0};try{I=su()}catch{}return Ky(),i.json({ok:!0,count:v.size(),diff:k,propagated:N,live_sweep:L,deferred_resolved:C,rebound:I})}),t.get("/api/terminal/registry",i=>i.json({terminals:v.all(),count:v.size()})),t.get("/api/terminal/sessions/:shellPid",i=>{let l=i.req.param("shellPid"),p=Number(l);if(!Number.isInteger(p)||p<=0)return i.json({error:"shellPid must be a positive integer"},400);let g=v.sessionsFor(p);return i.json({shell_pid:p,sessions:g})}),t.get("/api/sessions/:id/linked-terminal",i=>{let l=i.req.param("id"),p=v.all().find(_=>v.sessionsFor(_.shell_pid).includes(l)),g=[];if(!p){let _=f().prepare("SELECT cwd, started_at FROM sessions WHERE id = ?").get(l);if(_?.cwd&&_.started_at){let E=Date.parse(_.started_at);if(Number.isFinite(E)){let y=_.cwd.replace(/\/+$/,""),k=300*1e3;for(let N of v.all()){if(!N.cwd||N.cwd.replace(/\/+$/,"")!==y||de(N.tab_name))continue;let A=Date.parse(N.opened_at),L=Date.parse(N.last_seen_at);!Number.isFinite(A)||!Number.isFinite(L)||A>E||L+k<E||g.push({shell_pid:N.shell_pid,tab_name:N.tab_name,cwd:N.cwd,opened_at:N.opened_at,last_seen_at:N.last_seen_at,reason:"time-overlap"})}g.sort((N,A)=>Date.parse(A.last_seen_at)-Date.parse(N.last_seen_at))}}}return p?i.json({linked:{shell_pid:p.shell_pid,tab_name:p.tab_name,cwd:p.cwd},suggested:[]}):i.json({linked:null,suggested:g})}),t.post("/api/sessions/:id/auto-relink",async i=>{let l=i.req.param("id");if(Te(l))return i.json({applied:!1,reason:"has-alias"});if(v.all().some(y=>v.sessionsFor(y.shell_pid).includes(l)))return i.json({applied:!1,reason:"already-linked"});let g=f().prepare("SELECT cwd, git_branch, started_at FROM sessions WHERE id = ?").get(l);if(!g?.cwd)return i.json({applied:!1,reason:"no-cwd"});let _=g.cwd.replace(/\/+$/,""),E=v.all().filter(y=>y.cwd&&y.cwd.replace(/\/+$/,"")===_&&!de(y.tab_name));if(E.length===1){let y=E[0],k=Kt({sessionStartedAt:g.started_at??null,terminalOpenedAt:y.opened_at??null});if(!k.allowed)return i.json({applied:!1,reason:"temporal-guard",guard_reason:k.reason});let N=v.getOrigin(l),A=wt({tabName:y.tab_name,origin:N??null,cwd:g.cwd??null,gitBranch:g.git_branch??null});return A?(he(l,A),v.linkSession(l,y.shell_pid),i.json({applied:!0,alias:A,linked_pid:y.shell_pid,linked_tab_name:y.tab_name,method:"cwd-singleton"})):i.json({applied:!1,reason:"no-usable-name"})}if(E.length>1){let y=await tu(l);if(y){let N=v.get(y.shell_pid),A=Kt({sessionStartedAt:g.started_at??null,terminalOpenedAt:N?.opened_at??null});if(!A.allowed)return i.json({applied:!1,reason:"temporal-guard",guard_reason:A.reason});let L=v.getOrigin(l),C=wt({tabName:y.tab_name,origin:L??null,cwd:g.cwd??null,gitBranch:g.git_branch??null});if(C)return he(l,C),v.linkSession(l,y.shell_pid),i.json({applied:!0,alias:C,linked_pid:y.shell_pid,linked_tab_name:y.tab_name,matched_fingerprints:y.matched_fingerprints,method:"content-match"})}let k=6e4;if(g.started_at){let N=Date.parse(g.started_at);if(Number.isFinite(N)){let A=E.filter(C=>Kt({sessionStartedAt:g.started_at,terminalOpenedAt:C.opened_at??null}).allowed).map(C=>({t:C,gap:N-Date.parse(C.opened_at??"")})).filter(C=>Number.isFinite(C.gap)&&C.gap>=0&&C.gap<=k);if(A.length>=2)return i.json({applied:!1,reason:"ambiguous-temporal",candidate_count:A.length});let L=A[0];if(L){let C=v.getOrigin(l),I=wt({tabName:L.t.tab_name,origin:C??null,cwd:g.cwd??null,gitBranch:g.git_branch??null});if(I)return he(l,I),v.linkSession(l,L.t.shell_pid),i.json({applied:!0,alias:I,linked_pid:L.t.shell_pid,linked_tab_name:L.t.tab_name,method:"closest-before-temporal",gap_ms:L.gap})}}}return i.json({applied:!1,reason:"ambiguous",candidate_count:E.length})}return i.json({applied:!1,reason:"no-candidates"})}),t.post("/api/sessions/:id/relink",async i=>{let l=i.req.param("id"),p=await i.req.json().catch(()=>null);if(p?.clear)return v.unlinkSession(l),Xs(l),i.json({ok:!0,alias:null,linked_pid:null});if(!p||typeof p.shell_pid!="number")return i.json({error:"shell_pid required"},400);let g=v.get(p.shell_pid);if(!g)return i.json({error:"terminal not registered"},404);let _=v.getOrigin(l),E=f().prepare("SELECT cwd, git_branch FROM sessions WHERE id = ?").get(l),y=null,k=g.tab_name?.trim()??"";if(k&&!de(k)&&!ce(k))y=k;else if(k&&ce(k)){let N=yt(k);N&&!de(N)&&(y=N)}return y?(v.unlinkSession(l),he(l,y),i.json({ok:!0,alias:y,linked_pid:p.shell_pid,linked_tab_name:g.tab_name})):i.json({error:"terminal has no usable name, name the tab in your editor first, then retry the relink"},422)}),t.post("/api/sessions/:id/recorrelate",async i=>{let l=i.req.param("id"),p=f().prepare("SELECT file_path FROM sessions WHERE id = ?").get(l);if(!p?.file_path)return i.json({error:"session not found"},404);v.unlinkSession(l),Xs(l),await bn(p.file_path);let g=Te(l);return i.json({ok:!0,alias:g,linked_pid:v.all().find(_=>v.sessionsFor(_.shell_pid).includes(l))?.shell_pid??null})}),t.get("/api/paste-expand",async i=>{let l=i.req.query("session"),p=i.req.query("message"),g=i.req.query("path");if(!l||!p||!g)return i.json({error:"session, message and path are required"},400);let _=f(),E=_.prepare("SELECT rowid, content_text FROM messages WHERE uuid = ? AND session_id = ?").get(p,l);if(!E)return i.json({error:"message not found in session"},404);let y=g.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");if(!new RegExp(`\\[Pasted text #\\d+ \\+\\d+ lines\\]\\s*${y}`).test(E.content_text??""))return i.json({error:"path not referenced by this message"},403);let N=_.prepare(`SELECT content_text FROM messages
1957
+ ORDER BY COALESCE(timestamp, ''), rowid`).all(p),N=uc(y,k,{mode:g,includeSidechain:_,prelude:E});return i.text(N)}),t.get("/api/collections",i=>{let l=i.req.query("archived")==="1";return i.json({collections:Sc(l)})}),t.get("/api/collections/:id",i=>{let l=i.req.param("id"),p=He(l);if(!p)return i.json({error:"not found"},404);let g=Tc(l,!0);return i.json({collection:p,members:g})}),t.post("/api/collections",async i=>{let l=await i.req.json().catch(()=>null);if(!l||typeof l.name!="string")return i.json({error:"name required"},400);try{let p=Vt({name:l.name,description:l.description??null,icon:l.icon??null,color:l.color??null,parent_id:l.parent_id??null,sort_key:l.sort_key});return i.json(p,201)}catch(p){return i.json({error:p.message},400)}}),t.patch("/api/collections/:id",async i=>{let l=i.req.param("id"),p=await i.req.json().catch(()=>null);if(!p)return i.json({error:"body required"},400);try{let g=wc(l,p);return i.json(g)}catch(g){return i.json({error:g.message},400)}}),t.post("/api/collections/:id/archive",i=>{let l=i.req.param("id");try{let p=Rc(l);return i.json(p)}catch(p){return i.json({error:p.message},404)}}),t.post("/api/collections/:id/restore",i=>{let l=i.req.param("id");try{let p=kc(l);return i.json(p)}catch(p){return i.json({error:p.message},404)}}),t.post("/api/collections/:id/members",async i=>{let l=i.req.param("id"),p=await i.req.json().catch(()=>null);if(!p||typeof p.session_id!="string")return i.json({error:"session_id required"},400);try{let g=Zt(l,p.session_id,p.note??null);return i.json(g)}catch(g){return i.json({error:g.message},400)}}),t.delete("/api/collections/:id/members/:sid",i=>{let l=i.req.param("id"),p=i.req.param("sid");try{let g=Ac(l,p);return i.json(g)}catch(g){return i.json({error:g.message},400)}}),t.get("/api/sessions/:id/collections",i=>{let l=i.req.param("id");return i.json({collections:yc(l)})});let R=["cwd-prefix","project-id","tag","plan-file","git-branch-prefix"];t.get("/api/auto-collections/rules",i=>i.json({rules:Cc()})),t.post("/api/auto-collections/rules",async i=>{let l=await i.req.json().catch(()=>null);if(!l||typeof l.name!="string"||typeof l.pattern!="string"||!l.type||!R.includes(l.type))return i.json({error:"name, type, pattern required (type must be a known matcher)"},400);try{let p=zr({name:l.name,type:l.type,pattern:l.pattern,collection_id:l.collection_id,parent_collection_id:l.parent_collection_id,priority:l.priority,enabled:l.enabled});return i.json(p,201)}catch(p){return i.json({error:p.message},400)}}),t.patch("/api/auto-collections/rules/:id",async i=>{let l=i.req.param("id"),p=await i.req.json().catch(()=>null);if(!p)return i.json({error:"body required"},400);try{let g=Ic(l,p);return i.json(g)}catch(g){return i.json({error:g.message},400)}}),t.delete("/api/auto-collections/rules/:id",i=>{let l=i.req.param("id");try{let p=vc(l);return i.json(p)}catch(p){return i.json({error:p.message},400)}}),t.get("/api/auto-collections/suggestions",i=>{let l=i.req.query("dismissed")==="1";return i.json({suggestions:en({includeDismissed:l})})}),t.post("/api/auto-collections/suggestions/:id/accept",i=>{let l=i.req.param("id");try{let p=Mc(l);return i.json({rule:p})}catch(p){return i.json({error:p.message},400)}}),t.post("/api/auto-collections/suggestions/:id/dismiss",i=>{let l=i.req.param("id");try{return jc(l),i.json({ok:!0})}catch(p){return i.json({error:p.message},400)}}),t.post("/api/auto-collections/detect",i=>{let l=tn();return i.json({suggestions:l})}),t.get("/api/auto-collections/suggestions/:id/preview",i=>{let l=i.req.param("id"),p=Math.max(1,Math.min(20,Number(i.req.query("limit"))||3)),_=en({includeDismissed:!1}).find(y=>y.id===l);if(!_)return i.json({error:"suggestion not found"},404);let E=Oc(_.type,_.pattern,p);return i.json({sessions:E})}),t.get("/api/auto-collections/parents",i=>{let l=Array.from(Dc());return i.json({auto_collection_ids:l})}),t.get("/api/threads",i=>{let l=i.req.query("archived")==="1";return i.json({threads:Qr({includeArchived:l})})}),t.get("/api/threads/:id",i=>{let l=i.req.param("id"),p=oe(l);if(!p)return i.json({error:"thread not found"},404);let g=p.edges.map(_=>({..._,alias_source:_.alias==null?null:v.isSessionAutoLinked(_.session_id)?"auto":"manual"}));return i.json({thread:{...p,edges:g}})}),t.post("/api/threads",async i=>{let l=await i.req.json().catch(()=>({}));if(!l.name)return i.json({error:"name required"},400);try{let p=nn({name:l.name,summary:l.summary??null,originSessionId:l.originSessionId});return i.json({thread:p})}catch(p){return i.json({error:p.message},400)}}),t.patch("/api/threads/:id",async i=>{let l=i.req.param("id"),p=await i.req.json().catch(()=>({}));try{p.name&&Jc(l,p.name),p.close&&Gc(l),p.reopen&&Yc(l),p.archive&&zc(l),"folder_id"in p&&_l(l,p.folder_id??null);let g=oe(l);return g?i.json({thread:g}):i.json({error:"thread not found"},404)}catch(g){return i.json({error:g.message},400)}}),t.get("/api/thread-folders",i=>i.json({folders:no()})),t.post("/api/thread-folders",async i=>{let l=await i.req.json().catch(()=>({}));if(!l.name||typeof l.name!="string")return i.json({error:"name required"},400);try{let p=ll({name:l.name,parentFolderId:l.parent_folder_id??null,projectScope:l.project_scope??null});return i.json({folder:p})}catch(p){return i.json({error:p.message},400)}}),t.patch("/api/thread-folders/:id",async i=>{let l=i.req.param("id"),p=await i.req.json().catch(()=>({}));try{let g;return p.name&&(g=dl(l,p.name)),"parent_folder_id"in p&&(g=pl(l,p.parent_folder_id??null)),g?i.json({folder:g}):i.json({error:"no patch fields"},400)}catch(g){return i.json({error:g.message},400)}}),t.delete("/api/thread-folders/:id",i=>{let l=i.req.param("id");try{return gl(l),i.json({ok:!0})}catch(p){return i.json({error:p.message},400)}}),t.post("/api/thread-folders/reorder",async i=>{let l=await i.req.json().catch(()=>({})),p=l.ordered_ids;if(!Array.isArray(p))return i.json({error:"ordered_ids must be an array"},400);try{return ml(l.parent_folder_id??null,l.project_scope??null,p),i.json({ok:!0})}catch(g){return i.json({error:g.message},400)}}),t.post("/api/threads/:id/sessions",async i=>{let l=i.req.param("id"),p=await i.req.json().catch(()=>({}));if(!p.sessionId)return i.json({error:"sessionId required"},400);try{let g=rn({threadId:l,sessionId:p.sessionId,parentSessionId:p.parentSessionId??null,role:p.role});return i.json({edge:g})}catch(g){return i.json({error:g.message},400)}}),t.delete("/api/threads/:id/sessions/:sessionId",i=>{let l=i.req.param("id"),p=i.req.param("sessionId"),g=Xc(l,p);return i.json(g)}),t.patch("/api/threads/:id/sessions/:sessionId",async i=>{let l=i.req.param("id"),p=i.req.param("sessionId"),g=await i.req.json().catch(()=>({}));try{let _=Qt(l,p,g.parentSessionId??null);return i.json({edge:_})}catch(_){return i.json({error:_.message},400)}}),t.post("/api/threads/:id/merge",async i=>{let l=i.req.param("id"),p=await i.req.json().catch(()=>({}));if(!p.sourceId)return i.json({error:"sourceId required"},400);try{let g=Kc(p.sourceId,l);return i.json({thread:g})}catch(g){return i.json({error:g.message},400)}}),t.post("/api/threads/:id/split",async i=>{let l=i.req.param("id"),p=await i.req.json().catch(()=>({}));if(!p.sessionIds?.length||!p.newThreadName)return i.json({error:"sessionIds and newThreadName required"},400);try{let g=Vc({threadId:l,sessionIds:p.sessionIds,newThreadName:p.newThreadName});return i.json({thread:g})}catch(g){return i.json({error:g.message},400)}}),t.get("/api/sessions/:id/threads",i=>{let l=i.req.param("id");return i.json({threads:qc(l)})});let j=M.object({enabled:M.boolean(),band_lo:M.number().min(0).max(1).optional(),band_hi:M.number().min(0).max(1).optional(),model:M.string().regex(/^[a-zA-Z0-9._-]{1,100}$/).optional()}).optional(),x=M.object({project:M.string().min(1),threshold:M.number().min(0).max(1).optional(),model:M.string().regex(/^[a-zA-Z0-9._-]{1,100}$/).optional(),llm_rescore:j});t.post("/api/threads/scan/preflight",async i=>{let l=Ce(i);if(l)return l;let p=await i.req.json().catch(()=>null),g=x.safeParse(p);if(!g.success)return i.json({error:"invalid request body",details:g.error.format()},400);let _=_d({project:g.data.project,threshold:g.data.threshold,model:g.data.model,llm_rescore:g.data.llm_rescore});return"error"in _?i.json({error:_.error},400):i.json(_)});let q=M.object({project:M.string().min(1),threshold:M.number().min(0).max(1).optional(),llm_names:M.boolean().optional(),model:M.string().regex(/^[a-zA-Z0-9._-]{1,100}$/).optional(),llm_rescore:j});t.post("/api/threads/scan/apply",async i=>{let l=Ce(i);if(l)return l;let p=await i.req.json().catch(()=>null),g=q.safeParse(p);if(!g.success)return i.json({error:"invalid request body",details:g.error.format()},400);let _=hd({project:g.data.project,threshold:g.data.threshold,llm_names:g.data.llm_names,model:g.data.model,llm_rescore:g.data.llm_rescore});return"error"in _?i.json({error:_.error},400):i.json({jobId:_.jobId,reused:_.reused},_.reused?409:200)}),t.get("/api/threads/scan/jobs/:jobId/stream",i=>{let l=i.req.param("jobId");if(!Fo(l))return i.json({error:"job not found"},404);let g=Number(i.req.header("Last-Event-ID")??0);return Ye(i,async _=>{let E=!1,y=setInterval(()=>{E||_.writeSSE({event:"heartbeat",data:""}).catch(()=>{E=!0})},15e3);try{for await(let k of Ed(l,g))if(E||(await _.writeSSE({id:String(k.id),event:k.kind,data:JSON.stringify(k.data)}),k.kind==="done"))break}finally{E=!0,clearInterval(y)}})}),t.get("/api/threads/scan/jobs/:jobId",i=>{let l=Fo(i.req.param("jobId"));return l?i.json(l):i.json({error:"job not found"},404)}),t.delete("/api/threads/scan/jobs/:jobId",i=>{let l=Ce(i);return l||(bd(i.req.param("jobId"))?i.json({ok:!0}):i.json({error:"job not found or already done"},404))});let D=M.object({project_id:M.number().int().positive(),mode:M.enum(["preflight","apply"]),window_hours:M.number().min(.5).max(168).optional(),score_threshold:M.number().min(0).max(1).optional(),use_live_pids:M.boolean().optional()});t.post("/api/threads/sync-active",async i=>{let l=await i.req.json().catch(()=>null),p=D.safeParse(l);if(!p.success)return i.json({error:"invalid request body",details:p.error.format()},400);try{let g=await il(p.data.project_id,{windowHours:p.data.window_hours,scoreThreshold:p.data.score_threshold,useLivePids:p.data.use_live_pids});if(p.data.mode==="preflight")return i.json({plan:g});let _=al(g);return i.json({plan:g,result:_})}catch(g){return i.json({error:g.message},400)}}),t.get("/api/threads/:id/titles/preflight",i=>{let l=i.req.param("id"),p=oe(l);if(!p)return i.json({error:"thread not found"},404);let g=f(),_=0;for(let E of p.edges)g.prepare("SELECT auto_title_source FROM sessions WHERE id = ?").get(E.session_id)?.auto_title_source==="agent"&&(_+=1);return i.json({total:p.edges.length,alreadyTitled:_,untitled:p.edges.length-_})}),t.post("/api/threads/:id/titles/generate",async i=>{let l=i.req.param("id"),p=await i.req.json().catch(()=>({})),g=oe(l);if(!g)return i.json({error:"thread not found"},404);if(g.edges.length===0)return i.json({error:"thread has no sessions"},400);let _=Fe(),E=p.model??_.model,y=ql({threadId:l,force:p.force??!1,model:E});return i.json({jobId:y})}),t.get("/api/jobs/:jobId/stream",i=>{let l=i.req.param("jobId");if(!uo(l))return i.json({error:"job not found"},404);let g=Number(i.req.header("Last-Event-ID")??0);return Ye(i,async _=>{let E=!1,y=setInterval(()=>{E||_.writeSSE({event:"heartbeat",data:""}).catch(()=>{E=!0})},15e3);try{for await(let k of Xl(l,g))if(E||(await _.writeSSE({id:String(k.id),event:k.kind,data:JSON.stringify(k.data)}),k.kind==="done"))break}finally{E=!0,clearInterval(y)}})}),t.get("/api/jobs/:jobId",i=>{let l=uo(i.req.param("jobId"));return l?i.json(l):i.json({error:"job not found"},404)}),t.delete("/api/jobs/:jobId",i=>Jl(i.req.param("jobId"))?i.json({ok:!0}):i.json({error:"job not found or already done"},404)),t.post("/api/terminal/opened",async i=>{let l=await i.req.json().catch(()=>null);if(!l||typeof l.shell_pid!="number"||typeof l.tab_name!="string")return i.json({error:"shell_pid and tab_name required"},400);let p=v.claimPidOwnership(l.shell_pid,l.extension_instance_id??null);if(p==="rejected")return i.json({ok:!0,rejected:!0,reason:"pid-owned-by-other-extension-instance",count:v.size()});let g=v.upsert({shell_pid:l.shell_pid,tab_name:l.tab_name,cwd:l.cwd??null,opened_at:l.opened_at??new Date().toISOString()});return i.json({ok:!0,ownership:p,count:v.size(),entry:g})}),t.post("/api/terminal/renamed",async i=>{let l=await i.req.json().catch(()=>null);if(!l||typeof l.shell_pid!="number"||typeof l.tab_name!="string")return i.json({error:"shell_pid and tab_name required"},400);let p=v.claimPidOwnership(l.shell_pid,l.extension_instance_id??null);if(p==="rejected")return i.json({ok:!0,rejected:!0,reason:"pid-owned-by-other-extension-instance"});let g=v.rename(l.shell_pid,l.tab_name);if(!g)return i.json({error:"unknown shell_pid"},404);let _=jp(l.shell_pid,l.tab_name);return i.json({ok:!0,ownership:p,entry:g,propagated:_})}),t.post("/api/terminal/closed",async i=>{let l=await i.req.json().catch(()=>null);if(!l||typeof l.shell_pid!="number")return i.json({error:"shell_pid required"},400);let p=v.remove(l.shell_pid);return i.json({ok:!0,removed:p,count:v.size()})}),t.post("/api/terminal/claude-started",async i=>{let l=await i.req.json().catch(()=>null);return!l||typeof l.shell_pid!="number"?i.json({error:"shell_pid required"},400):(v.pushPending({shell_pid:l.shell_pid,tab_name:typeof l.tab_name=="string"?l.tab_name:"",cwd:typeof l.cwd=="string"?l.cwd:null,started_at:typeof l.started_at=="string"?l.started_at:new Date().toISOString()}),i.json({ok:!0,pending:v.pendingSize()}))}),t.post("/api/terminal/output",async i=>{let l=await i.req.json().catch(()=>null);if(!l||typeof l.shell_pid!="number"||typeof l.text!="string")return i.json({error:"shell_pid and text required"},400);let p=l.text.length>8192?l.text.slice(-8192):l.text,g=typeof l.captured_at=="string"?l.captured_at:new Date().toISOString();return v.setOutputTail(l.shell_pid,p,g),i.json({ok:!0})}),t.post("/api/terminal/sync",async i=>{let l=await i.req.json().catch(()=>null);if(!l||!Array.isArray(l.terminals))return i.json({error:"terminals array required"},400);let p=new Map;for(let L of v.all())p.set(L.shell_pid,L.tab_name);let g=l.terminals.filter(L=>!!L&&typeof L.shell_pid=="number"&&typeof L.tab_name=="string").map(L=>({shell_pid:L.shell_pid,tab_name:L.tab_name,cwd:L.cwd??null,opened_at:L.opened_at??new Date().toISOString()})),_=l.extension_instance_id??null,E=[],y=g.filter(L=>{let F=v.claimPidOwnership(L.shell_pid,_);return E.push({shell_pid:L.shell_pid,ownership:F}),F!=="rejected"}),k=v.sync(y),N=0;for(let L of y){let F=p.get(L.shell_pid),W=v.get(L.shell_pid)?.tab_name??L.tab_name,G=!!W&&!de(W)&&!ce(W)?W:L.tab_name;F!==void 0&&F!==G&&(N+=jp(L.shell_pid,G))}let A=E.filter(L=>L.ownership==="rejected").length;A>0&&console.log(`[terminal/sync] dropped ${A} tab_name update(s), pid(s) owned by a different extension instance`);let O=await Py(),C={resolved:0,expired:0};try{C=nu()}catch{}let I={rebound:0,ghosts:0,ambiguous:0};try{I=su()}catch{}return Ky(),i.json({ok:!0,count:v.size(),diff:k,propagated:N,live_sweep:O,deferred_resolved:C,rebound:I})}),t.get("/api/terminal/registry",i=>i.json({terminals:v.all(),count:v.size()})),t.get("/api/terminal/sessions/:shellPid",i=>{let l=i.req.param("shellPid"),p=Number(l);if(!Number.isInteger(p)||p<=0)return i.json({error:"shellPid must be a positive integer"},400);let g=v.sessionsFor(p);return i.json({shell_pid:p,sessions:g})}),t.get("/api/sessions/:id/linked-terminal",i=>{let l=i.req.param("id"),p=v.all().find(_=>v.sessionsFor(_.shell_pid).includes(l)),g=[];if(!p){let _=f().prepare("SELECT cwd, started_at FROM sessions WHERE id = ?").get(l);if(_?.cwd&&_.started_at){let E=Date.parse(_.started_at);if(Number.isFinite(E)){let y=_.cwd.replace(/\/+$/,""),k=300*1e3;for(let N of v.all()){if(!N.cwd||N.cwd.replace(/\/+$/,"")!==y||de(N.tab_name))continue;let A=Date.parse(N.opened_at),O=Date.parse(N.last_seen_at);!Number.isFinite(A)||!Number.isFinite(O)||A>E||O+k<E||g.push({shell_pid:N.shell_pid,tab_name:N.tab_name,cwd:N.cwd,opened_at:N.opened_at,last_seen_at:N.last_seen_at,reason:"time-overlap"})}g.sort((N,A)=>Date.parse(A.last_seen_at)-Date.parse(N.last_seen_at))}}}return p?i.json({linked:{shell_pid:p.shell_pid,tab_name:p.tab_name,cwd:p.cwd},suggested:[]}):i.json({linked:null,suggested:g})}),t.post("/api/sessions/:id/auto-relink",async i=>{let l=i.req.param("id");if(Te(l))return i.json({applied:!1,reason:"has-alias"});if(v.all().some(y=>v.sessionsFor(y.shell_pid).includes(l)))return i.json({applied:!1,reason:"already-linked"});let g=f().prepare("SELECT cwd, git_branch, started_at FROM sessions WHERE id = ?").get(l);if(!g?.cwd)return i.json({applied:!1,reason:"no-cwd"});let _=g.cwd.replace(/\/+$/,""),E=v.all().filter(y=>y.cwd&&y.cwd.replace(/\/+$/,"")===_&&!de(y.tab_name));if(E.length===1){let y=E[0],k=Kt({sessionStartedAt:g.started_at??null,terminalOpenedAt:y.opened_at??null});if(!k.allowed)return i.json({applied:!1,reason:"temporal-guard",guard_reason:k.reason});let N=v.getOrigin(l),A=wt({tabName:y.tab_name,origin:N??null,cwd:g.cwd??null,gitBranch:g.git_branch??null});return A?(he(l,A),v.linkSession(l,y.shell_pid),i.json({applied:!0,alias:A,linked_pid:y.shell_pid,linked_tab_name:y.tab_name,method:"cwd-singleton"})):i.json({applied:!1,reason:"no-usable-name"})}if(E.length>1){let y=await tu(l);if(y){let N=v.get(y.shell_pid),A=Kt({sessionStartedAt:g.started_at??null,terminalOpenedAt:N?.opened_at??null});if(!A.allowed)return i.json({applied:!1,reason:"temporal-guard",guard_reason:A.reason});let O=v.getOrigin(l),C=wt({tabName:y.tab_name,origin:O??null,cwd:g.cwd??null,gitBranch:g.git_branch??null});if(C)return he(l,C),v.linkSession(l,y.shell_pid),i.json({applied:!0,alias:C,linked_pid:y.shell_pid,linked_tab_name:y.tab_name,matched_fingerprints:y.matched_fingerprints,method:"content-match"})}let k=6e4;if(g.started_at){let N=Date.parse(g.started_at);if(Number.isFinite(N)){let A=E.filter(C=>Kt({sessionStartedAt:g.started_at,terminalOpenedAt:C.opened_at??null}).allowed).map(C=>({t:C,gap:N-Date.parse(C.opened_at??"")})).filter(C=>Number.isFinite(C.gap)&&C.gap>=0&&C.gap<=k);if(A.length>=2)return i.json({applied:!1,reason:"ambiguous-temporal",candidate_count:A.length});let O=A[0];if(O){let C=v.getOrigin(l),I=wt({tabName:O.t.tab_name,origin:C??null,cwd:g.cwd??null,gitBranch:g.git_branch??null});if(I)return he(l,I),v.linkSession(l,O.t.shell_pid),i.json({applied:!0,alias:I,linked_pid:O.t.shell_pid,linked_tab_name:O.t.tab_name,method:"closest-before-temporal",gap_ms:O.gap})}}}return i.json({applied:!1,reason:"ambiguous",candidate_count:E.length})}return i.json({applied:!1,reason:"no-candidates"})}),t.post("/api/sessions/:id/relink",async i=>{let l=i.req.param("id"),p=await i.req.json().catch(()=>null);if(p?.clear)return v.unlinkSession(l),Xs(l),i.json({ok:!0,alias:null,linked_pid:null});if(!p||typeof p.shell_pid!="number")return i.json({error:"shell_pid required"},400);let g=v.get(p.shell_pid);if(!g)return i.json({error:"terminal not registered"},404);let _=v.getOrigin(l),E=f().prepare("SELECT cwd, git_branch FROM sessions WHERE id = ?").get(l),y=null,k=g.tab_name?.trim()??"";if(k&&!de(k)&&!ce(k))y=k;else if(k&&ce(k)){let N=yt(k);N&&!de(N)&&(y=N)}return y?(v.unlinkSession(l),he(l,y),i.json({ok:!0,alias:y,linked_pid:p.shell_pid,linked_tab_name:g.tab_name})):i.json({error:"terminal has no usable name, name the tab in your editor first, then retry the relink"},422)}),t.post("/api/sessions/:id/recorrelate",async i=>{let l=i.req.param("id"),p=f().prepare("SELECT file_path FROM sessions WHERE id = ?").get(l);if(!p?.file_path)return i.json({error:"session not found"},404);v.unlinkSession(l),Xs(l),await bn(p.file_path);let g=Te(l);return i.json({ok:!0,alias:g,linked_pid:v.all().find(_=>v.sessionsFor(_.shell_pid).includes(l))?.shell_pid??null})}),t.get("/api/paste-expand",async i=>{let l=i.req.query("session"),p=i.req.query("message"),g=i.req.query("path");if(!l||!p||!g)return i.json({error:"session, message and path are required"},400);let _=f(),E=_.prepare("SELECT rowid, content_text FROM messages WHERE uuid = ? AND session_id = ?").get(p,l);if(!E)return i.json({error:"message not found in session"},404);let y=g.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");if(!new RegExp(`\\[Pasted text #\\d+ \\+\\d+ lines\\]\\s*${y}`).test(E.content_text??""))return i.json({error:"path not referenced by this message"},403);let N=_.prepare(`SELECT content_text FROM messages
1958
1958
  WHERE session_id = ? AND rowid > ?
1959
- ORDER BY rowid ASC LIMIT 10`).all(l,E.rowid);for(let A of N){let L=A.content_text??"";if(L.includes("**Tool result**")&&L.includes(g))return i.json({source:"tool-result",content:L});if(/^\s*1\t/.test(L)&&L.length>200)return i.json({source:"tool-result",content:L})}try{let A=await jy(g),L=Dy();if(!A.startsWith(L+"/")&&!A.startsWith(L+"\\"))return i.json({error:"path outside allowed root"},403);let C=[".ssh",".gnupg",".gpg",".aws",".kube",".docker",".password-store"],O=A.slice(L.length+1).split("/")[0].split("\\")[0];if(C.includes(O))return i.json({error:"path inside sensitive directory"},403);let F=await Iy(A),W=2*1024*1024;if(F.size>W)return i.json({error:"file too large",size:F.size,max:W},413);let K=await vy(A,"utf8");return i.json({source:"disk",content:K})}catch(A){return i.json({source:"missing",error:A.message})}}),t.get("/api/projects/:name/stats",i=>{let l=f(),p=i.req.param("name"),g=l.prepare(`SELECT
1959
+ ORDER BY rowid ASC LIMIT 10`).all(l,E.rowid);for(let A of N){let O=A.content_text??"";if(O.includes("**Tool result**")&&O.includes(g))return i.json({source:"tool-result",content:O});if(/^\s*1\t/.test(O)&&O.length>200)return i.json({source:"tool-result",content:O})}try{let A=await jy(g),O=Dy();if(!A.startsWith(O+"/")&&!A.startsWith(O+"\\"))return i.json({error:"path outside allowed root"},403);let C=[".ssh",".gnupg",".gpg",".aws",".kube",".docker",".password-store"],L=A.slice(O.length+1).split("/")[0].split("\\")[0];if(C.includes(L))return i.json({error:"path inside sensitive directory"},403);let F=await Iy(A),W=2*1024*1024;if(F.size>W)return i.json({error:"file too large",size:F.size,max:W},413);let K=await vy(A,"utf8");return i.json({source:"disk",content:K})}catch(A){return i.json({source:"missing",error:A.message})}}),t.get("/api/projects/:name/stats",i=>{let l=f(),p=i.req.param("name"),g=l.prepare(`SELECT
1960
1960
  (SELECT COUNT(*) FROM sessions s JOIN projects p ON p.id=s.project_id WHERE p.name=? AND s.message_count > 2) AS sessions,
1961
1961
  (SELECT COALESCE(SUM(s.message_count), 0) FROM sessions s JOIN projects p ON p.id=s.project_id WHERE p.name=?) AS messages,
1962
1962
  (SELECT MIN(s.started_at) FROM sessions s JOIN projects p ON p.id=s.project_id WHERE p.name=? AND s.started_at IS NOT NULL) AS earliest,