pi-blackhole 0.5.0 → 0.5.1
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/CHANGELOG.md +8 -0
- package/dist/index.js +2 -2
- package/package.json +1 -1
- package/src/om/inline-compaction.ts +17 -5
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
+
## [0.5.1] - 2026-09-06
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **Host inline-compaction adapter now prefers pi's already-loaded bundled runtime chunk over its `dist/index.js` barrel.** Probe on pi 0.85.1 showed the barrel import cost 484ms (fresh graph) while the bundled chunk pi already loaded cost 5ms (cache-hit); the adapter tried the barrel first and kept iterating after a successful chunk patch. Reordered candidates so the chunk is tried first per host path, with the barrel only as a fallback for roots that never resolve a chunk, while still patching every unique host identity. Adapter total 506ms → 16ms, factory ~490ms saved.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
5
13
|
## [0.5.0] - 2026-09-06
|
|
6
14
|
|
|
7
15
|
### Added
|
package/dist/index.js
CHANGED
|
@@ -20,7 +20,7 @@ ${fm}
|
|
|
20
20
|
`)||t.includes("\r")?"Must be a single-line string (no newlines)":void 0;case "text":return typeof t!="string"?`Must be a string, got ${typeof t}`:void 0;case "number":{if(typeof t!="number"||!Number.isFinite(t))return `Must be a finite number, got ${typeof t=="number"?String(t):typeof t}`;if(e.integer&&!Number.isInteger(t))return "Must be an integer";if(e.values!==void 0&&e.values.length>0&&!e.values.includes(t))return `Must be one of: ${e.values.join(", ")}`;if(typeof e.min=="number"&&t<e.min)return `Must be at least ${e.min}`;if(typeof e.max=="number"&&t>e.max)return `Must be at most ${e.max}`;if(e.step!==void 0&&e.step>0){let n=e.min??0,r=(t-n)%e.step;if(r>1e-9&&e.step-r>1e-9)return `Must be aligned to step ${e.step}`}return}case "secret":return typeof t!="string"?`Must be a string, got ${typeof t}`:void 0;case "path":return typeof t!="string"?`Must be a string, got ${typeof t}`:void 0;case "model":{if(typeof t!="object"||t===null)return `Must be an object with id, got ${typeof t}`;let n=t;return typeof n.id!="string"?`Must have a string id, got ${typeof n.id}`:void 0}case "action":case "custom":return;default:return}}var pi=P(()=>{});function Mg(e,t){if(e.tabs.length===0)return "";let n=[],o=e._keyToTabCache;if(!o||o.rows!==e.rows||o.rows.length!==e.rows.length){let i=new Map,s=e.tabs[0]?.id;for(let a of e.rows)i.set(a.field.key,a.field.tab??s);o={rows:e.rows,map:i},e._keyToTabCache=o;}let r=new Set;if(e.isBuffered&&e.dirtyKeys.size>0)for(let i of e.dirtyKeys){let s=o.map.get(i);s!==void 0&&r.add(s);}for(let i of e.tabs){let s=i.label;e.isBuffered&&r.has(i.id)&&(s+=" \u25CF Unsaved");let a=e.tabActionFocus>=0&&e.tabActionFocus<e.tabs.length&&e.tabs[e.tabActionFocus]?.id===i.id;if(i.id===e.activeTabId){let d=` ${a?"\u25B6":"\u25B8"} ${s} `;n.push(e.args.theme.fg("accent",e.args.theme.inverse(e.args.theme.bold(d))));}else if(a){let l=` \u25B6 ${s} `;n.push(e.args.theme.fg("accent",e.args.theme.inverse(e.args.theme.bold(l))));}else {let l=` ${s} `;n.push(e.args.theme.bg("selectedBg",e.args.theme.fg("accent",l)));}}return Oe(n.join(" "),t)}function Og(e,t,n=true){let o=e.args.theme.inverse(" "),r;if(e.search===""){let i=e.options.searchPlaceholder;if(!i){let a=e.tabs.find(l=>l.id===e.activeTabId)??e.tabs[0];i=a?`Search ${a.label}...`:"Search settings...";}let s=e.args.theme.fg("muted",i);r=` > ${o}${s}`;}else n?r=` > ${e.search}${o}`:r=` > ${e.args.theme.fg("warning",e.search)}${o}`;return e.args.theme.bg("toolPendingBg",Oe(r,t))}function yc(e,t,n){let o=Xe(e),r=[];o&&!e.options.readOnly&&o.field.type!=="section"&&!o.field.disabled&&(r=[...t(o.field).hints({field:o.field,value:o.value},{isEditing:o.isEditing})],!o.isEditing&&o.field.default!==void 0&&(r.some(u=>u.key==="alt+r"||u.key==="ctrl+r")||r.push({key:"alt+r",label:"reset"})));let i=[];(e.tabs.length>0||(e.options.actions?.length??0)>0)&&i.push({key:"tab/shift+tab",label:"cycle"}),e.tabActionFocus>=0&&!e.options.readOnly&&i.push({key:"\u2190/\u2192",label:"navigate"}),i.push({key:"\u2191\u2193",label:"move"}),!e.options.readOnly&&!o?.isEditing&&o?.field.reorderable&&i.push({key:"alt+\u2191\u2193",label:"reorder"});let s=e._anyFieldHasDefault;s===void 0&&(s=e.fields.some(c=>c.default!==void 0),e._anyFieldHasDefault=s),s&&!e.options.readOnly&&!o?.isEditing&&i.push({key:"ctrl+r",label:"reset field"}),e.options.readOnly&&e.tabs.length>0&&(e.options.actions?.length??0)>0&&i.push({key:"\u2190 \u2192",label:"navigate"}),e.isBuffered&&i.push({key:"ctrl+s",label:"save"}),e.options.enableSearch&&!o?.isEditing&&(e.search===""?i.push({key:"type",label:"to search"}):(i.push({key:"ctrl+w",label:"delete word"}),i.push({key:"ctrl+u",label:"clear"})));let a=[],l=e.options.actions?.length??0;e.tabActionFocus>=e.tabs.length&&e.tabActionFocus<e.tabs.length+l&&l>0?a.push({key:"enter",label:"trigger action"}):a.push(...r),e.confirm?(a.push({key:"\u2191\u2193",label:"select"}),a.push({key:"enter/space",label:"confirm"}),a.push({key:"esc",label:"cancel"})):e.options.enableSearch&&e.search!==""&&!o?.isEditing?a.push({key:"esc",label:"clear search"}):e.isBuffered&&ht(e)?a.push({key:"esc",label:"confirm \u2192"}):a.push({key:"esc",label:"close"});let d=[re(i,e.args.theme)];return (a.length>1||a.length===1&&a[0].key!=="esc")&&d.push(re(a,e.args.theme)),d}function Ag(e,t,n,o,r,i){if(o.field.type==="section"){let R=o.field.value,E=`${i?e.args.theme.fg("accent","\u258C "):" "}${e.args.theme.fg("dim",R)}`;return i?e.args.theme.bg("selectedBg",Oe(E,r)):truncateToWidth(E,r,"\u2026")}let s=o._labelCache;if(!s||s.width!==r){let R=Math.min(55,Math.max(28,Math.floor(r*.5))),x=truncateToWidth(o.field.label,R,"\u2026"),E=" ".repeat(Math.max(1,R-visibleWidth(x)));s={width:r,labelText:x,labelPadding:E,labelAlloc:R},o._labelCache=s;}let{labelText:a,labelPadding:l,labelAlloc:d}=s,c=Math.max(1,r-d-4),u=o.field.disabled?true:o.field.dim,p=typeof u=="function"?u():u,m=p===true?"muted":p===false||i?"text":"muted",g=e.args.theme.fg(m,a),h=t(o.field).renderValue({field:o.field,value:o.value},{width:c,selected:i,isEditing:o.isEditing,ctx:n}),b=l,y=o._depthIndent;y===void 0&&(y=" ".repeat(o.field.depth??0),o._depthIndent=y);let w=i?e.args.theme.fg("accent",`${y}\u258C `):`${y} `,v="",k=o.field.valueNote;if(k){let R=typeof k=="function"?k():k;R&&(v+=` ${e.args.theme.fg("dim",R)}`);}let S=`${w}${g}${b}${h}${v}`;return i?e.args.theme.bg("selectedBg",Oe(S,r)):truncateToWidth(S,r,"\u2026")}function vc(e,t,n,o,r){let i=[];e.tabs.length>0&&i.push(Mg(e,o));let s=vn(e);e.options.enableSearch&&(i.length>0&&i.push(""),i.push(Og(e,o,s.length>0))),i.length>0&&(i.push(""),i.push(un(o,e.args.theme)));let a=e.pathNoteRef.current??e.pathNote;a&&i.push(e.args.theme.fg("dim",truncateToWidth(a,o,"\u2026")));let l=Math.max(3,r-i.length-2-Pg(e));mc(e,l);let d=s.length,c=s.slice(e.scroll,e.scroll+l);if(c.length===0)if(e.search){let p=e.args.theme.fg("muted"," No matching settings for '"),m=e.args.theme.fg("warning",e.search),g=e.args.theme.fg("muted","'. (press esc or ctrl+u to clear)");i.push(`${p}${m}${g}`);}else i.push(e.args.theme.fg("muted"," No settings available in this tab."));else {e.scroll>0&&i.push(e.args.theme.fg("dim",` \u2191 ${e.scroll} earlier`));for(let m=0;m<c.length;m++){let g=c[m],f=e.scroll+m,h=e.rows[g];i.push(Ag(e,t,n,h,o,f===e.fieldSelected));}let p=Math.max(0,d-(e.scroll+l));p>0&&i.push(e.args.theme.fg("dim",` \u2193 ${p} more`));}let u=Xe(e);for(u&&_g(e,i,o,u);i.length<r;)i.push("");return i}function _g(e,t,n,o){let r=o.field.description??"",i=o.field;if(i.disabled){let l="This setting is currently disabled.";r=r?`${r} (${l})`:l;}if(i.type==="number"){let l=[];if(i.values?l.push(`values: ${i.values.join(", ")}`):(typeof i.min=="number"&&typeof i.max=="number"?l.push(`range: ${i.min} to ${i.max}`):typeof i.min=="number"?l.push(`min: ${i.min}`):typeof i.max=="number"&&l.push(`max: ${i.max}`),i.integer&&l.push("integer only")),l.length>0){let d=`(${l.join(", ")})`;r=r?`${r} ${d}`:d;}}if(r){t.push("");for(let l of fe(r,Math.max(1,n-4)))t.push(e.args.theme.fg("muted",` ${l}`));}let s;if(i.type==="boolean"){let l=i.valueDescriptions;if(l){let d=o.value?"on":"off";s=l[d];}}else if(i.type==="enum"){let l=i.valueDescriptions;l&&(s=l[o.value]);}else if(i.type==="number"){let l=i.valueDescriptions;l&&(s=l[String(o.value)]);}if(s){t.push("");let l=i.disabled?"muted":"accent";for(let d of fe(e.args.theme.fg(l,s),Math.max(1,n-4)))t.push(` ${d}`);}let a=jt(o.field,o.value);if(a){t.push("");for(let l of fe(a,Math.max(1,n-4)))t.push(e.args.theme.fg("warning",` ${l}`));}}function Pg(e){let t=Xe(e);if(!t)return 0;let n=0;(t.field.description||t.field.disabled)&&(n=2);let o=t.field;return o.type==="number"&&(typeof o.min=="number"||typeof o.max=="number"||o.integer)&&(n=Math.max(n,2)),jt(t.field,t.value)&&(n=Math.max(n,1)),n}var kc=P(()=>{Ae();pi();Co();fi();});function mi(e,t){let n=e.tabs??[],o=e.fields,i=(e.mode??"immediate")==="buffered",s=e.readOnly??false,a=e.initialTab??n[0]?.id,l=o.map(C=>({field:C,value:pc(C),isEditing:false,searchIndex:`${C.label}
|
|
21
21
|
${C.description??""}
|
|
22
22
|
${C.key}`.toLowerCase()})),d=new Map,c=new Map,u=new Set;if(i)for(let C of l){let M=C.value;c.set(C.field.key,typeof M=="object"&&M!==null?structuredClone(M):M);}let p,m,g={theme:t.theme,tui:t.tui,ctx:t.ctx,requestRender:()=>t.tui.requestRender(),editStates:d},f={options:e,args:t,tabs:n,fields:o,rows:l,editStates:d,isBuffered:i,initialValues:c,dirtyKeys:u,cachedVisibleIndices:[],activeTabId:a,search:"",fieldSelected:0,scroll:0,tabActionFocus:s&&n.length>0&&(e.actions?.length??0)>0?n.length:-1,pathNote:e.pathNote??"",pathNoteRef:{current:e.pathNote??""},overlay:p,confirm:m};ie(f,ce);function h(C,M){f.overlay={component:C,title:M},f.args.tui.requestRender();}function b(){f.overlay=void 0,f.args.tui.requestRender();}function y(){return f.activeTabId}function w(C){for(let M of f.rows)C[M.field.key]!==void 0&&(M.value=C[M.field.key]);f.dirtyKeys.clear(),f.initialValues.clear();for(let M of f.rows){let T=M.value;f.initialValues.set(M.field.key,typeof T=="object"&&T!==null?structuredClone(T):T);}f.args.tui.requestRender();}function v(){f.confirm=mt({message:["You have unsaved changes."],confirmLabel:"Discard",cancelLabel:"Cancel",danger:true},C=>{f.confirm=void 0,C&&(f.options.onCancel?.(),f.args.close()),f.args.tui.requestRender();},{tui:f.args.tui,theme:f.args.theme}),f.args.tui.requestRender();}function k(C,M){C.isEditing=M;}function S(C){return uc[C.type]}function R(C){return typeof C.disabled=="function"?C.disabled()?`${C.label} (disabled)`:C.label:C.disabled?`${C.label} (disabled)`:C.label}function x(C){if(s)return;let M=Xe(f);if(!M)return;let T=S(M.field);try{if(T){let A=T.handleKey({field:M.field,value:M.value},C,{isEditing:M.isEditing,ctx:g,setEditing:j=>k(M,j)});A.commit!==void 0&&So(f,M,A.commit),A.submenu&&h(A.submenu(j=>{b(),j!==void 0&&So(f,M,j);}),`${M.field.key} \u2192`);}}catch(A){gt(f,f.args.ctx,A);}}function E(C){if(s)return true;let M=di(f);if(M===void 0)return false;let T=f.rows[M];if(!T?.field.reorderable)return false;let A=vn(f),L=A.indexOf(M)+C;if(L<0||L>=A.length)return true;let K=A[L],I=f.rows[K];if(!I?.field.reorderable)return true;let O=A.filter(B=>f.rows[B]?.field.reorderable),$=O.indexOf(M),U=O.indexOf(K);f.rows[M]=I,f.rows[K]=T,f.fieldSelected=L,ie(f,ce);try{f.options.onReorder?.({fieldKey:T.field.key,fromIndex:$,toIndex:U});}catch(B){gt(f,f.args.ctx,B);}return f.args.tui.requestRender(),true}async function _(){if(f.options.onSave)try{let C=bc(f),M=f.options.onSave(C);M&&typeof M.then=="function"&&await M,f.options.closeOnSave!==!1&&f.args.close();}catch(C){gt(f,f.args.ctx,C);}}function F(C){if(f.confirm){f.confirm.handleInput?.(C);return}if(f.overlay){f.overlay.component.handleInput?.(C);return}let M=Xe(f);if(matchesKey(C,"ctrl+s")){f.isBuffered&&f.options.onSave&&_();return}if(matchesKey(C,"ctrl+r")||matchesKey(C,"alt+r")){if(s)return;let O=Xe(f);if(O&&!O.field.disabled&&!O.isEditing){let $=O.field.default;if($!==void 0){let U=typeof $=="object"&&$!==null?structuredClone($):$;So(f,O,U),f.args.tui.requestRender();return}}f.args.tui.requestRender();return}if(matchesKey(C,"alt+up")&&E(-1)||matchesKey(C,"alt+down")&&E(1))return;if(M?.isEditing&&matchesKey(C,"ctrl+c")){f.isBuffered&&ht(f)?v():f.args.close();return}if(M?.isEditing){x(C),f.args.tui.requestRender();return}if(matchesKey(C,"escape")){if(f.tabActionFocus>=0){f.tabActionFocus=-1,f.args.tui.requestRender();return}if(f.options.enableSearch&&f.search!==""){f.search="",f.fieldSelected=0,ie(f,ce),f.args.tui.requestRender();return}f.isBuffered&&ht(f)?f.options.onRequestExit?f.options.onRequestExit():v():f.args.close();return}if(matchesKey(C,"ctrl+c")){f.isBuffered&&ht(f)?f.options.onRequestExit?f.options.onRequestExit():v():f.args.close();return}let T=f.options.actions??[],A=s?f.tabs.length:f.tabs.length+T.length;if(matchesKey(C,"tab")){if(A===0){f.args.tui.requestRender();return}if(f.tabActionFocus===-1){let O=f.tabs.findIndex($=>$.id===f.activeTabId);O>=0?f.tabActionFocus=(O+1)%A:f.tabActionFocus=0;}else if(f.tabActionFocus<f.tabs.length)f.tabActionFocus=(f.tabActionFocus+1)%A;else if(s){let O=f.tabs.findIndex(B=>B.id===f.activeTabId),$=O>=0?(O+1)%f.tabs.length:0,U=f.tabs[$];U&&U.id!==f.activeTabId&&(f.activeTabId=U.id,f.fieldSelected=0,f.scroll=0,ie(f,ce),f.options.onActiveTabChange?.(U.id)),f.args.tui.requestRender();return}else f.tabActionFocus=(f.tabActionFocus+1)%A;if(f.tabActionFocus<f.tabs.length){let O=f.tabs[f.tabActionFocus];O&&O.id!==f.activeTabId&&(f.activeTabId=O.id,f.fieldSelected=0,f.scroll=0,ie(f,ce),f.options.onActiveTabChange?.(O.id));}f.args.tui.requestRender();return}if(matchesKey(C,"shift+tab")){if(A===0){f.args.tui.requestRender();return}if(f.tabActionFocus===-1){let O=f.tabs.findIndex($=>$.id===f.activeTabId);O>=0?f.tabActionFocus=(O-1+A)%A:f.tabActionFocus=A-1;}else if(f.tabActionFocus<f.tabs.length)f.tabActionFocus=(f.tabActionFocus-1+A)%A;else if(s){let O=f.tabs.findIndex(B=>B.id===f.activeTabId),$=O>=0?(O-1+f.tabs.length)%f.tabs.length:f.tabs.length-1,U=f.tabs[$];U&&U.id!==f.activeTabId&&(f.activeTabId=U.id,f.fieldSelected=0,f.scroll=0,ie(f,ce),f.options.onActiveTabChange?.(U.id)),f.args.tui.requestRender();return}else f.tabActionFocus=(f.tabActionFocus-1+A)%A;if(f.tabActionFocus<f.tabs.length){let O=f.tabs[f.tabActionFocus];O&&O.id!==f.activeTabId&&(f.activeTabId=O.id,f.fieldSelected=0,f.scroll=0,ie(f,ce),f.options.onActiveTabChange?.(O.id));}f.args.tui.requestRender();return}let j=f.options.actions??[],L=f.tabs.length+j.length;if((matchesKey(C,"left")||matchesKey(C,"right"))&&L>0){if(f.tabActionFocus>=0){let O=matchesKey(C,"right");if(s&&f.tabs.length>0&&j.length>0)if(f.tabActionFocus<f.tabs.length)O?f.tabActionFocus=f.tabs.length:f.tabActionFocus=f.tabs.length+j.length-1;else {let $=j.length,U=f.tabActionFocus-f.tabs.length;f.tabActionFocus=f.tabs.length+(O?(U+1)%$:(U-1+$)%$);}else if(f.tabActionFocus=O?(f.tabActionFocus+1)%L:(f.tabActionFocus-1+L)%L,f.tabActionFocus<f.tabs.length){let $=f.tabs[f.tabActionFocus];$&&$.id!==f.activeTabId&&(f.activeTabId=$.id,f.fieldSelected=0,f.scroll=0,ie(f,ce),f.options.onActiveTabChange?.($.id));}f.args.tui.requestRender();return}if(f.tabActionFocus===-1&&s&&j.length>0){matchesKey(C,"right")?f.tabActionFocus=f.tabs.length:f.tabActionFocus=f.tabs.length+j.length-1,f.args.tui.requestRender();return}}let K=ci(f),I=Math.max(0,K-1);if((matchesKey(C,"up")||matchesKey(C,"down")||matchesKey(C,"pageUp")||matchesKey(C,"pageDown"))&&(f.tabActionFocus=-1),matchesKey(C,"up")){f.fieldSelected=Math.max(0,f.fieldSelected-1),f.args.tui.requestRender();return}if(matchesKey(C,"down")){f.fieldSelected=Math.min(I,f.fieldSelected+1),f.args.tui.requestRender();return}if(matchesKey(C,"pageUp")){f.fieldSelected=Math.max(0,f.fieldSelected-5),f.args.tui.requestRender();return}if(matchesKey(C,"pageDown")){f.fieldSelected=Math.min(I,f.fieldSelected+5),f.args.tui.requestRender();return}if((matchesKey(C,"enter")||matchesKey(C,"return"))&&f.tabActionFocus>=0){if(f.tabActionFocus<f.tabs.length)f.tabActionFocus=-1;else if(T.length>0){let O=f.tabActionFocus-f.tabs.length,$=T[O];$&&((typeof $.disabled=="function"?$.disabled():$.disabled)||f.options.onAction?.($.id)),f.args.tui.requestRender();return}}if(f.options.enableSearch){if(matchesKey(C,"backspace")||matchesKey(C,"ctrl+h")){f.search=f.search.slice(0,-1),f.fieldSelected=0,ie(f,ce),f.args.tui.requestRender();return}if(matchesKey(C,"ctrl+u")){f.search="",f.fieldSelected=0,ie(f,ce),f.args.tui.requestRender();return}if(matchesKey(C,"ctrl+w")){f.search=wo(f.search),f.fieldSelected=0,ie(f,ce),f.args.tui.requestRender();return}}x(C),f.options.enableSearch&&C.length===1&&C>=" "&&C!=="\x7F"&&!M?.isEditing&&(!M||!M.isEditing)&&(f.search+=C,f.fieldSelected=0,ie(f,ce)),f.args.tui.requestRender();}return {render(C){let M=pt(f.args.tui.terminal.rows??24,$g,14);if(f.confirm){let J=f.confirm.render(Ce(C)),ke={title:f.options.title?`${f.options.title} \u2014 Discard changes?`:"Discard changes?",fixedInnerRows:M};return je(J,C,f.args.theme,ke)}if(f.overlay){let J=f.overlay.component.render(Ce(C)),ke={title:f.overlay.title??f.options.title,fixedInnerRows:M};return je(J,C,f.args.theme,ke)}let T=Ce(C),A=yc(f,S),j=1+A.length,L=f.options.actions??[],K=L.length>0?2:0,I=j+K,O=M-I,$=vc(f,S,g,T,O),U=f.isBuffered&&ht(f)?` ${f.args.theme.fg("accent","\u25CF Unsaved")}`:"",B=f.options.title?`${f.options.title}${U}`:f.options.title,W=je($,C,f.args.theme,{title:B,fixedInnerRows:O}),te=W.pop(),ne=W.pop(),N=J=>f.args.theme.fg("borderAccent",J),H=ho,X=`${N("\u2502")}${" ".repeat(H)}${f.args.theme.fg("dim","\u2500".repeat(Math.max(1,T)))}${" ".repeat(H)}${N("\u2502")}`;W.push(X);for(let J of A){let se=` ${J}`;W.push(`${N("\u2502")}${" ".repeat(H)}${Oe(se,T)}${" ".repeat(H)}${N("\u2502")}`);}if(L.length>0){let J=`${N("\u2502")}${" ".repeat(H)}${f.args.theme.fg("dim","\u2500".repeat(Math.max(1,T)))}${" ".repeat(H)}${N("\u2502")}`;W.push(J);let se=[];for(let it=0;it<L.length;it++){let Yt=L[it],Ln=f.tabActionFocus>=f.tabs.length&&f.tabActionFocus-f.tabs.length===it,Re=` ${R(Yt)} `;Ln?se.push(f.args.theme.fg("accent",f.args.theme.inverse(f.args.theme.bold(Re)))):Yt.danger?se.push(f.args.theme.bg("selectedBg",f.args.theme.fg("warning",Re))):se.push(f.args.theme.bg("selectedBg",f.args.theme.fg("accent",Re)));}let ke=Oe(se.join(" "),T);W.push(`${N("\u2502")}${" ".repeat(H)}${ke}${" ".repeat(H)}${N("\u2502")}`);}return W.push(ne),W.push(te),W},invalidate(){f.overlay?.component.invalidate?.(),f.confirm?.invalidate?.();},handleInput:F,mountOverlay:h,dismissOverlay:b,getActiveTabId:y,setValues:w}}var $g,wc=P(()=>{fc();Ae();mn();li();Co();fi();kc();ti();$g=45;});function Ng(e,t=true,n=[]){let o={global:e.scopes.global,project:e.scopes.project,session:e.scopes.session&&e.sessionInitialized},r=[];t&&r.push({id:"display-all",label:"Display all settings",available:true});for(let i of Fg){let s=o[i],a={id:i,label:Dg[i],available:s,note:s?void 0:i==="session"&&e.scopes.session?"(session not initialized)":"(disabled by extension)"};r.push(a);}return n.length>0&&r.push(...n),r}function xc(e){return Lg[e]??e}async function Cc(e,t=[],n){let o=await Sc(e,true,t);o.kind!=="cancel"&&(o.id==="display-all"?await Rc(e):t.some(r=>r.id===o.id)?n&&await n(o.id):await hi(e,o.id));}function Sc(e,t=true,n=[]){let o=Ng(e,t,n),{ctx:r}=e;return new Promise(i=>{let s=(a,l,d,c)=>Ml({title:e.label,subtitle:`Configure settings for ${e.label}`,entries:o,tui:a,theme:l,done(p){c(void 0),i(p);}});r.ui.custom(s,{overlay:true,overlayOptions:bi()});})}async function hi(e,t){let n=e.layerValues(t),o=e.inspect(),r=e.buildFields(n),i={...n},s=new Set;function a(v){let k=String(v.key),S=o.winners[k];if(!(!S||S===t)&&!s.has(k))return `(from ${xc(S)})`}let l=gi[t]??t,c=e.scopeSources().find(v=>v.scope===t);t==="session"?e.sessionNote:c?.note??"";let p=t==="env"?"environment variables (read-only)":t==="defaults"?"built-in defaults":t==="session"?c?.path??c?.note??"":c?.exists&&c.path?c.path:c?.note??"",m=r.map(v=>({...v,valueNote:()=>a(v)})),g;async function f(v,k,S){if(!await new Promise(x=>{let E=mt({message:[`Really save to ${l}?`],confirmLabel:"Save",danger:false},x,{tui:v,theme:k});(g??{}).mountOverlay(E,`Save to ${l}`);})){g?.dismissOverlay();return}try{let x=await e.save({...i},t);x.created&&t==="project"&&e.ctx.ui.notify(`Project config written to ${x.path}`,"info"),e.onSaved(i),s.clear(),S(void 0);}catch(x){e.ctx.ui.notify(x instanceof Error?x.message:String(x),"error");}}async function h(v,k,S){await new Promise(x=>{let E=mt({message:["Discard changes?"],confirmLabel:"Discard",danger:true},x,{tui:v,theme:k});(g??{}).mountOverlay(E);})?S(void 0):g?.dismissOverlay();}async function b(v,k){let S=gi[t]??t;if(!await new Promise(x=>{let E=mt({message:[`Really reset ${S} to defaults?`],confirmLabel:"Reset",danger:true},x,{tui:v,theme:k});(g??{}).mountOverlay(E);})){g?.dismissOverlay();return}try{await e.resetScope(t);let x=e.layerValues(t);g?.setValues(x),s.clear(),o=e.inspect(),g?.dismissOverlay();}catch(x){e.ctx.ui.notify(x instanceof Error?x.message:String(x),"error");}}async function y(v,k){let S=gi[t]??t;if(!await new Promise(x=>{let E=mt({message:[`Really delete the ${S} config file?`],confirmLabel:"Delete",danger:true},x,{tui:v,theme:k});(g??{}).mountOverlay(E);})){g?.dismissOverlay();return}try{await e.deleteScope(t);let x=e.layerValues(t);g?.setValues(x),s.clear(),o=e.inspect(),g?.dismissOverlay();}catch(x){e.ctx.ui.notify(x instanceof Error?x.message:String(x),"error");}}let w={onChange(v,k){i[v]=k,s.add(v),e.onChange?.(v,k);},onSave(v,k,S){return f(v,k,S)},onRequestExit(v,k,S){return h(v,k,S)},onAction(v,k,S,R){switch(v){case "save":f(k,S,R).catch(x=>e.ctx.ui.notify(x instanceof Error?x.message:String(x),"error"));break;case "discard":h(k,S,R).catch(x=>e.ctx.ui.notify(x instanceof Error?x.message:String(x),"error"));break;case "reset":b(k,S).catch(x=>e.ctx.ui.notify(x instanceof Error?x.message:String(x),"error"));break;case "delete":y(k,S).catch(x=>e.ctx.ui.notify(x instanceof Error?x.message:String(x),"error"));break}}};await e.ctx.ui.custom((v,k,S,R)=>{let x=mi({title:`${e.label} \u2014 ${l}`,fields:m,mode:"buffered",closeOnSave:false,enableSearch:true,pathNote:p,actions:t==="session"?[{id:"save",label:"Save"},{id:"discard",label:"Discard"},{id:"reset",label:"Reset",danger:true}]:[{id:"save",label:"Save"},{id:"discard",label:"Discard"},{id:"reset",label:"Reset",danger:true},{id:"delete",label:"Delete",danger:true}],onSave:()=>w.onSave(v,k,R),onChange:w.onChange,onRequestExit:()=>w.onRequestExit(v,k,R),onAction:E=>w.onAction(E,v,k,R)},{tui:v,theme:k,ctx:e.ctx,close:()=>R(void 0)});return g=x,x},{overlay:true,overlayOptions:bi()});}async function Rc(e){let t=e.inspect(),n=e.scopeSources(),o=[];e.scopes.global!==false&&o.push({id:"global",scope:"global",label:"Global"}),e.scopes.project!==false&&o.push({id:"project",scope:"project",label:"Project Local"}),e.env&&Object.keys(e.env).length>0&&o.push({id:"env",scope:"env",label:"Env"}),e.scopes.session&&e.sessionInitialized&&o.push({id:"session",scope:"session",label:"Session"}),o.push({id:"defaults",scope:"defaults",label:"Defaults"});n.map(d=>`${d.label}: ${d.note}`).join(`
|
|
23
|
-
`);let i={};for(let d of n)d.scope==="session"?i.session=d.path??d.note:i[d.scope]=d.exists&&d.path?d.path:d.note;i.env="environment variables (read-only)",i.defaults="built-in defaults";let s={};for(let d of o){let c;d.scope==="defaults"?c={...e.defaults}:c=e.layerValues(d.scope);let u=e.buildFields(c);s[d.id]=u.map(p=>({...p,tab:d.id,valueNote:Bg(p,d.id,t,e.env)}));}let a=o[0].id,l=o.flatMap(d=>s[d.id]??[]);await e.ctx.ui.custom((d,c,u,p)=>{let m={current:i[a]};return mi({title:e.label,tabs:o.map(f=>({id:f.id,label:f.label})),initialTab:a,fields:l,readOnly:true,pathNote:m.current,actions:[{id:"edit",label:"Edit"},{id:"cancel",label:"Cancel"}],onAction(f){switch(f){case "cancel":queueMicrotask(()=>p(void 0));break;case "edit":queueMicrotask(()=>p(void 0)),a==="global"&&e.scopes.global||a==="project"&&e.scopes.project||a==="session"&&e.scopes.session?hi(e,a).catch(b=>e.ctx.ui.notify(b instanceof Error?b.message:String(b),"error")):Sc(e,false).then(b=>{if(b.kind==="cancel")return Rc(e);if(b.id!=="display-all")return hi(e,b.id)}).catch(b=>e.ctx.ui.notify(b instanceof Error?b.message:String(b),"error"));break}},onActiveTabChange(f){a=f,m.current=i[f]??"";}},{tui:d,theme:c,ctx:e.ctx,close:()=>p(void 0)})},{overlay:true,overlayOptions:bi()});}function Bg(e,t,n,o){let r=String(e.key),i=n.winners[r];if(t==="env"&&o?.[r]){let s=o[r],a=i==="env";return typeof s=="string"?!process.env[s]?.trim()?`(${s}: unset)`:a?`(${s}) \u25B8 effective`:`(${s})`:!process.env[s.var]?.trim()?`(${s.var}: unset)`:a?`(${s.var}) \u25B8 effective`:`(${s.var})`}if(i===t)return "\u25B8 effective";if(i)return `(from ${xc(i)})`}function bi(){return {anchor:"center",width:"92%",maxHeight:"95%"}}var Fg,Lg,gi,Dg,Ic=P(()=>{ti();Ol();wc();Fg=["global","project","session"],Lg={global:"Global",project:"Project Local",env:"env",session:"Session",defaults:"default"},gi={global:"Global",project:"Project Local",session:"Session"},Dg={global:"Configure Global settings",project:"Configure Project local settings",session:"Configure Session settings"};});var Io,Ec=P(()=>{Rl();Kn();Ic();pi();Io=class{opts;_sessionId;_leafId;_entries;_appendEntry;_getEntries;_filename;_sessionPersist="unavailable";_sessionManager;_pendingCwd;_defaultConfigDir;constructor(t){if(this.opts=t,this._defaultConfigDir=t.configDir,!t.filename&&!t.id)throw new Error("ConfigManager requires either `id` or `filename` in options. Provide one so the config file can be resolved.");this._filename=t.filename??`${t.id}-config.json`;}getScopes(){let t=this.opts.scopes??{global:true,project:true,session:true};return {global:t.global!==false,project:t.project!==false,session:t.session!==false&&this.opts.sessionConfig!==false}}hasSession(){return this._sessionPersist==="persisted"||this._sessionPersist==="pending"}_ensureSession(t,n){if(this._sessionManager&&this._sessionId){let c=t.sessionManager?.getSessionId?.();if(c&&this._sessionId!==c){let u=this._sessionId,p=this._pendingCwd??process.cwd();at(this._getEntryType(),p,u,we),this._sessionId=void 0,this._leafId=void 0,this._entries=void 0,this._appendEntry=void 0,this._getEntries=void 0,this._sessionPersist="unavailable",this._sessionManager=void 0,this._pendingCwd=void 0;}}if(this._sessionPersist==="persisted"&&!this._sessionManager)return this._tryFlushSession(n),true;if(this._sessionPersist!=="unavailable"&&this._sessionManager)return t.sessionManager&&this._tryFlushSession(n),true;let o=t.sessionManager;if(!o)return this._sessionPersist="unavailable",false;if(this.opts.scopes?.session===false||this.opts.sessionConfig===false)return this._sessionPersist="unavailable",false;let r=o.getSessionFile?.();if(typeof r!="string")return this._sessionPersist="unavailable",false;let i=o.getLeafId?.()??null,s=o,a=typeof s.appendCustomEntry=="function";if(existsSync(r)&&i!=null&&a){let c=(u,p)=>{s.appendCustomEntry(u,p);};return this.initSession(o.getSessionId(),i,o.getEntries?.()??[],c,()=>o.getEntries?.()),this._sessionPersist="persisted",this._sessionManager=s,this._pendingCwd=n,true}let d=a?(c,u)=>{s.appendCustomEntry(c,u);}:void 0;return this._sessionId=o.getSessionId()??void 0,this._leafId=we,this._entries=o.getEntries?.(),this._appendEntry=d,this._getEntries=()=>o.getEntries?.()??[],this._sessionPersist="pending",this._sessionManager=s,this._pendingCwd=n,this._tryFlushSession(n),true}_tryFlushSession(t){if(this._sessionPersist!=="pending"||!this._sessionManager)return false;let n=this._sessionManager,r=typeof n.appendCustomEntry=="function"?(d,c)=>{n.appendCustomEntry(d,c);}:void 0;this._appendEntry=r;let i=n.getSessionFile();if(typeof i!="string"||!existsSync(i))return false;let s=n.getLeafId();if(s==null||!r)return false;let a=t??this._pendingCwd??process.cwd(),l=Un(this._getEntryType(),a,this._sessionId,we)??{};return Object.keys(l).length>0&&(jn(this._getEntryType(),a,this._sessionId,s,l),at(this._getEntryType(),a,this._sessionId,we),r(this._getEntryType(),{leafId:s,config:l})),this._leafId=s,this._sessionPersist="persisted",Object.keys(l).length>0}initSession(t,n,o,r,i){if(this.opts.scopes?.session===false||this.opts.sessionConfig===false)return;this._sessionId=t,this._leafId=n,this._entries=o,this._appendEntry=r,this._getEntries=i,this._sessionPersist="persisted",this._sessionManager=void 0,this._pendingCwd=void 0;let s=this._getEntryType();for(let a=o.length-1;a>=0;a--){let l=o[a];if(l.type==="custom"&&l.customType===s){let d=l.data;if(d&&d.leafId===n){jn(s,process.cwd(),t,n,d.config);break}}}}load(t,n){return this.loadWithWarnings(t,n).config}loadWithWarnings(t,n){let o=[],r=Os(this._filename,this.opts.defaults,{cwd:t,configDir:n??this._defaultConfigDir,merge:"deep"}),i=this.opts.validate?this.opts.validate(r):r;try{let u=this.opts.fields(i),p="global";for(let m of u){if(m.type==="action"||m.type==="custom")continue;let g=jt(m,m.value);g&&o.push({scope:p,key:String(m.key),message:`"${m.label}": ${g}`});}}catch{}let s=this.applyEnvOverrides(i),a=this.opts.scopes??{session:true},l=this.opts.sessionConfig!==false&&a.session!==false,d=this._getEntryType(),c=l?this.applySessionOverrides(s,t,d):s;return this._tryFlushSession(t),{config:c,warnings:o}}applyEnvOverrides(t){let n=this.opts.env;return n?go(t,n,this.opts.defaults):t}applySessionOverrides(t,n,o){let r=this._sessionId,i=this._leafId,s=this._getEntries?.()??this._entries;if(!r||!i||!s)return t;let a;i===we?a=Un(o,n??process.cwd(),r,we)??{}:a=ur(o,n??process.cwd(),r,i,s);let l=wt(t,a);return this.opts.validate?this.opts.validate(l):l}_applyValidation(t){return this.opts.validate?this.opts.validate(t):t}layerValues(t,n,o){let r=o??this._defaultConfigDir??Ie(),s={...this.opts.defaults},a=Le(this._filename,r)??{};if(s=wt(s,a),t==="global")return this._applyValidation(s);if(n){let u=join(n,".pi"),p=Le(this._filename,u)??{};s=wt(s,p);}if(t==="project"&&!n)throw new Error("cwd is required for project-scoped layerValues");if(t==="project")return this._applyValidation(s);if(this.opts.env&&(s=go(s,this.opts.env,this.opts.defaults)),t==="env")return this._applyValidation(s);let l=this.opts.scopes??{session:true},d=this.opts.sessionConfig!==false&&l.session!==false,c=this._getEntryType();return d&&(s=this.applySessionOverrides(s,n,c)),this._tryFlushSession(n),this._applyValidation(s)}inspect(t,n){let o=n??this._defaultConfigDir??Ie(),r={...this.opts.defaults},i=Le(this._filename,o)??{},s=t?Le(this._filename,join(t,".pi"))??{}:{},a={};if(this.opts.env){let b=go(s,this.opts.env,this.opts.defaults);for(let[y,w]of Object.entries(this.opts.env))this._envKeyIsActive(y,w,this.opts.defaults)&&(a[y]=b[y]);}let l={},d=this.opts.scopes??{session:true},c=this.opts.sessionConfig!==false&&d.session!==false,u=this._getEntryType();if(c&&this._sessionId&&this._leafId){let h;this._leafId===we?h=Un(u,t??process.cwd(),this._sessionId,we)??{}:h=ur(u,t??process.cwd(),this._sessionId,this._leafId,this._getEntries?.()??this._entries??[]),Object.assign(l,h);}let p={defaults:r,global:i,project:s,env:a,session:l},m=new Set;for(let h of Object.values(p))for(let b of Object.keys(h))m.add(b);let g={},f=["session","env","project","global","defaults"];for(let h of m)for(let b of f)if(h in p[b]){g[h]=b;break}return {layers:p,winners:g}}scopeSources(t,n){let o=[],r=this.opts.scopes??{global:true,project:true,session:true};if(r.global!==false){let i=n??this._defaultConfigDir??Ie(),s=join(i,this._filename),a=existsSync(s);o.push({scope:"global",label:"Global",path:s,exists:a,note:a?s:"(nonexistent \u2014 will be created on save)"});}if(r.project!==false&&t){let i=join(t,".pi",this._filename),s=existsSync(i);o.push({scope:"project",label:"Project Local",path:i,exists:s,note:s?i:"(nonexistent \u2014 will be created on save)"});}if(r.session!==false&&this.opts.sessionConfig!==false){let i=this._getEntryType(),s=this._sessionManager?.getSessionFile?.(),a=this._sessionPersist==="pending";o.push({scope:"session",label:"Session",exists:false,path:s,note:a?"in-memory until session file exists \u2014 will persist automatically":`in-memory per-leaf overrides (persisted to session JSONL as ${i})`});}return o}_envKeyIsActive(t,n,o){if(typeof n=="string"){let r=process.env[n]?.trim();if(!r)return false;let i=o[t];if(typeof i=="boolean")return ["1","true","yes","on","0","false","no","off"].includes(r.toLowerCase());if(typeof i=="number"){if(Number.isInteger(i)&&i>0){let a=Number.parseInt(r,10);return Number.isFinite(a)&&a>0}let s=Number.parseFloat(r);return Number.isFinite(s)}return false}else {let r=process.env[n.var]?.trim();if(!r)return false;try{return n.parse(r,void 0)!==void 0}catch{return false}}}save(t,n,o,r){if(n==="session"){if(!this._sessionId)throw new Error("Cannot save session config: session not initialized. Call initSession() first.");let f=o??process.cwd();return jn(this._getEntryType(),f,this._sessionId,this._leafId,t),this._pendingCwd=f,!this._tryFlushSession(f)&&this._sessionPersist==="persisted"&&this._appendEntry&&this._appendEntry(this._getEntryType(),{leafId:this._leafId,config:t}),{path:"",created:false,changed:true}}if(n==="project"&&!o)throw new Error("cwd is required for project-scoped config save");let i=n==="project"&&o?join(o,".pi"):r??this._defaultConfigDir??Ie(),s=join(i,this._filename),a=!existsSync(s),l=new Set(Object.keys(this.opts.defaults)),d=Le(this._filename,i)??{},c=new Map;try{let f=this.opts.fields(t);for(let h of f)c.set(String(h.key),h);}catch{}let u={},p=false;for(let f of Object.keys(this.opts.defaults)){let h=t[f],b=d[String(f)];if(Jt(h,b))continue;let y=c.get(String(f));y&&!(y.type==="action"||y.type==="custom")&&jt(y,h)||(u[String(f)]=h,p=true);}for(let[f,h]of Object.entries(d))l.has(f)||(u[f]=h,p=true);if(!p)return {path:s,created:a,changed:false};let m={...d,...u};if(!cr(this._filename,m,i))throw new Error(`Failed to save ${this._filename} \u2014 the config file may be read-only (e.g., managed by Nix). Runtime state was updated for this session only.`);return {path:s,created:a,changed:true}}_getEntryType(){return typeof this.opts.sessionConfig=="object"&&this.opts.sessionConfig.entryType?this.opts.sessionConfig.entryType:`session-config-${this.opts.id}`}async openSettings(t,n,o,r,i,s,a){r=r??this._defaultConfigDir,this.warnOnMalformedConfig(t,n,r);let l=this.getScopes(),d=this._ensureSession(t,n),c=this.scopeSources(n,r);await Cc({label:this.opts.label,ctx:t,cwd:n,scopes:l,sessionInitialized:d,sessionNote:c.find(u=>u.scope==="session")?.note??"",defaults:this.opts.defaults,env:this.opts.env,buildFields:u=>{let p=this.opts.fields(u),m=this.opts.defaults;for(let g of p)if(!(g.type==="action"||g.type==="custom")&&g.default===void 0){let f=String(g.key);f in m&&(g.default=m[f]);}return p},layerValues:u=>this.layerValues(u,n,r),inspect:()=>this.inspect(n,r),scopeSources:()=>this.scopeSources(n,r),save:async(u,p)=>{let m=this.opts.validate?this.opts.validate(u):u,g=this.save(m,p,n,r);return o(m),g},resetScope:u=>this.resetScope(u,n,r),deleteScope:u=>this.deleteScope(u,n,r),onSaved:()=>{},onChange:i},s,a);}resetScope(t,n,o){if(t==="session"){if(!this._sessionId)throw new Error("Cannot reset session config: session not initialized.");let l=n??process.cwd();this._sessionPersist==="pending"?at(this._getEntryType(),l,this._sessionId,we):at(this._getEntryType(),l,this._sessionId,this._leafId);return}if(t==="project"&&!n)throw new Error("cwd is required for project-scoped config reset");let r=t==="project"&&n?join(n,".pi"):o??this._defaultConfigDir??Ie(),i=new Set(Object.keys(this.opts.defaults)),s=Le(this._filename,r),a={};if(s&&typeof s=="object")for(let[l,d]of Object.entries(s))i.has(l)||(a[l]=d);Object.keys(a).length>0?cr(this._filename,a,r):dr(this._filename,r);}deleteScope(t,n,o){if(t==="session"){if(!this._sessionId)throw new Error("Cannot delete session config: session not initialized.");let i=n??process.cwd();this._sessionPersist==="pending"?at(this._getEntryType(),i,this._sessionId,we):at(this._getEntryType(),i,this._sessionId,this._leafId);return}if(t==="project"&&!n)throw new Error("cwd is required for project-scoped config delete");let r=t==="project"&&n?join(n,".pi"):o??this._defaultConfigDir??Ie();dr(this._filename,r);}warnOnMalformedConfig(t,n,o){let r=this._filename,i=fr(r,o);i.exists&&!i.valid&&t.ui.notify(`Config file "${r}" is ${i.error}. Using defaults.`,"warning");let s=join(n,".pi"),a=fr(r,s);a.exists&&!a.valid&&t.ui.notify(`Project config file ".pi/${r}" is ${a.error}. Using defaults.`,"warning");}};});var Fc={};Fe(Fc,{createChangelogViewer:()=>$c,entriesToPlainLines:()=>_c,getOwnPackageRoot:()=>yi,getPackageVersion:()=>Mc,openChangelogView:()=>vi,parseChangelogEntries:()=>Ac,readChangelogText:()=>Oc,renderChangelogEntries:()=>Pc,stripMarkdownInline:()=>wn});function yi(){try{let e=import.meta.url;if(typeof e=="string"&&e.length>0){let t=dirname(fileURLToPath(e));for(;t!==dirname(t);){try{if(JSON.parse(readFileSync(join(t,"package.json"),"utf-8")).name==="pi-blackhole")return t}catch{}let n=dirname(t);if(n===t)break;t=n;}}}catch{}try{let e=process.cwd();for(;e!==dirname(e);){try{if(JSON.parse(readFileSync(join(e,"package.json"),"utf-8")).name==="pi-blackhole")return e}catch{}let t=dirname(e);if(t===e)break;e=t;}}catch{}try{let e=process.argv[1];if(e){let t=dirname(e);for(;t!==dirname(t);){try{if(JSON.parse(readFileSync(join(t,"package.json"),"utf-8")).name==="pi-blackhole")return t}catch{}let n=dirname(t);if(n===t)break;t=n;}}}catch{}return process.cwd()}function Mc(e){let t=e??yi();try{let n=JSON.parse(readFileSync(join(t,"package.json"),"utf-8"));if(typeof n.version=="string"&&n.version.length>0)return n.version}catch{}}function wn(e){let t=e;return t=t.replace(/\[([^\]]+)\]\([^)]+\)/g,"$1"),t=t.replace(/`([^`]+)`/g,"$1"),t=t.replace(/\*\*\*([^*]+)\*\*\*/g,"$1"),t=t.replace(/\*\*([^*]+)\*\*/g,"$1"),t=t.replace(/__([^_]+)__/g,"$1"),t=t.replace(/(?<!\*)\*([^*\n]+)\*(?!\*)/g,"$1"),t=t.replace(/(?<!\w)_([^_\n]+)_(?!\w)/g,"$1"),t=t.replace(/~~([^~]+)~~/g,"$1"),t}function Oc(e){let t=e!==void 0,n=e??yi(),o=[join(n,"CHANGELOG.md"),join(n,"docs/CHANGELOG.md")];for(let r of o)try{if(existsSync(r))return readFileSync(r,"utf-8")}catch{}if(!t){try{let r=join(process.cwd(),"docs/CHANGELOG.md");if(existsSync(r))return readFileSync(r,"utf-8")}catch{}return Ug??void 0}}function Ac(e,t){let n=e.split(/\r?\n/),o=[],r,i,s=/^##\s+\[([^\]]+)\]\s*-?\s*(.*)\s*$/,a=/^###\s+(.+)\s*$/,l=/^\s*-\s+(.*)\s*$/;for(let d of n){let c=d.match(s);if(c){r&&o.push(r),r={version:c[1].trim(),date:c[2].trim()||void 0,sections:[]},i=void 0,t!==void 0&&o.length>=t;continue}if(!r)continue;let u=d.match(a);if(u){i={heading:wn(u[1].trim()),items:[]},r.sections.push(i);continue}let p=d.match(l);if(p&&i){i.items.push(wn(p[1].trim()));continue}if(i&&i.items.length>0&&d.length>0&&/^\s{2,}\S/.test(d)&&!d.startsWith("##")&&!d.startsWith("###")){let m=i.items.length-1;i.items[m]=`${i.items[m]} ${wn(d.trim())}`;}}return r&&o.push(r),t!==void 0?o.slice(0,t):o}function _c(e){let t=[];for(let n of e){let o=n.date?`## [${n.version}] - ${n.date}`:`## [${n.version}]`;if(t.push(o),t.push(""),n.sections.length===0){t.push("(no details)"),t.push("");continue}for(let r of n.sections){if(t.push(`### ${r.heading}`),r.items.length===0)t.push("(no items)");else for(let i of r.items)t.push(`- ${i}`);t.push("");}}return t}function Pc(e,t,n){let o=_c(e),r=[];for(let i of o)if(i.startsWith("## [")){let s=n.fg("accent",n.bold(i));r.push(...fe(s,t));}else if(i.startsWith("### "))r.push(...fe(n.fg("accent",i),t));else if(i.startsWith("- ")){let s=fe(i,t);for(let a=0;a<s.length;a++)a===0?r.push(s[a]):r.push(` ${s[a]}`);}else i===""?r.push(""):r.push(...fe(i,t));return r}function $c(e){let{tui:t,theme:n,done:o,packageRoot:r,maxEntries:i}=e,s=Mc(r),a=s?`pi-blackhole v${s} \u2014 Changelog`:"pi-blackhole \u2014 Changelog",l=Oc(r),d;if(!l)d=["Changelog not found.","Expected CHANGELOG.md at package root."];else {let g=Ac(l,i);if(g.length===0)d=l.split(/\r?\n/).map(h=>wn(h));else return Wg({tui:t,theme:n,done:o,title:a,entries:g})}let c=0,u=5;return {render:g=>{let f=pt(t.terminal.rows??24,Eo,14),h=Ce(g),b=[];for(let E of d)E===""?b.push(""):b.push(...fe(E,h));let y=Math.max(1,f-2),w=Math.max(0,b.length-y);c=Math.min(c,w);let v=b.slice(c,c+y);for(;v.length<y;)v.push("");let k=[];c>0&&k.push(n.fg("dim",` \u2191 ${c} earlier`)),k.push(...v),c+y<b.length&&k.push(n.fg("dim",` \u2193 ${b.length-c-y} more`));let R=n.fg("dim","\u2191\u2193 scroll \xB7 PgUp/PgDn \xB7 Esc close"),x=[...k,"",R];return je(x,g,n,{title:a,fixedInnerRows:f})},handleInput:g=>{let f=pt(t.terminal.rows??24,Eo,14),h=Ce(80),b=[];for(let v of d)v===""?b.push(""):b.push(...fe(v,h));let y=Math.max(1,f-2),w=Math.max(0,b.length-y);matchesKey(g,"up")?(c=Math.max(0,c-1),t.requestRender()):matchesKey(g,"down")?(c=Math.min(w,c+1),t.requestRender()):matchesKey(g,"pageUp")?(c=Math.max(0,c-u),t.requestRender()):matchesKey(g,"pageDown")?(c=Math.min(w,c+u),t.requestRender()):(matchesKey(g,"escape")||matchesKey(g,"ctrl+c"))&&o();},invalidate:()=>{}}}function Wg(e){let{tui:t,theme:n,done:o,title:r,entries:i}=e,s=0,a=5,l=80,d=[];function c(m){if(m===l&&d.length>0)return d;l=m;let g=Ce(m);return d=Pc(i,g,n),d}return {render:m=>{let g=pt(t.terminal.rows??24,Eo,14),f=c(m),h=Math.max(1,g-3),b=Math.max(0,f.length-h);s=Math.min(s,b);let y=f.slice(s,s+h);for(;y.length<h;)y.push("");let w=[];return s>0&&w.push(n.fg("dim",` \u2191 ${s} earlier`)),w.push(...y),s+h<f.length&&w.push(n.fg("dim",` \u2193 ${f.length-s-h} more`)),w.push(""),w.push(n.fg("dim"," \u2191\u2193 scroll \xB7 PgUp/PgDn \xB7 Esc close")),je(w,m,n,{title:r,fixedInnerRows:g})},handleInput:m=>{let g=pt(t.terminal.rows??24,Eo,14),f=c(l),h=Math.max(1,g-3),b=Math.max(0,f.length-h);matchesKey(m,"up")?(s=Math.max(0,s-1),t.requestRender()):matchesKey(m,"down")?(s=Math.min(b,s+1),t.requestRender()):matchesKey(m,"pageUp")?(s=Math.max(0,s-a),t.requestRender()):matchesKey(m,"pageDown")?(s=Math.min(b,s+a),t.requestRender()):(matchesKey(m,"escape")||matchesKey(m,"ctrl+c"))&&o();},invalidate:()=>{}}}async function vi(e){await e.ui.custom((t,n,o,r)=>$c({tui:t,theme:n,done:()=>r(void 0)}),{overlay:true,overlayOptions:{anchor:"center",width:"92%",maxHeight:"95%"}});}var Ug,Eo,ki=P(()=>{Ae();Ug='## [Unreleased]\n\n---\n\n## [0.5.0] - 2026-09-06\n\n### Added\n\n- **Newest-first retained tool-output budgeting.** At each Blackhole compaction boundary, retained historical tool and shell output text is projected through a dedicated budget (default 20000 tokens) and the resulting omission decisions are persisted with that compaction. Set `retainedToolOutputMaxTokens` to `0` to disable. When enabled, the provider-visible representation remains fixed until the next compaction, while raw session history and compaction inputs remain full fidelity. Pending results and non-text content remain visible, and omitted text points to `recall #N` when a stable transcript index is available. ([#66](https://github.com/k0valik/pi-blackhole/pull/66))\n- **`max` thinking level support.** Configured memory models now accept and forward Pi\'s highest reasoning level where supported by the provider ([upstream OM `e6c0fd9`](https://github.com/elpapi42/pi-observational-memory/commit/e6c0fd9d8e43ffb5d3c1a271bf1ec4a4640bde02)). ([#67](https://github.com/k0valik/pi-blackhole/pull/67))\n\n### Changed\n\n- **Lazy loading of optional command modules and memory workers.** The settings overlay, changelog viewer, cleanup handler, export pipeline, and OM worker imports now load on demand instead of at extension startup, cutting per-session startup cost. ([#71](https://github.com/k0valik/pi-blackhole/pull/71))\n- **Recall search now indexes bounded generic tool arguments, filters multi-term BM25 noise, caps results at 50, and reports pagination/truncation accurately \u2014 including in `/blackhole-recall`** ([upstream VCC `f7b80bb`, `4bb7115`](https://github.com/sting8k/pi-vcc/commit/f7b80bbbe22315acf9f7925c0c3be2d4ae9feee5)). ([#67](https://github.com/k0valik/pi-blackhole/pull/67))\n\n### Fixed\n\n- **Custom provider streams are now keyed by provider + API, not API alone.** Several extensions can register different providers sharing one wire API (e.g. `anthropic` and a Databricks-hosted Claude both speak `anthropic-messages`); keying by API alone let the first-registered provider hijack every model speaking that protocol, routing requests through the wrong transport/URL/auth. Streams also overwrite on re-registration instead of first-wins. ([#70](https://github.com/k0valik/pi-blackhole/pull/70))\n- **OM workers now receive provider environment substitutions, and stale ambient-credential availability snapshots are rechecked before rejecting request-time-signed providers** ([upstream OM `ce9fc98`, `699ccc7`](https://github.com/elpapi42/pi-observational-memory/commit/ce9fc982b3a219a7839f07c9f4a3e054e81a2b21)). ([#67](https://github.com/k0valik/pi-blackhole/pull/67))\n- **Empty Blackhole compaction output now delegates to Pi\'s native summarizer instead of replacing context with an empty summary** ([upstream OM PR #39](https://github.com/elpapi42/pi-observational-memory/pull/39)). ([#67](https://github.com/k0valik/pi-blackhole/pull/67))\n\n---\n\n## [0.4.10] - 2026-08-29\n\n### Changed\n\n- **Upgraded recall & export algorithms (BM25+, SimHash64, c-TF-IDF, and technical density scoring).**\n - Upgraded session history search to **BM25+** with lower-bound delta term ($\\delta = 0.5$) preventing length bias against concise observations.\n - Added lightweight morphological stemming (`stemToken`) to `dedup.ts` for higher token-set overlap across grammatical variants.\n - Added 64-bit SimHash locality-sensitive fingerprinting (`computeSimHash64`, `simHashHammingDistance`) and cluster drift guards to speed up pairwise candidate filtering and prevent transitive clustering drift.\n - Added technical entity density scoring (`technicalDensityFactor`) in `format-export.ts` to reward concrete code artifacts (paths, symbols, flags, hashes) over conversational transcripts.\n - Expanded stemming and technical-artifact detection for common software terminology, major language file types, framework constructs, API routes, DevOps/configuration signals, errors, and semantic versions.\n - Topic labels now preserve readable surface words while using stems only for internal matching and scoring.\n - Upgraded topic labeling from standard TF-IDF to **c-TF-IDF** (Class-based TF-IDF with sublinear saturation).\n - Export preamble now shows a best-effort heuristic warning instead of a Key Topics index.\n\n### Fixed\n\n- **Git-based installs no longer require interactive `pnpm approve-builds`.** `simple-git-hooks` is explicitly trusted through pnpm 11\'s workspace `allowBuilds` configuration, and the prepare lifecycle now initializes hooks and builds the bundle exactly once.\n- **Export pipeline hardening for large corpora.** Fixed `token.charCodeAt is not a function` crash caused by `TECHNICAL_ROOTS` prototype pollution on tokens like `constructor`/`toString` (now guarded with `hasOwnProperty`) and added a malformed-token guard in `computeSimHash64`; topic labels now preserve surface forms and the export warning clarifies heuristic ranking.\n\n---\n\n## [0.4.9] - 2026-08-28\n\n### Added\n\n- **Distilled project-memory export (`/blackhole-export`).** ([#65](https://github.com/k0valik/pi-blackhole/pull/65)) New command that scans project-scoped session JSONL files plus global OM pending buffers, deduplicates/clusters observations, and writes a single import-ready Markdown file (tiered as `Reflections \u2192 Critical \u2192 High \u2192 Medium \u2192 Low` plus an `Unattributed pending memory` section for orphaned buffers). Scoring is tier-weighted with recency decay, log-scaled recurrence and evidence-mass boosts, consensus rerank, burst penalty and length factor; viability gating keeps low/medium only with multi-session support or length/quality, high/critical always. Hierarchical topic assignment via S\xF8rensen-Dice graph + TF-IDF labeling; three-pass dedup (exact normalized, Levenshtein@0.88 after bigram-Jaccard prefilter, S\xF8rensen-Dice@0.70 with Levenshtein floor). Output parsing via `out:<path>.md` or a timestamped default; deterministic and stateless. New modules `src/project-recall/corpus.ts`, `dedup.ts`, `format-export.ts`, `session-dir.ts` and handler `src/commands/blackhole-export.ts` (wired in `index.ts`). Appendix A slice of the project-recall plan \u2014 future project-aware recall search remains out of scope for this release.\n\n### Fixed\n\n- **Capture `AgentSession` from bundled Pi CLI entrypoint.** ([#62](https://github.com/k0valik/pi-blackhole/pull/62), thanks @daoguademeng) `installHostInlineCompactionAdapter` now resolves the host `AgentSession` from the bundled CLI\'s runtime chunk (when the entrypoint is `dist/bundle/cli.js`) in addition to `dist/index.js`, so inline (mid-run) compaction works when Pi is launched via its bundled CLI instead of silently falling back to settled compaction.\n- **Unified `session_compact_failed` handling (pi >=0.84.3).** Ported from [ceblan/pi-blackhole#ceb-dev](https://github.com/ceblan/pi-blackhole/compare/main...ceblan:pi-blackhole:ceb-dev) (thanks @ceblan / Carlos Estrada): new `src/hooks/compact-failed.ts` closes gaps in failure coverage \u2014 structured `compact_failed.received` trace with corrected `attributedFromExtension` (`fromExtension || compactWasPiVcc || lastCompactCancelled`), defensive `compactInFlight` + `autoCompactionController` reset (aborts orphaned idle-wait so it cannot launch a second compaction after a later turn), overflow-retry `willRetry` visibility (`"overflow compaction aborted, retrying turn"`), and `compactionEngine: pi-default` noise filtering. `Runtime.lastCompactCancelled` is set on every `{ cancel: true }` from `before-compact` and consumed attempt-scoped with `compactWasPiVcc` (leak-free lifecycle: set at `session_before_compact` start, consumed on `session_compact` success or `session_compact_failed`). Covers pi #8328 overflow path.\n\n---\n\n## [0.4.8] - 2026-08-23\n\n### Added\n\n- **Opt-in append compaction (`compactionSummaryMode`).** New config key (`default` | `append`; `default` is the default) plus `PI_BLACKHOLE_COMPACTION_SUMMARY_MODE` override. In append mode each automatic Blackhole compaction appends one immutable provider-visible segment (`S1 | S2 | \u2026`) while every stored summary stays a complete fallback; `/blackhole` rebases the active chain into one clean segment; a legacy v1 summary enters through one marked rebase. A new `context` hook projects segments before each model call and fails closed to the fallback on any malformed state. When the projected chain passes half of the model\'s context window, the next automatic compaction folds it back into one segment. Falls back to rewrite surgery once per session when append mode encounters unsupported state. See `docs/APPEND_COMPACTION.md`. ([#58](https://github.com/k0valik/pi-blackhole/pull/58), thanks @sonSunnoi)\n\n### Changed\n\n- **Mid-run compaction failures now use exponential backoff** (1s doubling to a 30s cap) instead of suspending retries until context pressure drops. A single transient failure no longer wedges auto-compaction for the rest of the pressure episode; failure notices now include "retrying in Xs".\n- **Permanent inline-compaction unavailability (pi version lacks the adapter API) is now classified once** and reported as a single warning ("using settled compaction fallback") instead of surfacing as a retryable failure every episode. With `midRunCompaction: resume`, later turn-end attempts skip the adapter immediately, and agent start warns once if resume mode is configured against a known-unsupported adapter.\n- **Compaction token counting now uses real provider usage when available.** `rawTokensSinceLastCompaction` reads the last valid assistant message\'s usage (`calculateContextTokens`: `totalTokens` or the input/output/cache component sum) after the latest compaction entry, plus a chars/4 estimate for trailing entries, instead of estimating the whole window from characters. Chars/4 remains the fallback for sessions without usage data. Error/aborted assistant turns are never used as baselines; usage from before the latest compaction is ignored (it reflects the pre-compaction context). Approach from tavasti@360f24a (pi-vcc upstream PR #40); hardened implementation ported from plan-01 of the token-rework work.\n- **Minimal tails honor later Pi split-turn boundaries.** An oversized current turn can now be cut at Pi\'s safe assistant/user boundary instead of being retained whole after compaction.\n\n### Fixed\n\n- **Inline compaction ignores aborted/errored assistant turns.** Assistant messages with `stopReason: "error"` or `"aborted"` are now skipped when checking for trailing in-flight tool calls, matching Pi\'s own transform-messages behavior.\n- **Inline compaction ignores stale tool calls** that reference cleared state from a prior turn ([#57](https://github.com/k0valik/pi-blackhole/pull/57), thanks @daoguademeng)\n- **Settings modal footer and key dispatch guard against section rows.** Prevents a crash when the focused row in `/blackhole configure` is a section header instead of an editable field.\n\n---\n\n## [0.4.7] - 2026-08-15\n\n### Fixed\n\n- **Installation from git now works without a prebuilt `dist/`.** The package manifest entrypoint now points at `./index.ts` instead of `./dist/index.js`. Because `dist/` is gitignored, direct Git installs were missing the extension entrypoint and failing to load. Pi can load the TypeScript entrypoint directly, so this restores functionality for `npm install github:k0valik/pi-blackhole` and similar Git-based installs. Registry installs are unaffected (npm/pnpm/bun ship the prebuilt `dist/` bundle).\n\n---\n\n## [0.4.6] - 2026-08-14\n\n### Added\n\n- **Session-local config.** Config values can now be set at session scope via `/blackhole configure` or the config modal\'s scope selector. Session config is ephemeral \u2014 it lives only for the current session and overrides project-local and env values, so you can experiment with settings like `midRunCompaction` or `compactionEngine` without touching files or environment variables.\n\n- **All env overrides are visible in the config modal.** `PI_BLACKHOLE_MID_RUN_COMPACTION`, `PI_BLACKHOLE_COMPACTION`, and `PI_BLACKHOLE_COMPACTION_ENGINE` (alongside existing overrides like `PI_BLACKHOLE_SKIP_PROVIDERS` and `PI_BLACKHOLE_PROVIDER_IDLE_TIMEOUT_MS`) now appear in the env tab of the config modal with their current effective values, so you can see at a glance what the environment is contributing.\n\n### Changed\n\n- **Config modal migrated to the canonical `pi-base` config-rework surface.** The modal now uses the upstream scope-selector and config-flow, replacing the legacy `openSettingsModal` path. The layer precedence is: global \u2192 project \u2192 env \u2192 session, matching pi-utils behavior.\n\n### Removed\n\n- **Dead monolith-era config code.** Removed `src/pi-base/config-settings.ts`, `settings-registry.ts`, `settings-ui.ts`, `registry.ts`, `report.ts`, `llm.ts`, `hash.ts`, `context-provider.ts`, `once.ts`, `debug.ts`, `config-manager-howto.md`, `settings/README.md`, and the obsolete `scope-action.test.ts`. Blackhole-specific wiring (kitty decode, NixOS read-only warnings, key migration, clamping) remains in `blackhole-settings.ts`.\n\n### Fixed\n\n- **Recall drill-down honors lineage scope.** ([#54](https://github.com/k0valik/pi-blackhole/issues/54)) `#N:path` drill-down now checks the active lineage before expanding off-lineage entries, matching every other recall path. Off-lineage indices are blocked under the default `scope:"lineage"` and require `scope:"all"` to access.\n- **Inline compaction restores the Working indicator.** ([#52](https://github.com/k0valik/pi-blackhole/pull/52), thanks @daoguademeng) After inline compaction completes, the UI "Working" indicator is restored so the user sees activity resumed.\n\n### Dependencies\n\n- Bumped dev-dependency group across 2 PRs (#45, #53): `@typescript-eslint/eslint-plugin` to `8.66.0`, `eslint` to `10.8.0`, `lint-staged` to `17.3.0`, `typebox` to `1.3.10`, `typescript` to `6.0.3` (pinned for `@typescript-eslint` v8 compatibility), and `vitest` to `4.1.10`.\n\n## [0.4.4] - 2026-08-06\n\n### Added\n\n- **Experimental compatibility shim for pi-codex-compaction coexistence.** ([#47](https://github.com/k0valik/pi-blackhole/pull/47), thanks @danielmrdev) Optional `skipForProviders` (config key or `PI_BLACKHOLE_SKIP_PROVIDERS` env override) makes blackhole step aside entirely \u2014 no compaction, no observational-memory consolidation \u2014 for listed providers, giving exactly-one-engine semantics when pi-codex-compaction also registers a `session_before_compact` handler. **Niche surface by design**: unsurfaced in README/CONFIG.md until a second consumer exists (see shim notes in `src/core/provider-skip.ts`); surfaced only in example-config.json.\n\n- **Isolated provider idle timeout for background memory jobs.** ([#48](https://github.com/k0valik/pi-blackhole/pull/48), thanks @FelikZ) Optional `providerIdleTimeoutMs` lets observer/reflector/dropper worker HTTP requests tolerate longer silent provider intervals without forcing interactive Pi requests to wait equally long, by wrapping the provider `fetch` with an undici dispatcher that injects `bodyTimeout`. Unset inherits pi\'s global default; `0` disables; `> 0` sets a millisecond cap. Configurable via config file, `/blackhole configure`, or `PI_BLACKHOLE_PROVIDER_IDLE_TIMEOUT_MS`.\n\n### Fixed\n\n- **Credential-resolved provider endpoints are preserved for observational-memory workers on Pi versions whose registry exposes `getProviderAuth()`.** Observer, reflector, and dropper now use the endpoint selected by Pi\'s auth resolver, preventing GitHub Copilot Business/Enterprise requests from falling back to the Individual endpoint and returning HTTP 421. On older registries without `getProviderAuth()`, the fix degrades silently to the previous behavior.\n- **`midRunCompaction: "resume"` no longer aborts or replaces the active run.** ([#50](https://github.com/k0valik/pi-blackhole/pull/50), thanks @daoguademeng) The old `ctx.compact()` + `blackhole-resume` path propagated a false interrupt to background/subagent extensions and let nested child runners resolve before Blackhole\'s detached resume run finished. Resume mode now performs Pi\'s native compaction pipeline inline from the awaited `turn_end` handler, refreshes the next low-level turn from the compacted messages, and continues inside the original `session.prompt()` promise. Completed tool calls remain paired; no synthetic user/custom message is injected. `"resume"` is an **experimental opt-in** \u2014 it monkey-patches Pi host internals and can silently deactivate on host drift.\n- **Mid-run compaction compatibility fails closed.** A reload-idempotent, weakly referenced runtime adapter recognizes the known Pi 0.81 and 0.84 `AgentSession.compact()` shapes. Unknown internal drift refuses transparent compaction and leaves the active run alive instead of falling back to the unsafe aborting path. External abort/cancellation still passes through normally.\n\n### Testing\n\n- Added adapter contract coverage for Pi 0.81/0.84 compact shapes, no-abort behavior, compacted next-turn context refresh, external cancellation, unpaired-tool rejection, fail-closed drift handling, and reload idempotency. A real `AgentSession` + faux-provider integration test runs on both the 0.81.1 compatibility baseline and 0.84.0 dev baseline, proving the active run signal stays live, the next provider request receives the compacted context, and the original `session.prompt()` remains pending through compaction. Trigger tests prove no `ctx.compact()` or `blackhole-resume` dispatch.\n\n### Dependencies\n\n- Bumped `@earendil-works/pi-*` devDependencies from `0.83.0` to `0.84.0`; the peer range remains `>=0.81.1 <1.0.0`, and the adapter retains a tested legacy-shape path for the minimum supported host.\n\n## [0.4.3] - 2026-08-01\n\n### Added\n\n- **pi-base config modal for `/blackhole configure`.** ([#41](https://github.com/k0valik/pi-blackhole/pull/41)) The hand-rolled configure overlay is replaced with pi-base\'s ConfigManager + settings modal (vendored into `src/pi-base/`), with scope-aware editing: global config lives at `<agentDir>/pi-blackhole/` (respecting `PI_CODING_AGENT_DIR`), project config overlays `<cwd>/.pi/pi-blackhole-config.json`.\n\n### Changed\n\n- **Number fields edit inline in `/blackhole configure`.** Number fields (e.g. `compactAfterTokens`, `observeAfterTokens`) no longer cycle in fixed steps on every Enter \u2014 pressing Enter drops into inline editing where you type the value directly; `\u2190`/`\u2192` still fine-tune by step when not editing.\n- **Destructive-action confirmations are safer.** The delete/reset scope confirm now lists **Cancel first (pre-selected)** and shows a warning-color line stating what the action will do \u2014 tabbing into the confirm can never land on a destructive action by accident.\n- **Custom provider streams discovered through pi\'s model registry.** ([#42](https://github.com/k0valik/pi-blackhole/pull/42), thanks @FelikZ) The bridge that lets OM agents (observer/reflector/dropper) use custom providers (e.g. claude-bridge) now captures `streamSimple` functions from pi\'s public registry API (`getRegisteredProviderIds`/`getRegisteredProviderConfig`) on every `agent_start`, instead of wrapping `pi.registerProvider` and reading the private `registeredProviders` field. Works regardless of extension load order and includes providers added after startup; the legacy discovery path remains available for older pi releases.\n- **Precompiled extension bundle for faster startup.** The extension now ships a prebuilt `dist/index.js` bundle (tsup/esbuild) instead of being transpiled file-by-file by jiti at startup \u2014 module loading drops from ~85 source files to a single ESM file, measured ~1.6\u20132\xD7 faster extension load. The `@earendil-works/pi-*` packages and `typebox` stay external and resolve to the host pi\'s copies at runtime via its loader aliases. `pnpm build` produces the bundle; `prepare` builds automatically on install. The package manifest points at `./dist/index.js` and falls back to `index.ts` (slow path) when `dist/` is absent, so a fresh checkout still works pre-build.\n\n### Fixed\n\n- **Manual-mode pending files now contain full observation payloads.** ([#41](https://github.com/k0valik/pi-blackhole/pull/41)) The `noAutoCompact` \u2192 `compaction:\'manual\'` migration is completed: `isManualMode()` now checks both keys across all save/load gates, so manual-mode observations are written to the pending file (`savePendingObservation`) instead of falling through to `appendEntry()` (JSONL) \u2014 restoring crash-safe mid-run interruption recovery and `/blackhole flush` parity.\n- **Config modal could overwrite the user\'s config with defaults.** `openSettings` did not pass `globalConfigDir` to the settings modal, so the modal initialized every field from the schema default (it read a nonexistent config in the extensions dir) instead of the actual config file. Saving then wrote those defaults over the real values (e.g. `compactAfterTokens` 185000 \u2192 81000) while the runtime kept the correct values in memory \u2014 a confusing half-applied state. The modal now initializes from the real config file.\n- **Number-field editing could get stuck.** While inline-editing a number field, typing/backspace/escape were swallowed by the step-cycling branch, leaving the modal in an editing state with no way out (Enter showed a cursor but nothing worked, and `ctrl+c` couldn\'t close it). All editing keys now flow through the inline editor, and `ctrl+c` closes the modal even mid-edit.\n- **Typed input failed in Kitty terminals.** Kitty reports printable characters as CSI-u sequences (e.g. `5` arrives as `\\x1b[53u`); they were rejected by the input filter \u2014 and after the first fix, inserted as raw escape bytes. The input filter and the insert path now decode them, so typing works in Kitty terminals.\n- **Config save failures on read-only filesystems are now visible.** `ConfigManager.save()` throws when the write fails (e.g. config managed by Nix), and `/blackhole om-off`/`om-on` surface a warning \u2014 previously the failure was silently swallowed while the in-memory runtime state changed, diverging from disk without explanation.\n- **`PI_BLACKHOLE_*` env overrides now apply at runtime.** The declarative env map (`memory`, `debug`, `compactAfterTokens`, \u2026) was only honored by the modal path; the runtime config loader ignored it. The env map + application logic moved to a shared module used by both paths, so e.g. `PI_BLACKHOLE_COMPACT_AFTER_TOKENS=200000` now affects the actual compaction threshold, not just the modal display.\n\n### Testing\n\n- **Ported the upstream pi-base test suite (246 tests)** from `pi-utils/packages/pi-base` \u2014 config manager, settings modal (buffered mode, smoke, inline-edit, field validation) plus the 4 small modules (env, shell, types, ui) they cover. Only import-path adaptation was needed; zero semantic drift, which also confirms the vendored modal is behaviorally aligned with upstream.\n- **New regression tests pin this release\'s fixes:** config-manager `globalConfigDir` forwarding, number-field inline editing (including a Kitty CSI-u integration case driving the full renderer path), Kitty decode, and runtime env overrides.\n- **Tests no longer touch the system clipboard.** The memory-command tests ran the real `copyTextToClipboard` (spawning `wl-copy`/`xclip`/`xsel`) and overwrote the user\'s clipboard with fixture data; the module is now mocked and the mock\'s use is asserted so a regression fails the suite instead of mutating the clipboard.\n\n### Dependencies\n\n- **Bumped `@earendil-works/pi-*` devDependencies to `0.83.0`** (agent-core, ai, coding-agent, tui); the peer range stays `>=0.81.1 <1.0.0`. CI re-verifies typecheck + tests against the minimum supported `0.81.1` on every push/PR, so both the oldest and newest supported pi versions stay green.\n\n### Packaging\n\n- **Tolerant `prepare` build hook.** The `prepare` script is now a dependency-free `node scripts/prepare.mjs` that builds `dist/` only when the toolchain is present, and otherwise skips silently \u2014 it can never abort an install for git/checkout consumers running npm, pnpm, or bun in any devDependency configuration. Husky hooks install best-effort (dev checkouts only). Registry installs are unaffected (npm/pnpm/bun never run `prepare` on registry packages).\n- **npm publishing now uses provenance.** The publish workflow runs `npm publish --provenance` (GitHub OIDC attestation), so every tarball carries a signed signature linking it to this repo + workflow \u2014 verifiable with `npm audit signatures` / `gh attestation verify`. The release gate now matches CI (build, typecheck, lint, test, format check).\n- **Dev tooling.** Prettier (repo normalized once, enforced via lint-staged), husky pre-commit (lint+format staged files, then typecheck) and pre-push (typecheck + full test suite), ESLint extended to `tests/` and root configs, and CI now runs tests + format check alongside the build.\n\n---\n\n## [0.4.2] - 2026-07-27\n\n### Changed\n\n- **`midRunCompaction` default changed from `"resume"` to `"off"`.** ([#40](https://github.com/k0valik/pi-blackhole/issues/40), thanks @daoguademeng) `ctx.compact()` aborts the active agent operation before compacting, which is not lifecycle-safe at `turn_end` for subagent/background-work extensions: it propagates through the shared `AbortSignal` and cannot be distinguished from user cancellation. This affects both parent-side subagent workflows (active/queued children aborted, parent stalled) and child-side nested sessions (runner terminated, orphan transcript continues, `blackhole-resume` resumes a session the parent already sees as completed). `off` defers compaction to `agent_end`, which is the only currently safe boundary for extension-owned work. `resume` and `pause` are preserved as explicit opt-in for users without subagent workflows.\n\n---\n\n## [0.4.1] - 2026-07-24\n\n### Added\n\n- **Mid-run auto-compaction (`midRunCompaction`).** ([#38](https://github.com/k0valik/pi-blackhole/pull/38), thanks @daoguademeng) The threshold trigger previously only ran on `agent_end`, which never fires while the agent is looping through tool calls \u2014 during long runs `compactAfterTokens` could be exceeded many times over without a single evaluation, and the post-run wait was aborted by any new `agent_start`, deferring compaction indefinitely under continuous use. The threshold is now also evaluated at every `turn_end` (after each assistant message + tool executions). New config enum `midRunCompaction: "resume" | "pause" | "off"` (default `"resume"`): `resume` compacts at the threshold and injects a `blackhole-resume` message (`triggerTurn`) so the agent continues the task with the compacted context; `pause` compacts and hands control back; `off` restores the old end-of-run-only behavior. Available in `/blackhole configure`.\n- **`/blackhole <text>` follow-up prompt.** After compaction, `/blackhole` optionally sends `<text>` as a follow-up message so the model continues the task without re-typing. Wrapped in `void Promise.resolve(...).catch(() => {})` for robust error handling.\n- **Subcommand near-miss detection.** `/blackhole configure foo` now shows a warning instead of silently becoming a follow-up prompt.\n\n- **`/blackhole cleanup` command for orphaned pending files.** Per-session pending files (`*-pending.json`, `*-pending.stale.json`) accumulate when compaction is manual and sessions are abandoned or deleted. The command scans the `pi-blackhole/` directory, cross-references session IDs against all session JSONL files, and provides an interactive TUI picker to safely remove orphaned files. Non-TUI modes (RPC/JSON/print) list orphaned files as a notification without deleting.\n\n### Command formatting cleanup\n\n- `/blackhole` and `/blackhole-memory` subcommands and modes now use `[bracketed]` syntax (e.g. `[om-on]`, `[hybrid]`) with shortened descriptions, making the command palette visually consistent and easier to scan.\n\n### Notification & session goal reorg\n\n- Session goal now derives from the first user message and is persisted at the top across compactions, with `(#N)` entry indexing for traceability.\n- OM info notifications are gated to one per phase/turn \u2014 warnings and errors still fire immediately.\n- Git commit extraction now handles tool_call, bash, and post-convert user-text formats.\n- Cooldown skip messages now strip raw JSON from the reason for cleaner display, with a log pointer for debugging.\n\n### Fixed\n\n- **Mid-run compaction failure resilience.** ([#38](https://github.com/k0valik/pi-blackhole/pull/38), thanks @daoguademeng) If the before-compact hook cancels (or compaction errors) after `ctx.compact()` has already aborted the run, resume mode still re-triggers the agent so the task doesn\'t stall, and further mid-run attempts are suspended until a compaction lowers pressure below the threshold (prevents abort/cancel thrash loops).\n- **Early-session reflection/drop starvation on first compaction.** Added `fullFoldAlways` config flag (default `true`). When no prior full-fold boundary exists, reflections and drops now use the observation boundary instead of being excluded. Previously, fresh sessions silently lost all durable memory on the first compaction because there was no full-fold history to anchor the maintenance boundary.\n- **`capBrief` omission count now computed after `firstHeader` trim.** Previously the "N earlier lines omitted" header was computed before the section-header anchor trim, so the count was understated when headers caused additional trimming. This matched an upstream bug that was already fixed there.\n\n- **Recall-note bloat across multiple compactions.** `compile()` now strips OM content first, then removes all recall-note paragraphs from the previous summary using paragraph-level matching (instead of only stripping a trailing exact match). After 3+ compactions, the summary no longer accumulates 3+ embedded copies of the recall note.\n\n## [0.4.0] - 2026-07-24\n\n### Added\n\n- **`/blackhole cleanup` command for orphaned pending files.** Per-session pending files (`*-pending.json`, `*-pending.stale.json`) accumulate when compaction is manual and sessions are abandoned or deleted. Provides an interactive TUI picker to safely remove orphaned files. Non-TUI modes (RPC/JSON/print) list them without deleting.\n- **`dropperPressureThreshold` in configure overlay.** Already in config schema but missing from `/blackhole configure` TUI. Now editable alongside other OM thresholds.\n- **`fullFoldAlways` in TUI overlay.** Added to the configure overlay under Observational Memory section.\n- **Session goal from first user message.** Persisted at the top across compactions with `(#N)` entry indexing for traceability.\n- **OM info notifications gated to one per phase/turn.** Warnings and errors still fire immediately.\n- **Git commit extraction expanded.** Now handles `tool_call`, `bash`, and post-convert user-text formats.\n- **Cooldown skip messages strip raw JSON** from the reason for cleaner display, with a log pointer for debugging.\n- **`/blackhole` and `/blackhole-memory` subcommands now use `[bracketed]` syntax** (e.g. `[om-on]`, `[hybrid]`) with shortened descriptions for visual consistency.\n\n### Fixed\n\n- **Early-session reflection/drop starvation on first compaction.** Added `fullFoldAlways` config flag (default `true`). When no prior full-fold boundary exists, reflections and drops use the observation boundary instead of being excluded.\n- **Recall-note bloat across multiple compactions.** `compile()` strips OM content first, then removes all recall-note paragraphs using paragraph-level matching (instead of only stripping a trailing exact match).\n- **OAuth/ADC-backed providers (Vertex, custom OAuth) now accepted by OM pipeline.** `resolveModel` uses `modelRegistry.hasConfiguredAuth()` instead of requiring a truthy `auth.apiKey`. Falls back to legacy behavior on older pi versions. ([#38](https://github.com/k0valik/pi-blackhole/issues/38))\n- **`ResolveResult.apiKey` is always a string.** Defaults to `""` instead of casting `undefined`.\n- **jiti provider bridge type-safe for pi 0.81.1+.** `pi.registerProvider` wrapper satisfies the overloaded signature in pi-coding-agent 0.81.1.\n- **Config overlay blocks save on invalid JSON.** Red error banner and Ctrl+S block prevent wiping model configs on corrupt files. ([#35](https://github.com/k0valik/pi-blackhole/issues/35))\n- **Config reloads after overlay save.** `Runtime.reloadConfig()` forces a fresh disk read after `/blackhole configure` saves. ([#36](https://github.com/k0valik/pi-blackhole/issues/36))\n- **Invalid JSON warning surfaced via TUI.** Yellow warning notification shown at every config load point instead of only `console.warn`.\n- **Defensive null guards for `b.args` and `ui.notify`.** Prevents crashes from stale extension context.\n- **`streamSimple` import updated to `pi-ai/compat`.** Removed from main export in pi 0.80.3.\n- **Legacy fallback config errors now passed to `onWarn` callback.** JSON parse errors in legacy fallback files (`pi-vcc-config.json`, `settings.json`, `.pi/settings.json`) are surfaced via the warning callback, not just `console.warn`.\n- **`saveUnifiedConfig` warns before overwriting corrupt config.** If the config file has invalid JSON, a warning is logged before overwriting.\n- **`dropperPressureThreshold` clamped to `[0.01, 1]` in overlay save.** Previously could silently lose value on reload.\n- **`deleteOrphanedBatch` reports partial failures.** "Delete all" now shows `Deleted X/Y (Y-X failed)` when individual unlinks fail.\n- 4 new tests for `fullFoldAlways` behavior in `buildCompactionProjection`: reflections survive first compaction when enabled, excluded when disabled, full-fold boundary still takes precedence, and post-boundary reflections remain excluded.\n- 3 new tests for recall-note deduplication in `compile`: wrapped recall note stripped, OM content stripped before recall note, and three-cycle accumulation produces exactly one recall note.\n- 5 new tests for follow-up prompt: extraction, subcommand exclusion, empty-args suppression, send after completion, compaction-failure suppression.\n- 6 new tests for CompactionStats population: all fields populated, compactAll flag, totalUserTurns count, keptUserTurns count, compactAll zero kept, and format string coverage.\n- 2 new tests for capBrief omission count: header-trimmed count is correct (99 for 200 lines with header at line 100), and no-header fallback still correct.\n\n### Changed\n\n- **New config key:** `fullFoldAlways` (boolean, default `true`). Added to `UnifiedConfig` schema, defaults, and config file parsing.\n- **CompactionStats expanded from 3 to 11 fields.** Added `compactAll`, `totalUserTurns`, `keptUserTurns`, `requestedKeepUserTurns`, `keepUserTurnsExplicit`, `keepFallbackToCompactAll`, `smartKeepAdjusted`, `smartFromKeep`. All populated from `buildOwnCut` return data (Bug A fix).\n- **Shared `formatCompactionStats` exported.** Both the `/blackhole` command handler and hook\'s `session_compact` handler now use a single shared formatter, eliminating the duplicate inline toast strings and the private `formatTokens` helper.\n- **Dead ternary collapsed.** `effectiveTailBehavior` no longer has an `isPiVcc` branch with identical values on both sides (Bug B fix).\n- **Dependencies: bumped `@earendil-works/pi-*` packages to `0.81.1`** (agent-core, ai, coding-agent, tui).\n- **Removed 6 unused exports from `om/cleanup.ts`** (`scanPendingFiles`, `findSessionDirs`, `collectAllSessionIds`, `crossReference`, `formatSize`, `formatAge`).\n\n### Tests\n\n- 4 new tests for `fullFoldAlways` behavior in `buildCompactionProjection`.\n- 3 new tests for recall-note deduplication in `compile`.\n- 6 new tests for OAuth/ADC auth paths.\n- Tightened capping assertions in robust tests.\n- Added robust coverage for OM and CCC pipelines.\n\n---\n\n## [0.3.9] - 2026-06-24\n\n### Auto-compaction idle race fix (#31, #33)\n\nThe auto-compaction trigger used to bail permanently when `ctx.isIdle()`\nreturned `false` at the first `setTimeout(0)` check after `agent_end`.\nWhen another extension (e.g. pi-rewind) registered an async `agent_end`\nhandler whose I/O kept the agent state busy past the next macrotask,\nthe trigger logged `"bail: not_idle"` and never retried \u2014 auto-compaction\neffectively never fired in this configuration.\n\n**New behavior:** the trigger keeps `compactInFlight = true` and polls\n`isIdle()` every 200ms (in 50ms slices) until the agent truly settles,\nor one of two cancellation signals:\n\n- `agent_start` fires \u2014 the user (or another extension) started a new\n turn. `AbortController.abort()` cancels the wait; the new turn\'s own\n `agent_end` will re-evaluate and start a fresh wait if still needed.\n- Session change (e.g. `/resume`) \u2014 detected inside the wait loop.\n\nOnly the cell `compaction:auto + compactionEngine:blackhole` is affected.\nAll other config combinations (off, manual, pi-default) are unchanged.\n\n### CI: fallow audit job\n\n- Added `fallow-audit` CI job (PR only, changed-code audit with compact\n format, review comments, no SARIF)\n\n### Test cleanup\n\n- Removed stale `transcript-mode` tests left orphaned when the feature\n was deliberately dropped in v0.3.7 as redundant with hybrid search.\n\n---\n\n## [0.3.8] - 2026-06-19\n\n### Pipeline progress cursors - fix re-run loop (#28, #29)\n\nThe pipeline previously coupled progress tracking to output markers: if a stage\nproduced empty output or errored, no marker was written, causing the stage to\nre-process the same data on every `agent_start`/`turn_end` trigger. In real-world\nlogs the dropper ran 8,350\xD7 vs observer 1,124\xD7, with zero drops selected.\n\n- **Per-stage progress cursors** decouple progress from output. Each stage\n (observer, reflector, dropper) gets a cursor entry ID that advances whenever\n the stage runs - regardless of whether it produced output. "I looked and\n found nothing" is a valid answer that blocks re-processing.\n- **Cursor `state` field** (`recorded` | `empty` | `error` | `skipped` | `not_due` | `initial`)\n distinguishes empty runs from skipped stages from actual output.\n- **Reflector gates on new data.** If no new `OM_OBSERVATIONS_RECORDED` batches\n exist since the reflector cursor, and `reflectAfterTokens` threshold not met,\n skip entirely - no LLM call.\n- **Dropper gates on pressure or new data.** Runs only when pool \u2265 10% fullness AND\n (new data exists OR pool \u2265 `dropperPressureThreshold` \xD7 `reflectorInputMaxTokens`).\n Previously always returned `not_over_target` with 0 drops - now correctly skipped.\n- **Cursor storage:** in-memory primary (zero-I/O gating), async flush to\n `{sessionId}-pending.json` for durability across restarts. Degrades gracefully\n on read-only filesystems.\n- **Stale cursor recovery:** if a cursor\'s entry ID disappears (fork, navigation,\n compaction), falls back to coverage-marker logic for one run, then writes fresh cursors.\n\n### New config key: `dropperPressureThreshold`\n\n- Fraction of `reflectorInputMaxTokens` at which the dropper fires even without\n new data (pressure relief valve). Default `0.70` (70%). Set to `1.0` to disable\n pressure-driven dropper entirely.\n\n### Debug log additions\n\n- `observer.skip`, `reflector.start`, `reflector.skip`, `dropper.start`,\n `dropper.skip`, `cursor.loaded`, `cursor.saved`\n\n### Deferred pipeline concerns (pre-merge review)\n\nAudit surfaced 7 correctness/performance edge cases in the cursor pipeline.\nFour were fixed; three were deferred as harmless or cosmetic.\n\n**Fixed:**\n\n- **Session fork cursor bleed (#2).** `cursorsLoaded` was a one-shot boolean\n \u2014 on session fork, stale cursors bled into the new branch because\n `validateCursors` was never re-invoked. Now keyed by `cursorsLoadedSessionId`\n so cursors are re-loaded and re-validated whenever the session ID changes.\n- **Manual-mode pool fullness underestimation (#1).** `anyStageDue` had no\n visibility into pending observations in `compaction: "manual"` mode (branch\n has no OM markers). Reflector and dropper due checks now accept an optional\n `PendingOMState` so pending batches contribute to new-data scans and pool\n token counts. Prevents the pipeline from stalling after the first run in\n manual mode.\n- **foldLedger on every agent_start/turn_end (#3).** `dropperDue` called\n `foldLedger` (O(n) on branch) unconditionally \u2014 even when the observer or\n reflector alone made the pipeline due. Now short-circuits: the fold is\n only computed when both observer and reflector are not due.\n- **Observer cursor to non-source entry (#6).** When the observer skipped\n (not due), the cursor advanced to `entries.at(-1)` which could be a custom\n OM marker rather than a conversation source entry. Now advances to the\n last source entry (`findLast(isSourceEntry)`).\n\n**Deferred:**\n\n- **"unknown" magic entry ID (#4).** Functional but cosmetic \u2014 the sentinel\n triggers fallback on next load. 9 call sites; zero behavioral change.\n- **Observer re-checks tokens (#5).** Harmless \u2014 only reached when pipeline\n launched for a different stage. Correctly advances cursor to `not_due`.\n- **Dropper cursor fallback cascade (#7).** The 4-step `coversUpToId ??`\n `observationCoverageId ?? entries.at(-1)?.id` cascade is already reasonable\n fallback ordering.\n\n### Tests\n\n- 17 new tests for cursor gating, persistence, stale recovery, and debug log events\n- 3 new tests for manual-mode pending awareness (reflector, dropper, post-first-run)\n- `dropperPressureThreshold` added to config validation tests\n\n# Changelog\n\n## [0.3.7] - 2026-06-10\n\n### Recall tool simplification (#27)\n\n- Dropped `mode:transcript` \u2014 strict subset of `mode:hybrid` with no unique capability. (#27)\n- Consolidated 5 scattered `promptGuidelines` into 2 focused entries; removed "NOT semantic" redundancy and JSONL implementation leak. (#27)\n- Removed internal taxonomy from mode descriptions ("transcript + file indicators" \u2192 "all session content"). (#27)\n- Added `mode:touched` support to `/blackhole-recall` command (previously only worked via agent tool). (#27)\n- Collapsed drill-down examples to `#N:path with optional :offset:limit or :full`. (#27)\n\n### Stale context crash protection (#26)\n\n- Added `getErrorMessage()` to normalize cross-process error serialization (Error objects, plain objects with `message`, arbitrary thrown values). (#26)\n- Added `isStaleExtensionContextError()` to detect stale-context error patterns. (#26)\n- Added `notifySafely()` wrapper around `ui.notify()` calls to prevent stale-context notification errors from propagating. (#26)\n- Wrapped `agent_end` handler, async compaction callbacks (`onComplete`, `onError`), and deferred timer callback to silently bail on stale-context errors. (#26)\n\n### Lockstep sync \u2014 2026-06-05 (#25)\n\n- Ported [pi-observational-memory/58f05fa](https://github.com/elpapi42/pi-observational-memory/commit/58f05fa): remove `Math.min(100)` cap from `pct()` helper so overfull observation pool (>100%) is displayed accurately instead of silently capping at 100%. (#25)\n- Skipped [pi-observational-memory/58f05fa](https://github.com/elpapi42/pi-observational-memory/commit/58f05fa) command renames (`/om-status`\u2192`/om:status`, `/om-view`\u2192`/om:view`) \u2014 our equivalent commands (`/blackhole-memory`) already use a different naming scheme. (#25)\n- Deferred [pi-observational-memory/bf79ff7](https://github.com/elpapi42/pi-observational-memory/commit/bf79ff7) and [pi-observational-memory/52b5844](https://github.com/elpapi42/pi-observational-memory/commit/52b5844): pool metrics extraction + `budgetTokens`\u2192`targetTokens` rename. Blocking branch (`noautocompact-reflector-dropper`) is now stale/dropped, but changes touch heavily diverged files. (#25)\n\n## [0.3.5] - 2026-06-04\n\n### Added\n\n- **`sessionFallback` config option.** When `false`, skip the main session model as last-resort fallback when all OM-specific model candidates are exhausted. Default `true` for backward compatibility. Useful for keeping OM workers on cheaper/faster models. ([#20](https://github.com/k0valik/pi-blackhole/pull/20))\n- **Session-file LRU cache.** `loadAllMessages` now caches up to 3 session files with mtime + TTL (2s) invalidation. Reduces redundant I/O on repeated recall searches in the same session. ([#20](https://github.com/k0valik/pi-blackhole/pull/20))\n- **Pending state sanitization.** `readSessionState` now filters corrupted batch entries (missing `coversUpToId` or `data` fields) instead of returning them as-is. Prevents crashes from edge cases like a partial write to `pending.json`. ([#20](https://github.com/k0valik/pi-blackhole/pull/20))\n- **Shared `isRetryableError` / `RETRYABLE_ERROR_RE`.** Extracted from `cooldown.ts` and `compaction-trigger.ts` into `retryable-error.ts` \u2014 single source of truth, re-exports Pi\'s `isContextOverflow` for provider-specific overflow detection. ([#20](https://github.com/k0valik/pi-blackhole/pull/20))\n- **Shared provider-stream bridge.** `createBridgeStreamFn` extracted from all three OM agents (observer, reflector, dropper) into `provider-stream.ts`. Custom providers registered by other extensions (e.g., claude-bridge) continue working through jiti-loaded consolidation agents. ([#21](https://github.com/k0valik/pi-blackhole/pull/21))\n- **Async buffered debug logging.** `debugLog()` now buffers JSONL writes in memory and flushes on a 1-second background timer, with synchronous flush on `exit`. Reduces event-loop blocking during high-frequency debug events. ([#20](https://github.com/k0valik/pi-blackhole/pull/20))\n- **Windows path support in file extraction.** `longestCommonDirPrefix` normalizes backslashes and recognizes `C:\\`-style drive letters. ([#20](https://github.com/k0valik/pi-blackhole/pull/20))\n\n### Fixed\n\n- **Context window check uses actual input size, not configured cap.** Observer/reflector/dropper now compute `observerEstimatedInput` from the actual chunk tokens after capping, not from `observerChunkMaxTokens`. More accurate \u2014 fewer false "context window exceeded" rejections on smaller-than-cap inputs. ([#20](https://github.com/k0valik/pi-blackhole/pull/20))\n- **`coversUpToId` now points past capping, not before.** Observer stage captured the last entry ID before capping source entries to `maxChunkTokens`, so the coverage marker could point to an entry that was dropped. Now captured after capping. ([#20](https://github.com/k0valik/pi-blackhole/pull/20))\n- **`capSourceEntriesToTokens` counts all entry types.** Previously only `"message"` entries counted toward the token budget \u2014 custom OM entries (`observations_recorded`, `reflections_recorded`, etc.) and summary-bearing entries were invisible, risking context overflow in the observer. ([#20](https://github.com/k0valik/pi-blackhole/pull/20))\n- **Reflector/dropper avoid redundant disk reads.** Both stages now use the outer-scope `pending` variable (already read in the `noAutoCompact` block) instead of calling `readPendingState(sessionId)` again inside the for loop. Neutral correctness win. ([#20](https://github.com/k0valik/pi-blackhole/pull/20))\n- **Observer rejects invalid observation IDs gracefully.** `normalizeSourceEntryIds` now filters out unknown/duplicate IDs instead of returning `undefined` and discarding the entire observation batch. One hallucinated ID from the LLM no longer loses valid observations. ([#20](https://github.com/k0valik/pi-blackhole/pull/20))\n- **`pendingObservationsCreatedAfter` properly typed.** Changed from `pending: any` to `pending: PendingOMState` \u2014 catches type mismatches at compile time. ([#20](https://github.com/k0valik/pi-blackhole/pull/20))\n- **Section headers in summaries use line-boundary regex.** `sectionOf` and `stripOMContent` now match `## Reflections` / `## Observations` at the start of a line instead of using bare `indexOf`. Prevents false positives when those phrases appear inside file paths or conversation text. ([#20](https://github.com/k0valik/pi-blackhole/pull/20))\n- **Read+same-path-Modified dedup in file summaries.** `mergeFileLines` now removes a path from `Read` if it also appears in `Modified` \u2014 a file that was read then edited shouldn\'t show twice. ([#20](https://github.com/k0valik/pi-blackhole/pull/20))\n- **`reverse-recall` outputs related reflections.** The `_reflections` dead parameter is now used \u2014 related reflections are shown alongside observations when expanding session entries. ([#20](https://github.com/k0valik/pi-blackhole/pull/20))\n- **Cooldown reason in UI notification.** The `getCooldownEntry` function now returns the actual entry (with reason), so the status notification shows _why_ a model was cooled down, not just "cooldown active". ([#20](https://github.com/k0valik/pi-blackhole/pull/20))\n- **Env override validation.** Invalid `PI_BLACKHOLE_COMPACTION` / `PI_BLACKHOLE_COMPACTION_ENGINE` values now print a warning instead of being silently ignored. ([#20](https://github.com/k0valik/pi-blackhole/pull/20))\n- **`observerPreambleMaxTokens` accepts 0.** Now uses `nonNegativeInt` validator instead of `positiveInt` \u2014 0 means "auto-compute", which was the intended semantics. ([#20](https://github.com/k0valik/pi-blackhole/pull/20))\n\n### Changed\n\n- **Replaced hand-rolled text wrapping with `wrapTextWithAnsi` from pi-tui.** The custom `wrapLine` function was replaced with `wrapLineWithContinuation` using pi-tui\'s ANSI-aware wrapping. Handles list continuation indentation and ANSI mid-sequence splits correctly. ([#21](https://github.com/k0valik/pi-blackhole/pull/21))\n- **`visibleWidth` re-exported from pi-tui.** The local CJK-width implementation in `key-matcher.ts` was replaced with a re-export from `@earendil-works/pi-tui`. Fallback note retained if the import fails in overlay context. ([#21](https://github.com/k0valik/pi-blackhole/pull/21))\n- **Bash command compression improved.** Multi-line commands joined with semicolons instead of first-line-only. Pipe tails strip `awk`/`python3`/`node`/`bun` excluded (their output carries semantic meaning). Word-boundary truncation instead of mid-word cut. Up to 10 tail-strip iterations with stability guard. ([#20](https://github.com/k0valik/pi-blackhole/pull/20))\n- **`fuzzyMatch` \u2192 `prefixMatch`.** The `/blackhole` subcommand filter changed from fuzzy/subsequence matching to simple prefix matching. Predictable narrowing: typing "om" matches "om-on" and "om-off". ([#21](https://github.com/k0valik/pi-blackhole/pull/21))\n- **`read` tool summary field corrected.** `TOOL_SUMMARY_FIELDS` now maps lowercase `read` \u2192 `"path"` (not `"file_path"`), matching the actual tool argument. ([#20](https://github.com/k0valik/pi-blackhole/pull/20))\n- **Tool error blank-line suppression.** `stringifyBrief` now suppresses blank lines between consecutive tool/error summaries (previously only between consecutive tool summaries). ([#20](https://github.com/k0valik/pi-blackhole/pull/20))\n- **Recall header distinguishes matches vs expands.** The search result header now shows `"X matches (+ Y expanded)"` when entries were pulled in via `#N` expand rather than matching the query. ([#20](https://github.com/k0valik/pi-blackhole/pull/20))\n- **Compaction output instructions split into full/basic variants.** `CONTEXT_USAGE_INSTRUCTIONS` shortened to 4 lines (previously 10). When observations/reflections are present, the full version includes the bracketed-ids preamble + recall footer. When none exist (or OM is off), a basic 2-line recall-guidance footer is appended instead. `renderSummary` always returns a footer, and `stripOMContent` handles both variants to prevent compounding. ([#23](https://github.com/k0valik/pi-blackhole/pull/23))\n\n### Removed\n\n- **Dead `loadSettings()` / `PiVccSettings`.** Config loading unified in `unified-config.ts` \u2014 the `settings.ts` wrapper had zero callers. ([#20](https://github.com/k0valik/pi-blackhole/pull/20))\n- **Dead `transcriptEntries` from `SectionData`.** Removed from `sections.ts` and `build-sections.ts`. (dead since v0.3.3) ([#20](https://github.com/k0valik/pi-blackhole/pull/20))\n- **Dead toggle helpers.** `toggleCompaction`, `toggleCompactionEngine`, `toggleTailBehavior` removed from `unified-config.ts` (zero callers \u2014 toggling is handled by the configure overlay). ([#20](https://github.com/k0valik/pi-blackhole/pull/20))\n- **Dead `vcc-report.test.ts`.** Test file was testing a non-existent `src/core/report.js` module. ([#20](https://github.com/k0valik/pi-blackhole/pull/20))\n- **Dead `config-simplification.test.ts`.** Tested old config migration that\'s been stable since v0.3.3. ([#20](https://github.com/k0valik/pi-blackhole/pull/20))\n\n### Docs\n\n- **Renamed example configs.** `example-config-v2.json` \u2192 canonical `example-config.json` (new config surface). Old `example-config.json` \u2192 `example-config-old.json` (legacy keys). ([#21](https://github.com/k0valik/pi-blackhole/pull/21))\n- **README: updated "What the agent sees" example** to match actual output ordering and expanded RECALL_NOTE text. ([#21](https://github.com/k0valik/pi-blackhole/pull/21))\n- **README: added `sessionFallback` to settings table.** ([#20](https://github.com/k0valik/pi-blackhole/pull/20))\n- **example-config.json: added `sessionFallback` field.** ([#20](https://github.com/k0valik/pi-blackhole/pull/20))\n- **README: updated "What the agent sees" example** to match the new shorter CONTEXT_USAGE_INSTRUCTIONS text and note about basic footer when OM is off. ([#23](https://github.com/k0valik/pi-blackhole/pull/23))\n\n## [0.3.4] - 2026-06-02\n\n### Added\n\n- **`cooldownHours: 0` disables cooldown without disk writes.** Previously `cooldownHours: 0` was rejected by the positive-int validator and silently replaced with a 1-hour cooldown. Now 0 is a valid value that disables cooldown entirely \u2014 no disk writes, no persistent state. Failed models are tracked in-memory within each consolidation stage (via `failedInCycle` set) so the fallback chain still advances past them. ([#16](https://github.com/k0valik/pi-blackhole/issues/16), [#18](https://github.com/k0valik/pi-blackhole/pull/18))\n- **Kitty CSI-u keyboard protocol support for overlays.** The configure and status overlays use pi-tui\'s `matchesKey` (which handles both legacy terminal sequences and Kitty\'s CSI-u protocol) instead of the homegrown `matchKey`. Digit input uses `decodeKittyPrintable` to decode CSI-u encoded characters. ([#17](https://github.com/k0valik/pi-blackhole/issues/17), [#19](https://github.com/k0valik/pi-blackhole/pull/19))\n- **Per-stage failure notification isolation.** When cooldown is disabled, each consolidation stage (observer, reflector, dropper) now shows its own failure notification \u2014 observer failure no longer suppresses reflector/dropper notifications. ([#19](https://github.com/k0valik/pi-blackhole/pull/19))\n\n### Fixed\n\n- **Keyboard freeze in `/blackhole configure` on Kitty terminal.** The homegrown `matchKey` function did not recognize Kitty\'s CSI-u keyboard protocol sequences (used by Kitty, WezTerm, and other modern terminals). Switched to pi-tui\'s `matchesKey` which supports both legacy and CSI-u input. ([#17](https://github.com/k0valik/pi-blackhole/issues/17), [#19](https://github.com/k0valik/pi-blackhole/pull/19))\n- **Config error notifications no longer downgraded to info.** When a session model has no API key configured, the notification correctly shows a "warning" level message instead of the misleading "info" message previously shown when `failedInCycle` was non-empty. ([#16](https://github.com/k0valik/pi-blackhole/issues/16), [#18](https://github.com/k0valik/pi-blackhole/pull/18))\n\n### Changed\n\n- **Removed `key-matcher.ts` `matchKey` export** (replaced by pi-tui\'s `matchesKey`). The `visibleWidth` export is retained.\n\n## [0.3.3] - 2026-06-02\n\n### Added\n\n- **New config surface:** `compaction` (`"auto"` | `"manual"` | `"off"`), `compactionEngine` (`"blackhole"` | `"pi-default"`), `tailBehavior` (`"pi-default"` | `"minimal"`). These replace the old `overrideDefaultCompaction`, `noAutoCompact`, and `passive` keys. See [`MIGRATION-GUIDE.md`](MIGRATION-GUIDE.md) for the full mapping. ([#14](https://github.com/k0valik/pi-blackhole/pull/14))\n- **Config overlay (`/blackhole configure`):** interactive TUI with \u2191\u2193 navigation, Enter to edit/toggle, Ctrl+S to save. 17 fields across 3 sections (Compaction, Observational Memory, Debug) with inline help text. ([#14](https://github.com/k0valik/pi-blackhole/pull/14))\n- **Status overlay (`/blackhole-memory`):** new render with compaction config readout, OM pipeline state, and inline actions (configure, om-off/on). ([#14](https://github.com/k0valik/pi-blackhole/pull/14))\n- **Tail behavior control:** `tailBehavior: "minimal"` keeps only the last user message (aggressive pi-vcc cut, default); `tailBehavior: "pi-default"` keeps Pi\'s ~20k token tail visible (opt-in). Both auto-triggered and `/blackhole` now default to `"minimal"`. ([#14](https://github.com/k0valik/pi-blackhole/pull/14))\n- **12 permutation tests** covering all compaction \xD7 memory \xD7 threshold combinations for the new config keys. ([#14](https://github.com/k0valik/pi-blackhole/pull/14))\n- **Documentation:** CONFIG.md (new reference), OLD_CONFIG.md (legacy docs), MIGRATION-GUIDE.md (migration path from old keys), README.md and llms.txt updated for the new surface. ([#14](https://github.com/k0valik/pi-blackhole/pull/14))\n- **Per-model context window override:** `OmModelConfig` now supports an optional `contextWindow` field. When set on any stage model or fallback, it overrides Pi\'s model registry value for the context window check. Unset models inherit from Pi normally. ([#14](https://github.com/k0valik/pi-blackhole/pull/14))\n- **Context window pre-check:** before calling each OM stage agent (observer, reflector, dropper), the estimated input tokens (stage cap + 8K reserve for system prompt/tools/turns) are checked against the model\'s effective context window. If the input exceeds the window, the model is skipped and the next fallback is tried. If all models are exhausted, a warning is shown. Strictly opt-in \u2014 with default caps (40K\u201380K) and typical models (128K+), the check is a no-op unless a `contextWindow` override is explicitly set. ([#14](https://github.com/k0valik/pi-blackhole/pull/14))\n- **8 tests** covering context window parsing from config, priority resolution, rejection of invalid values, and `effectiveContextWindow` logic. ([#14](https://github.com/k0valik/pi-blackhole/pull/14))\n\n### Changed\n\n- **`memory: false` no longer blocks auto-compaction.** Memory and compaction are now truly independent \u2014 `memory: false` stops OM workers but compaction still runs. Use `compaction: "manual"` or `compaction: "off"` to control compaction separately. ([#14](https://github.com/k0valik/pi-blackhole/pull/14))\n- **`compaction: "off"` semantics refined:** blocks blackhole\'s auto-trigger and returns early from the before-compact hook for auto-triggered compactions (letting Pi handle them), but explicit `/blackhole` still uses blackhole\'s pipeline. ([#14](https://github.com/k0valik/pi-blackhole/pull/14))\n- **Config migration is automatic:** old keys (`overrideDefaultCompaction`, `noAutoCompact`, `passive`) are migrated to new keys in memory at load time. The on-disk file is never mutated. New keys take priority when present. ([#14](https://github.com/k0valik/pi-blackhole/pull/14))\n- **Permutation tests updated** to reflect the new behavior: `overrideDefaultCompaction` now gates the legacy trigger path, `memory` no longer gates the trigger, and the 16-permutation matrix uses the correct formula. ([#14](https://github.com/k0valik/pi-blackhole/pull/14))\n\n### Fixed\n\n- **Save error handling:** `save()` returns boolean and wraps writes in try/catch \u2014 read-only filesystems (e.g., Nix-managed config) no longer crash with an unhandled exception. ([#14](https://github.com/k0valik/pi-blackhole/pull/14))\n- **Number input restriction:** configure overlay now only accepts digits for number fields, preventing garbage values from being entered. ([#14](https://github.com/k0valik/pi-blackhole/pull/14))\n- **Defensive bounds:** section header pads in configure-overlay and status-overlay use `Math.max(0, ...)` / `Math.max(2, ...)` to prevent negative `.repeat()` counts on tiny terminals. ([#14](https://github.com/k0valik/pi-blackhole/pull/14))\n- **Config save failure warning:** `/blackhole configure` now shows a "warning" notification when the config file can\'t be written instead of a misleading "info" notification. ([#14](https://github.com/k0valik/pi-blackhole/pull/14))\n- **Legacy config tests:** updated `config.test.ts` to check new config keys (`compaction`, `compactionEngine`, `memory`) instead of deleted legacy fields (`passive`, `overrideDefaultCompaction`), fixing 10 pre-existing test failures. ([#14](https://github.com/k0valik/pi-blackhole/pull/14))\n- **pi-default non-message firstKeptEntryId resolution:** when Pi\'s `firstKeptEntryId` points to a non-message entry (e.g., OM metadata or compaction), `buildOwnCut` now resolves to the next actual message entry instead of falling through to the minimal cut. ([#15](https://github.com/k0valik/pi-blackhole/pull/15))\n- **Array micro-optimization in buildOwnCut:** replaced `branchEntries.slice(cutInBranch + 1).find()` with `branchEntries.find()` using an index check, avoiding a temporary array allocation. ([#15](https://github.com/k0valik/pi-blackhole/pull/15))\n\n## [0.3.2] - 2026-06-01\n\n### Fixed\n\n- **Auto-compaction gating:** added explicit guard at the top of the compaction trigger that returns early when `overrideDefaultCompaction` is `false` (the default). Previously, blackhole would still evaluate token thresholds and call Pi\'s default compaction hook even when not opted in \u2014 causing confusing log entries and unnecessary evaluations. Now blackhole stays completely out of Pi\'s compaction unless the user explicitly opts in. ([#13](https://github.com/k0valik/pi-blackhole/pull/13))\n\n### Added\n\n- **README top banner:** prominent NOTE at the top instructing users to set `"overrideDefaultCompaction": true` for blackhole to handle compaction automatically. Existing config matrix in the IMPORTANT section retained for reference.\n\n## [0.3.1] - 2026-05-31\n\n### Fixed\n\n- **Auto-compaction idle detection timing:** changed compaction scheduling from `queueMicrotask` to `setTimeout(..., 0)`. The microtask fired before Pi completed its post-response processing cycle, causing `ctx.isIdle()` to always return `false` and compaction to be deferred indefinitely. `setTimeout` yields to the event loop, allowing Pi to mark itself idle before the callback runs. ([#13](https://github.com/k0valik/pi-blackhole/pull/13))\n\n### Added\n\n- **Debug logging for compaction pipeline:** structured `debugLog` instrumentation at every decision point \u2014 guard checks, token threshold evaluation, branch entry inspection, session identity validation, idle check, and compaction completion/error. Opt-in via `"debugLog": true` in config, zero overhead otherwise. ([#13](https://github.com/k0valik/pi-blackhole/pull/13))\n- **Permutation test suite:** 36 new tests covering all 16 configuration knob combinations for auto-compaction trigger behavior. ([#13](https://github.com/k0valik/pi-blackhole/pull/13))\n\n## [0.2.4] - 2026-05-29\n\n### Recall: progressive discovery\n\n- **Touched mode (`mode:touched`):** aggregate view of all files written/edited across the session, grouped by path with entry indices. Accessible via `recall` tool and `/blackhole-recall` command. ([#12](https://github.com/k0valik/pi-blackhole/pull/12))\n- **Drill-down (`#N:path`):** read file content from tool call arguments in any transcript entry. Supports `#42:auth.ts` (preview first 30 lines), `#42:auth.ts:full` (all lines), `#42:auth.ts:offset:limit` (paged). Path auto-selects when unique; ambiguous paths list options. ([#12](https://github.com/k0valik/pi-blackhole/pull/12))\n- **Search mode filtering (`mode:file`, `mode:transcript`, `mode:hybrid`):** `mode:file` searches only write/edit file content; `mode:transcript` searches only conversation text; `mode:hybrid` (default) searches both. ([#12](https://github.com/k0valik/pi-blackhole/pull/12))\n- **Merged expand + search:** `#N` expand entries are now merged into search results (rather than being mutually exclusive), with proper pagination and sorting. ([#12](https://github.com/k0valik/pi-blackhole/pull/12))\n- **`scope` parameter as `StringEnum`:** tool schema now uses `StringEnum` (strict literal union) instead of `Type.Union` for `scope` and `mode` parameters. ([#12](https://github.com/k0valik/pi-blackhole/pull/12))\n\n### Fixed\n\n- **Null-safe entry IDs in `load-messages.ts`:** gracefully handles entries with `null` IDs instead of crashing with `String(null)` \u2192 `"null"`. ([#12](https://github.com/k0valik/pi-blackhole/pull/12))\n- **`formatRecallOutput` preserves legacy `files:[...]` format:** the expand-only path (no query) was silently dropping file info from entries that have the `files` field but no `fileMatches` \u2014 now falls back to the old `files:[path1, path2]` suffix. ([#12](https://github.com/k0valik/pi-blackhole/pull/12))\n\n### Crash protection \u2014 jiti bridge, EACCES guards, config safety\n\n- **Jiti bridge for custom providers:** `index.ts` now wraps `pi.registerProvider` to capture `streamSimple` functions into a `Symbol.for()` global, and scans `modelRegistry.registeredProviders` once on `agent_start`. This prevents crashes when consolidation agents (loaded via jiti with `moduleCache: false`) resolve a custom provider like `claude-bridge` \u2014 previously the jiti-loaded pi-ai instance had an empty `apiProviderRegistry` and threw `"No API provider registered"`. ([#11](https://github.com/k0valik/pi-blackhole/pull/11))\n- **Lazy bridge evaluation:** the bridge stream function now checks the provider map at call time instead of at import time, fixing an IIFE race condition where the bridge was permanently disabled because provider registration hadn\'t happened yet at module load. ([#11](https://github.com/k0valik/pi-blackhole/pull/11))\n- **Always-run fallback scan:** replaced `providerStreams.size > 0` guard with a dedicated `hasScannedFallback` flag \u2014 the fallback scan now always runs once regardless of how many providers the wrapper already captured, handling extensions that register before blackhole loads. ([#11](https://github.com/k0valik/pi-blackhole/pull/11))\n- **EACCES guards:** `writeCooldownMap()` and `writeSessionState()` now wrapped in try/catch. Prevents process crash on read-only filesystems (e.g., Nix-managed config). Cooldown loss is advisory (slightly more API traffic); pending state loss is safe (idempotent re-processing). ([#11](https://github.com/k0valik/pi-blackhole/pull/11))\n- **Numeric config validation:** all numeric fields are validated at load \u2014 NaN, infinity, and negative values are reset to defaults. Prevents silent math errors in pipeline logic. ([#11](https://github.com/k0valik/pi-blackhole/pull/11))\n- **`observerPreambleMaxTokens=0` explicitly allowed** in numeric validation (means "auto-compute"). ([#11](https://github.com/k0valik/pi-blackhole/pull/11))\n- **Better error messages for config save failures:** `/blackhole om-on` / `om-off` now use `"warning"`-level notification with an explanation about read-only filesystems when the config save fails, instead of a misleading `"info"`-level "Failed to save config.". ([#11](https://github.com/k0valik/pi-blackhole/pull/11))\n\n## [0.2.3] - 2026-05-27\n\n### Lockstep sync \u2014 2026-05-27\n\n- Ported upstream OM prompt refinements: coverage tiers in dropper prompt, "highest-resistance" critical framing in observer, coverage stewardship in reflector (#safe)\n- Ported upstream debug logging: `dropper.agent_start`, `dropper.tool_call`, `dropper.result` with full coverage/relevance diagnostics (#d6b02c0)\n- Ported upstream coverage-aware pruning: new `coverage.ts` module, drop candidate sort by coverage\u2192relevance\u2192age, critical observations no longer hard-rejected (#e00363a)\n- Adapted config: added `observationsPoolTargetTokens` as forward-compat no-op (upstream 52b5844 budgetTokens\u2192targetTokens rename)\n- Skipped upstream pool refactor (bf79ff7) and rename (52b5844): kept our ratio-based urgency algorithm\n- Recovered output cap from feat/compaction-output-cap: `buildCompactionProjection` now caps rendered observations to `observationsPoolMaxTokens` budget via relevance+recency scoring\n\n## [0.2.2] - 2026-05-26\n\n### Added\n\n- `/blackhole-memory` pipeline display reworked: renamed "Coverage" to "Pipeline", replaced percentage-based metrics with `X tokens (triggers at Y)` format to eliminate false-alarm 100% readings, added `[auto-disabled]` annotation for compaction in noAutoCompact mode, and show preamble cap in Pending section ([#7](https://github.com/k0valik/pi-blackhole/pull/7))\n- Default `observeAfterTokens` increased from 10,000 to 15,000 and `reflectAfterTokens` from 20,000 to 25,000 for better cost-efficiency on mid/high context sessions ([#7](https://github.com/k0valik/pi-blackhole/pull/7))\n- Observer preamble cap in noAutoCompact mode: the observer stage\'s `CURRENT OBSERVATIONS` preamble is now capped to prevent unbounded prompt growth from accumulated observation batches. High-relevance observations are always kept; medium and low observations are scored by relevance tier and relative recency (array position, not wall-clock time), with the best-scoring kept within the token budget. Reflections are never trimmed. The cap is governed by the new `observerPreambleMaxTokens` config setting (default `0` = auto-compute 30% of `observerChunkMaxTokens`). Only applies in `noAutoCompact` mode \u2014 the auto-compact path is unchanged. ([#7](https://github.com/k0valik/pi-blackhole/pull/7))\n- Accumulated batch history for noAutoCompact mode: the observer, reflector, and dropper stages now feed accumulated pending.json batches (observationBatches/reflectionBatches) to the LLM instead of reading from the (empty) branch. This restores the same historical context the pipeline receives in autoCompact mode \u2014 prior observations/reflections, existing summaries \u2014 but without writing markers to the visible branch. Each pipeline run appends its output batch to the pending store; on /blackhole flush, all accumulated batches are written as separate branch markers, preserving per-run coverage. ([#7](https://github.com/k0valik/pi-blackhole/pull/7))\n- Accumulated dropper batches (`droppedBatches`) in pending.json so that earlier dropper runs are not lost when a subsequent cycle overwrites `pending.dropped` before a /blackhole flush. The flush now writes all accumulated dropper batches to the branch, preventing observations dropped in earlier cycles from being "un-dropped" on compaction. ([#7](https://github.com/k0valik/pi-blackhole/pull/7))\n\n### Fixed\n\n- Reflector and dropper now read from `pending.json` in `noAutoCompact` mode instead of scanning the branch for observation markers that are never written there. Previously the early-exit gates in both stages returned immediately because `latestCoverageMarkerId(entries, OM_OBSERVATIONS_RECORDED)` found nothing in the branch (observations are saved to pending only). This caused the reflector and dropper to skip entirely, leaving the pipeline half-functional \u2014 no reflections were ever generated, the dropper never pruned, and the display showed misleading pool values. The fix adds `noAutoCompact`-aware early-exit gates that check `pending.observation`, `pending.reflection`, and `pending.dropped` state, using their `coversUpToId` values to calculate token gaps and gate correctly on `reflectAfterTokens`. Observations and reflections are fed from pending data instead of the empty branch. The notification token-adjustment logic (which already existed for all three stages) is now effective because the stages actually run. ([#6](https://github.com/k0valik/pi-blackhole/pull/6))\n\n## [0.2.1] - 2026-05-24\n\n### Fixed\n\n- Prevent repeated `Intl.Segmenter` constructor fallback retries on unsupported runtimes ([#5](https://github.com/k0valik/pi-blackhole/pull/5))\n- `/blackhole-memory` accumulated token counts now factor in pending `coversUpToId` as virtual coverage markers in `noAutoCompact` mode ([#5](https://github.com/k0valik/pi-blackhole/pull/5))\n- Pipeline notifications (observer/reflector/dropper) show accurate accumulated values accounting for pending coverage ([#5](https://github.com/k0valik/pi-blackhole/pull/5))\n- `stageThinkingLevel()` resolves per-model thinking config instead of using the primary stage model\'s setting for all fallback attempts ([#5](https://github.com/k0valik/pi-blackhole/pull/5))\n- Move `@earendil-works/*` packages to `peerDependencies` (provided by pi host at runtime), `typebox` to `devDependencies` (import type only) ([#5](https://github.com/k0valik/pi-blackhole/pull/5))\n- Dead code removal: deleted `src/om/compaction-hook.ts` and `src/core/report.ts` ([#5](https://github.com/k0valik/pi-blackhole/pull/5))\n- Module-level state leak: compaction stats moved to `Runtime` instance for session isolation ([#5](https://github.com/k0valik/pi-blackhole/pull/5))\n- Unified config loading: removed dual `loadSettings` path, `ensureConfig` called at handler start ([#5](https://github.com/k0valik/pi-blackhole/pull/5))\n- Stale context in deferred compaction: replaced `setTimeout(..., 0)` with `queueMicrotask` and session ID validation ([#5](https://github.com/k0valik/pi-blackhole/pull/5))\n- Silent JSON parse failures in `load-messages.ts` \u2014 now logged ([#5](https://github.com/k0valik/pi-blackhole/pull/5))\n- Silent `scaffoldConfig` errors \u2014 now logged ([#5](https://github.com/k0valik/pi-blackhole/pull/5))\n- `visibleProjection` falls through to `fullProjection` when no compaction has run ([#5](https://github.com/k0valik/pi-blackhole/pull/5))\n- `renderMessage` calls in `report.ts` and test types missing required `Message` properties ([#5](https://github.com/k0valik/pi-blackhole/pull/5))\n- CI publish workflow uses `npm` instead of `pnpm` (not available in runner) ([#5](https://github.com/k0valik/pi-blackhole/pull/5))\n- Added `typescript` devDependency for CI `tsc` check ([#5](https://github.com/k0valik/pi-blackhole/pull/5))\n\n### Changed\n\n- Improved model fallback: `resolveModel` iterates fallback chain (stage \u2192 fallbacks \u2192 base \u2192 session), records per-model cooldown on retryable errors ([#5](https://github.com/k0valik/pi-blackhole/pull/5))\n\n### Added\n\n- Bi-directional recall coupling: `#N` transcript expansion shows related OM observations/reflections; OM hex-id recall shows `#N` entry index annotations ([#5](https://github.com/k0valik/pi-blackhole/pull/5))\n- `id` field on `RenderedEntry` for cross-referencing with session entries ([#5](https://github.com/k0valik/pi-blackhole/pull/5))\n\n## [0.2.0] - 2026-05-24\n\n### Added\n\n- Initial release: unified compaction (pi-vcc) + observational memory (pi-observational-memory)\n- `/blackhole` command for manual compaction with OM content injection\n- `/blackhole-memory` command for pipeline status display\n- `/blackhole-recall` command for unified recall (transcript + OM)\n- Three-stage consolidation pipeline: observer \u2192 reflector \u2192 dropper with fallback retry\n- Per-session pending file isolation\n- Model cooldown persistence across restarts\n- CI/CD publish workflow for npm\n';Eo=45;});var To={};Fe(To,{GLOBAL_CONFIG_DIR:()=>wi,config:()=>Lc,openBlackholeSettings:()=>zg});async function zg(e){await Lc.openSettings(e,e.cwd,t=>{},wi,void 0,[{id:"changelog",label:"Display Changelog",available:true}],async t=>{t==="changelog"&&await vi(e);});}var Kg,wi,Lc,Mo=P(()=>{Ec();Nn();pr();lt();ki();Kg="pi-blackhole-config.json",wi=join(sr(),"pi-blackhole"),Lc=new Io({id:"pi-blackhole",label:"pi-blackhole",filename:Kg,configDir:wi,defaults:xe,scopes:{global:true,project:true,session:true},sessionConfig:{entryType:"session-config-pi-blackhole"},fields:e=>[{key:"compaction",type:"enum",label:"Compaction mode",description:"auto=trigger on threshold, manual=only /blackhole, off=auto:Pi handles, /blackhole:blackhole pipeline",value:e.compaction,options:["auto","manual","off"],optionLabels:{auto:"auto \u2014 trigger on threshold",manual:"manual \u2014 only /blackhole",off:"off \u2014 auto:Pi handles, /blackhole:blackhole pipeline"}},{key:"compactionEngine",type:"enum",label:"Compaction engine",description:"blackhole=structured summary+OM, pi-default=built-in Pi summarization",value:e.compactionEngine,options:["blackhole","pi-default"],optionLabels:{blackhole:"blackhole \u2014 structured summary + OM","pi-default":"pi-default \u2014 built-in Pi summarization"}},{key:"compactionSummaryMode",type:"enum",label:"Summary history",description:"default=replace one complete summary, append=freeze automatic segments and rebase on /blackhole",value:e.compactionSummaryMode,options:["default","append"],optionLabels:{default:"default \u2014 one complete replacement summary",append:"append \u2014 immutable auto segments; /blackhole rebases"}},{key:"tailBehavior",type:"enum",label:"Visible tail",description:"minimal=keep last user message only (default), pi-default=keep Pi's preserved visible context",value:e.tailBehavior,options:["minimal","pi-default"],optionLabels:{minimal:"minimal \u2014 keep last user message only (default)","pi-default":"pi-default \u2014 keep Pi's preserved visible context"}},{key:"midRunCompaction",type:"enum",label:"Mid-run compaction",description:"resume=compact transparently and continue the same run, pause=interrupt and stop, off=only check when run ends (default)",value:e.midRunCompaction,options:["resume","pause","off"],optionLabels:{resume:"resume \u2014 transparent compact, same run (experimental)",pause:"pause \u2014 interrupt, compact, and stop",off:"off \u2014 only check when run ends (default)"}},{key:"compactAfterTokens",type:"number",label:"Auto-compact threshold",description:"Token count that triggers auto-compaction when reached",value:e.compactAfterTokens,min:1e3,max:5e5,step:1e3},{key:"retainedToolOutputMaxTokens",type:"number",label:"Retained tool outputs",description:"Token budget for historical tool-output text; newest is retained first and older text remains available via recall",value:e.retainedToolOutputMaxTokens,min:1e3,max:2e5,step:1e3},{key:"memory",type:"boolean",label:"Observational memory",description:"Enable OM workers (observer, reflector, dropper) and content injection",value:e.memory,valueDescriptions:{on:"Active \u2014 OM workers + content injection enabled",off:"Suspended \u2014 OM disabled"}},{key:"sessionFallback",type:"boolean",label:"Session model fallback",description:"off=skip stage when all OM models fail, instead of falling back to the main coding model",value:e.sessionFallback??true},{key:"observeAfterTokens",type:"number",label:"Observer threshold",description:"Tokens accumulated since last observer run before triggering next observe",value:e.observeAfterTokens,min:1e3,max:2e5,step:1e3},{key:"reflectAfterTokens",type:"number",label:"Reflect + dropper threshold",description:"Tokens accumulated since last reflect before triggering reflector and dropper",value:e.reflectAfterTokens,min:1e3,max:2e5,step:1e3},{key:"observationsPoolMaxTokens",type:"number",label:"Observation pool max",description:"Max tokens in observation pool before dropper prunes (fold pressure)",value:e.observationsPoolMaxTokens,min:1e3,max:2e5,step:1e3},{key:"observationsPoolTargetTokens",type:"number",label:"Observation pool target",description:"Target tokens after dropper prunes (defaults to half of pool max)",value:e.observationsPoolTargetTokens,min:500,max:2e5,step:500},{key:"reflectorInputMaxTokens",type:"number",label:"Reflector input max",description:"Max prompt tokens for reflector model input (rolling window cap)",value:e.reflectorInputMaxTokens,min:1e3,max:5e5,step:1e3},{key:"dropperInputMaxTokens",type:"number",label:"Dropper input max",description:"Max prompt tokens for dropper model input (rolling window cap)",value:e.dropperInputMaxTokens,min:1e3,max:5e5,step:1e3},{key:"observerChunkMaxTokens",type:"number",label:"Observer chunk max",description:"Max source entry tokens sent to observer per chunk",value:e.observerChunkMaxTokens,min:1e3,max:2e5,step:1e3},{key:"observerPreambleMaxTokens",type:"number",label:"Observer preamble max",description:"Preamble budget in manual compaction mode (0=auto-compute 30% of chunk)",value:e.observerPreambleMaxTokens,min:0,max:1e5,step:500},{key:"dropperPressureThreshold",type:"number",label:"Dropper pressure threshold",description:"Fraction of reflectorInputMaxTokens that triggers pressure-driven dropper (0-1, default 0.70)",value:e.dropperPressureThreshold,min:.01,max:1,step:.01},{key:"dropperPoolFullnessThreshold",type:"number",label:"Dropper pool fullness threshold",description:"Min observation-pool fullness (fraction of pool max) before the dropper runs (0-1, default 0.10)",value:e.dropperPoolFullnessThreshold,min:.01,max:1,step:.01},{key:"agentMaxTurns",type:"number",label:"Max turns per agent",description:"Shared turn cap for background memory agents",value:e.agentMaxTurns,min:1,max:100,step:1},{key:"providerIdleTimeoutMs",type:"number",label:"Provider idle timeout (ms)",description:"Body-idle timeout for background provider streams; 0 = disabled, unset = inherit pi's default",value:e.providerIdleTimeoutMs??0,min:0,max:36e5,step:1e3},{key:"fullFoldAlways",type:"boolean",label:"Preserve OM on first compaction",description:"When true, early reflections/drops survive the first compaction in a fresh session",value:e.fullFoldAlways},{key:"debug",type:"boolean",label:"Debug snapshots",description:"Write detailed debug snapshots to /tmp/pi-blackhole-debug.json",value:e.debug},{key:"debugLog",type:"boolean",label:"Debug JSONL logging",description:"Write structured JSONL debug logs to agent directory",value:e.debugLog}],validate:e=>{let t={...e};t.compaction===void 0&&t.compactionEngine===void 0&&(t.passive===true?(t.compaction="off",t.memory=false):t.noAutoCompact===true&&(t.compaction="manual"),t.overrideDefaultCompaction===true?(t.compactionEngine="blackhole",t.tailBehavior===void 0&&(t.tailBehavior="minimal")):t.overrideDefaultCompaction===false&&(t.compactionEngine="pi-default"),delete t.passive,delete t.noAutoCompact,delete t.overrideDefaultCompaction);let n=process.env.PI_BLACKHOLE_PASSIVE??process.env.PI_VCC_OM_PASSIVE??process.env.PI_OBSERVATIONAL_MEMORY_PASSIVE;if(n!==void 0){let u=n.trim().toLowerCase();["1","true","yes","on"].includes(u)?(t.compaction="off",t.memory=false):["0","false","no","off"].includes(u)&&e.passive===true&&(delete t.compaction,delete t.memory);}let o=process.env.PI_BLACKHOLE_COMPACTION;if(o!==void 0){let u=o.trim().toLowerCase();["auto","manual","off"].includes(u)||console.warn(`blackhole: invalid PI_BLACKHOLE_COMPACTION value "${o}"; ignoring`);}let r=process.env.PI_BLACKHOLE_COMPACTION_ENGINE;if(r!==void 0){let u=r.trim().toLowerCase();["blackhole","pi-default"].includes(u)||console.warn(`blackhole: invalid PI_BLACKHOLE_COMPACTION_ENGINE value "${r}"; ignoring`);}let i=process.env.PI_BLACKHOLE_COMPACTION_SUMMARY_MODE;if(i!==void 0){let u=i.trim().toLowerCase();["default","append"].includes(u)||console.warn(`blackhole: invalid PI_BLACKHOLE_COMPACTION_SUMMARY_MODE value "${i}"; ignoring`);}let s=process.env.PI_BLACKHOLE_MID_RUN_COMPACTION;if(s!==void 0){let u=s.trim().toLowerCase();["resume","pause","off"].includes(u)||console.warn(`blackhole: invalid PI_BLACKHOLE_MID_RUN_COMPACTION value "${s}"; ignoring`);}let a={...xe,...t},l=["observeAfterTokens","reflectAfterTokens","compactAfterTokens","retainedToolOutputMaxTokens","observationsPoolMaxTokens","observationsPoolTargetTokens","reflectorInputMaxTokens","dropperInputMaxTokens","observerChunkMaxTokens","observerPreambleMaxTokens","agentMaxTurns"];for(let u of l){let p=a[u],m=u==="observerPreambleMaxTokens"?0:1;(typeof p!="number"||!Number.isFinite(p)||u==="retainedToolOutputMaxTokens"&&!Number.isInteger(p)||p<m)&&(a[u]=xe[u]);}let d=a.dropperPressureThreshold;(typeof d!="number"||!Number.isFinite(d)||d<=0||d>1)&&(a.dropperPressureThreshold=xe.dropperPressureThreshold);let c=a.dropperPoolFullnessThreshold;return (typeof c!="number"||!Number.isFinite(c)||c<=0||c>1)&&(a.dropperPoolFullnessThreshold=xe.dropperPoolFullnessThreshold),(a.observationsPoolTargetTokens===void 0||a.observationsPoolTargetTokens>=a.observationsPoolMaxTokens)&&(a.observationsPoolTargetTokens=Math.floor(a.observationsPoolMaxTokens/2)),a},env:zn});});var Dc=P(()=>{});function Hg(e){return e.endsWith(Ao)?e.slice(0,-Ao.length)||null:e.endsWith(xi)&&e.slice(0,-xi.length)||null}function qg(e=getAgentDir()){let t=join(e,Vc);if(!existsSync(t))return [];let n=[],o;try{o=readdirSync(t);}catch{return []}for(let r of o){let i=Hg(r);if(!i)continue;let s=join(t,r),a;try{a=statSync(s);}catch{continue}n.push({sessionId:i,filename:r,path:s,isStale:r.endsWith(Ao),sizeBytes:a.size,mtimeMs:a.mtimeMs});}return n.sort((r,i)=>i.mtimeMs-r.mtimeMs),n}function Gg(e){let t=new Set;if(!existsSync(e))return t;let n=[e];for(;n.length>0;){let o=n.pop(),r;try{r=readdirSync(o);}catch{continue}for(let i of r){let s=join(o,i),a;try{a=statSync(s);}catch{continue}if(a.isDirectory())n.push(s);else if(a.isFile()&&i.endsWith(".jsonl"))try{let l=readFileSync(s,"utf-8"),d=l.indexOf(`
|
|
23
|
+
`);let i={};for(let d of n)d.scope==="session"?i.session=d.path??d.note:i[d.scope]=d.exists&&d.path?d.path:d.note;i.env="environment variables (read-only)",i.defaults="built-in defaults";let s={};for(let d of o){let c;d.scope==="defaults"?c={...e.defaults}:c=e.layerValues(d.scope);let u=e.buildFields(c);s[d.id]=u.map(p=>({...p,tab:d.id,valueNote:Bg(p,d.id,t,e.env)}));}let a=o[0].id,l=o.flatMap(d=>s[d.id]??[]);await e.ctx.ui.custom((d,c,u,p)=>{let m={current:i[a]};return mi({title:e.label,tabs:o.map(f=>({id:f.id,label:f.label})),initialTab:a,fields:l,readOnly:true,pathNote:m.current,actions:[{id:"edit",label:"Edit"},{id:"cancel",label:"Cancel"}],onAction(f){switch(f){case "cancel":queueMicrotask(()=>p(void 0));break;case "edit":queueMicrotask(()=>p(void 0)),a==="global"&&e.scopes.global||a==="project"&&e.scopes.project||a==="session"&&e.scopes.session?hi(e,a).catch(b=>e.ctx.ui.notify(b instanceof Error?b.message:String(b),"error")):Sc(e,false).then(b=>{if(b.kind==="cancel")return Rc(e);if(b.id!=="display-all")return hi(e,b.id)}).catch(b=>e.ctx.ui.notify(b instanceof Error?b.message:String(b),"error"));break}},onActiveTabChange(f){a=f,m.current=i[f]??"";}},{tui:d,theme:c,ctx:e.ctx,close:()=>p(void 0)})},{overlay:true,overlayOptions:bi()});}function Bg(e,t,n,o){let r=String(e.key),i=n.winners[r];if(t==="env"&&o?.[r]){let s=o[r],a=i==="env";return typeof s=="string"?!process.env[s]?.trim()?`(${s}: unset)`:a?`(${s}) \u25B8 effective`:`(${s})`:!process.env[s.var]?.trim()?`(${s.var}: unset)`:a?`(${s.var}) \u25B8 effective`:`(${s.var})`}if(i===t)return "\u25B8 effective";if(i)return `(from ${xc(i)})`}function bi(){return {anchor:"center",width:"92%",maxHeight:"95%"}}var Fg,Lg,gi,Dg,Ic=P(()=>{ti();Ol();wc();Fg=["global","project","session"],Lg={global:"Global",project:"Project Local",env:"env",session:"Session",defaults:"default"},gi={global:"Global",project:"Project Local",session:"Session"},Dg={global:"Configure Global settings",project:"Configure Project local settings",session:"Configure Session settings"};});var Io,Ec=P(()=>{Rl();Kn();Ic();pi();Io=class{opts;_sessionId;_leafId;_entries;_appendEntry;_getEntries;_filename;_sessionPersist="unavailable";_sessionManager;_pendingCwd;_defaultConfigDir;constructor(t){if(this.opts=t,this._defaultConfigDir=t.configDir,!t.filename&&!t.id)throw new Error("ConfigManager requires either `id` or `filename` in options. Provide one so the config file can be resolved.");this._filename=t.filename??`${t.id}-config.json`;}getScopes(){let t=this.opts.scopes??{global:true,project:true,session:true};return {global:t.global!==false,project:t.project!==false,session:t.session!==false&&this.opts.sessionConfig!==false}}hasSession(){return this._sessionPersist==="persisted"||this._sessionPersist==="pending"}_ensureSession(t,n){if(this._sessionManager&&this._sessionId){let c=t.sessionManager?.getSessionId?.();if(c&&this._sessionId!==c){let u=this._sessionId,p=this._pendingCwd??process.cwd();at(this._getEntryType(),p,u,we),this._sessionId=void 0,this._leafId=void 0,this._entries=void 0,this._appendEntry=void 0,this._getEntries=void 0,this._sessionPersist="unavailable",this._sessionManager=void 0,this._pendingCwd=void 0;}}if(this._sessionPersist==="persisted"&&!this._sessionManager)return this._tryFlushSession(n),true;if(this._sessionPersist!=="unavailable"&&this._sessionManager)return t.sessionManager&&this._tryFlushSession(n),true;let o=t.sessionManager;if(!o)return this._sessionPersist="unavailable",false;if(this.opts.scopes?.session===false||this.opts.sessionConfig===false)return this._sessionPersist="unavailable",false;let r=o.getSessionFile?.();if(typeof r!="string")return this._sessionPersist="unavailable",false;let i=o.getLeafId?.()??null,s=o,a=typeof s.appendCustomEntry=="function";if(existsSync(r)&&i!=null&&a){let c=(u,p)=>{s.appendCustomEntry(u,p);};return this.initSession(o.getSessionId(),i,o.getEntries?.()??[],c,()=>o.getEntries?.()),this._sessionPersist="persisted",this._sessionManager=s,this._pendingCwd=n,true}let d=a?(c,u)=>{s.appendCustomEntry(c,u);}:void 0;return this._sessionId=o.getSessionId()??void 0,this._leafId=we,this._entries=o.getEntries?.(),this._appendEntry=d,this._getEntries=()=>o.getEntries?.()??[],this._sessionPersist="pending",this._sessionManager=s,this._pendingCwd=n,this._tryFlushSession(n),true}_tryFlushSession(t){if(this._sessionPersist!=="pending"||!this._sessionManager)return false;let n=this._sessionManager,r=typeof n.appendCustomEntry=="function"?(d,c)=>{n.appendCustomEntry(d,c);}:void 0;this._appendEntry=r;let i=n.getSessionFile();if(typeof i!="string"||!existsSync(i))return false;let s=n.getLeafId();if(s==null||!r)return false;let a=t??this._pendingCwd??process.cwd(),l=Un(this._getEntryType(),a,this._sessionId,we)??{};return Object.keys(l).length>0&&(jn(this._getEntryType(),a,this._sessionId,s,l),at(this._getEntryType(),a,this._sessionId,we),r(this._getEntryType(),{leafId:s,config:l})),this._leafId=s,this._sessionPersist="persisted",Object.keys(l).length>0}initSession(t,n,o,r,i){if(this.opts.scopes?.session===false||this.opts.sessionConfig===false)return;this._sessionId=t,this._leafId=n,this._entries=o,this._appendEntry=r,this._getEntries=i,this._sessionPersist="persisted",this._sessionManager=void 0,this._pendingCwd=void 0;let s=this._getEntryType();for(let a=o.length-1;a>=0;a--){let l=o[a];if(l.type==="custom"&&l.customType===s){let d=l.data;if(d&&d.leafId===n){jn(s,process.cwd(),t,n,d.config);break}}}}load(t,n){return this.loadWithWarnings(t,n).config}loadWithWarnings(t,n){let o=[],r=Os(this._filename,this.opts.defaults,{cwd:t,configDir:n??this._defaultConfigDir,merge:"deep"}),i=this.opts.validate?this.opts.validate(r):r;try{let u=this.opts.fields(i),p="global";for(let m of u){if(m.type==="action"||m.type==="custom")continue;let g=jt(m,m.value);g&&o.push({scope:p,key:String(m.key),message:`"${m.label}": ${g}`});}}catch{}let s=this.applyEnvOverrides(i),a=this.opts.scopes??{session:true},l=this.opts.sessionConfig!==false&&a.session!==false,d=this._getEntryType(),c=l?this.applySessionOverrides(s,t,d):s;return this._tryFlushSession(t),{config:c,warnings:o}}applyEnvOverrides(t){let n=this.opts.env;return n?go(t,n,this.opts.defaults):t}applySessionOverrides(t,n,o){let r=this._sessionId,i=this._leafId,s=this._getEntries?.()??this._entries;if(!r||!i||!s)return t;let a;i===we?a=Un(o,n??process.cwd(),r,we)??{}:a=ur(o,n??process.cwd(),r,i,s);let l=wt(t,a);return this.opts.validate?this.opts.validate(l):l}_applyValidation(t){return this.opts.validate?this.opts.validate(t):t}layerValues(t,n,o){let r=o??this._defaultConfigDir??Ie(),s={...this.opts.defaults},a=Le(this._filename,r)??{};if(s=wt(s,a),t==="global")return this._applyValidation(s);if(n){let u=join(n,".pi"),p=Le(this._filename,u)??{};s=wt(s,p);}if(t==="project"&&!n)throw new Error("cwd is required for project-scoped layerValues");if(t==="project")return this._applyValidation(s);if(this.opts.env&&(s=go(s,this.opts.env,this.opts.defaults)),t==="env")return this._applyValidation(s);let l=this.opts.scopes??{session:true},d=this.opts.sessionConfig!==false&&l.session!==false,c=this._getEntryType();return d&&(s=this.applySessionOverrides(s,n,c)),this._tryFlushSession(n),this._applyValidation(s)}inspect(t,n){let o=n??this._defaultConfigDir??Ie(),r={...this.opts.defaults},i=Le(this._filename,o)??{},s=t?Le(this._filename,join(t,".pi"))??{}:{},a={};if(this.opts.env){let b=go(s,this.opts.env,this.opts.defaults);for(let[y,w]of Object.entries(this.opts.env))this._envKeyIsActive(y,w,this.opts.defaults)&&(a[y]=b[y]);}let l={},d=this.opts.scopes??{session:true},c=this.opts.sessionConfig!==false&&d.session!==false,u=this._getEntryType();if(c&&this._sessionId&&this._leafId){let h;this._leafId===we?h=Un(u,t??process.cwd(),this._sessionId,we)??{}:h=ur(u,t??process.cwd(),this._sessionId,this._leafId,this._getEntries?.()??this._entries??[]),Object.assign(l,h);}let p={defaults:r,global:i,project:s,env:a,session:l},m=new Set;for(let h of Object.values(p))for(let b of Object.keys(h))m.add(b);let g={},f=["session","env","project","global","defaults"];for(let h of m)for(let b of f)if(h in p[b]){g[h]=b;break}return {layers:p,winners:g}}scopeSources(t,n){let o=[],r=this.opts.scopes??{global:true,project:true,session:true};if(r.global!==false){let i=n??this._defaultConfigDir??Ie(),s=join(i,this._filename),a=existsSync(s);o.push({scope:"global",label:"Global",path:s,exists:a,note:a?s:"(nonexistent \u2014 will be created on save)"});}if(r.project!==false&&t){let i=join(t,".pi",this._filename),s=existsSync(i);o.push({scope:"project",label:"Project Local",path:i,exists:s,note:s?i:"(nonexistent \u2014 will be created on save)"});}if(r.session!==false&&this.opts.sessionConfig!==false){let i=this._getEntryType(),s=this._sessionManager?.getSessionFile?.(),a=this._sessionPersist==="pending";o.push({scope:"session",label:"Session",exists:false,path:s,note:a?"in-memory until session file exists \u2014 will persist automatically":`in-memory per-leaf overrides (persisted to session JSONL as ${i})`});}return o}_envKeyIsActive(t,n,o){if(typeof n=="string"){let r=process.env[n]?.trim();if(!r)return false;let i=o[t];if(typeof i=="boolean")return ["1","true","yes","on","0","false","no","off"].includes(r.toLowerCase());if(typeof i=="number"){if(Number.isInteger(i)&&i>0){let a=Number.parseInt(r,10);return Number.isFinite(a)&&a>0}let s=Number.parseFloat(r);return Number.isFinite(s)}return false}else {let r=process.env[n.var]?.trim();if(!r)return false;try{return n.parse(r,void 0)!==void 0}catch{return false}}}save(t,n,o,r){if(n==="session"){if(!this._sessionId)throw new Error("Cannot save session config: session not initialized. Call initSession() first.");let f=o??process.cwd();return jn(this._getEntryType(),f,this._sessionId,this._leafId,t),this._pendingCwd=f,!this._tryFlushSession(f)&&this._sessionPersist==="persisted"&&this._appendEntry&&this._appendEntry(this._getEntryType(),{leafId:this._leafId,config:t}),{path:"",created:false,changed:true}}if(n==="project"&&!o)throw new Error("cwd is required for project-scoped config save");let i=n==="project"&&o?join(o,".pi"):r??this._defaultConfigDir??Ie(),s=join(i,this._filename),a=!existsSync(s),l=new Set(Object.keys(this.opts.defaults)),d=Le(this._filename,i)??{},c=new Map;try{let f=this.opts.fields(t);for(let h of f)c.set(String(h.key),h);}catch{}let u={},p=false;for(let f of Object.keys(this.opts.defaults)){let h=t[f],b=d[String(f)];if(Jt(h,b))continue;let y=c.get(String(f));y&&!(y.type==="action"||y.type==="custom")&&jt(y,h)||(u[String(f)]=h,p=true);}for(let[f,h]of Object.entries(d))l.has(f)||(u[f]=h,p=true);if(!p)return {path:s,created:a,changed:false};let m={...d,...u};if(!cr(this._filename,m,i))throw new Error(`Failed to save ${this._filename} \u2014 the config file may be read-only (e.g., managed by Nix). Runtime state was updated for this session only.`);return {path:s,created:a,changed:true}}_getEntryType(){return typeof this.opts.sessionConfig=="object"&&this.opts.sessionConfig.entryType?this.opts.sessionConfig.entryType:`session-config-${this.opts.id}`}async openSettings(t,n,o,r,i,s,a){r=r??this._defaultConfigDir,this.warnOnMalformedConfig(t,n,r);let l=this.getScopes(),d=this._ensureSession(t,n),c=this.scopeSources(n,r);await Cc({label:this.opts.label,ctx:t,cwd:n,scopes:l,sessionInitialized:d,sessionNote:c.find(u=>u.scope==="session")?.note??"",defaults:this.opts.defaults,env:this.opts.env,buildFields:u=>{let p=this.opts.fields(u),m=this.opts.defaults;for(let g of p)if(!(g.type==="action"||g.type==="custom")&&g.default===void 0){let f=String(g.key);f in m&&(g.default=m[f]);}return p},layerValues:u=>this.layerValues(u,n,r),inspect:()=>this.inspect(n,r),scopeSources:()=>this.scopeSources(n,r),save:async(u,p)=>{let m=this.opts.validate?this.opts.validate(u):u,g=this.save(m,p,n,r);return o(m),g},resetScope:u=>this.resetScope(u,n,r),deleteScope:u=>this.deleteScope(u,n,r),onSaved:()=>{},onChange:i},s,a);}resetScope(t,n,o){if(t==="session"){if(!this._sessionId)throw new Error("Cannot reset session config: session not initialized.");let l=n??process.cwd();this._sessionPersist==="pending"?at(this._getEntryType(),l,this._sessionId,we):at(this._getEntryType(),l,this._sessionId,this._leafId);return}if(t==="project"&&!n)throw new Error("cwd is required for project-scoped config reset");let r=t==="project"&&n?join(n,".pi"):o??this._defaultConfigDir??Ie(),i=new Set(Object.keys(this.opts.defaults)),s=Le(this._filename,r),a={};if(s&&typeof s=="object")for(let[l,d]of Object.entries(s))i.has(l)||(a[l]=d);Object.keys(a).length>0?cr(this._filename,a,r):dr(this._filename,r);}deleteScope(t,n,o){if(t==="session"){if(!this._sessionId)throw new Error("Cannot delete session config: session not initialized.");let i=n??process.cwd();this._sessionPersist==="pending"?at(this._getEntryType(),i,this._sessionId,we):at(this._getEntryType(),i,this._sessionId,this._leafId);return}if(t==="project"&&!n)throw new Error("cwd is required for project-scoped config delete");let r=t==="project"&&n?join(n,".pi"):o??this._defaultConfigDir??Ie();dr(this._filename,r);}warnOnMalformedConfig(t,n,o){let r=this._filename,i=fr(r,o);i.exists&&!i.valid&&t.ui.notify(`Config file "${r}" is ${i.error}. Using defaults.`,"warning");let s=join(n,".pi"),a=fr(r,s);a.exists&&!a.valid&&t.ui.notify(`Project config file ".pi/${r}" is ${a.error}. Using defaults.`,"warning");}};});var Fc={};Fe(Fc,{createChangelogViewer:()=>$c,entriesToPlainLines:()=>_c,getOwnPackageRoot:()=>yi,getPackageVersion:()=>Mc,openChangelogView:()=>vi,parseChangelogEntries:()=>Ac,readChangelogText:()=>Oc,renderChangelogEntries:()=>Pc,stripMarkdownInline:()=>wn});function yi(){try{let e=import.meta.url;if(typeof e=="string"&&e.length>0){let t=dirname(fileURLToPath(e));for(;t!==dirname(t);){try{if(JSON.parse(readFileSync(join(t,"package.json"),"utf-8")).name==="pi-blackhole")return t}catch{}let n=dirname(t);if(n===t)break;t=n;}}}catch{}try{let e=process.cwd();for(;e!==dirname(e);){try{if(JSON.parse(readFileSync(join(e,"package.json"),"utf-8")).name==="pi-blackhole")return e}catch{}let t=dirname(e);if(t===e)break;e=t;}}catch{}try{let e=process.argv[1];if(e){let t=dirname(e);for(;t!==dirname(t);){try{if(JSON.parse(readFileSync(join(t,"package.json"),"utf-8")).name==="pi-blackhole")return t}catch{}let n=dirname(t);if(n===t)break;t=n;}}}catch{}return process.cwd()}function Mc(e){let t=e??yi();try{let n=JSON.parse(readFileSync(join(t,"package.json"),"utf-8"));if(typeof n.version=="string"&&n.version.length>0)return n.version}catch{}}function wn(e){let t=e;return t=t.replace(/\[([^\]]+)\]\([^)]+\)/g,"$1"),t=t.replace(/`([^`]+)`/g,"$1"),t=t.replace(/\*\*\*([^*]+)\*\*\*/g,"$1"),t=t.replace(/\*\*([^*]+)\*\*/g,"$1"),t=t.replace(/__([^_]+)__/g,"$1"),t=t.replace(/(?<!\*)\*([^*\n]+)\*(?!\*)/g,"$1"),t=t.replace(/(?<!\w)_([^_\n]+)_(?!\w)/g,"$1"),t=t.replace(/~~([^~]+)~~/g,"$1"),t}function Oc(e){let t=e!==void 0,n=e??yi(),o=[join(n,"CHANGELOG.md"),join(n,"docs/CHANGELOG.md")];for(let r of o)try{if(existsSync(r))return readFileSync(r,"utf-8")}catch{}if(!t){try{let r=join(process.cwd(),"docs/CHANGELOG.md");if(existsSync(r))return readFileSync(r,"utf-8")}catch{}return Ug??void 0}}function Ac(e,t){let n=e.split(/\r?\n/),o=[],r,i,s=/^##\s+\[([^\]]+)\]\s*-?\s*(.*)\s*$/,a=/^###\s+(.+)\s*$/,l=/^\s*-\s+(.*)\s*$/;for(let d of n){let c=d.match(s);if(c){r&&o.push(r),r={version:c[1].trim(),date:c[2].trim()||void 0,sections:[]},i=void 0,t!==void 0&&o.length>=t;continue}if(!r)continue;let u=d.match(a);if(u){i={heading:wn(u[1].trim()),items:[]},r.sections.push(i);continue}let p=d.match(l);if(p&&i){i.items.push(wn(p[1].trim()));continue}if(i&&i.items.length>0&&d.length>0&&/^\s{2,}\S/.test(d)&&!d.startsWith("##")&&!d.startsWith("###")){let m=i.items.length-1;i.items[m]=`${i.items[m]} ${wn(d.trim())}`;}}return r&&o.push(r),t!==void 0?o.slice(0,t):o}function _c(e){let t=[];for(let n of e){let o=n.date?`## [${n.version}] - ${n.date}`:`## [${n.version}]`;if(t.push(o),t.push(""),n.sections.length===0){t.push("(no details)"),t.push("");continue}for(let r of n.sections){if(t.push(`### ${r.heading}`),r.items.length===0)t.push("(no items)");else for(let i of r.items)t.push(`- ${i}`);t.push("");}}return t}function Pc(e,t,n){let o=_c(e),r=[];for(let i of o)if(i.startsWith("## [")){let s=n.fg("accent",n.bold(i));r.push(...fe(s,t));}else if(i.startsWith("### "))r.push(...fe(n.fg("accent",i),t));else if(i.startsWith("- ")){let s=fe(i,t);for(let a=0;a<s.length;a++)a===0?r.push(s[a]):r.push(` ${s[a]}`);}else i===""?r.push(""):r.push(...fe(i,t));return r}function $c(e){let{tui:t,theme:n,done:o,packageRoot:r,maxEntries:i}=e,s=Mc(r),a=s?`pi-blackhole v${s} \u2014 Changelog`:"pi-blackhole \u2014 Changelog",l=Oc(r),d;if(!l)d=["Changelog not found.","Expected CHANGELOG.md at package root."];else {let g=Ac(l,i);if(g.length===0)d=l.split(/\r?\n/).map(h=>wn(h));else return Wg({tui:t,theme:n,done:o,title:a,entries:g})}let c=0,u=5;return {render:g=>{let f=pt(t.terminal.rows??24,Eo,14),h=Ce(g),b=[];for(let E of d)E===""?b.push(""):b.push(...fe(E,h));let y=Math.max(1,f-2),w=Math.max(0,b.length-y);c=Math.min(c,w);let v=b.slice(c,c+y);for(;v.length<y;)v.push("");let k=[];c>0&&k.push(n.fg("dim",` \u2191 ${c} earlier`)),k.push(...v),c+y<b.length&&k.push(n.fg("dim",` \u2193 ${b.length-c-y} more`));let R=n.fg("dim","\u2191\u2193 scroll \xB7 PgUp/PgDn \xB7 Esc close"),x=[...k,"",R];return je(x,g,n,{title:a,fixedInnerRows:f})},handleInput:g=>{let f=pt(t.terminal.rows??24,Eo,14),h=Ce(80),b=[];for(let v of d)v===""?b.push(""):b.push(...fe(v,h));let y=Math.max(1,f-2),w=Math.max(0,b.length-y);matchesKey(g,"up")?(c=Math.max(0,c-1),t.requestRender()):matchesKey(g,"down")?(c=Math.min(w,c+1),t.requestRender()):matchesKey(g,"pageUp")?(c=Math.max(0,c-u),t.requestRender()):matchesKey(g,"pageDown")?(c=Math.min(w,c+u),t.requestRender()):(matchesKey(g,"escape")||matchesKey(g,"ctrl+c"))&&o();},invalidate:()=>{}}}function Wg(e){let{tui:t,theme:n,done:o,title:r,entries:i}=e,s=0,a=5,l=80,d=[];function c(m){if(m===l&&d.length>0)return d;l=m;let g=Ce(m);return d=Pc(i,g,n),d}return {render:m=>{let g=pt(t.terminal.rows??24,Eo,14),f=c(m),h=Math.max(1,g-3),b=Math.max(0,f.length-h);s=Math.min(s,b);let y=f.slice(s,s+h);for(;y.length<h;)y.push("");let w=[];return s>0&&w.push(n.fg("dim",` \u2191 ${s} earlier`)),w.push(...y),s+h<f.length&&w.push(n.fg("dim",` \u2193 ${f.length-s-h} more`)),w.push(""),w.push(n.fg("dim"," \u2191\u2193 scroll \xB7 PgUp/PgDn \xB7 Esc close")),je(w,m,n,{title:r,fixedInnerRows:g})},handleInput:m=>{let g=pt(t.terminal.rows??24,Eo,14),f=c(l),h=Math.max(1,g-3),b=Math.max(0,f.length-h);matchesKey(m,"up")?(s=Math.max(0,s-1),t.requestRender()):matchesKey(m,"down")?(s=Math.min(b,s+1),t.requestRender()):matchesKey(m,"pageUp")?(s=Math.max(0,s-a),t.requestRender()):matchesKey(m,"pageDown")?(s=Math.min(b,s+a),t.requestRender()):(matchesKey(m,"escape")||matchesKey(m,"ctrl+c"))&&o();},invalidate:()=>{}}}async function vi(e){await e.ui.custom((t,n,o,r)=>$c({tui:t,theme:n,done:()=>r(void 0)}),{overlay:true,overlayOptions:{anchor:"center",width:"92%",maxHeight:"95%"}});}var Ug,Eo,ki=P(()=>{Ae();Ug='## [Unreleased]\n\n---\n\n## [0.5.1] - 2026-09-06\n\n### Fixed\n\n- **Host inline-compaction adapter now prefers pi\'s already-loaded bundled runtime chunk over its `dist/index.js` barrel.** Probe on pi 0.85.1 showed the barrel import cost 484ms (fresh graph) while the bundled chunk pi already loaded cost 5ms (cache-hit); the adapter tried the barrel first and kept iterating after a successful chunk patch. Reordered candidates so the chunk is tried first per host path, with the barrel only as a fallback for roots that never resolve a chunk, while still patching every unique host identity. Adapter total 506ms \u2192 16ms, factory ~490ms saved.\n\n---\n\n## [0.5.0] - 2026-09-06\n\n### Added\n\n- **Newest-first retained tool-output budgeting.** At each Blackhole compaction boundary, retained historical tool and shell output text is projected through a dedicated budget (default 20000 tokens) and the resulting omission decisions are persisted with that compaction. Set `retainedToolOutputMaxTokens` to `0` to disable. When enabled, the provider-visible representation remains fixed until the next compaction, while raw session history and compaction inputs remain full fidelity. Pending results and non-text content remain visible, and omitted text points to `recall #N` when a stable transcript index is available. ([#66](https://github.com/k0valik/pi-blackhole/pull/66))\n- **`max` thinking level support.** Configured memory models now accept and forward Pi\'s highest reasoning level where supported by the provider ([upstream OM `e6c0fd9`](https://github.com/elpapi42/pi-observational-memory/commit/e6c0fd9d8e43ffb5d3c1a271bf1ec4a4640bde02)). ([#67](https://github.com/k0valik/pi-blackhole/pull/67))\n\n### Changed\n\n- **Lazy loading of optional command modules and memory workers.** The settings overlay, changelog viewer, cleanup handler, export pipeline, and OM worker imports now load on demand instead of at extension startup, cutting per-session startup cost. ([#71](https://github.com/k0valik/pi-blackhole/pull/71))\n- **Recall search now indexes bounded generic tool arguments, filters multi-term BM25 noise, caps results at 50, and reports pagination/truncation accurately \u2014 including in `/blackhole-recall`** ([upstream VCC `f7b80bb`, `4bb7115`](https://github.com/sting8k/pi-vcc/commit/f7b80bbbe22315acf9f7925c0c3be2d4ae9feee5)). ([#67](https://github.com/k0valik/pi-blackhole/pull/67))\n\n### Fixed\n\n- **Custom provider streams are now keyed by provider + API, not API alone.** Several extensions can register different providers sharing one wire API (e.g. `anthropic` and a Databricks-hosted Claude both speak `anthropic-messages`); keying by API alone let the first-registered provider hijack every model speaking that protocol, routing requests through the wrong transport/URL/auth. Streams also overwrite on re-registration instead of first-wins. ([#70](https://github.com/k0valik/pi-blackhole/pull/70))\n- **OM workers now receive provider environment substitutions, and stale ambient-credential availability snapshots are rechecked before rejecting request-time-signed providers** ([upstream OM `ce9fc98`, `699ccc7`](https://github.com/elpapi42/pi-observational-memory/commit/ce9fc982b3a219a7839f07c9f4a3e054e81a2b21)). ([#67](https://github.com/k0valik/pi-blackhole/pull/67))\n- **Empty Blackhole compaction output now delegates to Pi\'s native summarizer instead of replacing context with an empty summary** ([upstream OM PR #39](https://github.com/elpapi42/pi-observational-memory/pull/39)). ([#67](https://github.com/k0valik/pi-blackhole/pull/67))\n\n---\n\n## [0.4.10] - 2026-08-29\n\n### Changed\n\n- **Upgraded recall & export algorithms (BM25+, SimHash64, c-TF-IDF, and technical density scoring).**\n - Upgraded session history search to **BM25+** with lower-bound delta term ($\\delta = 0.5$) preventing length bias against concise observations.\n - Added lightweight morphological stemming (`stemToken`) to `dedup.ts` for higher token-set overlap across grammatical variants.\n - Added 64-bit SimHash locality-sensitive fingerprinting (`computeSimHash64`, `simHashHammingDistance`) and cluster drift guards to speed up pairwise candidate filtering and prevent transitive clustering drift.\n - Added technical entity density scoring (`technicalDensityFactor`) in `format-export.ts` to reward concrete code artifacts (paths, symbols, flags, hashes) over conversational transcripts.\n - Expanded stemming and technical-artifact detection for common software terminology, major language file types, framework constructs, API routes, DevOps/configuration signals, errors, and semantic versions.\n - Topic labels now preserve readable surface words while using stems only for internal matching and scoring.\n - Upgraded topic labeling from standard TF-IDF to **c-TF-IDF** (Class-based TF-IDF with sublinear saturation).\n - Export preamble now shows a best-effort heuristic warning instead of a Key Topics index.\n\n### Fixed\n\n- **Git-based installs no longer require interactive `pnpm approve-builds`.** `simple-git-hooks` is explicitly trusted through pnpm 11\'s workspace `allowBuilds` configuration, and the prepare lifecycle now initializes hooks and builds the bundle exactly once.\n- **Export pipeline hardening for large corpora.** Fixed `token.charCodeAt is not a function` crash caused by `TECHNICAL_ROOTS` prototype pollution on tokens like `constructor`/`toString` (now guarded with `hasOwnProperty`) and added a malformed-token guard in `computeSimHash64`; topic labels now preserve surface forms and the export warning clarifies heuristic ranking.\n\n---\n\n## [0.4.9] - 2026-08-28\n\n### Added\n\n- **Distilled project-memory export (`/blackhole-export`).** ([#65](https://github.com/k0valik/pi-blackhole/pull/65)) New command that scans project-scoped session JSONL files plus global OM pending buffers, deduplicates/clusters observations, and writes a single import-ready Markdown file (tiered as `Reflections \u2192 Critical \u2192 High \u2192 Medium \u2192 Low` plus an `Unattributed pending memory` section for orphaned buffers). Scoring is tier-weighted with recency decay, log-scaled recurrence and evidence-mass boosts, consensus rerank, burst penalty and length factor; viability gating keeps low/medium only with multi-session support or length/quality, high/critical always. Hierarchical topic assignment via S\xF8rensen-Dice graph + TF-IDF labeling; three-pass dedup (exact normalized, Levenshtein@0.88 after bigram-Jaccard prefilter, S\xF8rensen-Dice@0.70 with Levenshtein floor). Output parsing via `out:<path>.md` or a timestamped default; deterministic and stateless. New modules `src/project-recall/corpus.ts`, `dedup.ts`, `format-export.ts`, `session-dir.ts` and handler `src/commands/blackhole-export.ts` (wired in `index.ts`). Appendix A slice of the project-recall plan \u2014 future project-aware recall search remains out of scope for this release.\n\n### Fixed\n\n- **Capture `AgentSession` from bundled Pi CLI entrypoint.** ([#62](https://github.com/k0valik/pi-blackhole/pull/62), thanks @daoguademeng) `installHostInlineCompactionAdapter` now resolves the host `AgentSession` from the bundled CLI\'s runtime chunk (when the entrypoint is `dist/bundle/cli.js`) in addition to `dist/index.js`, so inline (mid-run) compaction works when Pi is launched via its bundled CLI instead of silently falling back to settled compaction.\n- **Unified `session_compact_failed` handling (pi >=0.84.3).** Ported from [ceblan/pi-blackhole#ceb-dev](https://github.com/ceblan/pi-blackhole/compare/main...ceblan:pi-blackhole:ceb-dev) (thanks @ceblan / Carlos Estrada): new `src/hooks/compact-failed.ts` closes gaps in failure coverage \u2014 structured `compact_failed.received` trace with corrected `attributedFromExtension` (`fromExtension || compactWasPiVcc || lastCompactCancelled`), defensive `compactInFlight` + `autoCompactionController` reset (aborts orphaned idle-wait so it cannot launch a second compaction after a later turn), overflow-retry `willRetry` visibility (`"overflow compaction aborted, retrying turn"`), and `compactionEngine: pi-default` noise filtering. `Runtime.lastCompactCancelled` is set on every `{ cancel: true }` from `before-compact` and consumed attempt-scoped with `compactWasPiVcc` (leak-free lifecycle: set at `session_before_compact` start, consumed on `session_compact` success or `session_compact_failed`). Covers pi #8328 overflow path.\n\n---\n\n## [0.4.8] - 2026-08-23\n\n### Added\n\n- **Opt-in append compaction (`compactionSummaryMode`).** New config key (`default` | `append`; `default` is the default) plus `PI_BLACKHOLE_COMPACTION_SUMMARY_MODE` override. In append mode each automatic Blackhole compaction appends one immutable provider-visible segment (`S1 | S2 | \u2026`) while every stored summary stays a complete fallback; `/blackhole` rebases the active chain into one clean segment; a legacy v1 summary enters through one marked rebase. A new `context` hook projects segments before each model call and fails closed to the fallback on any malformed state. When the projected chain passes half of the model\'s context window, the next automatic compaction folds it back into one segment. Falls back to rewrite surgery once per session when append mode encounters unsupported state. See `docs/APPEND_COMPACTION.md`. ([#58](https://github.com/k0valik/pi-blackhole/pull/58), thanks @sonSunnoi)\n\n### Changed\n\n- **Mid-run compaction failures now use exponential backoff** (1s doubling to a 30s cap) instead of suspending retries until context pressure drops. A single transient failure no longer wedges auto-compaction for the rest of the pressure episode; failure notices now include "retrying in Xs".\n- **Permanent inline-compaction unavailability (pi version lacks the adapter API) is now classified once** and reported as a single warning ("using settled compaction fallback") instead of surfacing as a retryable failure every episode. With `midRunCompaction: resume`, later turn-end attempts skip the adapter immediately, and agent start warns once if resume mode is configured against a known-unsupported adapter.\n- **Compaction token counting now uses real provider usage when available.** `rawTokensSinceLastCompaction` reads the last valid assistant message\'s usage (`calculateContextTokens`: `totalTokens` or the input/output/cache component sum) after the latest compaction entry, plus a chars/4 estimate for trailing entries, instead of estimating the whole window from characters. Chars/4 remains the fallback for sessions without usage data. Error/aborted assistant turns are never used as baselines; usage from before the latest compaction is ignored (it reflects the pre-compaction context). Approach from tavasti@360f24a (pi-vcc upstream PR #40); hardened implementation ported from plan-01 of the token-rework work.\n- **Minimal tails honor later Pi split-turn boundaries.** An oversized current turn can now be cut at Pi\'s safe assistant/user boundary instead of being retained whole after compaction.\n\n### Fixed\n\n- **Inline compaction ignores aborted/errored assistant turns.** Assistant messages with `stopReason: "error"` or `"aborted"` are now skipped when checking for trailing in-flight tool calls, matching Pi\'s own transform-messages behavior.\n- **Inline compaction ignores stale tool calls** that reference cleared state from a prior turn ([#57](https://github.com/k0valik/pi-blackhole/pull/57), thanks @daoguademeng)\n- **Settings modal footer and key dispatch guard against section rows.** Prevents a crash when the focused row in `/blackhole configure` is a section header instead of an editable field.\n\n---\n\n## [0.4.7] - 2026-08-15\n\n### Fixed\n\n- **Installation from git now works without a prebuilt `dist/`.** The package manifest entrypoint now points at `./index.ts` instead of `./dist/index.js`. Because `dist/` is gitignored, direct Git installs were missing the extension entrypoint and failing to load. Pi can load the TypeScript entrypoint directly, so this restores functionality for `npm install github:k0valik/pi-blackhole` and similar Git-based installs. Registry installs are unaffected (npm/pnpm/bun ship the prebuilt `dist/` bundle).\n\n---\n\n## [0.4.6] - 2026-08-14\n\n### Added\n\n- **Session-local config.** Config values can now be set at session scope via `/blackhole configure` or the config modal\'s scope selector. Session config is ephemeral \u2014 it lives only for the current session and overrides project-local and env values, so you can experiment with settings like `midRunCompaction` or `compactionEngine` without touching files or environment variables.\n\n- **All env overrides are visible in the config modal.** `PI_BLACKHOLE_MID_RUN_COMPACTION`, `PI_BLACKHOLE_COMPACTION`, and `PI_BLACKHOLE_COMPACTION_ENGINE` (alongside existing overrides like `PI_BLACKHOLE_SKIP_PROVIDERS` and `PI_BLACKHOLE_PROVIDER_IDLE_TIMEOUT_MS`) now appear in the env tab of the config modal with their current effective values, so you can see at a glance what the environment is contributing.\n\n### Changed\n\n- **Config modal migrated to the canonical `pi-base` config-rework surface.** The modal now uses the upstream scope-selector and config-flow, replacing the legacy `openSettingsModal` path. The layer precedence is: global \u2192 project \u2192 env \u2192 session, matching pi-utils behavior.\n\n### Removed\n\n- **Dead monolith-era config code.** Removed `src/pi-base/config-settings.ts`, `settings-registry.ts`, `settings-ui.ts`, `registry.ts`, `report.ts`, `llm.ts`, `hash.ts`, `context-provider.ts`, `once.ts`, `debug.ts`, `config-manager-howto.md`, `settings/README.md`, and the obsolete `scope-action.test.ts`. Blackhole-specific wiring (kitty decode, NixOS read-only warnings, key migration, clamping) remains in `blackhole-settings.ts`.\n\n### Fixed\n\n- **Recall drill-down honors lineage scope.** ([#54](https://github.com/k0valik/pi-blackhole/issues/54)) `#N:path` drill-down now checks the active lineage before expanding off-lineage entries, matching every other recall path. Off-lineage indices are blocked under the default `scope:"lineage"` and require `scope:"all"` to access.\n- **Inline compaction restores the Working indicator.** ([#52](https://github.com/k0valik/pi-blackhole/pull/52), thanks @daoguademeng) After inline compaction completes, the UI "Working" indicator is restored so the user sees activity resumed.\n\n### Dependencies\n\n- Bumped dev-dependency group across 2 PRs (#45, #53): `@typescript-eslint/eslint-plugin` to `8.66.0`, `eslint` to `10.8.0`, `lint-staged` to `17.3.0`, `typebox` to `1.3.10`, `typescript` to `6.0.3` (pinned for `@typescript-eslint` v8 compatibility), and `vitest` to `4.1.10`.\n\n## [0.4.4] - 2026-08-06\n\n### Added\n\n- **Experimental compatibility shim for pi-codex-compaction coexistence.** ([#47](https://github.com/k0valik/pi-blackhole/pull/47), thanks @danielmrdev) Optional `skipForProviders` (config key or `PI_BLACKHOLE_SKIP_PROVIDERS` env override) makes blackhole step aside entirely \u2014 no compaction, no observational-memory consolidation \u2014 for listed providers, giving exactly-one-engine semantics when pi-codex-compaction also registers a `session_before_compact` handler. **Niche surface by design**: unsurfaced in README/CONFIG.md until a second consumer exists (see shim notes in `src/core/provider-skip.ts`); surfaced only in example-config.json.\n\n- **Isolated provider idle timeout for background memory jobs.** ([#48](https://github.com/k0valik/pi-blackhole/pull/48), thanks @FelikZ) Optional `providerIdleTimeoutMs` lets observer/reflector/dropper worker HTTP requests tolerate longer silent provider intervals without forcing interactive Pi requests to wait equally long, by wrapping the provider `fetch` with an undici dispatcher that injects `bodyTimeout`. Unset inherits pi\'s global default; `0` disables; `> 0` sets a millisecond cap. Configurable via config file, `/blackhole configure`, or `PI_BLACKHOLE_PROVIDER_IDLE_TIMEOUT_MS`.\n\n### Fixed\n\n- **Credential-resolved provider endpoints are preserved for observational-memory workers on Pi versions whose registry exposes `getProviderAuth()`.** Observer, reflector, and dropper now use the endpoint selected by Pi\'s auth resolver, preventing GitHub Copilot Business/Enterprise requests from falling back to the Individual endpoint and returning HTTP 421. On older registries without `getProviderAuth()`, the fix degrades silently to the previous behavior.\n- **`midRunCompaction: "resume"` no longer aborts or replaces the active run.** ([#50](https://github.com/k0valik/pi-blackhole/pull/50), thanks @daoguademeng) The old `ctx.compact()` + `blackhole-resume` path propagated a false interrupt to background/subagent extensions and let nested child runners resolve before Blackhole\'s detached resume run finished. Resume mode now performs Pi\'s native compaction pipeline inline from the awaited `turn_end` handler, refreshes the next low-level turn from the compacted messages, and continues inside the original `session.prompt()` promise. Completed tool calls remain paired; no synthetic user/custom message is injected. `"resume"` is an **experimental opt-in** \u2014 it monkey-patches Pi host internals and can silently deactivate on host drift.\n- **Mid-run compaction compatibility fails closed.** A reload-idempotent, weakly referenced runtime adapter recognizes the known Pi 0.81 and 0.84 `AgentSession.compact()` shapes. Unknown internal drift refuses transparent compaction and leaves the active run alive instead of falling back to the unsafe aborting path. External abort/cancellation still passes through normally.\n\n### Testing\n\n- Added adapter contract coverage for Pi 0.81/0.84 compact shapes, no-abort behavior, compacted next-turn context refresh, external cancellation, unpaired-tool rejection, fail-closed drift handling, and reload idempotency. A real `AgentSession` + faux-provider integration test runs on both the 0.81.1 compatibility baseline and 0.84.0 dev baseline, proving the active run signal stays live, the next provider request receives the compacted context, and the original `session.prompt()` remains pending through compaction. Trigger tests prove no `ctx.compact()` or `blackhole-resume` dispatch.\n\n### Dependencies\n\n- Bumped `@earendil-works/pi-*` devDependencies from `0.83.0` to `0.84.0`; the peer range remains `>=0.81.1 <1.0.0`, and the adapter retains a tested legacy-shape path for the minimum supported host.\n\n## [0.4.3] - 2026-08-01\n\n### Added\n\n- **pi-base config modal for `/blackhole configure`.** ([#41](https://github.com/k0valik/pi-blackhole/pull/41)) The hand-rolled configure overlay is replaced with pi-base\'s ConfigManager + settings modal (vendored into `src/pi-base/`), with scope-aware editing: global config lives at `<agentDir>/pi-blackhole/` (respecting `PI_CODING_AGENT_DIR`), project config overlays `<cwd>/.pi/pi-blackhole-config.json`.\n\n### Changed\n\n- **Number fields edit inline in `/blackhole configure`.** Number fields (e.g. `compactAfterTokens`, `observeAfterTokens`) no longer cycle in fixed steps on every Enter \u2014 pressing Enter drops into inline editing where you type the value directly; `\u2190`/`\u2192` still fine-tune by step when not editing.\n- **Destructive-action confirmations are safer.** The delete/reset scope confirm now lists **Cancel first (pre-selected)** and shows a warning-color line stating what the action will do \u2014 tabbing into the confirm can never land on a destructive action by accident.\n- **Custom provider streams discovered through pi\'s model registry.** ([#42](https://github.com/k0valik/pi-blackhole/pull/42), thanks @FelikZ) The bridge that lets OM agents (observer/reflector/dropper) use custom providers (e.g. claude-bridge) now captures `streamSimple` functions from pi\'s public registry API (`getRegisteredProviderIds`/`getRegisteredProviderConfig`) on every `agent_start`, instead of wrapping `pi.registerProvider` and reading the private `registeredProviders` field. Works regardless of extension load order and includes providers added after startup; the legacy discovery path remains available for older pi releases.\n- **Precompiled extension bundle for faster startup.** The extension now ships a prebuilt `dist/index.js` bundle (tsup/esbuild) instead of being transpiled file-by-file by jiti at startup \u2014 module loading drops from ~85 source files to a single ESM file, measured ~1.6\u20132\xD7 faster extension load. The `@earendil-works/pi-*` packages and `typebox` stay external and resolve to the host pi\'s copies at runtime via its loader aliases. `pnpm build` produces the bundle; `prepare` builds automatically on install. The package manifest points at `./dist/index.js` and falls back to `index.ts` (slow path) when `dist/` is absent, so a fresh checkout still works pre-build.\n\n### Fixed\n\n- **Manual-mode pending files now contain full observation payloads.** ([#41](https://github.com/k0valik/pi-blackhole/pull/41)) The `noAutoCompact` \u2192 `compaction:\'manual\'` migration is completed: `isManualMode()` now checks both keys across all save/load gates, so manual-mode observations are written to the pending file (`savePendingObservation`) instead of falling through to `appendEntry()` (JSONL) \u2014 restoring crash-safe mid-run interruption recovery and `/blackhole flush` parity.\n- **Config modal could overwrite the user\'s config with defaults.** `openSettings` did not pass `globalConfigDir` to the settings modal, so the modal initialized every field from the schema default (it read a nonexistent config in the extensions dir) instead of the actual config file. Saving then wrote those defaults over the real values (e.g. `compactAfterTokens` 185000 \u2192 81000) while the runtime kept the correct values in memory \u2014 a confusing half-applied state. The modal now initializes from the real config file.\n- **Number-field editing could get stuck.** While inline-editing a number field, typing/backspace/escape were swallowed by the step-cycling branch, leaving the modal in an editing state with no way out (Enter showed a cursor but nothing worked, and `ctrl+c` couldn\'t close it). All editing keys now flow through the inline editor, and `ctrl+c` closes the modal even mid-edit.\n- **Typed input failed in Kitty terminals.** Kitty reports printable characters as CSI-u sequences (e.g. `5` arrives as `\\x1b[53u`); they were rejected by the input filter \u2014 and after the first fix, inserted as raw escape bytes. The input filter and the insert path now decode them, so typing works in Kitty terminals.\n- **Config save failures on read-only filesystems are now visible.** `ConfigManager.save()` throws when the write fails (e.g. config managed by Nix), and `/blackhole om-off`/`om-on` surface a warning \u2014 previously the failure was silently swallowed while the in-memory runtime state changed, diverging from disk without explanation.\n- **`PI_BLACKHOLE_*` env overrides now apply at runtime.** The declarative env map (`memory`, `debug`, `compactAfterTokens`, \u2026) was only honored by the modal path; the runtime config loader ignored it. The env map + application logic moved to a shared module used by both paths, so e.g. `PI_BLACKHOLE_COMPACT_AFTER_TOKENS=200000` now affects the actual compaction threshold, not just the modal display.\n\n### Testing\n\n- **Ported the upstream pi-base test suite (246 tests)** from `pi-utils/packages/pi-base` \u2014 config manager, settings modal (buffered mode, smoke, inline-edit, field validation) plus the 4 small modules (env, shell, types, ui) they cover. Only import-path adaptation was needed; zero semantic drift, which also confirms the vendored modal is behaviorally aligned with upstream.\n- **New regression tests pin this release\'s fixes:** config-manager `globalConfigDir` forwarding, number-field inline editing (including a Kitty CSI-u integration case driving the full renderer path), Kitty decode, and runtime env overrides.\n- **Tests no longer touch the system clipboard.** The memory-command tests ran the real `copyTextToClipboard` (spawning `wl-copy`/`xclip`/`xsel`) and overwrote the user\'s clipboard with fixture data; the module is now mocked and the mock\'s use is asserted so a regression fails the suite instead of mutating the clipboard.\n\n### Dependencies\n\n- **Bumped `@earendil-works/pi-*` devDependencies to `0.83.0`** (agent-core, ai, coding-agent, tui); the peer range stays `>=0.81.1 <1.0.0`. CI re-verifies typecheck + tests against the minimum supported `0.81.1` on every push/PR, so both the oldest and newest supported pi versions stay green.\n\n### Packaging\n\n- **Tolerant `prepare` build hook.** The `prepare` script is now a dependency-free `node scripts/prepare.mjs` that builds `dist/` only when the toolchain is present, and otherwise skips silently \u2014 it can never abort an install for git/checkout consumers running npm, pnpm, or bun in any devDependency configuration. Husky hooks install best-effort (dev checkouts only). Registry installs are unaffected (npm/pnpm/bun never run `prepare` on registry packages).\n- **npm publishing now uses provenance.** The publish workflow runs `npm publish --provenance` (GitHub OIDC attestation), so every tarball carries a signed signature linking it to this repo + workflow \u2014 verifiable with `npm audit signatures` / `gh attestation verify`. The release gate now matches CI (build, typecheck, lint, test, format check).\n- **Dev tooling.** Prettier (repo normalized once, enforced via lint-staged), husky pre-commit (lint+format staged files, then typecheck) and pre-push (typecheck + full test suite), ESLint extended to `tests/` and root configs, and CI now runs tests + format check alongside the build.\n\n---\n\n## [0.4.2] - 2026-07-27\n\n### Changed\n\n- **`midRunCompaction` default changed from `"resume"` to `"off"`.** ([#40](https://github.com/k0valik/pi-blackhole/issues/40), thanks @daoguademeng) `ctx.compact()` aborts the active agent operation before compacting, which is not lifecycle-safe at `turn_end` for subagent/background-work extensions: it propagates through the shared `AbortSignal` and cannot be distinguished from user cancellation. This affects both parent-side subagent workflows (active/queued children aborted, parent stalled) and child-side nested sessions (runner terminated, orphan transcript continues, `blackhole-resume` resumes a session the parent already sees as completed). `off` defers compaction to `agent_end`, which is the only currently safe boundary for extension-owned work. `resume` and `pause` are preserved as explicit opt-in for users without subagent workflows.\n\n---\n\n## [0.4.1] - 2026-07-24\n\n### Added\n\n- **Mid-run auto-compaction (`midRunCompaction`).** ([#38](https://github.com/k0valik/pi-blackhole/pull/38), thanks @daoguademeng) The threshold trigger previously only ran on `agent_end`, which never fires while the agent is looping through tool calls \u2014 during long runs `compactAfterTokens` could be exceeded many times over without a single evaluation, and the post-run wait was aborted by any new `agent_start`, deferring compaction indefinitely under continuous use. The threshold is now also evaluated at every `turn_end` (after each assistant message + tool executions). New config enum `midRunCompaction: "resume" | "pause" | "off"` (default `"resume"`): `resume` compacts at the threshold and injects a `blackhole-resume` message (`triggerTurn`) so the agent continues the task with the compacted context; `pause` compacts and hands control back; `off` restores the old end-of-run-only behavior. Available in `/blackhole configure`.\n- **`/blackhole <text>` follow-up prompt.** After compaction, `/blackhole` optionally sends `<text>` as a follow-up message so the model continues the task without re-typing. Wrapped in `void Promise.resolve(...).catch(() => {})` for robust error handling.\n- **Subcommand near-miss detection.** `/blackhole configure foo` now shows a warning instead of silently becoming a follow-up prompt.\n\n- **`/blackhole cleanup` command for orphaned pending files.** Per-session pending files (`*-pending.json`, `*-pending.stale.json`) accumulate when compaction is manual and sessions are abandoned or deleted. The command scans the `pi-blackhole/` directory, cross-references session IDs against all session JSONL files, and provides an interactive TUI picker to safely remove orphaned files. Non-TUI modes (RPC/JSON/print) list orphaned files as a notification without deleting.\n\n### Command formatting cleanup\n\n- `/blackhole` and `/blackhole-memory` subcommands and modes now use `[bracketed]` syntax (e.g. `[om-on]`, `[hybrid]`) with shortened descriptions, making the command palette visually consistent and easier to scan.\n\n### Notification & session goal reorg\n\n- Session goal now derives from the first user message and is persisted at the top across compactions, with `(#N)` entry indexing for traceability.\n- OM info notifications are gated to one per phase/turn \u2014 warnings and errors still fire immediately.\n- Git commit extraction now handles tool_call, bash, and post-convert user-text formats.\n- Cooldown skip messages now strip raw JSON from the reason for cleaner display, with a log pointer for debugging.\n\n### Fixed\n\n- **Mid-run compaction failure resilience.** ([#38](https://github.com/k0valik/pi-blackhole/pull/38), thanks @daoguademeng) If the before-compact hook cancels (or compaction errors) after `ctx.compact()` has already aborted the run, resume mode still re-triggers the agent so the task doesn\'t stall, and further mid-run attempts are suspended until a compaction lowers pressure below the threshold (prevents abort/cancel thrash loops).\n- **Early-session reflection/drop starvation on first compaction.** Added `fullFoldAlways` config flag (default `true`). When no prior full-fold boundary exists, reflections and drops now use the observation boundary instead of being excluded. Previously, fresh sessions silently lost all durable memory on the first compaction because there was no full-fold history to anchor the maintenance boundary.\n- **`capBrief` omission count now computed after `firstHeader` trim.** Previously the "N earlier lines omitted" header was computed before the section-header anchor trim, so the count was understated when headers caused additional trimming. This matched an upstream bug that was already fixed there.\n\n- **Recall-note bloat across multiple compactions.** `compile()` now strips OM content first, then removes all recall-note paragraphs from the previous summary using paragraph-level matching (instead of only stripping a trailing exact match). After 3+ compactions, the summary no longer accumulates 3+ embedded copies of the recall note.\n\n## [0.4.0] - 2026-07-24\n\n### Added\n\n- **`/blackhole cleanup` command for orphaned pending files.** Per-session pending files (`*-pending.json`, `*-pending.stale.json`) accumulate when compaction is manual and sessions are abandoned or deleted. Provides an interactive TUI picker to safely remove orphaned files. Non-TUI modes (RPC/JSON/print) list them without deleting.\n- **`dropperPressureThreshold` in configure overlay.** Already in config schema but missing from `/blackhole configure` TUI. Now editable alongside other OM thresholds.\n- **`fullFoldAlways` in TUI overlay.** Added to the configure overlay under Observational Memory section.\n- **Session goal from first user message.** Persisted at the top across compactions with `(#N)` entry indexing for traceability.\n- **OM info notifications gated to one per phase/turn.** Warnings and errors still fire immediately.\n- **Git commit extraction expanded.** Now handles `tool_call`, `bash`, and post-convert user-text formats.\n- **Cooldown skip messages strip raw JSON** from the reason for cleaner display, with a log pointer for debugging.\n- **`/blackhole` and `/blackhole-memory` subcommands now use `[bracketed]` syntax** (e.g. `[om-on]`, `[hybrid]`) with shortened descriptions for visual consistency.\n\n### Fixed\n\n- **Early-session reflection/drop starvation on first compaction.** Added `fullFoldAlways` config flag (default `true`). When no prior full-fold boundary exists, reflections and drops use the observation boundary instead of being excluded.\n- **Recall-note bloat across multiple compactions.** `compile()` strips OM content first, then removes all recall-note paragraphs using paragraph-level matching (instead of only stripping a trailing exact match).\n- **OAuth/ADC-backed providers (Vertex, custom OAuth) now accepted by OM pipeline.** `resolveModel` uses `modelRegistry.hasConfiguredAuth()` instead of requiring a truthy `auth.apiKey`. Falls back to legacy behavior on older pi versions. ([#38](https://github.com/k0valik/pi-blackhole/issues/38))\n- **`ResolveResult.apiKey` is always a string.** Defaults to `""` instead of casting `undefined`.\n- **jiti provider bridge type-safe for pi 0.81.1+.** `pi.registerProvider` wrapper satisfies the overloaded signature in pi-coding-agent 0.81.1.\n- **Config overlay blocks save on invalid JSON.** Red error banner and Ctrl+S block prevent wiping model configs on corrupt files. ([#35](https://github.com/k0valik/pi-blackhole/issues/35))\n- **Config reloads after overlay save.** `Runtime.reloadConfig()` forces a fresh disk read after `/blackhole configure` saves. ([#36](https://github.com/k0valik/pi-blackhole/issues/36))\n- **Invalid JSON warning surfaced via TUI.** Yellow warning notification shown at every config load point instead of only `console.warn`.\n- **Defensive null guards for `b.args` and `ui.notify`.** Prevents crashes from stale extension context.\n- **`streamSimple` import updated to `pi-ai/compat`.** Removed from main export in pi 0.80.3.\n- **Legacy fallback config errors now passed to `onWarn` callback.** JSON parse errors in legacy fallback files (`pi-vcc-config.json`, `settings.json`, `.pi/settings.json`) are surfaced via the warning callback, not just `console.warn`.\n- **`saveUnifiedConfig` warns before overwriting corrupt config.** If the config file has invalid JSON, a warning is logged before overwriting.\n- **`dropperPressureThreshold` clamped to `[0.01, 1]` in overlay save.** Previously could silently lose value on reload.\n- **`deleteOrphanedBatch` reports partial failures.** "Delete all" now shows `Deleted X/Y (Y-X failed)` when individual unlinks fail.\n- 4 new tests for `fullFoldAlways` behavior in `buildCompactionProjection`: reflections survive first compaction when enabled, excluded when disabled, full-fold boundary still takes precedence, and post-boundary reflections remain excluded.\n- 3 new tests for recall-note deduplication in `compile`: wrapped recall note stripped, OM content stripped before recall note, and three-cycle accumulation produces exactly one recall note.\n- 5 new tests for follow-up prompt: extraction, subcommand exclusion, empty-args suppression, send after completion, compaction-failure suppression.\n- 6 new tests for CompactionStats population: all fields populated, compactAll flag, totalUserTurns count, keptUserTurns count, compactAll zero kept, and format string coverage.\n- 2 new tests for capBrief omission count: header-trimmed count is correct (99 for 200 lines with header at line 100), and no-header fallback still correct.\n\n### Changed\n\n- **New config key:** `fullFoldAlways` (boolean, default `true`). Added to `UnifiedConfig` schema, defaults, and config file parsing.\n- **CompactionStats expanded from 3 to 11 fields.** Added `compactAll`, `totalUserTurns`, `keptUserTurns`, `requestedKeepUserTurns`, `keepUserTurnsExplicit`, `keepFallbackToCompactAll`, `smartKeepAdjusted`, `smartFromKeep`. All populated from `buildOwnCut` return data (Bug A fix).\n- **Shared `formatCompactionStats` exported.** Both the `/blackhole` command handler and hook\'s `session_compact` handler now use a single shared formatter, eliminating the duplicate inline toast strings and the private `formatTokens` helper.\n- **Dead ternary collapsed.** `effectiveTailBehavior` no longer has an `isPiVcc` branch with identical values on both sides (Bug B fix).\n- **Dependencies: bumped `@earendil-works/pi-*` packages to `0.81.1`** (agent-core, ai, coding-agent, tui).\n- **Removed 6 unused exports from `om/cleanup.ts`** (`scanPendingFiles`, `findSessionDirs`, `collectAllSessionIds`, `crossReference`, `formatSize`, `formatAge`).\n\n### Tests\n\n- 4 new tests for `fullFoldAlways` behavior in `buildCompactionProjection`.\n- 3 new tests for recall-note deduplication in `compile`.\n- 6 new tests for OAuth/ADC auth paths.\n- Tightened capping assertions in robust tests.\n- Added robust coverage for OM and CCC pipelines.\n\n---\n\n## [0.3.9] - 2026-06-24\n\n### Auto-compaction idle race fix (#31, #33)\n\nThe auto-compaction trigger used to bail permanently when `ctx.isIdle()`\nreturned `false` at the first `setTimeout(0)` check after `agent_end`.\nWhen another extension (e.g. pi-rewind) registered an async `agent_end`\nhandler whose I/O kept the agent state busy past the next macrotask,\nthe trigger logged `"bail: not_idle"` and never retried \u2014 auto-compaction\neffectively never fired in this configuration.\n\n**New behavior:** the trigger keeps `compactInFlight = true` and polls\n`isIdle()` every 200ms (in 50ms slices) until the agent truly settles,\nor one of two cancellation signals:\n\n- `agent_start` fires \u2014 the user (or another extension) started a new\n turn. `AbortController.abort()` cancels the wait; the new turn\'s own\n `agent_end` will re-evaluate and start a fresh wait if still needed.\n- Session change (e.g. `/resume`) \u2014 detected inside the wait loop.\n\nOnly the cell `compaction:auto + compactionEngine:blackhole` is affected.\nAll other config combinations (off, manual, pi-default) are unchanged.\n\n### CI: fallow audit job\n\n- Added `fallow-audit` CI job (PR only, changed-code audit with compact\n format, review comments, no SARIF)\n\n### Test cleanup\n\n- Removed stale `transcript-mode` tests left orphaned when the feature\n was deliberately dropped in v0.3.7 as redundant with hybrid search.\n\n---\n\n## [0.3.8] - 2026-06-19\n\n### Pipeline progress cursors - fix re-run loop (#28, #29)\n\nThe pipeline previously coupled progress tracking to output markers: if a stage\nproduced empty output or errored, no marker was written, causing the stage to\nre-process the same data on every `agent_start`/`turn_end` trigger. In real-world\nlogs the dropper ran 8,350\xD7 vs observer 1,124\xD7, with zero drops selected.\n\n- **Per-stage progress cursors** decouple progress from output. Each stage\n (observer, reflector, dropper) gets a cursor entry ID that advances whenever\n the stage runs - regardless of whether it produced output. "I looked and\n found nothing" is a valid answer that blocks re-processing.\n- **Cursor `state` field** (`recorded` | `empty` | `error` | `skipped` | `not_due` | `initial`)\n distinguishes empty runs from skipped stages from actual output.\n- **Reflector gates on new data.** If no new `OM_OBSERVATIONS_RECORDED` batches\n exist since the reflector cursor, and `reflectAfterTokens` threshold not met,\n skip entirely - no LLM call.\n- **Dropper gates on pressure or new data.** Runs only when pool \u2265 10% fullness AND\n (new data exists OR pool \u2265 `dropperPressureThreshold` \xD7 `reflectorInputMaxTokens`).\n Previously always returned `not_over_target` with 0 drops - now correctly skipped.\n- **Cursor storage:** in-memory primary (zero-I/O gating), async flush to\n `{sessionId}-pending.json` for durability across restarts. Degrades gracefully\n on read-only filesystems.\n- **Stale cursor recovery:** if a cursor\'s entry ID disappears (fork, navigation,\n compaction), falls back to coverage-marker logic for one run, then writes fresh cursors.\n\n### New config key: `dropperPressureThreshold`\n\n- Fraction of `reflectorInputMaxTokens` at which the dropper fires even without\n new data (pressure relief valve). Default `0.70` (70%). Set to `1.0` to disable\n pressure-driven dropper entirely.\n\n### Debug log additions\n\n- `observer.skip`, `reflector.start`, `reflector.skip`, `dropper.start`,\n `dropper.skip`, `cursor.loaded`, `cursor.saved`\n\n### Deferred pipeline concerns (pre-merge review)\n\nAudit surfaced 7 correctness/performance edge cases in the cursor pipeline.\nFour were fixed; three were deferred as harmless or cosmetic.\n\n**Fixed:**\n\n- **Session fork cursor bleed (#2).** `cursorsLoaded` was a one-shot boolean\n \u2014 on session fork, stale cursors bled into the new branch because\n `validateCursors` was never re-invoked. Now keyed by `cursorsLoadedSessionId`\n so cursors are re-loaded and re-validated whenever the session ID changes.\n- **Manual-mode pool fullness underestimation (#1).** `anyStageDue` had no\n visibility into pending observations in `compaction: "manual"` mode (branch\n has no OM markers). Reflector and dropper due checks now accept an optional\n `PendingOMState` so pending batches contribute to new-data scans and pool\n token counts. Prevents the pipeline from stalling after the first run in\n manual mode.\n- **foldLedger on every agent_start/turn_end (#3).** `dropperDue` called\n `foldLedger` (O(n) on branch) unconditionally \u2014 even when the observer or\n reflector alone made the pipeline due. Now short-circuits: the fold is\n only computed when both observer and reflector are not due.\n- **Observer cursor to non-source entry (#6).** When the observer skipped\n (not due), the cursor advanced to `entries.at(-1)` which could be a custom\n OM marker rather than a conversation source entry. Now advances to the\n last source entry (`findLast(isSourceEntry)`).\n\n**Deferred:**\n\n- **"unknown" magic entry ID (#4).** Functional but cosmetic \u2014 the sentinel\n triggers fallback on next load. 9 call sites; zero behavioral change.\n- **Observer re-checks tokens (#5).** Harmless \u2014 only reached when pipeline\n launched for a different stage. Correctly advances cursor to `not_due`.\n- **Dropper cursor fallback cascade (#7).** The 4-step `coversUpToId ??`\n `observationCoverageId ?? entries.at(-1)?.id` cascade is already reasonable\n fallback ordering.\n\n### Tests\n\n- 17 new tests for cursor gating, persistence, stale recovery, and debug log events\n- 3 new tests for manual-mode pending awareness (reflector, dropper, post-first-run)\n- `dropperPressureThreshold` added to config validation tests\n\n# Changelog\n\n## [0.3.7] - 2026-06-10\n\n### Recall tool simplification (#27)\n\n- Dropped `mode:transcript` \u2014 strict subset of `mode:hybrid` with no unique capability. (#27)\n- Consolidated 5 scattered `promptGuidelines` into 2 focused entries; removed "NOT semantic" redundancy and JSONL implementation leak. (#27)\n- Removed internal taxonomy from mode descriptions ("transcript + file indicators" \u2192 "all session content"). (#27)\n- Added `mode:touched` support to `/blackhole-recall` command (previously only worked via agent tool). (#27)\n- Collapsed drill-down examples to `#N:path with optional :offset:limit or :full`. (#27)\n\n### Stale context crash protection (#26)\n\n- Added `getErrorMessage()` to normalize cross-process error serialization (Error objects, plain objects with `message`, arbitrary thrown values). (#26)\n- Added `isStaleExtensionContextError()` to detect stale-context error patterns. (#26)\n- Added `notifySafely()` wrapper around `ui.notify()` calls to prevent stale-context notification errors from propagating. (#26)\n- Wrapped `agent_end` handler, async compaction callbacks (`onComplete`, `onError`), and deferred timer callback to silently bail on stale-context errors. (#26)\n\n### Lockstep sync \u2014 2026-06-05 (#25)\n\n- Ported [pi-observational-memory/58f05fa](https://github.com/elpapi42/pi-observational-memory/commit/58f05fa): remove `Math.min(100)` cap from `pct()` helper so overfull observation pool (>100%) is displayed accurately instead of silently capping at 100%. (#25)\n- Skipped [pi-observational-memory/58f05fa](https://github.com/elpapi42/pi-observational-memory/commit/58f05fa) command renames (`/om-status`\u2192`/om:status`, `/om-view`\u2192`/om:view`) \u2014 our equivalent commands (`/blackhole-memory`) already use a different naming scheme. (#25)\n- Deferred [pi-observational-memory/bf79ff7](https://github.com/elpapi42/pi-observational-memory/commit/bf79ff7) and [pi-observational-memory/52b5844](https://github.com/elpapi42/pi-observational-memory/commit/52b5844): pool metrics extraction + `budgetTokens`\u2192`targetTokens` rename. Blocking branch (`noautocompact-reflector-dropper`) is now stale/dropped, but changes touch heavily diverged files. (#25)\n\n## [0.3.5] - 2026-06-04\n\n### Added\n\n- **`sessionFallback` config option.** When `false`, skip the main session model as last-resort fallback when all OM-specific model candidates are exhausted. Default `true` for backward compatibility. Useful for keeping OM workers on cheaper/faster models. ([#20](https://github.com/k0valik/pi-blackhole/pull/20))\n- **Session-file LRU cache.** `loadAllMessages` now caches up to 3 session files with mtime + TTL (2s) invalidation. Reduces redundant I/O on repeated recall searches in the same session. ([#20](https://github.com/k0valik/pi-blackhole/pull/20))\n- **Pending state sanitization.** `readSessionState` now filters corrupted batch entries (missing `coversUpToId` or `data` fields) instead of returning them as-is. Prevents crashes from edge cases like a partial write to `pending.json`. ([#20](https://github.com/k0valik/pi-blackhole/pull/20))\n- **Shared `isRetryableError` / `RETRYABLE_ERROR_RE`.** Extracted from `cooldown.ts` and `compaction-trigger.ts` into `retryable-error.ts` \u2014 single source of truth, re-exports Pi\'s `isContextOverflow` for provider-specific overflow detection. ([#20](https://github.com/k0valik/pi-blackhole/pull/20))\n- **Shared provider-stream bridge.** `createBridgeStreamFn` extracted from all three OM agents (observer, reflector, dropper) into `provider-stream.ts`. Custom providers registered by other extensions (e.g., claude-bridge) continue working through jiti-loaded consolidation agents. ([#21](https://github.com/k0valik/pi-blackhole/pull/21))\n- **Async buffered debug logging.** `debugLog()` now buffers JSONL writes in memory and flushes on a 1-second background timer, with synchronous flush on `exit`. Reduces event-loop blocking during high-frequency debug events. ([#20](https://github.com/k0valik/pi-blackhole/pull/20))\n- **Windows path support in file extraction.** `longestCommonDirPrefix` normalizes backslashes and recognizes `C:\\`-style drive letters. ([#20](https://github.com/k0valik/pi-blackhole/pull/20))\n\n### Fixed\n\n- **Context window check uses actual input size, not configured cap.** Observer/reflector/dropper now compute `observerEstimatedInput` from the actual chunk tokens after capping, not from `observerChunkMaxTokens`. More accurate \u2014 fewer false "context window exceeded" rejections on smaller-than-cap inputs. ([#20](https://github.com/k0valik/pi-blackhole/pull/20))\n- **`coversUpToId` now points past capping, not before.** Observer stage captured the last entry ID before capping source entries to `maxChunkTokens`, so the coverage marker could point to an entry that was dropped. Now captured after capping. ([#20](https://github.com/k0valik/pi-blackhole/pull/20))\n- **`capSourceEntriesToTokens` counts all entry types.** Previously only `"message"` entries counted toward the token budget \u2014 custom OM entries (`observations_recorded`, `reflections_recorded`, etc.) and summary-bearing entries were invisible, risking context overflow in the observer. ([#20](https://github.com/k0valik/pi-blackhole/pull/20))\n- **Reflector/dropper avoid redundant disk reads.** Both stages now use the outer-scope `pending` variable (already read in the `noAutoCompact` block) instead of calling `readPendingState(sessionId)` again inside the for loop. Neutral correctness win. ([#20](https://github.com/k0valik/pi-blackhole/pull/20))\n- **Observer rejects invalid observation IDs gracefully.** `normalizeSourceEntryIds` now filters out unknown/duplicate IDs instead of returning `undefined` and discarding the entire observation batch. One hallucinated ID from the LLM no longer loses valid observations. ([#20](https://github.com/k0valik/pi-blackhole/pull/20))\n- **`pendingObservationsCreatedAfter` properly typed.** Changed from `pending: any` to `pending: PendingOMState` \u2014 catches type mismatches at compile time. ([#20](https://github.com/k0valik/pi-blackhole/pull/20))\n- **Section headers in summaries use line-boundary regex.** `sectionOf` and `stripOMContent` now match `## Reflections` / `## Observations` at the start of a line instead of using bare `indexOf`. Prevents false positives when those phrases appear inside file paths or conversation text. ([#20](https://github.com/k0valik/pi-blackhole/pull/20))\n- **Read+same-path-Modified dedup in file summaries.** `mergeFileLines` now removes a path from `Read` if it also appears in `Modified` \u2014 a file that was read then edited shouldn\'t show twice. ([#20](https://github.com/k0valik/pi-blackhole/pull/20))\n- **`reverse-recall` outputs related reflections.** The `_reflections` dead parameter is now used \u2014 related reflections are shown alongside observations when expanding session entries. ([#20](https://github.com/k0valik/pi-blackhole/pull/20))\n- **Cooldown reason in UI notification.** The `getCooldownEntry` function now returns the actual entry (with reason), so the status notification shows _why_ a model was cooled down, not just "cooldown active". ([#20](https://github.com/k0valik/pi-blackhole/pull/20))\n- **Env override validation.** Invalid `PI_BLACKHOLE_COMPACTION` / `PI_BLACKHOLE_COMPACTION_ENGINE` values now print a warning instead of being silently ignored. ([#20](https://github.com/k0valik/pi-blackhole/pull/20))\n- **`observerPreambleMaxTokens` accepts 0.** Now uses `nonNegativeInt` validator instead of `positiveInt` \u2014 0 means "auto-compute", which was the intended semantics. ([#20](https://github.com/k0valik/pi-blackhole/pull/20))\n\n### Changed\n\n- **Replaced hand-rolled text wrapping with `wrapTextWithAnsi` from pi-tui.** The custom `wrapLine` function was replaced with `wrapLineWithContinuation` using pi-tui\'s ANSI-aware wrapping. Handles list continuation indentation and ANSI mid-sequence splits correctly. ([#21](https://github.com/k0valik/pi-blackhole/pull/21))\n- **`visibleWidth` re-exported from pi-tui.** The local CJK-width implementation in `key-matcher.ts` was replaced with a re-export from `@earendil-works/pi-tui`. Fallback note retained if the import fails in overlay context. ([#21](https://github.com/k0valik/pi-blackhole/pull/21))\n- **Bash command compression improved.** Multi-line commands joined with semicolons instead of first-line-only. Pipe tails strip `awk`/`python3`/`node`/`bun` excluded (their output carries semantic meaning). Word-boundary truncation instead of mid-word cut. Up to 10 tail-strip iterations with stability guard. ([#20](https://github.com/k0valik/pi-blackhole/pull/20))\n- **`fuzzyMatch` \u2192 `prefixMatch`.** The `/blackhole` subcommand filter changed from fuzzy/subsequence matching to simple prefix matching. Predictable narrowing: typing "om" matches "om-on" and "om-off". ([#21](https://github.com/k0valik/pi-blackhole/pull/21))\n- **`read` tool summary field corrected.** `TOOL_SUMMARY_FIELDS` now maps lowercase `read` \u2192 `"path"` (not `"file_path"`), matching the actual tool argument. ([#20](https://github.com/k0valik/pi-blackhole/pull/20))\n- **Tool error blank-line suppression.** `stringifyBrief` now suppresses blank lines between consecutive tool/error summaries (previously only between consecutive tool summaries). ([#20](https://github.com/k0valik/pi-blackhole/pull/20))\n- **Recall header distinguishes matches vs expands.** The search result header now shows `"X matches (+ Y expanded)"` when entries were pulled in via `#N` expand rather than matching the query. ([#20](https://github.com/k0valik/pi-blackhole/pull/20))\n- **Compaction output instructions split into full/basic variants.** `CONTEXT_USAGE_INSTRUCTIONS` shortened to 4 lines (previously 10). When observations/reflections are present, the full version includes the bracketed-ids preamble + recall footer. When none exist (or OM is off), a basic 2-line recall-guidance footer is appended instead. `renderSummary` always returns a footer, and `stripOMContent` handles both variants to prevent compounding. ([#23](https://github.com/k0valik/pi-blackhole/pull/23))\n\n### Removed\n\n- **Dead `loadSettings()` / `PiVccSettings`.** Config loading unified in `unified-config.ts` \u2014 the `settings.ts` wrapper had zero callers. ([#20](https://github.com/k0valik/pi-blackhole/pull/20))\n- **Dead `transcriptEntries` from `SectionData`.** Removed from `sections.ts` and `build-sections.ts`. (dead since v0.3.3) ([#20](https://github.com/k0valik/pi-blackhole/pull/20))\n- **Dead toggle helpers.** `toggleCompaction`, `toggleCompactionEngine`, `toggleTailBehavior` removed from `unified-config.ts` (zero callers \u2014 toggling is handled by the configure overlay). ([#20](https://github.com/k0valik/pi-blackhole/pull/20))\n- **Dead `vcc-report.test.ts`.** Test file was testing a non-existent `src/core/report.js` module. ([#20](https://github.com/k0valik/pi-blackhole/pull/20))\n- **Dead `config-simplification.test.ts`.** Tested old config migration that\'s been stable since v0.3.3. ([#20](https://github.com/k0valik/pi-blackhole/pull/20))\n\n### Docs\n\n- **Renamed example configs.** `example-config-v2.json` \u2192 canonical `example-config.json` (new config surface). Old `example-config.json` \u2192 `example-config-old.json` (legacy keys). ([#21](https://github.com/k0valik/pi-blackhole/pull/21))\n- **README: updated "What the agent sees" example** to match actual output ordering and expanded RECALL_NOTE text. ([#21](https://github.com/k0valik/pi-blackhole/pull/21))\n- **README: added `sessionFallback` to settings table.** ([#20](https://github.com/k0valik/pi-blackhole/pull/20))\n- **example-config.json: added `sessionFallback` field.** ([#20](https://github.com/k0valik/pi-blackhole/pull/20))\n- **README: updated "What the agent sees" example** to match the new shorter CONTEXT_USAGE_INSTRUCTIONS text and note about basic footer when OM is off. ([#23](https://github.com/k0valik/pi-blackhole/pull/23))\n\n## [0.3.4] - 2026-06-02\n\n### Added\n\n- **`cooldownHours: 0` disables cooldown without disk writes.** Previously `cooldownHours: 0` was rejected by the positive-int validator and silently replaced with a 1-hour cooldown. Now 0 is a valid value that disables cooldown entirely \u2014 no disk writes, no persistent state. Failed models are tracked in-memory within each consolidation stage (via `failedInCycle` set) so the fallback chain still advances past them. ([#16](https://github.com/k0valik/pi-blackhole/issues/16), [#18](https://github.com/k0valik/pi-blackhole/pull/18))\n- **Kitty CSI-u keyboard protocol support for overlays.** The configure and status overlays use pi-tui\'s `matchesKey` (which handles both legacy terminal sequences and Kitty\'s CSI-u protocol) instead of the homegrown `matchKey`. Digit input uses `decodeKittyPrintable` to decode CSI-u encoded characters. ([#17](https://github.com/k0valik/pi-blackhole/issues/17), [#19](https://github.com/k0valik/pi-blackhole/pull/19))\n- **Per-stage failure notification isolation.** When cooldown is disabled, each consolidation stage (observer, reflector, dropper) now shows its own failure notification \u2014 observer failure no longer suppresses reflector/dropper notifications. ([#19](https://github.com/k0valik/pi-blackhole/pull/19))\n\n### Fixed\n\n- **Keyboard freeze in `/blackhole configure` on Kitty terminal.** The homegrown `matchKey` function did not recognize Kitty\'s CSI-u keyboard protocol sequences (used by Kitty, WezTerm, and other modern terminals). Switched to pi-tui\'s `matchesKey` which supports both legacy and CSI-u input. ([#17](https://github.com/k0valik/pi-blackhole/issues/17), [#19](https://github.com/k0valik/pi-blackhole/pull/19))\n- **Config error notifications no longer downgraded to info.** When a session model has no API key configured, the notification correctly shows a "warning" level message instead of the misleading "info" message previously shown when `failedInCycle` was non-empty. ([#16](https://github.com/k0valik/pi-blackhole/issues/16), [#18](https://github.com/k0valik/pi-blackhole/pull/18))\n\n### Changed\n\n- **Removed `key-matcher.ts` `matchKey` export** (replaced by pi-tui\'s `matchesKey`). The `visibleWidth` export is retained.\n\n## [0.3.3] - 2026-06-02\n\n### Added\n\n- **New config surface:** `compaction` (`"auto"` | `"manual"` | `"off"`), `compactionEngine` (`"blackhole"` | `"pi-default"`), `tailBehavior` (`"pi-default"` | `"minimal"`). These replace the old `overrideDefaultCompaction`, `noAutoCompact`, and `passive` keys. See [`MIGRATION-GUIDE.md`](MIGRATION-GUIDE.md) for the full mapping. ([#14](https://github.com/k0valik/pi-blackhole/pull/14))\n- **Config overlay (`/blackhole configure`):** interactive TUI with \u2191\u2193 navigation, Enter to edit/toggle, Ctrl+S to save. 17 fields across 3 sections (Compaction, Observational Memory, Debug) with inline help text. ([#14](https://github.com/k0valik/pi-blackhole/pull/14))\n- **Status overlay (`/blackhole-memory`):** new render with compaction config readout, OM pipeline state, and inline actions (configure, om-off/on). ([#14](https://github.com/k0valik/pi-blackhole/pull/14))\n- **Tail behavior control:** `tailBehavior: "minimal"` keeps only the last user message (aggressive pi-vcc cut, default); `tailBehavior: "pi-default"` keeps Pi\'s ~20k token tail visible (opt-in). Both auto-triggered and `/blackhole` now default to `"minimal"`. ([#14](https://github.com/k0valik/pi-blackhole/pull/14))\n- **12 permutation tests** covering all compaction \xD7 memory \xD7 threshold combinations for the new config keys. ([#14](https://github.com/k0valik/pi-blackhole/pull/14))\n- **Documentation:** CONFIG.md (new reference), OLD_CONFIG.md (legacy docs), MIGRATION-GUIDE.md (migration path from old keys), README.md and llms.txt updated for the new surface. ([#14](https://github.com/k0valik/pi-blackhole/pull/14))\n- **Per-model context window override:** `OmModelConfig` now supports an optional `contextWindow` field. When set on any stage model or fallback, it overrides Pi\'s model registry value for the context window check. Unset models inherit from Pi normally. ([#14](https://github.com/k0valik/pi-blackhole/pull/14))\n- **Context window pre-check:** before calling each OM stage agent (observer, reflector, dropper), the estimated input tokens (stage cap + 8K reserve for system prompt/tools/turns) are checked against the model\'s effective context window. If the input exceeds the window, the model is skipped and the next fallback is tried. If all models are exhausted, a warning is shown. Strictly opt-in \u2014 with default caps (40K\u201380K) and typical models (128K+), the check is a no-op unless a `contextWindow` override is explicitly set. ([#14](https://github.com/k0valik/pi-blackhole/pull/14))\n- **8 tests** covering context window parsing from config, priority resolution, rejection of invalid values, and `effectiveContextWindow` logic. ([#14](https://github.com/k0valik/pi-blackhole/pull/14))\n\n### Changed\n\n- **`memory: false` no longer blocks auto-compaction.** Memory and compaction are now truly independent \u2014 `memory: false` stops OM workers but compaction still runs. Use `compaction: "manual"` or `compaction: "off"` to control compaction separately. ([#14](https://github.com/k0valik/pi-blackhole/pull/14))\n- **`compaction: "off"` semantics refined:** blocks blackhole\'s auto-trigger and returns early from the before-compact hook for auto-triggered compactions (letting Pi handle them), but explicit `/blackhole` still uses blackhole\'s pipeline. ([#14](https://github.com/k0valik/pi-blackhole/pull/14))\n- **Config migration is automatic:** old keys (`overrideDefaultCompaction`, `noAutoCompact`, `passive`) are migrated to new keys in memory at load time. The on-disk file is never mutated. New keys take priority when present. ([#14](https://github.com/k0valik/pi-blackhole/pull/14))\n- **Permutation tests updated** to reflect the new behavior: `overrideDefaultCompaction` now gates the legacy trigger path, `memory` no longer gates the trigger, and the 16-permutation matrix uses the correct formula. ([#14](https://github.com/k0valik/pi-blackhole/pull/14))\n\n### Fixed\n\n- **Save error handling:** `save()` returns boolean and wraps writes in try/catch \u2014 read-only filesystems (e.g., Nix-managed config) no longer crash with an unhandled exception. ([#14](https://github.com/k0valik/pi-blackhole/pull/14))\n- **Number input restriction:** configure overlay now only accepts digits for number fields, preventing garbage values from being entered. ([#14](https://github.com/k0valik/pi-blackhole/pull/14))\n- **Defensive bounds:** section header pads in configure-overlay and status-overlay use `Math.max(0, ...)` / `Math.max(2, ...)` to prevent negative `.repeat()` counts on tiny terminals. ([#14](https://github.com/k0valik/pi-blackhole/pull/14))\n- **Config save failure warning:** `/blackhole configure` now shows a "warning" notification when the config file can\'t be written instead of a misleading "info" notification. ([#14](https://github.com/k0valik/pi-blackhole/pull/14))\n- **Legacy config tests:** updated `config.test.ts` to check new config keys (`compaction`, `compactionEngine`, `memory`) instead of deleted legacy fields (`passive`, `overrideDefaultCompaction`), fixing 10 pre-existing test failures. ([#14](https://github.com/k0valik/pi-blackhole/pull/14))\n- **pi-default non-message firstKeptEntryId resolution:** when Pi\'s `firstKeptEntryId` points to a non-message entry (e.g., OM metadata or compaction), `buildOwnCut` now resolves to the next actual message entry instead of falling through to the minimal cut. ([#15](https://github.com/k0valik/pi-blackhole/pull/15))\n- **Array micro-optimization in buildOwnCut:** replaced `branchEntries.slice(cutInBranch + 1).find()` with `branchEntries.find()` using an index check, avoiding a temporary array allocation. ([#15](https://github.com/k0valik/pi-blackhole/pull/15))\n\n## [0.3.2] - 2026-06-01\n\n### Fixed\n\n- **Auto-compaction gating:** added explicit guard at the top of the compaction trigger that returns early when `overrideDefaultCompaction` is `false` (the default). Previously, blackhole would still evaluate token thresholds and call Pi\'s default compaction hook even when not opted in \u2014 causing confusing log entries and unnecessary evaluations. Now blackhole stays completely out of Pi\'s compaction unless the user explicitly opts in. ([#13](https://github.com/k0valik/pi-blackhole/pull/13))\n\n### Added\n\n- **README top banner:** prominent NOTE at the top instructing users to set `"overrideDefaultCompaction": true` for blackhole to handle compaction automatically. Existing config matrix in the IMPORTANT section retained for reference.\n\n## [0.3.1] - 2026-05-31\n\n### Fixed\n\n- **Auto-compaction idle detection timing:** changed compaction scheduling from `queueMicrotask` to `setTimeout(..., 0)`. The microtask fired before Pi completed its post-response processing cycle, causing `ctx.isIdle()` to always return `false` and compaction to be deferred indefinitely. `setTimeout` yields to the event loop, allowing Pi to mark itself idle before the callback runs. ([#13](https://github.com/k0valik/pi-blackhole/pull/13))\n\n### Added\n\n- **Debug logging for compaction pipeline:** structured `debugLog` instrumentation at every decision point \u2014 guard checks, token threshold evaluation, branch entry inspection, session identity validation, idle check, and compaction completion/error. Opt-in via `"debugLog": true` in config, zero overhead otherwise. ([#13](https://github.com/k0valik/pi-blackhole/pull/13))\n- **Permutation test suite:** 36 new tests covering all 16 configuration knob combinations for auto-compaction trigger behavior. ([#13](https://github.com/k0valik/pi-blackhole/pull/13))\n\n## [0.2.4] - 2026-05-29\n\n### Recall: progressive discovery\n\n- **Touched mode (`mode:touched`):** aggregate view of all files written/edited across the session, grouped by path with entry indices. Accessible via `recall` tool and `/blackhole-recall` command. ([#12](https://github.com/k0valik/pi-blackhole/pull/12))\n- **Drill-down (`#N:path`):** read file content from tool call arguments in any transcript entry. Supports `#42:auth.ts` (preview first 30 lines), `#42:auth.ts:full` (all lines), `#42:auth.ts:offset:limit` (paged). Path auto-selects when unique; ambiguous paths list options. ([#12](https://github.com/k0valik/pi-blackhole/pull/12))\n- **Search mode filtering (`mode:file`, `mode:transcript`, `mode:hybrid`):** `mode:file` searches only write/edit file content; `mode:transcript` searches only conversation text; `mode:hybrid` (default) searches both. ([#12](https://github.com/k0valik/pi-blackhole/pull/12))\n- **Merged expand + search:** `#N` expand entries are now merged into search results (rather than being mutually exclusive), with proper pagination and sorting. ([#12](https://github.com/k0valik/pi-blackhole/pull/12))\n- **`scope` parameter as `StringEnum`:** tool schema now uses `StringEnum` (strict literal union) instead of `Type.Union` for `scope` and `mode` parameters. ([#12](https://github.com/k0valik/pi-blackhole/pull/12))\n\n### Fixed\n\n- **Null-safe entry IDs in `load-messages.ts`:** gracefully handles entries with `null` IDs instead of crashing with `String(null)` \u2192 `"null"`. ([#12](https://github.com/k0valik/pi-blackhole/pull/12))\n- **`formatRecallOutput` preserves legacy `files:[...]` format:** the expand-only path (no query) was silently dropping file info from entries that have the `files` field but no `fileMatches` \u2014 now falls back to the old `files:[path1, path2]` suffix. ([#12](https://github.com/k0valik/pi-blackhole/pull/12))\n\n### Crash protection \u2014 jiti bridge, EACCES guards, config safety\n\n- **Jiti bridge for custom providers:** `index.ts` now wraps `pi.registerProvider` to capture `streamSimple` functions into a `Symbol.for()` global, and scans `modelRegistry.registeredProviders` once on `agent_start`. This prevents crashes when consolidation agents (loaded via jiti with `moduleCache: false`) resolve a custom provider like `claude-bridge` \u2014 previously the jiti-loaded pi-ai instance had an empty `apiProviderRegistry` and threw `"No API provider registered"`. ([#11](https://github.com/k0valik/pi-blackhole/pull/11))\n- **Lazy bridge evaluation:** the bridge stream function now checks the provider map at call time instead of at import time, fixing an IIFE race condition where the bridge was permanently disabled because provider registration hadn\'t happened yet at module load. ([#11](https://github.com/k0valik/pi-blackhole/pull/11))\n- **Always-run fallback scan:** replaced `providerStreams.size > 0` guard with a dedicated `hasScannedFallback` flag \u2014 the fallback scan now always runs once regardless of how many providers the wrapper already captured, handling extensions that register before blackhole loads. ([#11](https://github.com/k0valik/pi-blackhole/pull/11))\n- **EACCES guards:** `writeCooldownMap()` and `writeSessionState()` now wrapped in try/catch. Prevents process crash on read-only filesystems (e.g., Nix-managed config). Cooldown loss is advisory (slightly more API traffic); pending state loss is safe (idempotent re-processing). ([#11](https://github.com/k0valik/pi-blackhole/pull/11))\n- **Numeric config validation:** all numeric fields are validated at load \u2014 NaN, infinity, and negative values are reset to defaults. Prevents silent math errors in pipeline logic. ([#11](https://github.com/k0valik/pi-blackhole/pull/11))\n- **`observerPreambleMaxTokens=0` explicitly allowed** in numeric validation (means "auto-compute"). ([#11](https://github.com/k0valik/pi-blackhole/pull/11))\n- **Better error messages for config save failures:** `/blackhole om-on` / `om-off` now use `"warning"`-level notification with an explanation about read-only filesystems when the config save fails, instead of a misleading `"info"`-level "Failed to save config.". ([#11](https://github.com/k0valik/pi-blackhole/pull/11))\n\n## [0.2.3] - 2026-05-27\n\n### Lockstep sync \u2014 2026-05-27\n\n- Ported upstream OM prompt refinements: coverage tiers in dropper prompt, "highest-resistance" critical framing in observer, coverage stewardship in reflector (#safe)\n- Ported upstream debug logging: `dropper.agent_start`, `dropper.tool_call`, `dropper.result` with full coverage/relevance diagnostics (#d6b02c0)\n- Ported upstream coverage-aware pruning: new `coverage.ts` module, drop candidate sort by coverage\u2192relevance\u2192age, critical observations no longer hard-rejected (#e00363a)\n- Adapted config: added `observationsPoolTargetTokens` as forward-compat no-op (upstream 52b5844 budgetTokens\u2192targetTokens rename)\n- Skipped upstream pool refactor (bf79ff7) and rename (52b5844): kept our ratio-based urgency algorithm\n- Recovered output cap from feat/compaction-output-cap: `buildCompactionProjection` now caps rendered observations to `observationsPoolMaxTokens` budget via relevance+recency scoring\n\n## [0.2.2] - 2026-05-26\n\n### Added\n\n- `/blackhole-memory` pipeline display reworked: renamed "Coverage" to "Pipeline", replaced percentage-based metrics with `X tokens (triggers at Y)` format to eliminate false-alarm 100% readings, added `[auto-disabled]` annotation for compaction in noAutoCompact mode, and show preamble cap in Pending section ([#7](https://github.com/k0valik/pi-blackhole/pull/7))\n- Default `observeAfterTokens` increased from 10,000 to 15,000 and `reflectAfterTokens` from 20,000 to 25,000 for better cost-efficiency on mid/high context sessions ([#7](https://github.com/k0valik/pi-blackhole/pull/7))\n- Observer preamble cap in noAutoCompact mode: the observer stage\'s `CURRENT OBSERVATIONS` preamble is now capped to prevent unbounded prompt growth from accumulated observation batches. High-relevance observations are always kept; medium and low observations are scored by relevance tier and relative recency (array position, not wall-clock time), with the best-scoring kept within the token budget. Reflections are never trimmed. The cap is governed by the new `observerPreambleMaxTokens` config setting (default `0` = auto-compute 30% of `observerChunkMaxTokens`). Only applies in `noAutoCompact` mode \u2014 the auto-compact path is unchanged. ([#7](https://github.com/k0valik/pi-blackhole/pull/7))\n- Accumulated batch history for noAutoCompact mode: the observer, reflector, and dropper stages now feed accumulated pending.json batches (observationBatches/reflectionBatches) to the LLM instead of reading from the (empty) branch. This restores the same historical context the pipeline receives in autoCompact mode \u2014 prior observations/reflections, existing summaries \u2014 but without writing markers to the visible branch. Each pipeline run appends its output batch to the pending store; on /blackhole flush, all accumulated batches are written as separate branch markers, preserving per-run coverage. ([#7](https://github.com/k0valik/pi-blackhole/pull/7))\n- Accumulated dropper batches (`droppedBatches`) in pending.json so that earlier dropper runs are not lost when a subsequent cycle overwrites `pending.dropped` before a /blackhole flush. The flush now writes all accumulated dropper batches to the branch, preventing observations dropped in earlier cycles from being "un-dropped" on compaction. ([#7](https://github.com/k0valik/pi-blackhole/pull/7))\n\n### Fixed\n\n- Reflector and dropper now read from `pending.json` in `noAutoCompact` mode instead of scanning the branch for observation markers that are never written there. Previously the early-exit gates in both stages returned immediately because `latestCoverageMarkerId(entries, OM_OBSERVATIONS_RECORDED)` found nothing in the branch (observations are saved to pending only). This caused the reflector and dropper to skip entirely, leaving the pipeline half-functional \u2014 no reflections were ever generated, the dropper never pruned, and the display showed misleading pool values. The fix adds `noAutoCompact`-aware early-exit gates that check `pending.observation`, `pending.reflection`, and `pending.dropped` state, using their `coversUpToId` values to calculate token gaps and gate correctly on `reflectAfterTokens`. Observations and reflections are fed from pending data instead of the empty branch. The notification token-adjustment logic (which already existed for all three stages) is now effective because the stages actually run. ([#6](https://github.com/k0valik/pi-blackhole/pull/6))\n\n## [0.2.1] - 2026-05-24\n\n### Fixed\n\n- Prevent repeated `Intl.Segmenter` constructor fallback retries on unsupported runtimes ([#5](https://github.com/k0valik/pi-blackhole/pull/5))\n- `/blackhole-memory` accumulated token counts now factor in pending `coversUpToId` as virtual coverage markers in `noAutoCompact` mode ([#5](https://github.com/k0valik/pi-blackhole/pull/5))\n- Pipeline notifications (observer/reflector/dropper) show accurate accumulated values accounting for pending coverage ([#5](https://github.com/k0valik/pi-blackhole/pull/5))\n- `stageThinkingLevel()` resolves per-model thinking config instead of using the primary stage model\'s setting for all fallback attempts ([#5](https://github.com/k0valik/pi-blackhole/pull/5))\n- Move `@earendil-works/*` packages to `peerDependencies` (provided by pi host at runtime), `typebox` to `devDependencies` (import type only) ([#5](https://github.com/k0valik/pi-blackhole/pull/5))\n- Dead code removal: deleted `src/om/compaction-hook.ts` and `src/core/report.ts` ([#5](https://github.com/k0valik/pi-blackhole/pull/5))\n- Module-level state leak: compaction stats moved to `Runtime` instance for session isolation ([#5](https://github.com/k0valik/pi-blackhole/pull/5))\n- Unified config loading: removed dual `loadSettings` path, `ensureConfig` called at handler start ([#5](https://github.com/k0valik/pi-blackhole/pull/5))\n- Stale context in deferred compaction: replaced `setTimeout(..., 0)` with `queueMicrotask` and session ID validation ([#5](https://github.com/k0valik/pi-blackhole/pull/5))\n- Silent JSON parse failures in `load-messages.ts` \u2014 now logged ([#5](https://github.com/k0valik/pi-blackhole/pull/5))\n- Silent `scaffoldConfig` errors \u2014 now logged ([#5](https://github.com/k0valik/pi-blackhole/pull/5))\n- `visibleProjection` falls through to `fullProjection` when no compaction has run ([#5](https://github.com/k0valik/pi-blackhole/pull/5))\n- `renderMessage` calls in `report.ts` and test types missing required `Message` properties ([#5](https://github.com/k0valik/pi-blackhole/pull/5))\n- CI publish workflow uses `npm` instead of `pnpm` (not available in runner) ([#5](https://github.com/k0valik/pi-blackhole/pull/5))\n- Added `typescript` devDependency for CI `tsc` check ([#5](https://github.com/k0valik/pi-blackhole/pull/5))\n\n### Changed\n\n- Improved model fallback: `resolveModel` iterates fallback chain (stage \u2192 fallbacks \u2192 base \u2192 session), records per-model cooldown on retryable errors ([#5](https://github.com/k0valik/pi-blackhole/pull/5))\n\n### Added\n\n- Bi-directional recall coupling: `#N` transcript expansion shows related OM observations/reflections; OM hex-id recall shows `#N` entry index annotations ([#5](https://github.com/k0valik/pi-blackhole/pull/5))\n- `id` field on `RenderedEntry` for cross-referencing with session entries ([#5](https://github.com/k0valik/pi-blackhole/pull/5))\n\n## [0.2.0] - 2026-05-24\n\n### Added\n\n- Initial release: unified compaction (pi-vcc) + observational memory (pi-observational-memory)\n- `/blackhole` command for manual compaction with OM content injection\n- `/blackhole-memory` command for pipeline status display\n- `/blackhole-recall` command for unified recall (transcript + OM)\n- Three-stage consolidation pipeline: observer \u2192 reflector \u2192 dropper with fallback retry\n- Per-session pending file isolation\n- Model cooldown persistence across restarts\n- CI/CD publish workflow for npm\n';Eo=45;});var To={};Fe(To,{GLOBAL_CONFIG_DIR:()=>wi,config:()=>Lc,openBlackholeSettings:()=>zg});async function zg(e){await Lc.openSettings(e,e.cwd,t=>{},wi,void 0,[{id:"changelog",label:"Display Changelog",available:true}],async t=>{t==="changelog"&&await vi(e);});}var Kg,wi,Lc,Mo=P(()=>{Ec();Nn();pr();lt();ki();Kg="pi-blackhole-config.json",wi=join(sr(),"pi-blackhole"),Lc=new Io({id:"pi-blackhole",label:"pi-blackhole",filename:Kg,configDir:wi,defaults:xe,scopes:{global:true,project:true,session:true},sessionConfig:{entryType:"session-config-pi-blackhole"},fields:e=>[{key:"compaction",type:"enum",label:"Compaction mode",description:"auto=trigger on threshold, manual=only /blackhole, off=auto:Pi handles, /blackhole:blackhole pipeline",value:e.compaction,options:["auto","manual","off"],optionLabels:{auto:"auto \u2014 trigger on threshold",manual:"manual \u2014 only /blackhole",off:"off \u2014 auto:Pi handles, /blackhole:blackhole pipeline"}},{key:"compactionEngine",type:"enum",label:"Compaction engine",description:"blackhole=structured summary+OM, pi-default=built-in Pi summarization",value:e.compactionEngine,options:["blackhole","pi-default"],optionLabels:{blackhole:"blackhole \u2014 structured summary + OM","pi-default":"pi-default \u2014 built-in Pi summarization"}},{key:"compactionSummaryMode",type:"enum",label:"Summary history",description:"default=replace one complete summary, append=freeze automatic segments and rebase on /blackhole",value:e.compactionSummaryMode,options:["default","append"],optionLabels:{default:"default \u2014 one complete replacement summary",append:"append \u2014 immutable auto segments; /blackhole rebases"}},{key:"tailBehavior",type:"enum",label:"Visible tail",description:"minimal=keep last user message only (default), pi-default=keep Pi's preserved visible context",value:e.tailBehavior,options:["minimal","pi-default"],optionLabels:{minimal:"minimal \u2014 keep last user message only (default)","pi-default":"pi-default \u2014 keep Pi's preserved visible context"}},{key:"midRunCompaction",type:"enum",label:"Mid-run compaction",description:"resume=compact transparently and continue the same run, pause=interrupt and stop, off=only check when run ends (default)",value:e.midRunCompaction,options:["resume","pause","off"],optionLabels:{resume:"resume \u2014 transparent compact, same run (experimental)",pause:"pause \u2014 interrupt, compact, and stop",off:"off \u2014 only check when run ends (default)"}},{key:"compactAfterTokens",type:"number",label:"Auto-compact threshold",description:"Token count that triggers auto-compaction when reached",value:e.compactAfterTokens,min:1e3,max:5e5,step:1e3},{key:"retainedToolOutputMaxTokens",type:"number",label:"Retained tool outputs",description:"Token budget for historical tool-output text; newest is retained first and older text remains available via recall",value:e.retainedToolOutputMaxTokens,min:1e3,max:2e5,step:1e3},{key:"memory",type:"boolean",label:"Observational memory",description:"Enable OM workers (observer, reflector, dropper) and content injection",value:e.memory,valueDescriptions:{on:"Active \u2014 OM workers + content injection enabled",off:"Suspended \u2014 OM disabled"}},{key:"sessionFallback",type:"boolean",label:"Session model fallback",description:"off=skip stage when all OM models fail, instead of falling back to the main coding model",value:e.sessionFallback??true},{key:"observeAfterTokens",type:"number",label:"Observer threshold",description:"Tokens accumulated since last observer run before triggering next observe",value:e.observeAfterTokens,min:1e3,max:2e5,step:1e3},{key:"reflectAfterTokens",type:"number",label:"Reflect + dropper threshold",description:"Tokens accumulated since last reflect before triggering reflector and dropper",value:e.reflectAfterTokens,min:1e3,max:2e5,step:1e3},{key:"observationsPoolMaxTokens",type:"number",label:"Observation pool max",description:"Max tokens in observation pool before dropper prunes (fold pressure)",value:e.observationsPoolMaxTokens,min:1e3,max:2e5,step:1e3},{key:"observationsPoolTargetTokens",type:"number",label:"Observation pool target",description:"Target tokens after dropper prunes (defaults to half of pool max)",value:e.observationsPoolTargetTokens,min:500,max:2e5,step:500},{key:"reflectorInputMaxTokens",type:"number",label:"Reflector input max",description:"Max prompt tokens for reflector model input (rolling window cap)",value:e.reflectorInputMaxTokens,min:1e3,max:5e5,step:1e3},{key:"dropperInputMaxTokens",type:"number",label:"Dropper input max",description:"Max prompt tokens for dropper model input (rolling window cap)",value:e.dropperInputMaxTokens,min:1e3,max:5e5,step:1e3},{key:"observerChunkMaxTokens",type:"number",label:"Observer chunk max",description:"Max source entry tokens sent to observer per chunk",value:e.observerChunkMaxTokens,min:1e3,max:2e5,step:1e3},{key:"observerPreambleMaxTokens",type:"number",label:"Observer preamble max",description:"Preamble budget in manual compaction mode (0=auto-compute 30% of chunk)",value:e.observerPreambleMaxTokens,min:0,max:1e5,step:500},{key:"dropperPressureThreshold",type:"number",label:"Dropper pressure threshold",description:"Fraction of reflectorInputMaxTokens that triggers pressure-driven dropper (0-1, default 0.70)",value:e.dropperPressureThreshold,min:.01,max:1,step:.01},{key:"dropperPoolFullnessThreshold",type:"number",label:"Dropper pool fullness threshold",description:"Min observation-pool fullness (fraction of pool max) before the dropper runs (0-1, default 0.10)",value:e.dropperPoolFullnessThreshold,min:.01,max:1,step:.01},{key:"agentMaxTurns",type:"number",label:"Max turns per agent",description:"Shared turn cap for background memory agents",value:e.agentMaxTurns,min:1,max:100,step:1},{key:"providerIdleTimeoutMs",type:"number",label:"Provider idle timeout (ms)",description:"Body-idle timeout for background provider streams; 0 = disabled, unset = inherit pi's default",value:e.providerIdleTimeoutMs??0,min:0,max:36e5,step:1e3},{key:"fullFoldAlways",type:"boolean",label:"Preserve OM on first compaction",description:"When true, early reflections/drops survive the first compaction in a fresh session",value:e.fullFoldAlways},{key:"debug",type:"boolean",label:"Debug snapshots",description:"Write detailed debug snapshots to /tmp/pi-blackhole-debug.json",value:e.debug},{key:"debugLog",type:"boolean",label:"Debug JSONL logging",description:"Write structured JSONL debug logs to agent directory",value:e.debugLog}],validate:e=>{let t={...e};t.compaction===void 0&&t.compactionEngine===void 0&&(t.passive===true?(t.compaction="off",t.memory=false):t.noAutoCompact===true&&(t.compaction="manual"),t.overrideDefaultCompaction===true?(t.compactionEngine="blackhole",t.tailBehavior===void 0&&(t.tailBehavior="minimal")):t.overrideDefaultCompaction===false&&(t.compactionEngine="pi-default"),delete t.passive,delete t.noAutoCompact,delete t.overrideDefaultCompaction);let n=process.env.PI_BLACKHOLE_PASSIVE??process.env.PI_VCC_OM_PASSIVE??process.env.PI_OBSERVATIONAL_MEMORY_PASSIVE;if(n!==void 0){let u=n.trim().toLowerCase();["1","true","yes","on"].includes(u)?(t.compaction="off",t.memory=false):["0","false","no","off"].includes(u)&&e.passive===true&&(delete t.compaction,delete t.memory);}let o=process.env.PI_BLACKHOLE_COMPACTION;if(o!==void 0){let u=o.trim().toLowerCase();["auto","manual","off"].includes(u)||console.warn(`blackhole: invalid PI_BLACKHOLE_COMPACTION value "${o}"; ignoring`);}let r=process.env.PI_BLACKHOLE_COMPACTION_ENGINE;if(r!==void 0){let u=r.trim().toLowerCase();["blackhole","pi-default"].includes(u)||console.warn(`blackhole: invalid PI_BLACKHOLE_COMPACTION_ENGINE value "${r}"; ignoring`);}let i=process.env.PI_BLACKHOLE_COMPACTION_SUMMARY_MODE;if(i!==void 0){let u=i.trim().toLowerCase();["default","append"].includes(u)||console.warn(`blackhole: invalid PI_BLACKHOLE_COMPACTION_SUMMARY_MODE value "${i}"; ignoring`);}let s=process.env.PI_BLACKHOLE_MID_RUN_COMPACTION;if(s!==void 0){let u=s.trim().toLowerCase();["resume","pause","off"].includes(u)||console.warn(`blackhole: invalid PI_BLACKHOLE_MID_RUN_COMPACTION value "${s}"; ignoring`);}let a={...xe,...t},l=["observeAfterTokens","reflectAfterTokens","compactAfterTokens","retainedToolOutputMaxTokens","observationsPoolMaxTokens","observationsPoolTargetTokens","reflectorInputMaxTokens","dropperInputMaxTokens","observerChunkMaxTokens","observerPreambleMaxTokens","agentMaxTurns"];for(let u of l){let p=a[u],m=u==="observerPreambleMaxTokens"?0:1;(typeof p!="number"||!Number.isFinite(p)||u==="retainedToolOutputMaxTokens"&&!Number.isInteger(p)||p<m)&&(a[u]=xe[u]);}let d=a.dropperPressureThreshold;(typeof d!="number"||!Number.isFinite(d)||d<=0||d>1)&&(a.dropperPressureThreshold=xe.dropperPressureThreshold);let c=a.dropperPoolFullnessThreshold;return (typeof c!="number"||!Number.isFinite(c)||c<=0||c>1)&&(a.dropperPoolFullnessThreshold=xe.dropperPoolFullnessThreshold),(a.observationsPoolTargetTokens===void 0||a.observationsPoolTargetTokens>=a.observationsPoolMaxTokens)&&(a.observationsPoolTargetTokens=Math.floor(a.observationsPoolMaxTokens/2)),a},env:zn});});var Dc=P(()=>{});function Hg(e){return e.endsWith(Ao)?e.slice(0,-Ao.length)||null:e.endsWith(xi)&&e.slice(0,-xi.length)||null}function qg(e=getAgentDir()){let t=join(e,Vc);if(!existsSync(t))return [];let n=[],o;try{o=readdirSync(t);}catch{return []}for(let r of o){let i=Hg(r);if(!i)continue;let s=join(t,r),a;try{a=statSync(s);}catch{continue}n.push({sessionId:i,filename:r,path:s,isStale:r.endsWith(Ao),sizeBytes:a.size,mtimeMs:a.mtimeMs});}return n.sort((r,i)=>i.mtimeMs-r.mtimeMs),n}function Gg(e){let t=new Set;if(!existsSync(e))return t;let n=[e];for(;n.length>0;){let o=n.pop(),r;try{r=readdirSync(o);}catch{continue}for(let i of r){let s=join(o,i),a;try{a=statSync(s);}catch{continue}if(a.isDirectory())n.push(s);else if(a.isFile()&&i.endsWith(".jsonl"))try{let l=readFileSync(s,"utf-8"),d=l.indexOf(`
|
|
24
24
|
`),c=d>=0?l.slice(0,d):l,u=JSON.parse(c);u.type==="session"&&typeof u.id=="string"&&u.id.length>0&&t.add(u.id);}catch{}}}return t}function Yg(){try{let e=join(getAgentDir(),"settings.json");if(!existsSync(e))return;let n=JSON.parse(readFileSync(e,"utf-8")).sessionDir;if(typeof n=="string"&&n.trim().length>0){let o=n.startsWith("~")?join(process.env.HOME??"/home",n.slice(2)):n;return resolve(o)}}catch{}}function Jg(){return join(getAgentDir(),"sessions")}function Xg(){let e=[],t=Jg();e.push(t);let n=Yg();return n&&n!==t&&existsSync(n)&&e.push(n),e}function Zg(e){let t=Xg(),n=new Set;for(let o of t){let r=Gg(o);for(let i of r)n.add(i);}return n}function Qg(e,t){let n=[],o=[];for(let r of e)t.has(r.sessionId)?o.push(r):n.push(r);return {all:e,orphaned:n,active:o}}function Kc(e,t){let n=qg(e);if(n.length===0)return {all:[],orphaned:[],active:[]};let o=Zg();return Qg(n,o)}function th(e,t){if(!e||typeof e!="string")return {ok:false};if(!eh.test(e))return {ok:false};let n=resolve(t),o=join(n,`${e}${xi}`),r=join(n,`${e}${Ao}`),i=resolve(o),s=resolve(r);return i.startsWith(n+sep)?s.startsWith(n+sep)?{ok:true,pendingPath:i,stalePath:s}:{ok:false}:{ok:false}}function Ci(e,t){let n=join(getAgentDir(),Vc),o=th(e,n);if(!o.ok)return false;let{pendingPath:r,stalePath:i}=o,s=false;try{existsSync(r)&&(unlinkSync(r),s=!0);}catch{}try{existsSync(i)&&(unlinkSync(i),s=!0);}catch{}return s}function zc(e,t){let n=0;for(let o of e)Ci(o.sessionId)&&n++;return n}function nh(e){return e<1024?`${e} B`:e<1024*1024?`${(e/1024).toFixed(1)} KB`:`${(e/(1024*1024)).toFixed(1)} MB`}function oh(e,t=Date.now()){let n=t-e,o=Math.floor(n/1e3);if(o<60)return `${o}s ago`;let r=Math.floor(o/60);if(r<60)return `${r}m ago`;let i=Math.floor(r/60);if(i<24)return `${i}h ago`;let s=Math.floor(i/24);return s<365?`${s}d ago`:`${Math.floor(s/365)}y ago`}function Si(e,t){let n=e.isStale?"stale":"pending";return `${e.sessionId.slice(0,8)}\u2026 ${n} ${nh(e.sizeBytes)} ${oh(e.mtimeMs,t)}`}var Vc,xi,Ao,eh,Hc=P(()=>{Vc="pi-blackhole",xi="-pending.json",Ao="-pending.stale.json";eh=/^[a-zA-Z0-9][-a-zA-Z0-9]*$/;});var Gc={};Fe(Gc,{handleCleanup:()=>sh});function Vt(e,t){return t===0?0:Math.max(0,Math.min(e,t-1))}function qc(e,t,n,o,r){let i=Math.max(1,e-2);if(!o&&!r)return t(`\u250F${"\u2501".repeat(i)}\u2513`);let s=r?` ${r} `:"",a=Math.max(1,i-visibleWidth(s)-1),l=o?` ${o.slice(0,Math.max(1,a-2))}${o.length>a-2?"\u2026":""} `:"",d=Math.max(1,i-visibleWidth(l)-visibleWidth(s));return t(`\u250F${l}${"\u2501".repeat(d)}${r?n(s):""}\u2513`)}function ih(e,t,n){let o=0,r=0,i=false;function s(){let m=e.length;o=Vt(o,m);let g=Math.max(0,m-Qe);r=Math.max(0,Math.min(r,g)),o<r&&(r=o),o>=r+Qe&&m>0&&(r=o-Qe+1);}let a=m=>t.fg("border",m),l=m=>t.fg("dim",m),d=m=>t.fg("accent",m),c=m=>t.fg("error",m),u=Date.now();function p(m,g,f){let h=Si(m,u),b=g?d(" \u25B6"):" ",y=g?d(h):h,w=`${b} ${y}`;return ` ${w}${" ".repeat(Math.max(0,f-visibleWidth(w)))} `}return {invalidate(){},render(m){let g=Math.max(40,m),f=Math.max(1,g-2),b=Math.max(1,f-1*2),y=[];if(i){let v=`Delete ${e.length} orphaned file${e.length===1?"":"s"}?`;y.push(qc(g,a,l,"Cleanup Pending Files")),y.push(a(`\u2503${" ".repeat(f)}\u2503`)),y.push(a(`\u2503 ${c(v)}${" ".repeat(Math.max(0,b-visibleWidth(v)))} \u2503`)),y.push(a(`\u2503${" ".repeat(f)}\u2503`));let k="Enter confirm \xB7 Esc cancel";return y.push(a(`\u2503 ${l(k)}${" ".repeat(Math.max(0,b-visibleWidth(k)))} \u2503`)),y.push(a(`\u2517${"\u2501".repeat(f)}\u251B`)),y}s();let w=`${e.length} file${e.length===1?"":"s"}`;if(y.push(qc(g,a,l,"Orphaned Pending Files",w)),y.push(a(`\u2503${" ".repeat(f)}\u2503`)),e.length===0)y.push(a(`\u2503 ${l("No orphaned pending files found")}${" ".repeat(Math.max(0,b-29))} \u2503`));else {let v=e.slice(r,r+Qe);for(let k=0;k<v.length;k++){let S=r+k,R=v[k];if(!R)continue;let x=S===o,E=p(R,x,b);y.push(x?t.bg("selectedBg",a(`\u2503${E}\u2503`)):a(`\u2503${E}\u2503`));}}for(let v=Math.min(e.length,Qe);v<Qe;v++)y.push(a(`\u2503${" ".repeat(f)}\u2503`));if(y.push(a(`\u2503${" ".repeat(f)}\u2503`)),e.length>0){let v=e.reduce((x,E)=>x+E.sizeBytes,0),S=`Total: ${v>0?(v/1024).toFixed(1):"0.0"} KB`,R="\u2191\u2193 navigate Enter delete D delete all Esc cancel";y.push(a(`\u2503 ${l(S)}${" ".repeat(Math.max(0,b-visibleWidth(S)))} \u2503`)),y.push(a(`\u2503 ${l(R)}${" ".repeat(Math.max(0,b-visibleWidth(R)))} \u2503`));}else {let v="Esc close";y.push(a(`\u2503 ${l(v)}${" ".repeat(Math.max(0,b-visibleWidth(v)))} \u2503`));}return y.push(a(`\u2517${"\u2501".repeat(f)}\u251B`)),y},handleInput(m){let g=decodeKittyPrintable(m)??m;if(i){if(matchesKey(m,"enter")||matchesKey(m,"return")){n("deleteAll");return}if(matchesKey(m,"escape")){i=false;return}return}if(e.length===0){matchesKey(m,"escape")&&n("cancel");return}if(matchesKey(m,"up")){o=Vt(o-1,e.length),s();return}if(matchesKey(m,"down")){o=Vt(o+1,e.length),s();return}if(matchesKey(m,"pageUp")||g==="-"){o=Vt(o-Qe,e.length),s();return}if(matchesKey(m,"pageDown")||g==="="){o=Vt(o+Qe,e.length),s();return}if(matchesKey(m,"home")){o=0,r=0;return}if(matchesKey(m,"end")){o=Math.max(0,e.length-1),s();return}if(matchesKey(m,"enter")||matchesKey(m,"return")){let f=e[o];if(f){if(Ci(f.sessionId),e.splice(o,1),e.length===0){n("cancel");return}o=Vt(o,e.length),s();}return}if(g==="d"||g==="D"){i=true;return}if(matchesKey(m,"escape")){n("cancel");return}}}}async function sh(e){let{orphaned:t}=Kc();if(t.length===0){e.ui.notify("pi-blackhole: No orphaned pending files found.","info");return}if(e.mode==="rpc"||e.mode==="json"||e.mode==="print"){let i=t.reduce((a,l)=>a+l.sizeBytes,0),s=[`Orphaned pending files: ${t.length} (${(i/1024).toFixed(1)} KB)`,"",...t.map(a=>` ${Si(a)}`),"","Use /blackhole cleanup in TUI mode to delete these files."];e.ui.notify(s.join(`
|
|
25
25
|
`),"warning");return}let o=[...t];if(await e.ui.custom((i,s,a,l)=>ih(o,s,l),{overlay:true})==="deleteAll"){let i=zc(o),s=o.length;i===s?e.ui.notify(`pi-blackhole: Deleted ${s} orphaned pending file${s===1?"":"s"}.`,"info"):e.ui.notify(`pi-blackhole: Deleted ${i}/${s} orphaned pending file${s===1?"":"s"} (${s-i} failed).`,"warning");}else if(o.length>0&&o.length<t.length){let i=o.reduce((s,a)=>s+a.sizeBytes,0);e.ui.notify(`pi-blackhole: ${t.length-o.length} deleted, ${o.length} remain (${(i/1024).toFixed(1)} KB).`,"info");}else o.length===0&&t.length>0&&e.ui.notify(`pi-blackhole: All ${t.length} orphaned pending file${t.length===1?"":"s"} removed.`,"info");}var Qe,Yc=P(()=>{Dc();Hc();Qe=10;});var Id={};Fe(Id,{findGitRoot:()=>Nh});async function Nh(e){try{let{stdout:t}=await Dh("git",["rev-parse","--show-toplevel"],{cwd:e,timeout:5e3});return {root:t.trim()||null}}catch(t){let n=t;if(n.code==="ENOENT"||n.code==="EAGAIN"){let o=n.message??String(t);return {root:null,warning:`[pi-blackhole] git lookup failed for ${e}: ${o}; falling back to cwd-only scoping`}}return {root:null}}}var Dh,Ed=P(()=>{Dh=promisify(execFile);});var Pd={};Fe(Pd,{buildProjectMemoryCorpus:()=>Vh,buildProjectMemoryCorpusAsync:()=>Kh,encodeScopeDir:()=>Ve});function Ve(e){return `--${e.replace(/^[/\\]/,"").replace(/[/\\:]/g,"-")}--`}function Ad(e){return typeof e=="string"&&Mr.includes(e)?e:"low"}function Vo(e){if(typeof e!="string"||!e)return null;if(Number.isNaN(Date.parse(e))){let t=e.replace(" ","T")+":00Z";return Number.isNaN(Date.parse(t))?null:t}return e}function Ko(e){let t;try{t=openSync(e,"r");}catch{return null}try{let n=Buffer.alloc(Mi),o=0;for(;o<Mi;){let r=readSync(t,n,o,Math.min(Wh,Mi-o),o);if(r<=0)break;let s=n.subarray(o,o+r).indexOf(10);if(s!==-1)return n.subarray(0,o+s).toString("utf-8");o+=r;}return o>0?n.subarray(0,o).toString("utf-8"):null}catch{return null}finally{try{closeSync(t);}catch{}}}function zo(e){if(!e)return null;try{let t=JSON.parse(e);return t?.type!=="session"?null:{id:typeof t.id=="string"?t.id:"",cwd:typeof t.cwd=="string"?t.cwd:null}}catch{return null}}function _d(e,t,n,o,r){for(let i of e){if(i.type!=="custom")continue;let s=i.customType,a=i.data??{},l=Vo(i.timestamp);if(s===G||s===Td){let d=s===Td?a.records??[]:a.observations??[];for(let c of d)typeof c.content!="string"||!c.content.trim()||n.push({id:typeof c.id=="string"?c.id:null,content:c.content,relevance:Ad(c.relevance),timestamp:Vo(c.timestamp)??l,sessionId:t,source:"branch"});}else if(s===ee)for(let d of a.reflections??[])typeof d.content!="string"||!d.content.trim()||o.push({content:d.content,supportingObservationIds:Array.isArray(d.supportingObservationIds)?d.supportingObservationIds:[],timestamp:l,sessionId:t,source:"branch"});else if(s===le&&Array.isArray(a.observationIds))for(let d of a.observationIds)typeof d=="string"&&r.add(d);}}function Ho(e,t,n,o,r,i){let s=e.observationBatches&&e.observationBatches.length>0?e.observationBatches:e.observation?[e.observation]:[],a=e.reflectionBatches&&e.reflectionBatches.length>0?e.reflectionBatches:e.reflection?[e.reflection]:[],l=e.droppedBatches&&e.droppedBatches.length>0?e.droppedBatches:e.dropped?[e.dropped]:[],d=null;for(let u of s)for(let p of u.data?.observations??[]){let m=Vo(p.timestamp);if(m!=null){let g=Date.parse(m);!Number.isNaN(g)&&(d===null||g>d)&&(d=g);}}let c=d!=null?new Date(d).toISOString():null;for(let u of s)for(let p of u.data?.observations??[])typeof p.content!="string"||!p.content.trim()||o.push({id:typeof p.id=="string"?p.id:null,content:p.content,relevance:Ad(p.relevance),timestamp:Vo(p.timestamp),sessionId:t,source:n});for(let u of a)for(let p of u.data?.reflections??[])typeof p.content!="string"||!p.content.trim()||r.push({content:p.content,supportingObservationIds:Array.isArray(p.supportingObservationIds)?p.supportingObservationIds:[],timestamp:c,sessionId:t,source:n});for(let u of l)if(Array.isArray(u.data?.observationIds))for(let p of u.data?.observationIds)typeof p=="string"&&i.add(p);}function Vh(e){let t=e.agentDir??getAgentDir(),n=e.gitRoot||e.cwd,o=e.activeSessionFile&&existsSync(e.activeSessionFile)?e.activeSessionFile:void 0,r={projectRoot:n,sessionsConsidered:0,filesWithMarkers:0,observations:[],reflections:[],droppedIds:new Set,knownSessionIds:new Set,orphanedSessions:0},i=[...new Set([Ve(e.cwd),...e.gitRoot&&e.gitRoot!==e.cwd?[Ve(e.gitRoot)]:[]])].map(l=>join(t,"sessions",l)),s=new Set;for(let l of i){let d;try{if(!statSync(l).isDirectory())continue;d=readdirSync(l);}catch{continue}for(let c of d){if(!c.endsWith(".jsonl")||s.has(c))continue;s.add(c);let u=join(l,c);if(o&&u===o)continue;r.sessionsConsidered++;let m=zo(Ko(u))?.id||c.slice(0,-6);r.knownSessionIds.add(m);let g;try{g=readFileSync(u,"utf-8");}catch{continue}if(!g.includes("om."))continue;r.filesWithMarkers++;let f=[];for(let h of g.split(`
|
|
26
26
|
`)){let b=h.trim();if(b)try{f.push(JSON.parse(b));}catch{}}_d(f,m,r.observations,r.reflections,r.droppedIds);}}let a=join(t,Od);if(existsSync(a)){let l;try{l=readdirSync(a);}catch{l=[];}let d=new Set;for(let p of l)p.endsWith(ot)&&d.add(p.slice(0,-ot.length));let c=null,u=()=>{if(c)return c;c=new Set(r.knownSessionIds);let p=join(t,"sessions"),m;try{m=readdirSync(p);}catch{return c}for(let g of m){let f=join(p,g),h=null;try{h=statSync(f);}catch{continue}if(!h.isDirectory()||g===Ve(e.cwd)||e.gitRoot&&e.gitRoot!==e.cwd&&g===Ve(e.gitRoot))continue;let y;try{y=readdirSync(f);}catch{continue}for(let w of y){if(!w.endsWith(".jsonl"))continue;let k=zo(Ko(join(f,w)))?.id||(w.includes("_")?w.slice(w.lastIndexOf("_")+1,-6):w.slice(0,-6));k&&c.add(k);}}return c};for(let p of l){let m=p.endsWith(ot),g=p.endsWith(Wo);if(!m&&!g)continue;let f=m?p.slice(0,-ot.length):p.slice(0,-Wo.length);if(!f||g&&d.has(f))continue;let h;try{h=JSON.parse(readFileSync(join(a,p),"utf-8"));}catch{continue}if(Array.isArray(h.observationBatches)&&h.observationBatches.length>0||Array.isArray(h.reflectionBatches)&&h.reflectionBatches.length>0||Array.isArray(h.droppedBatches)&&h.droppedBatches.length>0||typeof h.observation=="object"&&h.observation!==null||typeof h.reflection=="object"&&h.reflection!==null||typeof h.dropped=="object"&&h.dropped!==null)if(r.knownSessionIds.has(f))Ho(h,f,"pending",r.observations,r.reflections,r.droppedIds);else {if(u().has(f))continue;r.orphanedSessions++,Ho(h,f,"orphan",r.observations,r.reflections,r.droppedIds);}}}return r}function No(){return new Promise(e=>setImmediate(e))}async function Kh(e,t){let n=e.agentDir??getAgentDir(),o=e.gitRoot||e.cwd,r=e.activeSessionFile&&existsSync(e.activeSessionFile)?e.activeSessionFile:void 0,i={projectRoot:o,sessionsConsidered:0,filesWithMarkers:0,observations:[],reflections:[],droppedIds:new Set,knownSessionIds:new Set,orphanedSessions:0},s=[...new Set([Ve(e.cwd),...e.gitRoot&&e.gitRoot!==e.cwd?[Ve(e.gitRoot)]:[]])].map(p=>join(n,"sessions",p)),a=[],l=new Set;for(let p of s){let m;try{if(!statSync(p).isDirectory())continue;m=readdirSync(p);}catch{continue}for(let g of m){if(!g.endsWith(".jsonl")||l.has(g))continue;l.add(g);let f=join(p,g);r&&f===r||a.push(f);}}let d=a.length;t&&d>0&&t({scanned:0,total:d,phase:"scanning"});let c=0;for(let p of a){c++;let m=p.slice(p.lastIndexOf("/")+1);i.sessionsConsidered++;let f=zo(Ko(p))?.id||m.slice(0,-6);i.knownSessionIds.add(f);let h;try{h=readFileSync(p,"utf-8");}catch{c%Oi===0&&await No(),t&&c%25===0&&t({scanned:c,total:d,phase:"scanning"});continue}if(!h.includes("om.")){c%Oi===0&&await No(),t&&c%25===0&&t({scanned:c,total:d,phase:"scanning"});continue}i.filesWithMarkers++;let b=[];for(let y of h.split(`
|
|
@@ -442,7 +442,7 @@ ${r.join(`
|
|
|
442
442
|
`),display:true});}});};Be();var zi=/overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|connection.?lost|websocket.?closed|websocket.?error|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|ended without|http2 request did not get a response|timed? out|timeout|terminated|retry delay/i;function Mn(e){let t=e instanceof Error?e.message:String(e||"");return zi.test(t)}function Se(e){let t;return e instanceof Error?t=e.message:e&&typeof e=="object"&&"message"in e?t=String(e.message):t=String(e||""),t.includes("extension ctx is stale")||t.includes("ctx is stale")}Lt();An();lt();qe();var os=8e3,rs=10;function Gb(e,t){return e.slice(t+1).filter(io)}function Yb(e,t){let n=0,o=[];for(let r=e.length-1;r>=0;r--){let i=e[r],s=0;if(i.type==="message"&&i.message){let l=i.message;if(typeof l.content=="string")s=l.content.length;else if(Array.isArray(l.content))for(let d of l.content)d.text&&(s+=d.text.length);}else i.type==="custom"&&(i.customType===G||i.customType===ee||i.customType===le)?s=String(JSON.stringify(i.data??{})).length:i.summary&&(s=String(i.summary).length);let a=Math.ceil(s/4);if(n+a>t&&o.length>0)break;if(n+a>t&&o.length===0){o.unshift(i);break}o.unshift(i),n+=a;}return o}function is(e,t,n){e.appendEntry(t,n);}function Jb(e,t){let n=new Set(e.map(r=>r.id)),o=[...e];for(let r of t)n.has(r.id)||(n.add(r.id),o.push(r));return o}function Du(e,t,n){let o=e.observationBatches??[];if(!n||z(t,n)<0)return o.flatMap(s=>s.data?.observations??[]);let r=z(t,n),i=[];for(let s of o){let a=z(t,s.coversUpToId);a>=0&&a>r&&i.push(...s.data?.observations??[]);}return i}function Xb(e,t,n){let o=t.config,r=t.cursors??{},i=(()=>{let l=r.observer;if(!l)return ut(e)>=o.observeAfterTokens;let d=z(e,l.entryId);return (d>=0?Q(e,d):ut(e))>=o.observeAfterTokens})(),s=(()=>{let l=r.reflector;if(!l)return At(e)>=o.reflectAfterTokens;let d=z(e,l.entryId);if(d<0)return At(e)>=o.reflectAfterTokens;if(Q(e,d)<o.reflectAfterTokens)return false;for(let u=d+1;u<e.length;u++){let p=e[u];if(p.type==="custom"&&p.customType===G){let m=p.data?.coversUpToId;if(m){let g=z(e,m);if(g>=0&&g<=d)continue}return true}}if(n){let u=n.observationBatches??[];for(let p of u)if(p.coversUpToId){let m=z(e,p.coversUpToId);if(m>=0&&m>d)return true}}return false})(),a=i||s?false:(()=>{let d=Pt(e).activeObservations.reduce((f,h)=>f+(h.tokenCount??0),0);if(n){let f=n.observationBatches??[];for(let h of f)d+=(h.data?.observations??[]).reduce((b,y)=>b+(y.tokenCount??0),0);}if((o.observationsPoolMaxTokens>0?d/o.observationsPoolMaxTokens:0)<(o.dropperPoolFullnessThreshold??.1))return false;if(d>=o.dropperPressureThreshold*o.reflectorInputMaxTokens)return true;let p=r.dropper;if(!p)return (n?(n.observationBatches?.length??0)>0||(n.reflectionBatches?.length??0)>0:false)?true:Ke(e)>=o.reflectAfterTokens;let m=z(e,p.entryId);if(m<0)return Ke(e)>=o.reflectAfterTokens;if(Q(e,m)<o.reflectAfterTokens)return false;for(let f=m+1;f<e.length;f++){let h=e[f];if(h.type==="custom"&&(h.customType===G||h.customType===ee)){let b=h.data?.coversUpToId;if(b){let y=z(e,b);if(y>=0&&y<=m)continue}return true}}if(n){let f=n.observationBatches??[],h=n.reflectionBatches??[];for(let b of [...f,...h])if(b.coversUpToId){let y=z(e,b.coversUpToId);if(y>=0&&y>m)return true}}return false})();return i||s||a}function rt(e,t){return t==="observer"?e.config.observerModel:t==="reflector"?e.config.reflectorModel:e.config.dropperModel}function yt(e,t){return t==="observer"?e.config.observerFallbackModels??[]:t==="reflector"?e.config.reflectorFallbackModels??[]:e.config.dropperFallbackModels??[]}function ss(e,t,n){return (n??rt(e,t))?.thinking??e.config.model?.thinking??"low"}function Zb(e,t){return async n=>{let o=yt(e,n),r=await e.resolveModel({model:t.model,modelRegistry:t.modelRegistry,hasUI:t.hasUI,ui:t.ui,stageModel:rt(e,n),stageFallbacks:o});if(r.ok)return e.resolveFailureNotified=false,r;if(D(`${n}.model_unavailable`,{reason:r.reason}),!e.resolveFailureNotified&&t.hasUI&&t.ui){if(e.failedInCycle.size>0&&r.reason.includes("all candidates exhausted")){let i=o.length===0?"no fallbacks configured":"no available fallbacks";e.tryEmitInfo(true,t.ui,`Observational memory: ${n} skipped \u2014 model unavailable (cooldown set to 0, ${i}, will retry next run)`);}else t.ui.notify(`Observational memory: ${n} skipped \u2014 ${r.reason}`,"warning");e.resolveFailureNotified=true;}}}function Nu(e,t){let n=(o,r)=>{ey(e,t,r);};e.on("agent_start",n),e.on("turn_end",n);}function Qb(e,t){let n=t.cursors??{};if(n.observer&&z(e,n.observer.entryId)<0){let o=dt(e,G);o?n.observer={entryId:o,state:"initial"}:delete n.observer;}if(n.reflector&&z(e,n.reflector.entryId)<0){let o=dt(e,ee);o?n.reflector={entryId:o,state:"initial"}:delete n.reflector;}if(n.dropper&&z(e,n.dropper.entryId)<0){let o=dt(e,le);o?n.dropper={entryId:o,state:"initial"}:delete n.dropper;}}function ey(e,t,n){if(t.ensureConfig(n.cwd,l=>n.ui?.notify?.(l,"warning")),t.config.memory===false||to(t.config,n.model)||t.config.compaction===void 0&&t.config.compactionEngine===void 0&&t.config.passive===true||t.consolidationInFlight||t.isConsolidationRetryGated())return;let o;try{o=n.sessionManager.getSessionId();}catch(l){if(Se(l))return;throw l}if(t.cursorsLoadedSessionId!==o){typeof t.loadCursorsFromPending=="function"&&t.loadCursorsFromPending(o);let l;try{l=n.sessionManager.getBranch();}catch(c){if(Se(c))return;throw c}Qb(l,t),t.cursorsLoadedSessionId=o;let d=t.cursors??{};D("cursor.loaded",{observer:d.observer??null,reflector:d.reflector??null,dropper:d.dropper??null});}let r;try{r=n.sessionManager.getBranch();}catch(l){if(Se(l))return;throw l}let i=Z(t.config)?ue(o):void 0;if(!Xb(r,t,i))return;let s=`consolidation-${Date.now().toString(36)}-${Math.random().toString(16).slice(2,8)}`,a={cwd:n.cwd,hasUI:n.hasUI,ui:n.ui,model:n.model,modelRegistry:n.modelRegistry,sessionManager:n.sessionManager};t.launchConsolidationTask(n,async()=>qa({enabled:t.config.debugLog===true,cwd:n.cwd,runId:s},async()=>{await ty(e,t,a);}));}async function ty(e,t,n){let o=Zb(t,n);t.consolidationPhase="observer",t.failedInCycle.clear(),t.resolveFailureNotified=false;try{if(await ny(e,t,n,o)==="abort")return}catch(a){D("observer.error",{errorMessage:t.recordConsolidationStageError(n,"observer",a)});return}t.consolidationPhase="reflector",t.failedInCycle.clear(),t.resolveFailureNotified=false;let r;try{if(r=await oy(e,t,n,o),r.outcome==="abort")return}catch(a){D("reflector.error",{errorMessage:t.recordConsolidationStageError(n,"reflector",a)});return}t.consolidationPhase="dropper",t.failedInCycle.clear(),t.resolveFailureNotified=false;try{await ry(e,t,n,o,r.sameRunReflections,r.effectiveReflectionCoverageId);}catch(a){D("dropper.error",{errorMessage:t.recordConsolidationStageError(n,"dropper",a)});}let i;try{i=n.sessionManager.getSessionId();}catch(a){if(Se(a)){D("pipeline.stale_ctx",{error:String(a)});return}throw a}t.scheduleCursorFlush(i);let s=t.cursors??{};D("cursor.saved",{observer:s.observer??null,reflector:s.reflector??null,dropper:s.dropper??null});}async function ny(e,t,n,o){let r,i;try{r=n.sessionManager.getBranch(),i=n.sessionManager.getSessionId();}catch(y){if(Se(y))return D("observer.stale_ctx",{error:String(y)}),"abort";throw y}let s=t.getCursor("observer"),a;if(s){let y=z(r,s.entryId);a=y>=0?y:Ot(r,G);}else {let y=Ot(r,G);a=y>=0?y:ao(r);}let l=a>=0?Q(r,a):0;if(l<t.config.observeAfterTokens){let y=[...r].reverse().find(w=>io(w))?.id;return y&&t.advanceCursor("observer",y,"not_due"),"continue"}let d=Gb(r,a),c=t.config.observerChunkMaxTokens;l>c&&(d=Yb(d,c));let u=d.at(-1)?.id;if(!u)return "continue";let{text:p,sourceEntryIds:m}=au(d);if(!p.trim()||m.length===0)return "continue";let g=Math.ceil(p.length/4),f=ft(r),h=f.reflections.map(Me),b=f.observations.map(Te);if(Z(t.config)){let y=ue(i),w=(y.reflectionBatches??[]).flatMap(R=>R.data.reflections??[]),v=(y.observationBatches??[]).flatMap(R=>R.data.observations??[]),k=t.config.observerPreambleMaxTokens>0?t.config.observerPreambleMaxTokens:Math.round(t.config.observerChunkMaxTokens*.3),S=[...f.observations,...v];b=co(S,k).map(Te),h=[...h,...w.map(Me)];}if(Z(t.config)&&wl(i,u))return D("observer.pending_skip",{coversUpToId:u,sessionId:i}),"continue";for(let y=0;y<rs;y++){let w=await o("observer");if(!w)return "abort";let v=l;if(Z(t.config)){let x=ue(i);if(x.observation?.coversUpToId){let E=z(r,x.observation.coversUpToId);E>=0&&(v=Q(r,E));}}t.tryEmitInfo(n.hasUI,n.ui,`Observational memory: observer running on ~${g.toLocaleString()}-token chunk (of ${v.toLocaleString()} accumulated)`),D("observer.start",{tokens:l,coversUpToId:u,sourceEntryIds:m,sourceEntryCount:m.length,priorReflections:h.length,priorObservations:b.length});let k=t.findCandidateConfig(w.model,{model:n.model,modelRegistry:n.modelRegistry,hasUI:n.hasUI,ui:n.ui,stageModel:rt(t,"observer"),stageFallbacks:yt(t,"observer")}),S=Ft(w.model,k),R=g+os;if(R>S){D("observer.context_window_exceeded",{estimatedInput:R,effectiveCtx:S,model:`${w.model.provider}/${w.model.id}`}),t.recordRetryableError(k,new Error(`context window ${S} too small for estimated input ${R}`),"observer"),t.tryEmitInfo(n.hasUI,n.ui,`Observational memory: observer skipping ${w.model.provider}/${w.model.id} (context window ${S.toLocaleString()} too small for ~${R.toLocaleString()}-token input)`);continue}try{let{runObserver:x}=await Promise.resolve().then(()=>(hu(),gu)),E=await x({model:w.model,apiKey:w.apiKey,headers:w.headers,env:w.env,priorReflections:h,priorObservations:b,chunk:p,allowedSourceEntryIds:m,maxTurns:t.config.agentMaxTurns,thinkingLevel:ss(t,"observer",k),providerIdleTimeoutMs:t.config.providerIdleTimeoutMs});if(E.observations&&E.observations.length>0){let M=$a(E.observations,u);return M?(D("observer.records",{count:E.observations.length,observationTokens:E.observations.reduce((T,A)=>T+A.tokenCount,0),coversUpToId:u}),Z(t.config)?(yl(i,{coversUpToId:u,data:M}),D("observer.pending",{count:E.observations.length,coversUpToId:u,sessionId:i})):(is(e,G,M),D("observer.appended",{count:E.observations.length,coversUpToId:u})),t.advanceCursor("observer",u,"recorded"),t.tryEmitInfo(n.hasUI,n.ui,`Observational memory: ${E.observations.length} observation${E.observations.length===1?"":"s"} recorded`),"continue"):(t.advanceCursor("observer",u,"empty"),"continue")}let _=E.emptyReason,F=_?_.kind==="tool_not_called"?"model did not call the observation tool":_.kind==="all_rejected"?`${_.count} observation(s) rejected for invalid sourceEntryIds`:_.kind==="all_duplicates"?`${_.count} observation(s) were duplicates of already-recorded entries`:_.kind==="empty_array"?"model called the tool but submitted an empty observations array":"nothing new to record":"unknown reason",C=_&&(_.kind==="no_new_content"||_.kind==="all_duplicates")?"info":"warning";return D("observer.empty",{coversUpToId:u,reason:_?.kind}),t.advanceCursor("observer",u,"empty"),C==="warning"?n.hasUI&&n.ui?.notify(`Observational memory: no observations \u2014 ${F}`,"warning"):t.tryEmitInfo(n.hasUI,n.ui,`Observational memory: no observations \u2014 ${F}`),"continue"}catch(x){if(Se(x))return D("observer.stale_ctx",{error:String(x)}),"abort";let E=t.findCandidateConfig(w.model,{model:n.model,modelRegistry:n.modelRegistry,hasUI:n.hasUI,ui:n.ui,stageModel:rt(t,"observer"),stageFallbacks:yt(t,"observer")});t.recordRetryableError(E,x,"observer"),D("observer.error",{error:String(x),retryable:Mn(x)});continue}}return t.recordConsolidationStageError(n,"observer",new Error("Observer: all model candidates exhausted")),"abort"}async function oy(e,t,n,o){let r,i;try{r=n.sessionManager.getBranch(),i=n.sessionManager.getSessionId();}catch(l){if(Se(l))return D("reflector.stale_ctx",{error:String(l)}),{outcome:"abort",sameRunReflections:[]};throw l}let s=0,a;if(Z(t.config)){let l=ue(i);if(!(l.observationBatches??[]).some(c=>c.data?.observations?.length))return t.advanceCursor("reflector",r.at(-1)?.id??"unknown","skipped"),{outcome:"continue",sameRunReflections:[]};if(a=l.observation?.coversUpToId,l.reflection?.coversUpToId){let c=z(r,l.observation?.coversUpToId??""),u=z(r,l.reflection.coversUpToId);if(c>=0&&u>=0&&c<=u)return t.advanceCursor("reflector",l.reflection.coversUpToId,"skipped"),{outcome:"continue",sameRunReflections:[]};if(u>=0){if(s=Q(r,u),s<t.config.reflectAfterTokens)return t.advanceCursor("reflector",l.reflection.coversUpToId,"not_due"),{outcome:"continue",sameRunReflections:[]}}else s=ut(r);}else s=ut(r);}else {if(s=At(r),s<t.config.reflectAfterTokens)return t.advanceCursor("reflector",r.at(-1)?.id??"unknown","not_due"),{outcome:"continue",sameRunReflections:[]};if(a=dt(r,G),!a)return t.advanceCursor("reflector",r.at(-1)?.id??"unknown","skipped"),{outcome:"continue",sameRunReflections:[]}}for(let l=0;l<rs;l++){let d=await o("reflector");if(!d)return {outcome:"abort",sameRunReflections:[]};let c=Pt(r),u=Z(t.config)?ue(i):void 0,p=u?-1:Ot(r,ee),m=u?Du(u,r,u.reflection?.coversUpToId):Fr(r,p),g=u?[]:Ba(r,p),f=Math.ceil((m.reduce((S,R)=>S+R.content.length,0)+g.reduce((S,R)=>S+R.content.length,0))/4),h=Math.floor(t.config.reflectorInputMaxTokens*.15)*2,b=Math.min(f+h,t.config.reflectorInputMaxTokens),y=s;if(Z(t.config)&&u?.reflection?.coversUpToId){let S=z(r,u.reflection.coversUpToId);S>=0&&(y=Q(r,S));}D("reflector.start",{tokens:y,inputTokens:b,newObsCount:m.length,newRefCount:g.length}),t.tryEmitInfo(n.hasUI,n.ui,`Observational memory: reflector running (~${y.toLocaleString()} tokens accumulated, ~${b.toLocaleString()}-token input)`);let w=t.findCandidateConfig(d.model,{model:n.model,modelRegistry:n.modelRegistry,hasUI:n.hasUI,ui:n.ui,stageModel:rt(t,"reflector"),stageFallbacks:yt(t,"reflector")}),v=Ft(d.model,w),k=b+os;if(k>v){D("reflector.context_window_exceeded",{estimatedInput:k,effectiveCtx:v,model:`${d.model.provider}/${d.model.id}`}),t.recordRetryableError(w,new Error(`context window ${v} too small for estimated input ${k}`),"reflector"),t.tryEmitInfo(n.hasUI,n.ui,`Observational memory: reflector skipping ${d.model.provider}/${d.model.id} (context window ${v.toLocaleString()} too small for ~${k.toLocaleString()}-token input)`);continue}try{let S=u?[...c.reflections,...(u.reflectionBatches??[]).flatMap(M=>M.data?.reflections??[])]:c.reflections,R=u?[...c.activeObservations,...(u.observationBatches??[]).flatMap(M=>M.data?.observations??[])]:c.activeObservations,x=ja(S,Math.floor(t.config.reflectorInputMaxTokens*.15)),E=Lr(R.filter(M=>!m.some(T=>T.id===M.id)),Math.floor(t.config.reflectorInputMaxTokens*.15)),{runReflector:_}=await Promise.resolve().then(()=>(xu(),wu)),F=await _({model:d.model,apiKey:d.apiKey,headers:d.headers,env:d.env,reflections:g,observations:m,existingReflectionsSummary:x||void 0,existingObservationsSummary:E||void 0,maxTurns:t.config.agentMaxTurns,thinkingLevel:ss(t,"reflector",w),providerIdleTimeoutMs:t.config.providerIdleTimeoutMs});if(!F||F.length===0)return t.advanceCursor("reflector",a??r.at(-1)?.id??"unknown","empty"),{outcome:"continue",sameRunReflections:[]};if(!a)return t.advanceCursor("reflector",r.at(-1)?.id??"unknown","empty"),{outcome:"continue",sameRunReflections:[]};let C=Fa(F,a);return C?(Z(t.config)?vl(i,{coversUpToId:C.coversUpToId,data:C}):is(e,ee,C),t.advanceCursor("reflector",C.coversUpToId,"recorded"),{outcome:"continue",sameRunReflections:F,effectiveReflectionCoverageId:C.coversUpToId}):(t.advanceCursor("reflector",a,"empty"),{outcome:"continue",sameRunReflections:[]})}catch(S){if(Se(S))return D("reflector.stale_ctx",{error:String(S)}),{outcome:"abort",sameRunReflections:[]};let R=t.findCandidateConfig(d.model,{model:n.model,modelRegistry:n.modelRegistry,hasUI:n.hasUI,ui:n.ui,stageModel:rt(t,"reflector"),stageFallbacks:yt(t,"reflector")});t.recordRetryableError(R,S,"reflector"),D("reflector.error",{error:String(S),retryable:Mn(S)});continue}}return t.recordConsolidationStageError(n,"reflector",new Error("Reflector: all model candidates exhausted")),{outcome:"abort",sameRunReflections:[]}}async function ry(e,t,n,o,r,i){let s,a;try{s=n.sessionManager.getBranch(),a=n.sessionManager.getSessionId();}catch(c){if(Se(c))return D("dropper.stale_ctx",{error:String(c)}),"abort";throw c}let l=0,d;if(Z(t.config)){let c=ue(a);if(!(c.observationBatches??[]).some(p=>p.data?.observations?.length))return t.advanceCursor("dropper",s.at(-1)?.id??"unknown","skipped"),"continue";if(d=c.observation?.coversUpToId,c.dropped?.coversUpToId){let p=z(s,c.observation?.coversUpToId??""),m=z(s,c.dropped.coversUpToId);if(p>=0&&m>=0&&p<=m)return t.advanceCursor("dropper",c.dropped.coversUpToId,"skipped"),"continue";if(m>=0){if(l=Q(s,m),l<t.config.reflectAfterTokens)return t.advanceCursor("dropper",c.dropped.coversUpToId,"not_due"),"continue"}else l=Ke(s);}else l=Ke(s);}else {if(l=Ke(s),l<t.config.reflectAfterTokens)return t.advanceCursor("dropper",s.at(-1)?.id??"unknown","not_due"),"continue";if(d=dt(s,G),!d)return t.advanceCursor("dropper",s.at(-1)?.id??"unknown","skipped"),"continue"}for(let c=0;c<rs;c++){let u=await o("dropper");if(!u)return "abort";let p=Pt(s),m=Z(t.config)?ue(a):void 0,g=m?-1:Ot(s,le),f=m?Du(m,s,m.dropped?.coversUpToId):Fr(s,g),h=Math.ceil(f.reduce((v,k)=>v+k.content.length,0)/4),b=Math.floor(t.config.dropperInputMaxTokens*.2),y=Math.min(h+b,t.config.dropperInputMaxTokens),w=l;if(Z(t.config)&&m?.dropped?.coversUpToId){let v=z(s,m.dropped.coversUpToId);v>=0&&(w=Q(s,v));}t.tryEmitInfo(n.hasUI,n.ui,`Observational memory: dropper running (~${w.toLocaleString()} tokens accumulated, ~${y.toLocaleString()}-token input)`);try{let v=m?[...p.activeObservations,...(m.observationBatches??[]).flatMap(L=>L.data?.observations??[])]:p.activeObservations,k=Lr(v.filter(L=>!f.some(K=>K.id===L.id)),Math.floor(t.config.dropperInputMaxTokens*.2)),S=m?[...p.reflections,...(m.reflectionBatches??[]).flatMap(L=>L.data?.reflections??[])]:p.reflections,R=Jb(S,r),x=t.findCandidateConfig(u.model,{model:n.model,modelRegistry:n.modelRegistry,hasUI:n.hasUI,ui:n.ui,stageModel:rt(t,"dropper"),stageFallbacks:yt(t,"dropper")}),E=Ft(u.model,x),_=y+os;if(_>E){D("dropper.context_window_exceeded",{estimatedInput:_,effectiveCtx:E,model:`${u.model.provider}/${u.model.id}`}),t.recordRetryableError(x,new Error(`context window ${E} too small for estimated input ${_}`),"dropper"),t.tryEmitInfo(n.hasUI,n.ui,`Observational memory: dropper skipping ${u.model.provider}/${u.model.id} (context window ${E.toLocaleString()} too small for ~${_.toLocaleString()}-token input)`);continue}let{runDropper:F}=await Promise.resolve().then(()=>(Lu(),Fu)),C=await F({model:u.model,apiKey:u.apiKey,headers:u.headers,env:u.env,reflections:R,observations:f,existingObservationsSummary:k||void 0,budgetTokens:t.config.observationsPoolMaxTokens,skipFullness:t.config.dropperPoolFullnessThreshold,maxTurns:t.config.agentMaxTurns,thinkingLevel:ss(t,"dropper",x),providerIdleTimeoutMs:t.config.providerIdleTimeoutMs}),M=Z(t.config)?m?.reflection?.coversUpToId:dt(s,ee),A=Na(s,d,i??M),j=A&&C?La(C,A):void 0;return j&&A?(Z(t.config)?kl(a,{coversUpToId:A,data:j}):is(e,le,j),t.advanceCursor("dropper",A,"recorded")):t.advanceCursor("dropper",A??d??s.at(-1)?.id??"unknown","empty"),"continue"}catch(v){if(Se(v))return D("dropper.stale_ctx",{error:String(v)}),"abort";let k=t.findCandidateConfig(u.model,{model:n.model,modelRegistry:n.modelRegistry,hasUI:n.hasUI,ui:n.ui,stageModel:rt(t,"dropper"),stageFallbacks:yt(t,"dropper")});t.recordRetryableError(k,v,"dropper"),D("dropper.error",{error:String(v),retryable:Mn(v)});continue}}return t.recordConsolidationStageError(n,"dropper",new Error("Dropper: all model candidates exhausted")),"abort"}qe();Be();var Bu=Symbol.for("pi-blackhole:inline-compaction-adapter:v1"),vt=class extends Error{constructor(t){super(t),this.name="InlineCompactionUnavailableError";}};function fs(){let e=globalThis,t=e[Bu];if(t)return t.hostCandidateCount??=0,t.capturedSessionCount??=0,t;let n={installs:new WeakMap,sessions:new WeakMap,refreshInstalled:new WeakSet,refreshPending:new WeakSet,compactionInFlight:new WeakSet,hostCandidateCount:0,capturedSessionCount:0};return e[Bu]=n,n}function dy(e){let t=e.split(""),n=d=>{t[d]!==`
|
|
443
443
|
`&&t[d]!=="\r"&&(t[d]=" ");},o=(d,c)=>{let u=d;for(n(u++);u<e.length;){let p=e[u];if(n(u++),p==="\\"&&u<e.length){n(u++);continue}if(p===c)break}return u},r=d=>{let c=d;for(n(c++),n(c++);c<e.length&&e[c]!==`
|
|
444
444
|
`;)n(c++);return c},i=d=>{let c=d;for(n(c++),n(c++);c<e.length;){let u=e[c],p=e[c+1];if(n(c++),u==="*"&&p==="/"){n(c++);break}}return c};function s(d){let c=d,u=1;for(;c<e.length&&u>0;){let p=e[c],m=e[c+1];if(p==='"'||p==="'"){c=o(c,p);continue}if(p==="`"){c=a(c);continue}if(p==="/"&&m==="/"){c=r(c);continue}if(p==="/"&&m==="*"){c=i(c);continue}p==="{"&&(u+=1),p==="}"&&(u-=1),n(c++);}return c}function a(d){let c=d;for(n(c++);c<e.length;){let u=e[c],p=e[c+1];if(u==="\\"){n(c++),c<e.length&&n(c++);continue}if(u==="`"){n(c++);break}if(u==="$"&&p==="{"){n(c++),n(c++),c=s(c);continue}n(c++);}return c}let l=0;for(;l<e.length;){let d=e[l],c=e[l+1];if(d==='"'||d==="'"){l=o(l,d);continue}if(d==="`"){l=a(l);continue}if(d==="/"&&c==="/"){l=r(l);continue}if(d==="/"&&c==="*"){l=i(l);continue}l+=1;}return t.join("")}function as(e,t){let n=new RegExp(`this\\.${t}\\s*\\(\\s*\\)`,"g");return e.match(n)?.length??0}function uy(e){if(typeof e.compact!="function")return "AgentSession.compact() is missing";if(typeof e.abort!="function")return "AgentSession.abort() is missing";if(typeof e._bindExtensionCore!="function")return "AgentSession._bindExtensionCore() is missing";let t=dy(Function.prototype.toString.call(e.compact));if(as(t,"abort")!==1||!t.includes("appendCompaction")||!t.includes("agent.state.messages"))return "unsupported AgentSession.compact() shape";let o=as(t,"_disconnectFromAgent"),r=as(t,"_reconnectToAgent");return o===0&&r===0?{disconnectsAgent:false}:o===1&&r===1&&typeof e._disconnectFromAgent=="function"&&typeof e._reconnectToAgent=="function"?{disconnectsAgent:true}:"unsupported AgentSession disconnect/reconnect shape"}function ju(e,t,n){let o=e,r=Object.getOwnPropertyDescriptor(e,t);return Object.defineProperty(e,t,{configurable:true,enumerable:r?.enumerable??false,writable:true,value:n}),()=>{r?Object.defineProperty(e,t,r):delete o[t];}}function fy(e,t){let n=e.agent;if(t.refreshInstalled.has(n))return;let o=n.prepareNextTurnWithContext;n.prepareNextTurnWithContext=async(r,i)=>{let s=await o?.call(n,r,i);if(!t.refreshPending.has(e))return s;t.refreshPending.delete(e);let a=s?.context??r.context;return {...s,context:{...a,messages:e.agent.state.messages.slice()}}},t.refreshInstalled.add(n);}function py(e,t,n){!t.originalCompact||!t.shape||!e.sessionManager||typeof e.sessionManager!="object"||typeof e.sessionManager.buildSessionContext!="function"||(n.sessions.has(e.sessionManager)||(n.capturedSessionCount=(n.capturedSessionCount??0)+1),n.sessions.set(e.sessionManager,{session:e,originalCompact:t.originalCompact,shape:t.shape}),fy(e,n));}function ds(e){let t;try{t=dirname(realpathSync(e));}catch{return}let n=parse(t).root;for(;t!==n;){let o=join(t,"package.json");if(existsSync(o))try{if(JSON.parse(readFileSync(o,"utf8")).name==="@earendil-works/pi-coding-agent")return t}catch{}t=dirname(t);}}function my(e){let t=[];for(let n of e.split(`
|
|
445
|
-
`)){let o=n.match(/\((.+):\d+:\d+\)\s*$/)??n.match(/\bat (.+):\d+:\d+\s*$/);if(!o?.[1])continue;let r=o[1].trim();if(r.replaceAll("\\","/").includes("@earendil-works/pi-coding-agent")&&!(!r.startsWith("file://")&&!r.startsWith("/")&&!r.startsWith("\\\\")&&!/^[A-Za-z]:[\\/]/.test(r)))try{t.push(r.startsWith("file://")?fileURLToPath(r):r);}catch{}}return t}function gy(e,t){let n,o;try{n=realpathSync(e),o=readFileSync(n,"utf8");}catch{return}let r=/\bimport\s*\{([^}]*)\}\s*from\s*["']([^"']+)["']/g;for(let i of o.matchAll(r)){let s=i[1].split(",").map(l=>l.trim().split(/\s+as\s+/)[0]),a=i[2];if(!(!s.includes("main")||!a.startsWith(".")))try{let l=realpathSync(join(dirname(n),a));if(ds(l)===t)return l}catch{}}}async function Wu(e={}){let t=new Set,n=new Set,o=e.stack??new Error().stack??"";for(let d of my(o))n.add(d);let r=e.entrypoint??process.argv[1];r&&n.add(r);for(let d of n){let c=ds(d);c&&t.add(c);}let i=new Set;for(let d of t){i.add(join(d,"dist","index.js"));for(let c of n){if(ds(c)!==d)continue;let u=gy(c,d);u&&i.add(u);}}fs().hostCandidateCount=i.size;let s,a=[];for(let d of i)try{let c=await import(pathToFileURL(d).href);if(!c.AgentSession){a.push(`${d}: AgentSession export missing`);continue}let u=hy({sessionClass:c.AgentSession});u.supported?s??=u:a.push(`${d}: ${u.reason??"unsupported"}`);}catch(c){a.push(`${d}: ${c instanceof Error?c.message:String(c)}`);}return s||{supported:false,reason:"Blackhole inline compaction is unavailable: host AgentSession module could not be resolved"+(a.length>0?` (${a.join("; ")})`:"")}}function hy(e={}){let n=(e.sessionClass??AgentSession).prototype,o=fs(),r=o.installs.get(n);if(r)return r.status;let i=uy(n);if(typeof i=="string"){let d={supported:false,reason:i};return o.installs.set(n,{status:d}),d}let s=n.compact,a=n._bindExtensionCore,l={status:{supported:true},originalCompact:s,shape:i};return n._bindExtensionCore=function(c){return py(this,l,o),a.call(this,c)},o.installs.set(n,l),l.status}function by(e){if(!e||typeof e!="object")return;let t=e;return t.type==="toolCall"&&typeof t.id=="string"?t.id:void 0}function yy(e){for(let t=e.length-1;t>=0;t-=1){let n=e[t];if(!n||typeof n!="object")continue;let o=n;if(o.role!=="assistant")continue;if(!Array.isArray(o.content))return false;let r=o.stopReason;if(r==="error"||r==="aborted")continue;let i=new Set;for(let s of o.content){let a=by(s);a&&i.add(a);}if(i.size===0)return false;for(let s=t+1;s<e.length;s+=1){let a=e[s];if(!a||typeof a!="object")continue;let l=a;l.role==="toolResult"&&typeof l.toolCallId=="string"&&i.delete(l.toolCallId);}return i.size>0}return false}async function Vu(e,t){let n=fs(),o=n.sessions.get(e);if(!o)throw new vt(`Blackhole inline compaction is unavailable: owning AgentSession was not captured or Pi internals are unsupported (host candidates: ${n.hostCandidateCount??0}; captured sessions: ${n.capturedSessionCount??0})`);let{session:r,originalCompact:i,shape:s}=o;if(n.compactionInFlight.has(r)||r._compactionAbortController||r._autoCompactionAbortController)throw new Error("Compaction already in progress");let a=r.sessionManager.buildSessionContext().messages;if(yy(a))throw new Error("Cannot compact inline while a tool call is still in flight");n.compactionInFlight.add(r);let l=[],d=r.abort,c=false,u=false,p=r.agent.state.messages,m,g=false,f,h=[];try{try{if(s.disconnectsAgent){let b=r._disconnectFromAgent;if(!b)throw new vt("Blackhole inline compaction is unavailable: disconnect hook disappeared");l.push(ju(r,"_disconnectFromAgent",function(){if(!u){u=!0;return}b.call(this);}));}if(l.push(ju(r,"abort",async function(){if(!c){c=!0;return}this._compactionAbortController?.abort(),await d.call(this);})),m=await i.call(r,t),n.refreshPending.add(r),!c||s.disconnectsAgent&&!u)throw new vt("Blackhole inline compaction invariant failed: Pi quiesce hooks were not invoked as expected")}catch(b){r.agent.state.messages!==p&&n.refreshPending.add(r),g=!0,f=b;}}finally{n.compactionInFlight.delete(r);for(let b=l.length-1;b>=0;b-=1)try{l[b]();}catch(y){h.push(y);}}if(g)throw h.length>0?new AggregateError([f,...h],"Blackhole inline compaction failed and could not restore all session properties"):f;if(h.length>0)throw new AggregateError(h,"Blackhole inline compaction could not restore all session properties");return m}function ms(e){return e instanceof Error?e.message:e&&typeof e=="object"&&"message"in e?String(e.message):String(e)}function Gt(e){let t=ms(e);return t.includes("extension ctx is stale")||t.includes("ctx is stale")}function qt(e,t,n,o){if(e)try{t?.notify(n,o);}catch(r){if(!Gt(r))throw r}}function Hu(e){if(e.config.compaction==="off")return "compaction_off";if(e.config.compaction==="manual")return "compaction_manual";if(e.config.compactionEngine==="pi-default")return "compactionEngine_pi_default";if(e.config.compaction===void 0&&e.config.compactionEngine===void 0){if(e.config.passive===true)return "passive";if(e.config.noAutoCompact===true)return "manual";if(e.config.overrideDefaultCompaction===false)return "overrideDefaultCompaction_false"}return null}var vy=3e4;function ps(e){e.midRunCompactionRetry={failures:0,retryAfter:0};}function Ku(e){let t=e.midRunCompactionRetry.failures+1,n=Math.min(vy,1e3*2**(t-1));return e.midRunCompactionRetry={failures:t,retryAfter:Date.now()+n},n}var zu=e=>`; retrying in ${Math.ceil(e/1e3)}s`;function qu(e,t,n=Vu){e.on("agent_start",(o,r)=>{t.resetInfoGate(),t.autoCompactionController&&(t.autoCompactionController.abort(),t.autoCompactionController=null,t.compactInFlight=false),t.config.midRunCompaction==="resume"&&t.inlineCompactionAdapterStatus?.supported===false&&!t.inlineCompactionWarningEmitted&&(t.inlineCompactionWarningEmitted=true,qt(r?.hasUI??false,r?.ui,`Observational memory: mid-run compaction (resume) unavailable: ${t.inlineCompactionAdapterStatus.reason}; using settled compaction fallback`,"warning"));}),e.on("agent_end",(o,r)=>{try{wy(o,r,t);}catch(i){if(Gt(i))return;throw i}}),e.on("turn_end",async(o,r)=>{try{await ky(r,t,n);}catch(i){if(Gt(i))return;throw i}});}async function ky(e,t,n){t.ensureConfig(e.cwd,c=>e.ui?.notify?.(c,"warning"));let o=(c,u)=>D(c,u,t.config.debugLog===true),r=t.config.midRunCompaction??"off";if(r==="off"){o("compaction_trigger.turn_end.skip",{reason:"midRunCompaction_off"});return}let i=Hu(t);if(i){o("compaction_trigger.turn_end.skip",{reason:i});return}if(t.compactInFlight){o("compaction_trigger.turn_end.skip",{reason:"compactInFlight"});return}if(e.signal?.aborted===true){o("compaction_trigger.turn_end.skip",{reason:"active_run_aborted"});return}let s=e.sessionManager.getBranch(),a=_t(s);if(a<t.config.compactAfterTokens){ps(t);return}if(Date.now()<t.midRunCompactionRetry.retryAfter){o("compaction_trigger.turn_end.skip",{reason:"inline_retry_backoff",tokens:a,failures:t.midRunCompactionRetry.failures,retryAfter:t.midRunCompactionRetry.retryAfter});return}if(r==="resume"&&t.inlineCompactionAdapterStatus?.supported===false){o("compaction_trigger.turn_end.skip",{reason:"inline_adapter_unsupported",tokens:a,reason_detail:t.inlineCompactionAdapterStatus.reason});return}let l=e.hasUI,d=e.ui;if(o("compaction_trigger.turn_end.threshold_reached",{tokens:a,threshold:t.config.compactAfterTokens,mode:r}),t.tryEmitInfo(l,d,`Observational memory: compaction threshold reached mid-run (~${a.toLocaleString()} tokens); compacting${r==="resume"?" inline":" and pausing"}`),t.compactInFlight=true,r==="resume"){try{await n(e.sessionManager),ps(t),o("compaction_trigger.turn_end.inline_complete"),t.tryEmitInfo(l,d,"Observational memory: transparent mid-run compaction complete");}catch(c){if(Gt(c))throw c;let u=ms(c);if(c instanceof vt){t.inlineCompactionAdapterStatus={supported:false,reason:u},o("compaction_trigger.turn_end.inline_adapter_unavailable",{message:u}),t.inlineCompactionWarningEmitted||(t.inlineCompactionWarningEmitted=true,qt(l,d,`Observational memory: ${u}; using settled compaction fallback`,"warning"));return}let p=Ku(t);o("compaction_trigger.turn_end.inline_error",{message:u,failures:t.midRunCompactionRetry.failures,retryAfter:t.midRunCompactionRetry.retryAfter}),u!=="Compaction cancelled"&&qt(l,d,`Observational memory: transparent mid-run compaction failed: ${u}${zu(p)}`,"error");}finally{t.compactInFlight=false,l&&e.signal?.aborted!==true&&d?.setWorkingVisible?.(true);}return}e.compact({onComplete:()=>{t.compactInFlight=false,ps(t),o("compaction_trigger.turn_end.pause_complete"),t.tryEmitInfo(l,d,"Observational memory: mid-run compaction complete; agent paused");},onError:c=>{t.compactInFlight=false;let u=Ku(t),p=c?.message??String(c);o("compaction_trigger.turn_end.pause_error",{message:p,failures:t.midRunCompactionRetry.failures,retryAfter:t.midRunCompactionRetry.retryAfter}),p!=="Compaction cancelled"&&qt(l,d,`Observational memory: mid-run compaction failed: ${p}${zu(u)}`,"error");}});}function wy(e,t,n){n.ensureConfig(t.cwd,m=>t.ui?.notify?.(m,"warning")),n.resetInfoGate();let o=(m,g)=>D(m,g,n.config.debugLog===true);o("compaction_trigger.agent_end",{passive:n.config.passive,memory:n.config.memory,manualMode:n.config.compaction==="manual"||n.config.noAutoCompact===true,overrideDefaultCompaction:n.config.overrideDefaultCompaction,compactInFlight:n.compactInFlight,compactAfterTokens:n.config.compactAfterTokens});let r=Hu(n);if(r){o("compaction_trigger.skip",{reason:r});return}if(n.compactInFlight){o("compaction_trigger.skip",{reason:"compactInFlight"});return}let i=[...e.messages].reverse().find(m=>m.role==="assistant");if(i&&i.stopReason==="error"&&i.errorMessage&&zi.test(i.errorMessage))return;let s=t.sessionManager.getBranch();o("compaction_trigger.branch_check",{branchLength:s.length,hasLastEntry:s.length>0,lastEntryType:s.length>0?s[s.length-1].type:"none"});let a=_t(s);if(o("compaction_trigger.tokens",{tokens:a,compactAfterTokens:n.config.compactAfterTokens,branchLength:s.length}),a<n.config.compactAfterTokens){o("compaction_trigger.skip",{reason:"below_threshold",tokens:a,threshold:n.config.compactAfterTokens});return}if(Date.now()<n.midRunCompactionRetry.retryAfter){o("compaction_trigger.skip",{reason:"mid_run_retry_backoff",tokens:a,failures:n.midRunCompactionRetry.failures,retryAfter:n.midRunCompactionRetry.retryAfter});return}let l=t.hasUI,d=t.ui,c=t.sessionManager.getSessionId();o("compaction_trigger.threshold_reached",{tokens:a,sessionId:c,hasUI:l}),n.tryEmitInfo(l,d,`Observational memory: compaction threshold reached (~${a.toLocaleString()} tokens); triggering compaction`),n.compactInFlight=true;let u=new AbortController;n.autoCompactionController=u;let p=u.signal;o("compaction_trigger.scheduled",{compactInFlight:n.compactInFlight}),(async()=>{try{await new Promise(h=>setTimeout(h,0));let m=!1;for(;!m;){if(p.aborted){o("compaction_trigger.microtask.bail",{reason:"aborted_agent_start"});return}let h;try{h=t.sessionManager.getSessionId();}catch(b){if(Gt(b)){n.compactInFlight=!1,n.autoCompactionController=null,o("compaction_trigger.microtask.bail",{reason:"stale_ctx"});return}throw b}if(o("compaction_trigger.microtask.session_check",{currentSessionId:h,expectedSessionId:c,match:h===c}),h!==c){n.compactInFlight=!1,n.autoCompactionController=null,o("compaction_trigger.microtask.bail",{reason:"session_changed"}),n.tryEmitInfo(l,d,"Observational memory: compaction cancelled \u2014 session changed before compaction");return}if(m=t.isIdle(),o("compaction_trigger.microtask.idle_check",{isIdle:m}),!m){let y=Date.now()+200;for(;Date.now()<y;){if(p.aborted){o("compaction_trigger.microtask.bail",{reason:"aborted_agent_start"});return}await new Promise(w=>setTimeout(w,50));}}}if(p.aborted){o("compaction_trigger.microtask.bail",{reason:"aborted_agent_start"});return}let g=t.sessionManager.getBranch(),f=_t(g);if(o("compaction_trigger.microtask.recheck_tokens",{currentTokens:f,threshold:n.config.compactAfterTokens,ok:f>=n.config.compactAfterTokens}),f<n.config.compactAfterTokens){n.compactInFlight=!1,n.autoCompactionController=null,o("compaction_trigger.microtask.bail",{reason:"pressure_relieved",currentTokens:f,threshold:n.config.compactAfterTokens}),n.tryEmitInfo(l,d,"Observational memory: compaction skipped \u2014 another compaction already ran before deferred compaction");return}o("compaction_trigger.microtask.calling_compact",{}),n.autoCompactionController=null,t.compact({onComplete:h=>{n.compactInFlight=!1,o("compaction_trigger.onComplete",{result:!!h}),n.tryEmitInfo(l,d,"Observational memory: compaction complete");},onError:h=>{n.compactInFlight=!1,o("compaction_trigger.onError",{message:h?.message??String(h)}),h.message!=="Compaction cancelled"&&qt(l,d,`Observational memory: ${h.message}`,"error");}});}catch(m){n.compactInFlight=false,n.autoCompactionController=null;let g=ms(m);if(Gt(m)){o("compaction_trigger.microtask.bail",{reason:"stale_ctx",message:g});return}o("compaction_trigger.microtask.error",{message:g}),qt(l,d,`Observational memory: compact threw: ${g}`,"error");}})();}function xy(e){if(!Array.isArray(e))return [];let t=[];for(let n of e){if(!n||n.type!=="toolCall")continue;let o=n.arguments??{};if(!St(o))continue;let r=De(o);if(!r)continue;let i={name:n.name??"",path:r};typeof o.content=="string"&&(i.content=o.content),Array.isArray(o.edits)&&(i.edits=o.edits.filter(s=>s!==null&&typeof s=="object")),typeof o.oldText=="string"&&!Array.isArray(o.edits)&&(i.oldText=o.oldText),typeof o.newText=="string"&&!Array.isArray(o.edits)&&(i.newText=o.newText),t.push(i);}return t}function Gu(e,t,n){let o;e.content?o=e.content:e.edits?o=e.edits.map((u,p)=>`--- edit ${p+1} ---
|
|
445
|
+
`)){let o=n.match(/\((.+):\d+:\d+\)\s*$/)??n.match(/\bat (.+):\d+:\d+\s*$/);if(!o?.[1])continue;let r=o[1].trim();if(r.replaceAll("\\","/").includes("@earendil-works/pi-coding-agent")&&!(!r.startsWith("file://")&&!r.startsWith("/")&&!r.startsWith("\\\\")&&!/^[A-Za-z]:[\\/]/.test(r)))try{t.push(r.startsWith("file://")?fileURLToPath(r):r);}catch{}}return t}function gy(e,t){let n,o;try{n=realpathSync(e),o=readFileSync(n,"utf8");}catch{return}let r=/\bimport\s*\{([^}]*)\}\s*from\s*["']([^"']+)["']/g;for(let i of o.matchAll(r)){let s=i[1].split(",").map(l=>l.trim().split(/\s+as\s+/)[0]),a=i[2];if(!(!s.includes("main")||!a.startsWith(".")))try{let l=realpathSync(join(dirname(n),a));if(ds(l)===t)return l}catch{}}}async function Wu(e={}){let t=new Set,n=new Set,o=e.stack??new Error().stack??"";for(let c of my(o))n.add(c);let r=e.entrypoint??process.argv[1];r&&n.add(r);for(let c of n){let u=ds(c);u&&t.add(u);}let i=new Set,s=new Set;for(let c of n)for(let u of t){if(ds(c)!==u)continue;let p=gy(c,u);p&&(i.add(p),s.add(u));}for(let c of t)s.has(c)||i.add(join(c,"dist","index.js"));fs().hostCandidateCount=i.size;let a=[],l;for(let c of i)try{let u=await import(pathToFileURL(c).href);if(!u.AgentSession){a.push(`${c}: AgentSession export missing`);continue}let p=hy({sessionClass:u.AgentSession});p.supported?l??=p:a.push(`${c}: ${p.reason??"unsupported"}`);}catch(u){a.push(`${c}: ${u instanceof Error?u.message:String(u)}`);}return l||{supported:false,reason:"Blackhole inline compaction is unavailable: host AgentSession module could not be resolved"+(a.length>0?` (${a.join("; ")})`:"")}}function hy(e={}){let n=(e.sessionClass??AgentSession).prototype,o=fs(),r=o.installs.get(n);if(r)return r.status;let i=uy(n);if(typeof i=="string"){let d={supported:false,reason:i};return o.installs.set(n,{status:d}),d}let s=n.compact,a=n._bindExtensionCore,l={status:{supported:true},originalCompact:s,shape:i};return n._bindExtensionCore=function(c){return py(this,l,o),a.call(this,c)},o.installs.set(n,l),l.status}function by(e){if(!e||typeof e!="object")return;let t=e;return t.type==="toolCall"&&typeof t.id=="string"?t.id:void 0}function yy(e){for(let t=e.length-1;t>=0;t-=1){let n=e[t];if(!n||typeof n!="object")continue;let o=n;if(o.role!=="assistant")continue;if(!Array.isArray(o.content))return false;let r=o.stopReason;if(r==="error"||r==="aborted")continue;let i=new Set;for(let s of o.content){let a=by(s);a&&i.add(a);}if(i.size===0)return false;for(let s=t+1;s<e.length;s+=1){let a=e[s];if(!a||typeof a!="object")continue;let l=a;l.role==="toolResult"&&typeof l.toolCallId=="string"&&i.delete(l.toolCallId);}return i.size>0}return false}async function Vu(e,t){let n=fs(),o=n.sessions.get(e);if(!o)throw new vt(`Blackhole inline compaction is unavailable: owning AgentSession was not captured or Pi internals are unsupported (host candidates: ${n.hostCandidateCount??0}; captured sessions: ${n.capturedSessionCount??0})`);let{session:r,originalCompact:i,shape:s}=o;if(n.compactionInFlight.has(r)||r._compactionAbortController||r._autoCompactionAbortController)throw new Error("Compaction already in progress");let a=r.sessionManager.buildSessionContext().messages;if(yy(a))throw new Error("Cannot compact inline while a tool call is still in flight");n.compactionInFlight.add(r);let l=[],d=r.abort,c=false,u=false,p=r.agent.state.messages,m,g=false,f,h=[];try{try{if(s.disconnectsAgent){let b=r._disconnectFromAgent;if(!b)throw new vt("Blackhole inline compaction is unavailable: disconnect hook disappeared");l.push(ju(r,"_disconnectFromAgent",function(){if(!u){u=!0;return}b.call(this);}));}if(l.push(ju(r,"abort",async function(){if(!c){c=!0;return}this._compactionAbortController?.abort(),await d.call(this);})),m=await i.call(r,t),n.refreshPending.add(r),!c||s.disconnectsAgent&&!u)throw new vt("Blackhole inline compaction invariant failed: Pi quiesce hooks were not invoked as expected")}catch(b){r.agent.state.messages!==p&&n.refreshPending.add(r),g=!0,f=b;}}finally{n.compactionInFlight.delete(r);for(let b=l.length-1;b>=0;b-=1)try{l[b]();}catch(y){h.push(y);}}if(g)throw h.length>0?new AggregateError([f,...h],"Blackhole inline compaction failed and could not restore all session properties"):f;if(h.length>0)throw new AggregateError(h,"Blackhole inline compaction could not restore all session properties");return m}function ms(e){return e instanceof Error?e.message:e&&typeof e=="object"&&"message"in e?String(e.message):String(e)}function Gt(e){let t=ms(e);return t.includes("extension ctx is stale")||t.includes("ctx is stale")}function qt(e,t,n,o){if(e)try{t?.notify(n,o);}catch(r){if(!Gt(r))throw r}}function Hu(e){if(e.config.compaction==="off")return "compaction_off";if(e.config.compaction==="manual")return "compaction_manual";if(e.config.compactionEngine==="pi-default")return "compactionEngine_pi_default";if(e.config.compaction===void 0&&e.config.compactionEngine===void 0){if(e.config.passive===true)return "passive";if(e.config.noAutoCompact===true)return "manual";if(e.config.overrideDefaultCompaction===false)return "overrideDefaultCompaction_false"}return null}var vy=3e4;function ps(e){e.midRunCompactionRetry={failures:0,retryAfter:0};}function Ku(e){let t=e.midRunCompactionRetry.failures+1,n=Math.min(vy,1e3*2**(t-1));return e.midRunCompactionRetry={failures:t,retryAfter:Date.now()+n},n}var zu=e=>`; retrying in ${Math.ceil(e/1e3)}s`;function qu(e,t,n=Vu){e.on("agent_start",(o,r)=>{t.resetInfoGate(),t.autoCompactionController&&(t.autoCompactionController.abort(),t.autoCompactionController=null,t.compactInFlight=false),t.config.midRunCompaction==="resume"&&t.inlineCompactionAdapterStatus?.supported===false&&!t.inlineCompactionWarningEmitted&&(t.inlineCompactionWarningEmitted=true,qt(r?.hasUI??false,r?.ui,`Observational memory: mid-run compaction (resume) unavailable: ${t.inlineCompactionAdapterStatus.reason}; using settled compaction fallback`,"warning"));}),e.on("agent_end",(o,r)=>{try{wy(o,r,t);}catch(i){if(Gt(i))return;throw i}}),e.on("turn_end",async(o,r)=>{try{await ky(r,t,n);}catch(i){if(Gt(i))return;throw i}});}async function ky(e,t,n){t.ensureConfig(e.cwd,c=>e.ui?.notify?.(c,"warning"));let o=(c,u)=>D(c,u,t.config.debugLog===true),r=t.config.midRunCompaction??"off";if(r==="off"){o("compaction_trigger.turn_end.skip",{reason:"midRunCompaction_off"});return}let i=Hu(t);if(i){o("compaction_trigger.turn_end.skip",{reason:i});return}if(t.compactInFlight){o("compaction_trigger.turn_end.skip",{reason:"compactInFlight"});return}if(e.signal?.aborted===true){o("compaction_trigger.turn_end.skip",{reason:"active_run_aborted"});return}let s=e.sessionManager.getBranch(),a=_t(s);if(a<t.config.compactAfterTokens){ps(t);return}if(Date.now()<t.midRunCompactionRetry.retryAfter){o("compaction_trigger.turn_end.skip",{reason:"inline_retry_backoff",tokens:a,failures:t.midRunCompactionRetry.failures,retryAfter:t.midRunCompactionRetry.retryAfter});return}if(r==="resume"&&t.inlineCompactionAdapterStatus?.supported===false){o("compaction_trigger.turn_end.skip",{reason:"inline_adapter_unsupported",tokens:a,reason_detail:t.inlineCompactionAdapterStatus.reason});return}let l=e.hasUI,d=e.ui;if(o("compaction_trigger.turn_end.threshold_reached",{tokens:a,threshold:t.config.compactAfterTokens,mode:r}),t.tryEmitInfo(l,d,`Observational memory: compaction threshold reached mid-run (~${a.toLocaleString()} tokens); compacting${r==="resume"?" inline":" and pausing"}`),t.compactInFlight=true,r==="resume"){try{await n(e.sessionManager),ps(t),o("compaction_trigger.turn_end.inline_complete"),t.tryEmitInfo(l,d,"Observational memory: transparent mid-run compaction complete");}catch(c){if(Gt(c))throw c;let u=ms(c);if(c instanceof vt){t.inlineCompactionAdapterStatus={supported:false,reason:u},o("compaction_trigger.turn_end.inline_adapter_unavailable",{message:u}),t.inlineCompactionWarningEmitted||(t.inlineCompactionWarningEmitted=true,qt(l,d,`Observational memory: ${u}; using settled compaction fallback`,"warning"));return}let p=Ku(t);o("compaction_trigger.turn_end.inline_error",{message:u,failures:t.midRunCompactionRetry.failures,retryAfter:t.midRunCompactionRetry.retryAfter}),u!=="Compaction cancelled"&&qt(l,d,`Observational memory: transparent mid-run compaction failed: ${u}${zu(p)}`,"error");}finally{t.compactInFlight=false,l&&e.signal?.aborted!==true&&d?.setWorkingVisible?.(true);}return}e.compact({onComplete:()=>{t.compactInFlight=false,ps(t),o("compaction_trigger.turn_end.pause_complete"),t.tryEmitInfo(l,d,"Observational memory: mid-run compaction complete; agent paused");},onError:c=>{t.compactInFlight=false;let u=Ku(t),p=c?.message??String(c);o("compaction_trigger.turn_end.pause_error",{message:p,failures:t.midRunCompactionRetry.failures,retryAfter:t.midRunCompactionRetry.retryAfter}),p!=="Compaction cancelled"&&qt(l,d,`Observational memory: mid-run compaction failed: ${p}${zu(u)}`,"error");}});}function wy(e,t,n){n.ensureConfig(t.cwd,m=>t.ui?.notify?.(m,"warning")),n.resetInfoGate();let o=(m,g)=>D(m,g,n.config.debugLog===true);o("compaction_trigger.agent_end",{passive:n.config.passive,memory:n.config.memory,manualMode:n.config.compaction==="manual"||n.config.noAutoCompact===true,overrideDefaultCompaction:n.config.overrideDefaultCompaction,compactInFlight:n.compactInFlight,compactAfterTokens:n.config.compactAfterTokens});let r=Hu(n);if(r){o("compaction_trigger.skip",{reason:r});return}if(n.compactInFlight){o("compaction_trigger.skip",{reason:"compactInFlight"});return}let i=[...e.messages].reverse().find(m=>m.role==="assistant");if(i&&i.stopReason==="error"&&i.errorMessage&&zi.test(i.errorMessage))return;let s=t.sessionManager.getBranch();o("compaction_trigger.branch_check",{branchLength:s.length,hasLastEntry:s.length>0,lastEntryType:s.length>0?s[s.length-1].type:"none"});let a=_t(s);if(o("compaction_trigger.tokens",{tokens:a,compactAfterTokens:n.config.compactAfterTokens,branchLength:s.length}),a<n.config.compactAfterTokens){o("compaction_trigger.skip",{reason:"below_threshold",tokens:a,threshold:n.config.compactAfterTokens});return}if(Date.now()<n.midRunCompactionRetry.retryAfter){o("compaction_trigger.skip",{reason:"mid_run_retry_backoff",tokens:a,failures:n.midRunCompactionRetry.failures,retryAfter:n.midRunCompactionRetry.retryAfter});return}let l=t.hasUI,d=t.ui,c=t.sessionManager.getSessionId();o("compaction_trigger.threshold_reached",{tokens:a,sessionId:c,hasUI:l}),n.tryEmitInfo(l,d,`Observational memory: compaction threshold reached (~${a.toLocaleString()} tokens); triggering compaction`),n.compactInFlight=true;let u=new AbortController;n.autoCompactionController=u;let p=u.signal;o("compaction_trigger.scheduled",{compactInFlight:n.compactInFlight}),(async()=>{try{await new Promise(h=>setTimeout(h,0));let m=!1;for(;!m;){if(p.aborted){o("compaction_trigger.microtask.bail",{reason:"aborted_agent_start"});return}let h;try{h=t.sessionManager.getSessionId();}catch(b){if(Gt(b)){n.compactInFlight=!1,n.autoCompactionController=null,o("compaction_trigger.microtask.bail",{reason:"stale_ctx"});return}throw b}if(o("compaction_trigger.microtask.session_check",{currentSessionId:h,expectedSessionId:c,match:h===c}),h!==c){n.compactInFlight=!1,n.autoCompactionController=null,o("compaction_trigger.microtask.bail",{reason:"session_changed"}),n.tryEmitInfo(l,d,"Observational memory: compaction cancelled \u2014 session changed before compaction");return}if(m=t.isIdle(),o("compaction_trigger.microtask.idle_check",{isIdle:m}),!m){let y=Date.now()+200;for(;Date.now()<y;){if(p.aborted){o("compaction_trigger.microtask.bail",{reason:"aborted_agent_start"});return}await new Promise(w=>setTimeout(w,50));}}}if(p.aborted){o("compaction_trigger.microtask.bail",{reason:"aborted_agent_start"});return}let g=t.sessionManager.getBranch(),f=_t(g);if(o("compaction_trigger.microtask.recheck_tokens",{currentTokens:f,threshold:n.config.compactAfterTokens,ok:f>=n.config.compactAfterTokens}),f<n.config.compactAfterTokens){n.compactInFlight=!1,n.autoCompactionController=null,o("compaction_trigger.microtask.bail",{reason:"pressure_relieved",currentTokens:f,threshold:n.config.compactAfterTokens}),n.tryEmitInfo(l,d,"Observational memory: compaction skipped \u2014 another compaction already ran before deferred compaction");return}o("compaction_trigger.microtask.calling_compact",{}),n.autoCompactionController=null,t.compact({onComplete:h=>{n.compactInFlight=!1,o("compaction_trigger.onComplete",{result:!!h}),n.tryEmitInfo(l,d,"Observational memory: compaction complete");},onError:h=>{n.compactInFlight=!1,o("compaction_trigger.onError",{message:h?.message??String(h)}),h.message!=="Compaction cancelled"&&qt(l,d,`Observational memory: ${h.message}`,"error");}});}catch(m){n.compactInFlight=false,n.autoCompactionController=null;let g=ms(m);if(Gt(m)){o("compaction_trigger.microtask.bail",{reason:"stale_ctx",message:g});return}o("compaction_trigger.microtask.error",{message:g}),qt(l,d,`Observational memory: compact threw: ${g}`,"error");}})();}function xy(e){if(!Array.isArray(e))return [];let t=[];for(let n of e){if(!n||n.type!=="toolCall")continue;let o=n.arguments??{};if(!St(o))continue;let r=De(o);if(!r)continue;let i={name:n.name??"",path:r};typeof o.content=="string"&&(i.content=o.content),Array.isArray(o.edits)&&(i.edits=o.edits.filter(s=>s!==null&&typeof s=="object")),typeof o.oldText=="string"&&!Array.isArray(o.edits)&&(i.oldText=o.oldText),typeof o.newText=="string"&&!Array.isArray(o.edits)&&(i.newText=o.newText),t.push(i);}return t}function Gu(e,t,n){let o;e.content?o=e.content:e.edits?o=e.edits.map((u,p)=>`--- edit ${p+1} ---
|
|
446
446
|
${u.oldText??""}
|
|
447
447
|
--- becomes ---
|
|
448
448
|
${u.newText??""}`).join(`
|
package/package.json
CHANGED
|
@@ -461,19 +461,31 @@ export async function installHostInlineCompactionAdapter(
|
|
|
461
461
|
if (root) packageRoots.add(root);
|
|
462
462
|
}
|
|
463
463
|
|
|
464
|
+
// Collect fast candidates first: per package root, pi's already-loaded
|
|
465
|
+
// bundled runtime chunk (cache-hit, ~0-10ms). A root that resolves a chunk
|
|
466
|
+
// never needs its modular dist/index.js barrel (fresh graph, ~400-500ms).
|
|
464
467
|
const modulePaths = new Set<string>();
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
for (const
|
|
468
|
+
const rootsWithChunk = new Set<string>();
|
|
469
|
+
for (const hostPath of hostPaths) {
|
|
470
|
+
for (const packageRoot of packageRoots) {
|
|
468
471
|
if (findPiPackageRoot(hostPath) !== packageRoot) continue;
|
|
469
472
|
const bundledRuntime = findBundledRuntimeModule(hostPath, packageRoot);
|
|
470
|
-
if (bundledRuntime)
|
|
473
|
+
if (bundledRuntime) {
|
|
474
|
+
modulePaths.add(bundledRuntime);
|
|
475
|
+
rootsWithChunk.add(packageRoot);
|
|
476
|
+
}
|
|
471
477
|
}
|
|
472
478
|
}
|
|
479
|
+
// Fallback barrels: only for roots that had no bundled runtime (e.g. pi
|
|
480
|
+
// versions whose entrypoint is the plain unbundled dist layout).
|
|
481
|
+
for (const packageRoot of packageRoots) {
|
|
482
|
+
if (rootsWithChunk.has(packageRoot)) continue;
|
|
483
|
+
modulePaths.add(join(packageRoot, "dist", "index.js"));
|
|
484
|
+
}
|
|
473
485
|
getRegistry().hostCandidateCount = modulePaths.size;
|
|
474
486
|
|
|
475
|
-
let supportedStatus: InlineCompactionAdapterStatus | undefined;
|
|
476
487
|
const failureReasons: string[] = [];
|
|
488
|
+
let supportedStatus: InlineCompactionAdapterStatus | undefined;
|
|
477
489
|
for (const modulePath of modulePaths) {
|
|
478
490
|
try {
|
|
479
491
|
const hostModule = (await import(pathToFileURL(modulePath).href)) as {
|