char-slop/berry
multipurpose self-modifiable agentic harness
git clone https://git.t4t.associates/char-slop/berry
e9d146d
main
1import type { JsonValue } from "@berry/api" ; 2import type { HeaderContext } from "@berry/api/web.ts" ; 3import type { OpenFile , Save , Saved } from "./edit-types.ts" ; 4 5export type Draft = { opened :OpenFile ; content :string ; pending ?:Save }; 6export type Stored = { 7id :string ; 8saved :boolean ; 9closed :boolean ; 10drafts :Record < string , Draft >; 11/** Receipts of this session's saves, carried to the report when the session ends. */ 12saves :Saved []; 13}; 14 15const PREFIX = "berry.diff.edits." ; 16const recovering = new Map < string , Promise < void >>(); 17const reporting = new Map < string , Promise < void >>(); 18 19export function editId () { 20return Array . from ( 21crypto . getRandomValues ( new Uint8Array ( 16 )), 22( byte ) => byte . toString ( 16 ). padStart ( 2 , "0" ), 23). join ( "" ); 24} 25 26export function draftKey ( tid :string ) { 27const storage = document . defaultView ! . sessionStorage ; 28let tab = storage . getItem ( "berry.diff.tab" ); 29if ( ! tab ) storage . setItem ( "berry.diff.tab" , tab = editId ()); 30return ` ${ PREFIX } ${ tid } . ${ tab } ` ; 31} 32 33export function loadDrafts ( key :string ) :Stored { 34try { 35const value = JSON . parse ( localStorage . getItem ( key ) ?? "null" ); 36if ( value ?. id && value . drafts ) { 37return { 38 ...value , 39drafts :Object . assign ( Object . create ( null ), value . drafts ), 40saves :Array . isArray ( value . saves ) ?value . saves :[], 41}; 42} 43} catch { /* a broken recovery record must not stop reviewing */ } 44return { id :editId (), saved :false , closed :false , drafts :Object . create ( null ), saves :[] }; 45} 46 47export function storeDrafts ( key :string , stored :Stored ) { 48localStorage . setItem ( key , JSON . stringify ( stored )); 49} 50 51/** Report a closed session's saves and retire it; concurrent callers share the one report. */ 52function reportStored ( key :string , context :HeaderContext ) :Promise < void > { 53const running = reporting . get ( key ); 54if ( running ) return running ; 55const result = ( async () => { 56const stored = loadDrafts ( key ); 57if ( ! stored . saved ) return ; 58await context . command ( 59"diff-finish" , 60JSON . stringify ({ session :stored . id , saves :stored . saves }), 61); 62const current = loadDrafts ( key ); 63if ( Object . keys ( current . drafts ). length ) { 64storeDrafts ( key , { ...current , id :editId (), saved :false , closed :false , saves :[] }); 65} else localStorage . removeItem ( key ); 66})(); 67reporting . set ( key , result ); 68void result . finally (() => reporting . delete ( key )). catch (() => {}); 69return result ; 70} 71 72/** 73* Land a closed tab's unacknowledged saves. Reporting them is only kicked off: it may wait for the agent's 74* turn, which must not hold the recovering view open; `recoverClosed` awaits it instead. 75*/ 76export function recover ( key :string , context :HeaderContext ) :Promise < void > { 77const running = recovering . get ( key ); 78if ( running ) return running ; 79const result = ( async () => { 80const stored = loadDrafts ( key ); 81if ( ! stored . closed ) return ; 82let failed :unknown ; 83for ( const draft of Object . values ( stored . drafts )) { 84if ( ! draft . pending ) continue ; 85try { 86const saved = await context . request ( 87"save" , 88draft . pending as unknown as JsonValue , 89) as unknown as Saved ; 90draft . opened . content = draft . pending . content ; 91draft . opened . version = saved . version ; 92draft . opened . working = saved . working ; 93delete draft . pending ; 94if ( ! stored . saves . some (( receipt ) => receipt . id === saved . id )) stored . saves . push ( saved ); 95stored . saved = true ; 96storeDrafts ( key , stored ); 97} catch ( error ) { 98failed ??= error ; 99} 100} 101// a failed report stays saved in storage, to retry on a later visit 102if ( stored . saved ) void reportStored ( key , context ). catch (() => {}); 103if ( failed ) { 104throw new Error ( "Some saves could not be recovered. Their drafts are kept in the diff view." ); 105} 106})(); 107recovering . set ( key , result ); 108void result . finally (() => recovering . delete ( key )). catch (() => {}); 109return result ; 110} 111 112export async function recoverClosed ( context :HeaderContext ) { 113const prefix = ` ${ PREFIX } ${ context . tid } .` ; 114const keys = Array . from ({ length :localStorage . length }, ( _ , i ) => localStorage . key ( i ) ! ) 115. filter (( key ) => key . startsWith ( prefix )); 116for ( const key of keys ) { 117await recover ( key , context ); 118if ( loadDrafts ( key ). closed ) await reportStored ( key , context ); 119} 120}