Something went wrong. Try again.
fork of prime agent with changes i want but are not upstreamable probqbly (and some fixes)
Something went wrong. Try again.
536 kB · 14537 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305430643074308430943104311431243134314431543164317431843194320432143224323432443254326432743284329433043314332433343344335433643374338433943404341434243434344434543464347434843494350435143524353435443554356435743584359436043614362436343644365436643674368436943704371437243734374437543764377437843794380438143824383438443854386438743884389439043914392439343944395439643974398439944004401440244034404440544064407440844094410441144124413441444154416441744184419442044214422442344244425442644274428442944304431443244334434443544364437443844394440444144424443444444454446444744484449445044514452445344544455445644574458445944604461446244634464446544664467446844694470447144724473447444754476447744784479448044814482448344844485448644874488448944904491449244934494449544964497449844994500450145024503450445054506450745084509451045114512451345144515451645174518451945204521452245234524452545264527452845294530453145324533453445354536453745384539454045414542454345444545454645474548454945504551455245534554455545564557455845594560456145624563456445654566456745684569457045714572457345744575457645774578457945804581458245834584458545864587458845894590459145924593459445954596459745984599460046014602460346044605460646074608460946104611461246134614461546164617461846194620462146224623462446254626462746284629463046314632463346344635463646374638463946404641464246434644464546464647464846494650465146524653465446554656465746584659466046614662466346644665466646674668466946704671467246734674467546764677467846794680468146824683468446854686468746884689469046914692469346944695469646974698469947004701470247034704470547064707470847094710471147124713471447154716471747184719472047214722472347244725472647274728472947304731473247334734473547364737473847394740474147424743474447454746474747484749475047514752475347544755475647574758475947604761476247634764476547664767476847694770477147724773477447754776477747784779478047814782478347844785478647874788478947904791479247934794479547964797479847994800480148024803480448054806480748084809481048114812481348144815481648174818481948204821482248234824482548264827482848294830483148324833483448354836483748384839484048414842484348444845484648474848484948504851485248534854485548564857485848594860486148624863486448654866486748684869487048714872487348744875487648774878487948804881488248834884488548864887488848894890489148924893489448954896489748984899490049014902490349044905490649074908490949104911491249134914491549164917491849194920492149224923492449254926492749284929493049314932493349344935493649374938493949404941494249434944494549464947494849494950495149524953495449554956495749584959496049614962496349644965496649674968496949704971497249734974497549764977497849794980498149824983498449854986498749884989499049914992499349944995499649974998499950005001500250035004500550065007500850095010501150125013501450155016501750185019502050215022502350245025502650275028502950305031503250335034503550365037503850395040504150425043504450455046504750485049505050515052505350545055505650575058505950605061506250635064506550665067506850695070507150725073507450755076507750785079508050815082508350845085508650875088508950905091509250935094509550965097509850995100510151025103510451055106510751085109511051115112511351145115511651175118511951205121512251235124512551265127512851295130513151325133513451355136513751385139514051415142514351445145514651475148514951505151515251535154515551565157515851595160516151625163516451655166516751685169517051715172517351745175517651775178517951805181518251835184518551865187518851895190519151925193519451955196519751985199520052015202520352045205520652075208520952105211521252135214521552165217521852195220522152225223522452255226522752285229523052315232523352345235523652375238523952405241524252435244524552465247524852495250525152525253525452555256525752585259526052615262526352645265526652675268526952705271527252735274527552765277527852795280528152825283528452855286528752885289529052915292529352945295529652975298529953005301530253035304530553065307530853095310531153125313531453155316531753185319532053215322532353245325532653275328532953305331533253335334533553365337533853395340534153425343534453455346534753485349535053515352535353545355535653575358535953605361536253635364536553665367536853695370537153725373537453755376537753785379538053815382538353845385538653875388538953905391539253935394539553965397539853995400540154025403540454055406540754085409541054115412541354145415541654175418541954205421542254235424542554265427542854295430543154325433543454355436543754385439544054415442544354445445544654475448544954505451545254535454545554565457545854595460546154625463546454655466546754685469547054715472547354745475547654775478547954805481548254835484548554865487548854895490549154925493549454955496549754985499550055015502550355045505550655075508550955105511551255135514551555165517551855195520552155225523552455255526552755285529553055315532553355345535553655375538553955405541554255435544554555465547554855495550555155525553555455555556555755585559556055615562556355645565556655675568556955705571557255735574557555765577557855795580558155825583558455855586558755885589559055915592559355945595559655975598559956005601560256035604560556065607560856095610561156125613561456155616561756185619562056215622562356245625562656275628562956305631563256335634563556365637563856395640564156425643564456455646564756485649565056515652565356545655565656575658565956605661566256635664566556665667566856695670567156725673567456755676567756785679568056815682568356845685568656875688568956905691569256935694569556965697569856995700570157025703570457055706570757085709571057115712571357145715571657175718571957205721572257235724572557265727572857295730573157325733573457355736573757385739574057415742574357445745574657475748574957505751575257535754575557565757575857595760576157625763576457655766576757685769577057715772577357745775577657775778577957805781578257835784578557865787578857895790579157925793579457955796579757985799580058015802580358045805580658075808580958105811581258135814581558165817581858195820582158225823582458255826582758285829583058315832583358345835583658375838583958405841584258435844584558465847584858495850585158525853585458555856585758585859586058615862586358645865586658675868586958705871587258735874587558765877587858795880588158825883588458855886588758885889589058915892589358945895589658975898589959005901590259035904590559065907590859095910591159125913591459155916591759185919592059215922592359245925592659275928592959305931593259335934593559365937593859395940594159425943594459455946594759485949595059515952595359545955595659575958595959605961596259635964596559665967596859695970597159725973597459755976597759785979598059815982598359845985598659875988598959905991599259935994599559965997599859996000600160026003600460056006600760086009601060116012601360146015601660176018601960206021602260236024602560266027602860296030603160326033603460356036603760386039604060416042604360446045604660476048604960506051605260536054605560566057605860596060606160626063606460656066606760686069607060716072607360746075607660776078607960806081608260836084608560866087608860896090609160926093609460956096609760986099610061016102610361046105610661076108610961106111611261136114611561166117611861196120612161226123612461256126612761286129613061316132613361346135613661376138613961406141614261436144614561466147614861496150615161526153615461556156615761586159616061616162616361646165616661676168616961706171617261736174617561766177617861796180618161826183618461856186618761886189619061916192619361946195619661976198619962006201620262036204620562066207620862096210621162126213621462156216621762186219622062216222622362246225622662276228622962306231623262336234623562366237623862396240624162426243624462456246624762486249625062516252625362546255625662576258625962606261626262636264626562666267626862696270627162726273627462756276627762786279628062816282628362846285628662876288628962906291629262936294629562966297629862996300630163026303630463056306630763086309631063116312631363146315631663176318631963206321632263236324632563266327632863296330633163326333633463356336633763386339634063416342634363446345634663476348634963506351635263536354635563566357635863596360636163626363636463656366636763686369637063716372637363746375637663776378637963806381638263836384638563866387638863896390639163926393639463956396639763986399640064016402640364046405640664076408640964106411641264136414641564166417641864196420642164226423642464256426642764286429643064316432643364346435643664376438643964406441644264436444644564466447644864496450645164526453645464556456645764586459646064616462646364646465646664676468646964706471647264736474647564766477647864796480648164826483648464856486648764886489649064916492649364946495649664976498649965006501650265036504650565066507650865096510651165126513651465156516651765186519652065216522652365246525652665276528652965306531653265336534653565366537653865396540654165426543654465456546654765486549655065516552655365546555655665576558655965606561656265636564656565666567656865696570657165726573657465756576657765786579658065816582658365846585658665876588658965906591659265936594659565966597659865996600660166026603660466056606660766086609661066116612661366146615661666176618661966206621662266236624662566266627662866296630663166326633663466356636663766386639664066416642664366446645664666476648664966506651665266536654665566566657665866596660666166626663666466656666666766686669667066716672667366746675667666776678667966806681668266836684668566866687668866896690669166926693669466956696669766986699670067016702670367046705670667076708670967106711671267136714671567166717671867196720672167226723672467256726672767286729673067316732673367346735673667376738673967406741674267436744674567466747674867496750675167526753675467556756675767586759676067616762676367646765676667676768676967706771677267736774677567766777677867796780678167826783678467856786678767886789679067916792679367946795679667976798679968006801680268036804680568066807680868096810681168126813681468156816681768186819682068216822682368246825682668276828682968306831683268336834683568366837683868396840684168426843684468456846684768486849685068516852685368546855685668576858685968606861686268636864686568666867686868696870687168726873687468756876687768786879688068816882688368846885688668876888688968906891689268936894689568966897689868996900690169026903690469056906690769086909691069116912691369146915691669176918691969206921692269236924692569266927692869296930693169326933693469356936693769386939694069416942694369446945694669476948694969506951695269536954695569566957695869596960696169626963696469656966696769686969697069716972697369746975697669776978697969806981698269836984698569866987698869896990699169926993699469956996699769986999700070017002700370047005700670077008700970107011701270137014701570167017701870197020702170227023702470257026702770287029703070317032703370347035703670377038703970407041704270437044704570467047704870497050705170527053705470557056705770587059706070617062706370647065706670677068706970707071707270737074707570767077707870797080708170827083708470857086708770887089709070917092709370947095709670977098709971007101710271037104710571067107710871097110711171127113711471157116711771187119712071217122712371247125712671277128712971307131713271337134713571367137713871397140714171427143714471457146714771487149715071517152715371547155715671577158715971607161716271637164716571667167716871697170717171727173717471757176717771787179718071817182718371847185718671877188718971907191719271937194719571967197719871997200720172027203720472057206720772087209721072117212721372147215721672177218721972207221722272237224722572267227722872297230723172327233723472357236723772387239724072417242724372447245724672477248724972507251725272537254725572567257725872597260726172627263726472657266726772687269727072717272727372747275727672777278727972807281728272837284728572867287728872897290729172927293729472957296729772987299730073017302730373047305730673077308730973107311731273137314731573167317731873197320732173227323732473257326732773287329733073317332733373347335733673377338733973407341734273437344734573467347734873497350735173527353735473557356735773587359736073617362736373647365736673677368736973707371737273737374737573767377737873797380738173827383738473857386738773887389739073917392739373947395739673977398739974007401740274037404740574067407740874097410741174127413741474157416741774187419742074217422742374247425742674277428742974307431743274337434743574367437743874397440744174427443744474457446744774487449745074517452745374547455745674577458745974607461746274637464746574667467746874697470747174727473747474757476747774787479748074817482748374847485748674877488748974907491749274937494749574967497749874997500750175027503750475057506750775087509751075117512751375147515751675177518751975207521752275237524752575267527752875297530753175327533753475357536753775387539754075417542754375447545754675477548754975507551755275537554755575567557755875597560756175627563756475657566756775687569757075717572757375747575757675777578757975807581758275837584758575867587758875897590759175927593759475957596759775987599760076017602760376047605760676077608760976107611761276137614761576167617761876197620762176227623762476257626762776287629763076317632763376347635763676377638763976407641764276437644764576467647764876497650765176527653765476557656765776587659766076617662766376647665766676677668766976707671767276737674767576767677767876797680768176827683768476857686768776887689769076917692769376947695769676977698769977007701770277037704770577067707770877097710771177127713771477157716771777187719772077217722772377247725772677277728772977307731773277337734773577367737773877397740774177427743774477457746774777487749775077517752775377547755775677577758775977607761776277637764776577667767776877697770777177727773777477757776777777787779778077817782778377847785778677877788778977907791779277937794779577967797779877997800780178027803780478057806780778087809781078117812781378147815781678177818781978207821782278237824782578267827782878297830783178327833783478357836783778387839784078417842784378447845784678477848784978507851785278537854785578567857785878597860786178627863786478657866786778687869787078717872787378747875787678777878787978807881788278837884788578867887788878897890789178927893789478957896789778987899790079017902790379047905790679077908790979107911791279137914791579167917791879197920792179227923792479257926792779287929793079317932793379347935793679377938793979407941794279437944794579467947794879497950795179527953795479557956795779587959796079617962796379647965796679677968796979707971797279737974797579767977797879797980798179827983798479857986798779887989799079917992799379947995799679977998799980008001800280038004800580068007800880098010801180128013801480158016801780188019802080218022802380248025802680278028802980308031803280338034803580368037803880398040804180428043804480458046804780488049805080518052805380548055805680578058805980608061806280638064806580668067806880698070807180728073807480758076807780788079808080818082808380848085808680878088808980908091809280938094809580968097809880998100810181028103810481058106810781088109811081118112811381148115811681178118811981208121812281238124812581268127812881298130813181328133813481358136813781388139814081418142814381448145814681478148814981508151815281538154815581568157815881598160816181628163816481658166816781688169817081718172817381748175817681778178817981808181818281838184818581868187818881898190819181928193819481958196819781988199820082018202820382048205820682078208820982108211821282138214821582168217821882198220822182228223822482258226822782288229823082318232823382348235823682378238823982408241824282438244824582468247824882498250825182528253825482558256825782588259826082618262826382648265826682678268826982708271827282738274827582768277827882798280828182828283828482858286828782888289829082918292829382948295829682978298829983008301830283038304830583068307830883098310831183128313831483158316831783188319832083218322832383248325832683278328832983308331833283338334833583368337833883398340834183428343834483458346834783488349835083518352835383548355835683578358835983608361836283638364836583668367836883698370837183728373837483758376837783788379838083818382838383848385838683878388838983908391839283938394839583968397839883998400840184028403840484058406840784088409841084118412841384148415841684178418841984208421842284238424842584268427842884298430843184328433843484358436843784388439844084418442844384448445844684478448844984508451845284538454845584568457845884598460846184628463846484658466846784688469847084718472847384748475847684778478847984808481848284838484848584868487848884898490849184928493849484958496849784988499850085018502850385048505850685078508850985108511851285138514851585168517851885198520852185228523852485258526852785288529853085318532853385348535853685378538853985408541854285438544854585468547854885498550855185528553855485558556855785588559856085618562856385648565856685678568856985708571857285738574857585768577857885798580858185828583858485858586858785888589859085918592859385948595859685978598859986008601860286038604860586068607860886098610861186128613861486158616861786188619862086218622862386248625862686278628862986308631863286338634863586368637863886398640864186428643864486458646864786488649865086518652865386548655865686578658865986608661866286638664866586668667866886698670867186728673867486758676867786788679868086818682868386848685868686878688868986908691869286938694869586968697869886998700870187028703870487058706870787088709871087118712871387148715871687178718871987208721872287238724872587268727872887298730873187328733873487358736873787388739874087418742874387448745874687478748874987508751875287538754875587568757875887598760876187628763876487658766876787688769877087718772877387748775877687778778877987808781878287838784878587868787878887898790879187928793879487958796879787988799880088018802880388048805880688078808880988108811881288138814881588168817881888198820882188228823882488258826882788288829883088318832883388348835883688378838883988408841884288438844884588468847884888498850885188528853885488558856885788588859886088618862886388648865886688678868886988708871887288738874887588768877887888798880888188828883888488858886888788888889889088918892889388948895889688978898889989008901890289038904890589068907890889098910891189128913891489158916891789188919892089218922892389248925892689278928892989308931893289338934893589368937893889398940894189428943894489458946894789488949895089518952895389548955895689578958895989608961896289638964896589668967896889698970897189728973897489758976897789788979898089818982898389848985898689878988898989908991899289938994899589968997899889999000900190029003900490059006900790089009901090119012901390149015901690179018901990209021902290239024902590269027902890299030903190329033903490359036903790389039904090419042904390449045904690479048904990509051905290539054905590569057905890599060906190629063906490659066906790689069907090719072907390749075907690779078907990809081908290839084908590869087908890899090909190929093909490959096909790989099910091019102910391049105910691079108910991109111911291139114911591169117911891199120912191229123912491259126912791289129913091319132913391349135913691379138913991409141914291439144914591469147914891499150915191529153915491559156915791589159916091619162916391649165916691679168916991709171917291739174917591769177917891799180918191829183918491859186918791889189919091919192919391949195919691979198919992009201920292039204920592069207920892099210921192129213921492159216921792189219922092219222922392249225922692279228922992309231923292339234923592369237923892399240924192429243924492459246924792489249925092519252925392549255925692579258925992609261926292639264926592669267926892699270927192729273927492759276927792789279928092819282928392849285928692879288928992909291929292939294929592969297929892999300930193029303930493059306930793089309931093119312931393149315931693179318931993209321932293239324932593269327932893299330933193329333933493359336933793389339934093419342934393449345934693479348934993509351935293539354935593569357935893599360936193629363936493659366936793689369937093719372937393749375937693779378937993809381938293839384938593869387938893899390939193929393939493959396939793989399940094019402940394049405940694079408940994109411941294139414941594169417941894199420942194229423942494259426942794289429943094319432943394349435943694379438943994409441944294439444944594469447944894499450945194529453945494559456945794589459946094619462946394649465946694679468946994709471947294739474947594769477947894799480948194829483948494859486948794889489949094919492949394949495949694979498949995009501950295039504950595069507950895099510951195129513951495159516951795189519952095219522952395249525952695279528952995309531953295339534953595369537953895399540954195429543954495459546954795489549955095519552955395549555955695579558955995609561956295639564956595669567956895699570957195729573957495759576957795789579958095819582958395849585958695879588958995909591959295939594959595969597959895999600960196029603960496059606960796089609961096119612961396149615961696179618961996209621962296239624962596269627962896299630963196329633963496359636963796389639964096419642964396449645964696479648964996509651965296539654965596569657965896599660966196629663966496659666966796689669967096719672967396749675967696779678967996809681968296839684968596869687968896899690969196929693969496959696969796989699970097019702970397049705970697079708970997109711971297139714971597169717971897199720972197229723972497259726972797289729973097319732973397349735973697379738973997409741974297439744974597469747974897499750975197529753975497559756975797589759976097619762976397649765976697679768976997709771977297739774977597769777977897799780978197829783978497859786978797889789979097919792979397949795979697979798979998009801980298039804980598069807980898099810981198129813981498159816981798189819982098219822982398249825982698279828982998309831983298339834983598369837983898399840984198429843984498459846984798489849985098519852985398549855985698579858985998609861986298639864986598669867986898699870987198729873987498759876987798789879988098819882988398849885988698879888988998909891989298939894989598969897989898999900990199029903990499059906990799089909991099119912991399149915991699179918991999209921992299239924992599269927992899299930993199329933993499359936993799389939994099419942994399449945994699479948994999509951995299539954995599569957995899599960996199629963996499659966996799689969997099719972997399749975997699779978997999809981998299839984998599869987998899899990999199929993999499959996999799989999100001000110002100031000410005100061000710008100091001010011100121001310014100151001610017100181001910020100211002210023100241002510026100271002810029100301003110032100331003410035100361003710038100391004010041100421004310044100451004610047100481004910050100511005210053100541005510056100571005810059100601006110062100631006410065100661006710068100691007010071100721007310074100751007610077100781007910080100811008210083100841008510086100871008810089100901009110092100931009410095100961009710098100991010010101101021010310104101051010610107101081010910110101111011210113101141011510116101171011810119101201012110122101231012410125101261012710128101291013010131101321013310134101351013610137101381013910140101411014210143101441014510146101471014810149101501015110152101531015410155101561015710158101591016010161101621016310164101651016610167101681016910170101711017210173101741017510176101771017810179101801018110182101831018410185101861018710188101891019010191101921019310194101951019610197101981019910200102011020210203102041020510206102071020810209102101021110212102131021410215102161021710218102191022010221102221022310224102251022610227102281022910230102311023210233102341023510236102371023810239102401024110242102431024410245102461024710248102491025010251102521025310254102551025610257102581025910260102611026210263102641026510266102671026810269102701027110272102731027410275102761027710278102791028010281102821028310284102851028610287102881028910290102911029210293102941029510296102971029810299103001030110302103031030410305103061030710308103091031010311103121031310314103151031610317103181031910320103211032210323103241032510326103271032810329103301033110332103331033410335103361033710338103391034010341103421034310344103451034610347103481034910350103511035210353103541035510356103571035810359103601036110362103631036410365103661036710368103691037010371103721037310374103751037610377103781037910380103811038210383103841038510386103871038810389103901039110392103931039410395103961039710398103991040010401104021040310404104051040610407104081040910410104111041210413104141041510416104171041810419104201042110422104231042410425104261042710428104291043010431104321043310434104351043610437104381043910440104411044210443104441044510446104471044810449104501045110452104531045410455104561045710458104591046010461104621046310464104651046610467104681046910470104711047210473104741047510476104771047810479104801048110482104831048410485104861048710488104891049010491104921049310494104951049610497104981049910500105011050210503105041050510506105071050810509105101051110512105131051410515105161051710518105191052010521105221052310524105251052610527105281052910530105311053210533105341053510536105371053810539105401054110542105431054410545105461054710548105491055010551105521055310554105551055610557105581055910560105611056210563105641056510566105671056810569105701057110572105731057410575105761057710578105791058010581105821058310584105851058610587105881058910590105911059210593105941059510596105971059810599106001060110602106031060410605106061060710608106091061010611106121061310614106151061610617106181061910620106211062210623106241062510626106271062810629106301063110632106331063410635106361063710638106391064010641106421064310644106451064610647106481064910650106511065210653106541065510656106571065810659106601066110662106631066410665106661066710668106691067010671106721067310674106751067610677106781067910680106811068210683106841068510686106871068810689106901069110692106931069410695106961069710698106991070010701107021070310704107051070610707107081070910710107111071210713107141071510716107171071810719107201072110722107231072410725107261072710728107291073010731107321073310734107351073610737107381073910740107411074210743107441074510746107471074810749107501075110752107531075410755107561075710758107591076010761107621076310764107651076610767107681076910770107711077210773107741077510776107771077810779107801078110782107831078410785107861078710788107891079010791107921079310794107951079610797107981079910800108011080210803108041080510806108071080810809108101081110812108131081410815108161081710818108191082010821108221082310824108251082610827108281082910830108311083210833108341083510836108371083810839108401084110842108431084410845108461084710848108491085010851108521085310854108551085610857108581085910860108611086210863108641086510866108671086810869108701087110872108731087410875108761087710878108791088010881108821088310884108851088610887108881088910890108911089210893108941089510896108971089810899109001090110902109031090410905109061090710908109091091010911109121091310914109151091610917109181091910920109211092210923109241092510926109271092810929109301093110932109331093410935109361093710938109391094010941109421094310944109451094610947109481094910950109511095210953109541095510956109571095810959109601096110962109631096410965109661096710968109691097010971109721097310974109751097610977109781097910980109811098210983109841098510986109871098810989109901099110992109931099410995109961099710998109991100011001110021100311004110051100611007110081100911010110111101211013110141101511016110171101811019110201102111022110231102411025110261102711028110291103011031110321103311034110351103611037110381103911040110411104211043110441104511046110471104811049110501105111052110531105411055110561105711058110591106011061110621106311064110651106611067110681106911070110711107211073110741107511076110771107811079110801108111082110831108411085110861108711088110891109011091110921109311094110951109611097110981109911100111011110211103111041110511106111071110811109111101111111112111131111411115111161111711118111191112011121111221112311124111251112611127111281112911130111311113211133111341113511136111371113811139111401114111142111431114411145111461114711148111491115011151111521115311154111551115611157111581115911160111611116211163111641116511166111671116811169111701117111172111731117411175111761117711178111791118011181111821118311184111851118611187111881118911190111911119211193111941119511196111971119811199112001120111202112031120411205112061120711208112091121011211112121121311214112151121611217112181121911220112211122211223112241122511226112271122811229112301123111232112331123411235112361123711238112391124011241112421124311244112451124611247112481124911250112511125211253112541125511256112571125811259112601126111262112631126411265112661126711268112691127011271112721127311274112751127611277112781127911280112811128211283112841128511286112871128811289112901129111292112931129411295112961129711298112991130011301113021130311304113051130611307113081130911310113111131211313113141131511316113171131811319113201132111322113231132411325113261132711328113291133011331113321133311334113351133611337113381133911340113411134211343113441134511346113471134811349113501135111352113531135411355113561135711358113591136011361113621136311364113651136611367113681136911370113711137211373113741137511376113771137811379113801138111382113831138411385113861138711388113891139011391113921139311394113951139611397113981139911400114011140211403114041140511406114071140811409114101141111412114131141411415114161141711418114191142011421114221142311424114251142611427114281142911430114311143211433114341143511436114371143811439114401144111442114431144411445114461144711448114491145011451114521145311454114551145611457114581145911460114611146211463114641146511466114671146811469114701147111472114731147411475114761147711478114791148011481114821148311484114851148611487114881148911490114911149211493114941149511496114971149811499115001150111502115031150411505115061150711508115091151011511115121151311514115151151611517115181151911520115211152211523115241152511526115271152811529115301153111532115331153411535115361153711538115391154011541115421154311544115451154611547115481154911550115511155211553115541155511556115571155811559115601156111562115631156411565115661156711568115691157011571115721157311574115751157611577115781157911580115811158211583115841158511586115871158811589115901159111592115931159411595115961159711598115991160011601116021160311604116051160611607116081160911610116111161211613116141161511616116171161811619116201162111622116231162411625116261162711628116291163011631116321163311634116351163611637116381163911640116411164211643116441164511646116471164811649116501165111652116531165411655116561165711658116591166011661116621166311664116651166611667116681166911670116711167211673116741167511676116771167811679116801168111682116831168411685116861168711688116891169011691116921169311694116951169611697116981169911700117011170211703117041170511706117071170811709117101171111712117131171411715117161171711718117191172011721117221172311724117251172611727117281172911730117311173211733117341173511736117371173811739117401174111742117431174411745117461174711748117491175011751117521175311754117551175611757117581175911760117611176211763117641176511766117671176811769117701177111772117731177411775117761177711778117791178011781117821178311784117851178611787117881178911790117911179211793117941179511796117971179811799118001180111802118031180411805118061180711808118091181011811118121181311814118151181611817118181181911820118211182211823118241182511826118271182811829118301183111832118331183411835118361183711838118391184011841118421184311844118451184611847118481184911850118511185211853118541185511856118571185811859118601186111862118631186411865118661186711868118691187011871118721187311874118751187611877118781187911880118811188211883118841188511886118871188811889118901189111892118931189411895118961189711898118991190011901119021190311904119051190611907119081190911910119111191211913119141191511916119171191811919119201192111922119231192411925119261192711928119291193011931119321193311934119351193611937119381193911940119411194211943119441194511946119471194811949119501195111952119531195411955119561195711958119591196011961119621196311964119651196611967119681196911970119711197211973119741197511976119771197811979119801198111982119831198411985119861198711988119891199011991119921199311994119951199611997119981199912000120011200212003120041200512006120071200812009120101201112012120131201412015120161201712018120191202012021120221202312024120251202612027120281202912030120311203212033120341203512036120371203812039120401204112042120431204412045120461204712048120491205012051120521205312054120551205612057120581205912060120611206212063120641206512066120671206812069120701207112072120731207412075120761207712078120791208012081120821208312084120851208612087120881208912090120911209212093120941209512096120971209812099121001210112102121031210412105121061210712108121091211012111121121211312114121151211612117121181211912120121211212212123121241212512126121271212812129121301213112132121331213412135121361213712138121391214012141121421214312144121451214612147121481214912150121511215212153121541215512156121571215812159121601216112162121631216412165121661216712168121691217012171121721217312174121751217612177121781217912180121811218212183121841218512186121871218812189121901219112192121931219412195121961219712198121991220012201122021220312204122051220612207122081220912210122111221212213122141221512216122171221812219122201222112222122231222412225122261222712228122291223012231122321223312234122351223612237122381223912240122411224212243122441224512246122471224812249122501225112252122531225412255122561225712258122591226012261122621226312264122651226612267122681226912270122711227212273122741227512276122771227812279122801228112282122831228412285122861228712288122891229012291122921229312294122951229612297122981229912300123011230212303123041230512306123071230812309123101231112312123131231412315123161231712318123191232012321123221232312324123251232612327123281232912330123311233212333123341233512336123371233812339123401234112342123431234412345123461234712348123491235012351123521235312354123551235612357123581235912360123611236212363123641236512366123671236812369123701237112372123731237412375123761237712378123791238012381123821238312384123851238612387123881238912390123911239212393123941239512396123971239812399124001240112402124031240412405124061240712408124091241012411124121241312414124151241612417124181241912420124211242212423124241242512426124271242812429124301243112432124331243412435124361243712438124391244012441124421244312444124451244612447124481244912450124511245212453124541245512456124571245812459124601246112462124631246412465124661246712468124691247012471124721247312474124751247612477124781247912480124811248212483124841248512486124871248812489124901249112492124931249412495124961249712498124991250012501125021250312504125051250612507125081250912510125111251212513125141251512516125171251812519125201252112522125231252412525125261252712528125291253012531125321253312534125351253612537125381253912540125411254212543125441254512546125471254812549125501255112552125531255412555125561255712558125591256012561125621256312564125651256612567125681256912570125711257212573125741257512576125771257812579125801258112582125831258412585125861258712588125891259012591125921259312594125951259612597125981259912600126011260212603126041260512606126071260812609126101261112612126131261412615126161261712618126191262012621126221262312624126251262612627126281262912630126311263212633126341263512636126371263812639126401264112642126431264412645126461264712648126491265012651126521265312654126551265612657126581265912660126611266212663126641266512666126671266812669126701267112672126731267412675126761267712678126791268012681126821268312684126851268612687126881268912690126911269212693126941269512696126971269812699127001270112702127031270412705127061270712708127091271012711127121271312714127151271612717127181271912720127211272212723127241272512726127271272812729127301273112732127331273412735127361273712738127391274012741127421274312744127451274612747127481274912750127511275212753127541275512756127571275812759127601276112762127631276412765127661276712768127691277012771127721277312774127751277612777127781277912780127811278212783127841278512786127871278812789127901279112792127931279412795127961279712798127991280012801128021280312804128051280612807128081280912810128111281212813128141281512816128171281812819128201282112822128231282412825128261282712828128291283012831128321283312834128351283612837128381283912840128411284212843128441284512846128471284812849128501285112852128531285412855128561285712858128591286012861128621286312864128651286612867128681286912870128711287212873128741287512876128771287812879128801288112882128831288412885128861288712888128891289012891128921289312894128951289612897128981289912900129011290212903129041290512906129071290812909129101291112912129131291412915129161291712918129191292012921129221292312924129251292612927129281292912930129311293212933129341293512936129371293812939129401294112942129431294412945129461294712948129491295012951129521295312954129551295612957129581295912960129611296212963129641296512966129671296812969129701297112972129731297412975129761297712978129791298012981129821298312984129851298612987129881298912990129911299212993129941299512996129971299812999130001300113002130031300413005130061300713008130091301013011130121301313014130151301613017130181301913020130211302213023130241302513026130271302813029130301303113032130331303413035130361303713038130391304013041130421304313044130451304613047130481304913050130511305213053130541305513056130571305813059130601306113062130631306413065130661306713068130691307013071130721307313074130751307613077130781307913080130811308213083130841308513086130871308813089130901309113092130931309413095130961309713098130991310013101131021310313104131051310613107131081310913110131111311213113131141311513116131171311813119131201312113122131231312413125131261312713128131291313013131131321313313134131351313613137131381313913140131411314213143131441314513146131471314813149131501315113152131531315413155131561315713158131591316013161131621316313164131651316613167131681316913170131711317213173131741317513176131771317813179131801318113182131831318413185131861318713188131891319013191131921319313194131951319613197131981319913200132011320213203132041320513206132071320813209132101321113212132131321413215132161321713218132191322013221132221322313224132251322613227132281322913230132311323213233132341323513236132371323813239132401324113242132431324413245132461324713248132491325013251132521325313254132551325613257132581325913260132611326213263132641326513266132671326813269132701327113272132731327413275132761327713278132791328013281132821328313284132851328613287132881328913290132911329213293132941329513296132971329813299133001330113302133031330413305133061330713308133091331013311133121331313314133151331613317133181331913320133211332213323133241332513326133271332813329133301333113332133331333413335133361333713338133391334013341133421334313344133451334613347133481334913350133511335213353133541335513356133571335813359133601336113362133631336413365133661336713368133691337013371133721337313374133751337613377133781337913380133811338213383133841338513386133871338813389133901339113392133931339413395133961339713398133991340013401134021340313404134051340613407134081340913410134111341213413134141341513416134171341813419134201342113422134231342413425134261342713428134291343013431134321343313434134351343613437134381343913440134411344213443134441344513446134471344813449134501345113452134531345413455134561345713458134591346013461134621346313464134651346613467134681346913470134711347213473134741347513476134771347813479134801348113482134831348413485134861348713488134891349013491134921349313494134951349613497134981349913500135011350213503135041350513506135071350813509135101351113512135131351413515135161351713518135191352013521135221352313524135251352613527135281352913530135311353213533135341353513536135371353813539135401354113542135431354413545135461354713548135491355013551135521355313554135551355613557135581355913560135611356213563135641356513566135671356813569135701357113572135731357413575135761357713578135791358013581135821358313584135851358613587135881358913590135911359213593135941359513596135971359813599136001360113602136031360413605136061360713608136091361013611136121361313614136151361613617136181361913620136211362213623136241362513626136271362813629136301363113632136331363413635136361363713638136391364013641136421364313644136451364613647136481364913650136511365213653136541365513656136571365813659136601366113662136631366413665136661366713668136691367013671136721367313674136751367613677136781367913680136811368213683136841368513686136871368813689136901369113692136931369413695136961369713698136991370013701137021370313704137051370613707137081370913710137111371213713137141371513716137171371813719137201372113722137231372413725137261372713728137291373013731137321373313734137351373613737137381373913740137411374213743137441374513746137471374813749137501375113752137531375413755137561375713758137591376013761137621376313764137651376613767137681376913770137711377213773137741377513776137771377813779137801378113782137831378413785137861378713788137891379013791137921379313794137951379613797137981379913800138011380213803138041380513806138071380813809138101381113812138131381413815138161381713818138191382013821138221382313824138251382613827138281382913830138311383213833138341383513836138371383813839138401384113842138431384413845138461384713848138491385013851138521385313854138551385613857138581385913860138611386213863138641386513866138671386813869138701387113872138731387413875138761387713878138791388013881138821388313884138851388613887138881388913890138911389213893138941389513896138971389813899139001390113902139031390413905139061390713908139091391013911139121391313914139151391613917139181391913920139211392213923139241392513926139271392813929139301393113932139331393413935139361393713938139391394013941139421394313944139451394613947139481394913950139511395213953139541395513956139571395813959139601396113962139631396413965139661396713968139691397013971139721397313974139751397613977139781397913980139811398213983139841398513986139871398813989139901399113992139931399413995139961399713998139991400014001140021400314004140051400614007140081400914010140111401214013140141401514016140171401814019140201402114022140231402414025140261402714028140291403014031140321403314034140351403614037140381403914040140411404214043140441404514046140471404814049140501405114052140531405414055140561405714058140591406014061140621406314064140651406614067140681406914070140711407214073140741407514076140771407814079140801408114082140831408414085140861408714088140891409014091140921409314094140951409614097140981409914100141011410214103141041410514106141071410814109141101411114112141131411414115141161411714118141191412014121141221412314124141251412614127141281412914130141311413214133141341413514136141371413814139141401414114142141431414414145141461414714148141491415014151141521415314154141551415614157141581415914160141611416214163141641416514166141671416814169141701417114172141731417414175141761417714178141791418014181141821418314184141851418614187141881418914190141911419214193141941419514196141971419814199142001420114202142031420414205142061420714208142091421014211142121421314214142151421614217142181421914220142211422214223142241422514226142271422814229142301423114232142331423414235142361423714238142391424014241142421424314244142451424614247142481424914250142511425214253142541425514256142571425814259142601426114262142631426414265142661426714268142691427014271142721427314274142751427614277142781427914280142811428214283142841428514286142871428814289142901429114292142931429414295142961429714298142991430014301143021430314304143051430614307143081430914310143111431214313143141431514316143171431814319143201432114322143231432414325143261432714328143291433014331143321433314334143351433614337143381433914340143411434214343143441434514346143471434814349143501435114352143531435414355143561435714358143591436014361143621436314364143651436614367143681436914370143711437214373143741437514376143771437814379143801438114382143831438414385143861438714388143891439014391143921439314394143951439614397143981439914400144011440214403144041440514406144071440814409144101441114412144131441414415144161441714418144191442014421144221442314424144251442614427144281442914430144311443214433144341443514436144371443814439144401444114442144431444414445144461444714448144491445014451144521445314454144551445614457144581445914460144611446214463144641446514466144671446814469144701447114472144731447414475144761447714478144791448014481144821448314484144851448614487144881448914490144911449214493144941449514496144971449814499145001450114502145031450414505145061450714508145091451014511145121451314514145151451614517145181451914520145211452214523145241452514526145271452814529145301453114532145331453414535145361453714538import { AsyncLocalStorage } from "node:async_hooks";import { randomUUID } from "node:crypto";import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";import { tmpdir } from "node:os";import { basename, dirname, join, resolve } from "node:path";import { Agent, type AgentContext, AgentContinueError, type AgentEvent, type AgentMessage, type AgentState, type AgentTool, type GetContinuationMessagesContext, type ShouldStopAfterTurnContext, type ThinkingLevel,} from "@earendil-works/pi-agent-core";import { type Api, type AssistantMessage, clampThinkingLevel, classifyStreamFailure, cleanupSessionResources, getSupportedThinkingLevels, type ImageContent, isContextOverflow, type Model, modelsAreEqual, resetApiProviders, type ServiceTier, supportsFastMode, type TextContent, type Usage, type UserMessage,} from "@earendil-works/pi-ai";import { theme } from "../modes/interactive/theme/theme.js";import { stripFrontmatter } from "../utils/frontmatter.js";import { sleep } from "../utils/sleep.js";import { AGENT_MESSAGE_CUSTOM_TYPE, AGENT_MESSAGE_RECEIVED_PREVIEW_LABEL, AGENT_MESSAGE_SKILL_NAME, type AgentFamilyCatalogEntry, type AgentFamilyRelationship, type AgentSessionMessage, type AgentSessionMessageAgentSummary, type AgentSessionMessageController, type AgentSessionMessageListResult, type AgentSessionMessageReceipt, agentFamilyMemberName, assertAgentSessionNameAvailable, assertDirectAgentMessageTarget, createAgentMessageHostHandlers, formatAgentSessionNameUnavailable, isAgentSessionMessage, isAgentSessionMessageId, isAgentSessionMessagePrompt, normalizeAgentSessionMessage, parseAgentSessionMessagePromptId, startsAgentRun,} from "./agent-messages.js";import { AGENT_OBSERVE_SKILL_NAME, type AgentObserveAgentSnapshot, type AgentObserveController, type AgentObserveListResult, type AgentObserveRecentMessagesResult, createAgentObserveHostHandlers, normalizeObserveLimit, normalizeObserveMaxChars, ORCHESTRATION_HEARTBEAT_SKILL_NAME,} from "./agent-observe.js";import { type AgentSessionController, createAgentSessionHostHandlers } from "./agent-sessions.js";import { addLoginGuidanceToAuthError, formatAuthenticationFailedMessage, formatNoApiKeyFoundMessage, formatNoModelSelectedMessage, isLikelyAuthenticationError,} from "./auth-guidance.js";import type { AuthSourceToken } from "./auth-storage.js";import { type AgentAutonomousConfig, type AgentAutonomousStatus, type AutonomousRuntimeState, addAutonomousContinuation, addAutonomousUsage, autonomousLimitReason, autonomousStatus, createAutonomousContinuationMessage, createAutonomousGateFailureContinuationMessage, createAutonomousRuntimeState, createAutonomousSubagentKeepAliveMessage, isUnlimitedAutonomousLimit, MAX_SUBAGENT_KEEP_ALIVE_MS, nextAutonomousContinuation, refreshAutonomousQualityGates, setAutonomousEnabled, setAutonomousLimits, shouldAutonomouslyContinue, UNLIMITED_AUTONOMOUS_LIMIT,} from "./autonomous.js";import { type BashResult, executeBashWithOperations } from "./bash-executor.js";import { COMPACT_SKILL_NAME, type CompactionResult, type CompactionSettings, calculateContextTokens, calculatePromptPayloadCeiling, collectEntriesForBranchSummary, compact, estimateContextTokens, generateBranchSummary, parseCompactCommandArgs, prepareCompaction, serializeConversation, shouldCompact,} from "./compaction/index.js";import { type ContextTreeNode, type ContextWindowResolver, computeOwnAndTotalUsage, loadContextTreeChildFromDisk, loadContextTreeChildrenFromDisk,} from "./context-tree.js";import type { AgentCronJob, AgentRlmHeartbeatController, AgentRlmHeartbeatStatusUpdate } from "./cron-jobs.js";import { normalizeHeartbeatDeliveryMode } from "./cron-jobs.js";import { DEFAULT_THINKING_LEVEL } from "./defaults.js";import { exportSessionToHtml, type ToolHtmlRenderer } from "./export-html/index.js";import { createToolHtmlRenderer } from "./export-html/tool-renderer.js";import { type ContextUsage, type ExtensionCommandContextActions, type ExtensionErrorListener, ExtensionRunner, type ExtensionUIContext, type InputSource, type MessageEndEvent, type MessageStartEvent, type MessageUpdateEvent, type ReplacedSessionContext, type SessionBeforeCompactResult, type SessionBeforeRefineResult, type SessionBeforeTreeResult, type SessionStartEvent, type ShutdownHandler, type ToolDefinition, type ToolExecutionEndEvent, type ToolExecutionStartEvent, type ToolExecutionUpdateEvent, type ToolInfo, type TreePreparation, type TurnEndEvent, type TurnStartEvent, wrapRegisteredTools,} from "./extensions/index.js";import { emitSessionShutdownEvent } from "./extensions/runner.js";import { createGoalContextMessage, emptyGoalState, GOAL_CONTEXT_CUSTOM_TYPE, GOAL_CONTEXT_PREVIEW_LABEL, GOAL_SKILL_NAME, GOAL_STATE_CUSTOM_TYPE, type GoalContextDetails, type GoalHostResponse, type GoalState, type GoalStatus, goalHostResponse, goalTokenDeltaForUsage, isPersistedGoalState, normalizeGoalState, validateGoalBudget, validateGoalObjective,} from "./goals.js";import type { HostRequestHandlers, KernelSentAgentMessage } from "./kernel/index.js";import { ensureRemoteKernelRuntime } from "./kernel/remote-bootstrap.js";import { type RestoreResult, snapshotPathIn } from "./kernel/state-snapshot.js";import type { AcpMcpServerConfig } from "./mcp/acp-mcp-types.js";import type { McpManager } from "./mcp/mcp-manager.js";import { ASYNC_BASH_COMPLETION_CUSTOM_TYPE, ASYNC_BASH_COMPLETION_PREVIEW_LABEL, type AsyncBashCompletionDetails, type BashExecutionMessage, type CompactionOutcome, type CompactionOutcomeReason, type CustomMessage, convertToLlm, createAsyncBashCompletionMessage, createCompactionOutcomeMessage, createHarnessDigestMessage, createHeartbeatPromptMessage, createInterruptedToolResultMessage, createLanguageDriftNoticeMessage, createRefinementNoticeMessage, createRefinementOutcomeMessage, createRlmChildFailureMessage, createRlmChildTerminalNoticeMessage, createSessionSlashCommandMessage, createSessionSlashCommandResultMessage, detectLanguageDriftScript, HARNESS_DIGEST_CUSTOM_TYPE, type HarnessDigestDetails, HEARTBEAT_PROMPT_CUSTOM_TYPE, HEARTBEAT_PROMPT_PREVIEW_LABEL, IPYTHON_STATE_RESTORED_CUSTOM_TYPE, isSessionSlashCommandMessage, type RefinementSource, RLM_CHILD_FAILURE_CUSTOM_TYPE, RLM_CHILD_TERMINAL_NOTICE_CUSTOM_TYPE, type RlmChildTerminalNoticeDetails, unansweredToolCalls,} from "./messages.js";import type { ModelRegistry } from "./model-registry.js";import { findExactModelReferenceMatch } from "./model-resolver.js";import { throwIfPromptAdmissionCancelled } from "./prompt-admission.js";import { evaluateActiveCacheTtl, extractSessionCacheState, type PromptCacheSnapshot } from "./prompt-cache.js";import { expandPromptTemplate, type PromptTemplate, parseCommandArgs } from "./prompt-templates.js";import { isAgentLifecycleFailure, isFauxProviderQueueExhausted, isPermanentProviderFailureKind, type ProviderWaitPolicy, parseProviderResetMs, providerRetryDelay, providerRetryPolicy, providerStreamFailureKind, providerStreamFailureRetryAfterMs, providerStreamFailureStatus, providerWaitClass, providerWaitDecision,} from "./provider-retry.js";import { type AutoRefineReason, type AutoRefineReview, appendGlobalRefinement, applyRefinementProposal, formatHarnessStateForPrompt, generateRefinementId, getGlobalHarnessStateDir, getLocalHarnessStateDir, getRefinementHistory, type HarnessQueryTerms, type HarnessState, harnessQueryTerms, inferRefinementResultScope, loadGlobalRefinementHistory, loadHarnessState, mergeHarnessStates, mergeRefinementHistory, normalizeRefinementProposal, planRefinement, REFINE_SKILL_NAME, type RefinementPlan, type RefinementResult, reviewAutoRefine, saveHarnessState,} from "./refinement/index.js";import { resolveConfigValue } from "./resolve-config-value.js";import type { ResourceExtensionPaths, ResourceLoader } from "./resource-loader.js";import { RlmModelsStore } from "./rlm-models.js";import { type CreateRlmSubagentRuntimeOptions, createAsyncBashCompletionHostHandler, createAsyncBashConsumedHostHandler, createDefaultRlmSubagentSessionName, createRlmCollectHostHandler, createRlmCreateSessionHostHandler, createRlmDeleteSubagentHostHandler, createRlmListSubagentsHostHandler, createRlmModelsHostHandler, createRlmProgressNoteHostHandler, createRlmRunHostHandler, findUniqueRlmShortFormModelMatch, formatRlmModelUnavailableError, normalizeRequestedRlmSubagentModel, normalizeRequestedRlmSubagentSessionName, normalizeRequestedRlmSubagentThinkingLevel, type RlmCollectResult, type RlmCollectResultEntry, type RlmCreateSessionResult, type RlmDeleteSubagentResult, type RlmListSubagentsResult, type RlmProgressNoteResult, type RlmSpawnHandle, type RlmSubagentRegistryEntry, type RlmSubagentRuntime, type SubagentRuntimeHost,} from "./rlm-runtime.js";import { modelRequestHeaders, SemanticEdgeRecorder, semanticEdgeLedgerPath, wrapStreamFnWithSemanticEdges,} from "./semantic-edges.js";import { ActionStore, type ActionTicket, canSelectSessionAction, type DeliveryPolicy, type DeliveryRecord, type QueuedMessageLane, type QueuedMessageMutation, type QueuedMessageMutationStatus, queuedMessageLaneDeliveryPolicy, type RuntimeActivity, type SessionAction, type SessionActionPlacement, type SessionActionPriority, type SessionActionSnapshot, type SessionCommandPayload, type SessionTurnPayload, transitionSessionAction, type WakePolicy,} from "./session-action-store.js";import type { BranchSummaryEntry, ChildUsageAttributionEntry, CompactionEntry, SessionContext, SessionEntry, SessionMessageEntry,} from "./session-manager.js";import { CURRENT_SESSION_VERSION, getLatestCompactionEntry, type SessionHeader, SessionManager,} from "./session-manager.js";import type { SessionStats } from "./session-stats.js";import type { SettingsManager } from "./settings-manager.js";import { getPythonSkillRuntimeInfo, type Skill } from "./skills.js";import { BUILTIN_SLASH_COMMANDS, findSlashCommandSuggestion, isBuiltinSlashCommandName, parseRefineCommandOptions, parseSessionSlashCommand, parseSlashCommand, SESSION_SLASH_COMMAND_NAMES, type SessionSlashCommand, type SlashCommandInfo,} from "./slash-commands.js";import { createSyntheticSourceInfo, type SourceInfo } from "./source-info.js";import { type BuildSystemPromptOptions, buildSystemPrompt } from "./system-prompt.js";import { THINKING_LEVELS } from "./thinking-levels.js";import { acpMcpToolNames, createAcpMcpToolDefinitions } from "./tools/acp-mcp.js";import { type BashOperations, createLocalBashOperations } from "./tools/bash.js";import { createAllToolDefinitions } from "./tools/index.js";import { IpythonKernelProvisioner } from "./tools/ipython.js";import { createToolDefinitionFromAgentTool } from "./tools/tool-definition-wrapper.js";import { addAssistantUsage, cloneUsage, emptyUsage, type SessionUsageSummary, sessionUsageSummaryFrom, subtractAssistantUsage,} from "./usage.js";import { SERPER_CREDENTIAL_ID, SERPER_ENV_VAR, WEBSEARCH_SKILL_NAME } from "./websearch-credential.js";
export type { GoalState, GoalStatus } from "./goals.js";export type { SessionStats } from "./session-stats.js";export { type ParsedSkillBlock, parseSkillBlock } from "./skill-blocks.js";
export type RlmChildAgentStatus = "queued" | "running" | "done" | "error" | "cancelled";
export interface RlmChildAgentActivity { kind: "waiting" | "writing" | "executing"; toolName?: string;}
export interface RlmChildAgentSnapshot { id: string; parentId?: string; activeSessionId?: string; sessionName?: string; model?: string; label: string; status: RlmChildAgentStatus; durationMs?: number; answerPreview?: string; toolUseCount?: number; tokenCount?: number; recap?: string; sessionDir: string; activity?: RlmChildAgentActivity; repliedSinceTask?: boolean; /** Latest child progress note (`rlm.progress.note`), newest wins. */ progressNote?: string; /** Wall-clock ms of the last tracked child activity (seeded at admission, then model/tool/note events). */ lastActivityAt?: number; /** * Active time in ms since the last tracked activity, once past the staleness * threshold, for a running child that is not executing a tool call. * Measured against the monotonic clock, so a host sleep that freezes the * session does not flag every running child stale on wake. */ activityStaleMs?: number; error?: string; remoteHost?: string;}
export type CompactionReason = "manual" | "threshold" | "overflow" | "requested";
export type AgentSessionEvent = | AgentEvent | { type: "ipython_sent_agent_message"; toolCallId: string; message: KernelSentAgentMessage; } | { type: "session_action_update"; actions: SessionActionSnapshot } | { type: "compaction_start"; reason: CompactionReason; customInstructions?: string; } | { type: "session_info_changed"; name: string | undefined } | { type: "thinking_level_changed"; level: ThinkingLevel } | { type: "service_tier_changed"; serviceTier: ServiceTier } | { type: "compaction_end"; reason: CompactionReason; result: CompactionResult | undefined; aborted: boolean; willRetry: boolean; errorMessage?: string; errorSeverity?: "warning" | "error"; customInstructions?: string; } | { type: "auto_retry_start"; attempt: number; maxAttempts: number; delayMs: number; errorMessage: string; /** Why the retry loop re-issues the turn; absent = ordinary quick retry. */ reason?: "usage" | "unavailable" | "backup"; /** Present when reason is "backup": "provider/model-id" of the backup. */ backupModel?: string; } | { type: "auto_retry_end"; success: boolean; attempt: number; finalError?: string; /** "provider/model-id" restored after a backup-model retry succeeded. */ restoredModel?: string; } | { type: "auth_stale"; provider: string; sourceTokens?: readonly AuthSourceToken[]; } | { type: "rlm_child_update"; child: RlmChildAgentSnapshot } | { type: "rlm_progress_note"; message: string; timestamp: number } | { type: "recap_update"; recap: string | undefined } | { type: "goal_update"; goal: GoalState } | { type: "bash_start"; command: string; excludeFromContext: boolean; transient?: boolean; runId?: string; } | { type: "bash_output"; chunk: string } | { type: "bash_end"; exitCode: number | undefined; cancelled: boolean; truncated: boolean; fullOutputPath?: string; errorMessage?: string; transient?: boolean; runId?: string; } | { type: "refine_complete"; result: RefinementResult } | { type: "refine_failed"; error: string };
export type AgentSessionEventListener = (event: AgentSessionEvent) => void;
type UserBashEndDetails = { exitCode: number | undefined; cancelled: boolean; truncated: boolean; fullOutputPath?: string; errorMessage?: string;};
export class CompactionSkippedError extends Error {}
/** Thrown when a session_before_refine extension skips the refinement round. */export class RefineSkippedError extends Error {}
export interface AgentSessionConfig { agent: Agent; sessionManager: SessionManager; settingsManager: SettingsManager; serviceTierPreference?: ServiceTier; cwd: string; agentDir?: string; scopedModels?: Array<{ model: Model<any>; thinkingLevel?: ThinkingLevel }>; resourceLoader: ResourceLoader; customTools?: ToolDefinition[]; modelRegistry: ModelRegistry; initialActiveToolNames?: string[]; allowedToolNames?: string[]; /** * Whether the built-in long-running goals feature is available: the bundled * goal skill in the Python kernel, its goal.* host handlers, and /goal. * Default: true. */ includeGoals?: boolean; agentMessageController?: AgentSessionMessageController; agentObserveController?: AgentObserveController; agentSessionController?: AgentSessionController; /** * Whether the bundled compact skill and its compact.* host handlers are * available to the model. Default: the compaction.agentCallable setting. */ includeCompactSkill?: boolean; /** * Optional host-side controller for the bundled rlm-heartbeat Python skill. * When omitted, rlm_heartbeat.* host requests are unavailable. */ rlmHeartbeatController?: AgentRlmHeartbeatController; /** * Optional MCP integration manager. When present, its mcp.* host requests * (refresh, begin_login) are exposed to the kernel. */ mcpManager?: McpManager; /** * Override base tools (useful for custom runtimes). * * These are synthesized into minimal ToolDefinitions internally so AgentSession can keep * a definition-first registry even when callers provide plain AgentTool instances. */ baseToolsOverride?: Record<string, AgentTool>; extensionRunnerRef?: { current?: ExtensionRunner }; sessionStartEvent?: SessionStartEvent; rlmDepth?: number; rlmMaxDepth?: number; /** * Let this session's skills publish (`git push`, force reset) without the parent's * explicit authorization. Set only by a spawn that passed `allow_push=True`. */ rlmAllowPush?: boolean; /** Override the models configuration file. Defaults to <agent dir>/rlm-models.toml. */ rlmModelsFilePath?: string | null; rlmSessionDir?: string; rlmParentNodeId?: string; rlmParentAgent?: string; semanticParentSessionId?: string; semanticSpawnedByRequestId?: string; /** Remote host destination if the session's Python kernel executes over SSH. */ remoteHost?: string; subagentRuntimeHost?: SubagentRuntimeHost; autonomous?: AgentAutonomousConfig; prewarmIpythonKernel?: boolean; autoRefineReviewer?: AutoRefineReviewer; /** * When true, auto-refine runs synchronously between turns at the * shouldStopAfterTurn boundary instead of in the background after * agent_end. Used for print/headless autonomous runs so refinement * never overlaps the primary model request. Default: false. */ serializedRefine?: boolean; /** * Initial goal to seed at session creation. Only applied when rlmDepth * is 0 and no persisted thread_goal_state entry exists in the branch. */ initialGoal?: { objective: string; tokenBudget?: number };}
export interface ExtensionBindings { uiContext?: ExtensionUIContext; commandContextActions?: ExtensionCommandContextActions; shutdownHandler?: ShutdownHandler; onError?: ExtensionErrorListener;}
export interface AutoRefineReviewRequest { reason: AutoRefineReason; turnsSinceLastReview: number;}
/** * Discriminated result from a serialized-mode background planning pass. * - "plan": review approved and planning succeeded; carry the exact plan, * options, and abort controller so the boundary can apply directly * without a second planning request. * - "skip": reviewer declined; no refine needed. * - "failure": review or planning threw; boundary should not retry. */export type SerializedBackgroundPlanResult = | { status: "plan"; plan: RefinementPlan; options: { instructions?: string; rollbackId?: string; global?: boolean }; abort: AbortController; branchVersion: number; source: Exclude<RefinementSource, "user">; } | { status: "skip"; explicit?: boolean } | { status: "invalidated"; branchVersion: number } | { status: "failure"; explicit: boolean; options: { instructions?: string; rollbackId?: string; global?: boolean }; branchVersion: number; };
export type AutoRefineReviewer = (request: AutoRefineReviewRequest, signal?: AbortSignal) => Promise<AutoRefineReview>;
export interface PromptOptions { expandPromptTemplates?: boolean; images?: ImageContent[]; streamingBehavior?: "steer" | "followUp"; followUpQueueKey?: string; source?: InputSource; preflightResult?: (success: boolean, queued?: boolean) => void; queueIfBusy?: boolean; resumeIfIdle?: boolean; internalPrompt?: boolean; suppressAutonomousContinuation?: boolean; skipInputHandlers?: boolean; signal?: AbortSignal; admissionCommitted?: () => void; agentMessageId?: string; content?: (TextContent | ImageContent)[]; customMessage?: CustomMessage; /** Overrides the queue priority inferred from the source and message. */ priority?: SessionActionPriority;}
interface InternalPromptOptions extends PromptOptions { skipPrePromptWork?: boolean; returnAfterAccepted?: boolean; agentMessageId?: string;}
type SubmissionExtensionCommandPolicy = "execute" | "reject" | "ignore";
interface SubmissionNormalizationPolicy { parseSessionCommands: boolean; extensionCommands: SubmissionExtensionCommandPolicy; inputSource?: InputSource; expandSkills: boolean; expandPromptTemplates: boolean;}
type NormalizedSubmission = | { kind: "prompt"; text: string; images?: ImageContent[] } | { kind: "sessionCommand"; text: string; images?: ImageContent[]; command: SessionSlashCommand; } | { kind: "extensionCommand"; completion: Promise<void> } | { kind: "handled" };
type PreTurnCompactionTiming = "beforeModelSelection" | "afterModelSelection" | "skip";type RefineBarrierPolicy = "always" | "ifInFlight" | "skip";
interface CommitPreparationPolicy { initialRefineBarrier: RefineBarrierPolicy; flushPendingBashBeforeValidation: boolean; validateModelAndAuth: boolean; awaitPendingModelSelection: boolean; preTurnCompaction: PreTurnCompactionTiming; finalRefineBarrier: RefineBarrierPolicy;}
interface CommitPreparationSteps<TPrepared, TCommitted> { afterValidation?: () => void; prepare: () => Promise<TPrepared>; shouldCommit?: (prepared: TPrepared) => boolean; beforeFinalRefineBarrier?: (prepared: TPrepared) => void; commit: (prepared: TPrepared, passedFinalRefineBarrier: boolean) => TCommitted;}
type QueuedAgentMessage = UserMessage | CustomMessage;type SessionInputSchedule = "steer" | "followUp";
export interface TurnExecutionPolicy { preparation: CommitPreparationPolicy; runBeforeAgentStart: boolean; nextTurnContextTiming: "preparation" | "commit" | "skip"; preserveEmptyExtensionPrompt: boolean; completionIncludesRetryChain: boolean;}
/** * The failure a child already knows about when its last turn ended in a provider error. * Such a turn resolves normally, so the run would otherwise be recorded as `done` and the * parent would see only a no-reply notice, without the cause. */function childRlmTerminalFailure(child: AgentSession): string | undefined { const messages = child.messages; for (let index = messages.length - 1; index >= 0; index -= 1) { const message = messages[index]; if (message.role !== "assistant") continue; if (message.stopReason !== "error") return undefined; return message.errorMessage?.trim() || "the child's last turn failed before it produced an answer"; } return undefined;}
function turnExecutionPoliciesEqual(left: TurnExecutionPolicy, right: TurnExecutionPolicy): boolean { return ( left.preparation.initialRefineBarrier === right.preparation.initialRefineBarrier && left.preparation.flushPendingBashBeforeValidation === right.preparation.flushPendingBashBeforeValidation && left.preparation.validateModelAndAuth === right.preparation.validateModelAndAuth && left.preparation.awaitPendingModelSelection === right.preparation.awaitPendingModelSelection && left.preparation.preTurnCompaction === right.preparation.preTurnCompaction && left.preparation.finalRefineBarrier === right.preparation.finalRefineBarrier && left.runBeforeAgentStart === right.runBeforeAgentStart && left.nextTurnContextTiming === right.nextTurnContextTiming && left.preserveEmptyExtensionPrompt === right.preserveEmptyExtensionPrompt && left.completionIncludesRetryChain === right.completionIncludesRetryChain );}
interface PreparedTurnPayload extends SessionTurnPayload { images?: ImageContent[]; content?: (TextContent | ImageContent)[]; customMessage?: CustomMessage; prepared?: PreparedPromptPreparation; executionPolicy: TurnExecutionPolicy; queueVisible: boolean; acceptedAgentMessage: boolean; acceptedBeforeCompletion: boolean; captureRunMessages?: Set<AgentMessage>; cancelledDispatchEnded?: boolean;}
interface PreparedCommandPayload extends SessionCommandPayload { images?: ImageContent[];}
type QueuedSessionAction = SessionAction<PreparedTurnPayload | PreparedCommandPayload>;
interface PreparedPromptPreparation { result: Awaited<ReturnType<ExtensionRunner["emitBeforeAgentStart"]>>; basePromptSnapshot: string;}
class DeferredSessionInputError extends Error {}
class SessionInputAdmissionPausedError extends Error {}
function oncePreflight( preflightResult: ((success: boolean, queued?: boolean) => void) | undefined,): (success: boolean, queued?: boolean) => void { let settled = false; return (success, queued = false) => { if (!settled) { settled = true; preflightResult?.(success, queued); } };}
interface RestoredPromptInput { text: string; content?: (TextContent | ImageContent)[]; images?: ImageContent[]; queueKey?: string; agentMessageId?: string; customMessage?: CustomMessage; prefixMessages?: CustomMessage[];}
export const SESSION_ACTION_RECOVERY_FORMAT_VERSION = 1;
export interface SessionActionRecoveryRecord { id: string; role: DeliveryRecord["role"]; message: QueuedAgentMessage; ownerActionId: string;}
export type SessionActionRecoveryPayload = | { kind: "turn"; text: string; preview?: string; records: SessionActionRecoveryRecord[]; images?: ImageContent[]; content?: (TextContent | ImageContent)[]; customMessage?: CustomMessage; executionPolicy: TurnExecutionPolicy; queueVisible: boolean; acceptedAgentMessage: boolean; acceptedBeforeCompletion: boolean; } | { kind: "session_command"; text: string; command: SessionSlashCommand; images?: ImageContent[]; };
export interface SessionActionRecoveryAction { id: string; source: InputSource | "internal"; delivery: DeliveryPolicy; wake: WakePolicy; priority?: SessionActionPriority; payload: SessionActionRecoveryPayload; queueKey?: string; agentMessageId?: string; suppressAutonomousContinuation?: boolean;}
export interface SessionActionRecoverySnapshot { formatVersion: typeof SESSION_ACTION_RECOVERY_FORMAT_VERSION; actions: SessionActionRecoveryAction[];}
function cloneCustomMessage(message: CustomMessage): CustomMessage { return { ...message, content: Array.isArray(message.content) ? message.content.map((block) => ({ ...block })) : message.content, };}
function cloneQueuedAgentMessage(message: QueuedAgentMessage): QueuedAgentMessage { if (message.role === "custom") return cloneCustomMessage(message); return { ...message, content: Array.isArray(message.content) ? message.content.map((block) => ({ ...block })) : message.content, };}
function isHumanInputSource(source: InputSource | "internal"): boolean { return source === "interactive" || source === "rpc";}
/** * Only input a person submits outranks the rest of the queue. Agent-to-agent prompts carry an * agent message id, and every machine-generated turn (bash completions, heartbeats, continuations, * restored internal input) is either a custom message or admitted from a non-human source. Ids * callers mint to await their own prompt are not agent traffic and keep human priority. */function sessionActionPriorityFor( humanSource: boolean, message: QueuedAgentMessage | undefined, agentMessageId: string | undefined,): SessionActionPriority { if (!humanSource || isAgentSessionMessageId(agentMessageId)) return "background"; return message === undefined || message.role === "user" ? "user" : "background";}
function primaryDeliveryRecord(action: QueuedSessionAction): DeliveryRecord { if (action.payload.kind !== "turn") throw new Error(`Session action ${action.id} is not a turn`); const record = action.payload.records.find((candidate) => candidate.role === "primary"); if (!record) throw new Error(`Turn action ${action.id} has no primary delivery record`); return record;}
function normalizeMessageContent(content: string | (TextContent | ImageContent)[]): { text: string; images?: ImageContent[];} { if (typeof content === "string") return { text: content }; const text = content .filter((part): part is TextContent => part.type === "text") .map((part) => part.text) .join("\n"); const images = content.filter((part): part is ImageContent => part.type === "image"); return { text, ...(images.length > 0 ? { images } : {}) };}
function queuedAgentMessagePreview(action: QueuedSessionAction): string { const payload = action.payload; if (payload.kind === "session_command") return payload.text; if (payload.customMessage && isAgentSessionMessage(payload.customMessage)) { return `${AGENT_MESSAGE_RECEIVED_PREVIEW_LABEL}: ${payload.customMessage.details.message}`; } if (payload.customMessage?.customType === ASYNC_BASH_COMPLETION_CUSTOM_TYPE) { const details = payload.customMessage.details as AsyncBashCompletionDetails | undefined; return details ? `${ASYNC_BASH_COMPLETION_PREVIEW_LABEL}: pid ${details.pid}, exit ${details.exitCode}` : ASYNC_BASH_COMPLETION_PREVIEW_LABEL; } return payload.preview ?? payload.text;}
function visibleSessionActionProjection(actions: readonly QueuedSessionAction[]): readonly QueuedSessionAction[] { return actions.filter( (action) => action.payload.kind === "session_command" || action.payload.queueVisible || action.payload.acceptedAgentMessage, );}
const IPYTHON_SENT_AGENT_MESSAGE_CUSTOM_ENTRY = "ipython_sent_agent_message";
interface PersistedIpythonSentAgentMessage { toolCallId: string; message: KernelSentAgentMessage;}
function isObjectRecord(value: unknown): value is Record<string, unknown> { return typeof value === "object" && value !== null && !Array.isArray(value);}
function parsePersistedIpythonSentAgentMessage(value: unknown): PersistedIpythonSentAgentMessage | undefined { if (!isObjectRecord(value) || typeof value.toolCallId !== "string" || !isObjectRecord(value.message)) { return undefined; } const { id, message, deliveryStatus, target } = value.message; if ( typeof id !== "string" || typeof message !== "string" || (deliveryStatus !== "delivered" && deliveryStatus !== "queued") || !isObjectRecord(target) || typeof target.activeSessionId !== "string" || typeof target.sessionId !== "string" ) { return undefined; } return { toolCallId: value.toolCallId, message: { id, message, deliveryStatus, target: { activeSessionId: target.activeSessionId, sessionId: target.sessionId, ...(typeof target.sessionName === "string" ? { sessionName: target.sessionName } : {}), }, }, };}
function appendSentAgentMessageToToolResult( message: AgentMessage, toolCallId: string, sentMessage: KernelSentAgentMessage,): boolean { if (message.role !== "toolResult" || message.toolName !== "ipython" || message.toolCallId !== toolCallId) { return false; } const details = isObjectRecord(message.details) ? message.details : {}; const current = Array.isArray(details.sentAgentMessages) ? details.sentAgentMessages : []; if (current.some((entry) => isObjectRecord(entry) && entry.id === sentMessage.id)) { return true; } message.details = { ...details, sentAgentMessages: [...current, sentMessage], }; return true;}
function injectedMessagePreviewLabel(message: CustomMessage): string | undefined { switch (message.customType) { case HEARTBEAT_PROMPT_CUSTOM_TYPE: return HEARTBEAT_PROMPT_PREVIEW_LABEL; case ASYNC_BASH_COMPLETION_CUSTOM_TYPE: return ASYNC_BASH_COMPLETION_PREVIEW_LABEL; case GOAL_CONTEXT_CUSTOM_TYPE: return GOAL_CONTEXT_PREVIEW_LABEL; default: return undefined; }}
interface AgentMessageDeferred { promise: Promise<void>; resolve: () => void; reject: (error: Error) => void;}
interface AgentMessageOutcome { delivery?: AgentMessageDeferred; completion?: AgentMessageDeferred;}
function createAgentMessageDeferred(): AgentMessageDeferred { const deferred = {} as AgentMessageDeferred; deferred.promise = new Promise<void>((resolve, reject) => { deferred.resolve = resolve; deferred.reject = reject; }); deferred.promise.catch(() => undefined); return deferred;}
/** One-shot settlement for a scheduled post-compaction continuation; a settled failure is never re-exposed to later waiters. */interface PostCompactionContinuationSettlement extends AgentMessageDeferred { continueAfterSessionInput: boolean; settled: boolean;}
function createPostCompactionContinuationSettlement(): PostCompactionContinuationSettlement { return { ...createAgentMessageDeferred(), continueAfterSessionInput: false, settled: false };}
export interface ModelCycleResult { model: Model<any>; thinkingLevel: ThinkingLevel; serviceTier: ServiceTier; isScoped: boolean;}
interface ModelSelectOptions { waitForExtensions?: boolean;}
interface ToolDefinitionEntry { definition: ToolDefinition; sourceInfo: SourceInfo;}
type GoalSlashCommand = | { kind: "status" } | { kind: "clear" } | { kind: "pause" } | { kind: "resume" } | { kind: "start"; objective: string; tokenBudget?: number };
type AutonomousSlashCommand = { kind: "status" } | { kind: "on"; config?: AgentAutonomousConfig } | { kind: "off" };
import type { RlmMaxDepthSource, RlmMaxDepthStatus, SetRlmMaxDepthResult } from "./rlm-max-depth.js";
export type { RlmMaxDepthSource, RlmMaxDepthStatus, SetRlmMaxDepthResult } from "./rlm-max-depth.js";
interface PersistedRlmMaxDepthState { maxDepth: number;}
type AutonomousRuntimeSnapshot = Pick< AutonomousRuntimeState, "continuationsUsed" | "gateAttempts" | "lastGateFailure" | "lastGateFailureSnapshot">;
interface RlmChildRun { id: string; prompt: string; sessionName: string; sessionDir: string; model: Model<Api>; status: RlmChildAgentStatus; durationMs?: number; answerPreview?: string; toolUseCount: number; activity?: RlmChildAgentActivity; /** Bounded ring of the child's latest progress notes (newest last). */ progressNotes: string[]; /** * Wall-clock ms of the last tracked child activity; carried into snapshots. * Seeded at admission so a child that never emits a tracked event still * crosses the staleness threshold once running. */ lastActivityAt?: number; /** * Monotonic counterpart of lastActivityAt (performance.now()), written by * the same events. Staleness measures this so wall-clock jumps (a host * sleep freezing the whole session) do not inflate it. */ lastActivityMonotonicAt?: number; error?: string; remoteHost?: string; abort: () => void; publication: AgentMessageDeferred; /** Resolves after terminal result publication and detached-run cleanup finish. */ settlement: AgentMessageDeferred; /** Child session, once its runtime exists. Used to cancel nested child runs. */ session?: AgentSession; settled: boolean; /** Do not inject a late terminal notice after the parent session is aborted. */ suppressTerminalNotice?: boolean; /** Excluded from future strong barriers after an authoritative cancellation cut. */ abandonedForQuiescence?: boolean; /** Selector snapshot for an admitted explicit delete. */ detachedDeletion?: RlmSubagentRegistryEntry; /** Shared physical runtime cleanup owned by the explicit-delete path. */ deletionCleanup?: Promise<void>; deletionCleanupObserver?: Promise<boolean>; /** Resolves when a deletion may release its selector reservation. */ deletionReservation: AgentMessageDeferred; deletionCleanupFailed?: boolean; deletionRunFinished?: boolean; deletionNotice?: Promise<void>; deletionFailureNotice?: Promise<void>; deletionNeedsCompletionNotice?: boolean; completeDeletion?: () => Promise<void>; reportDeletionCleanupFailure?: (error: unknown) => Promise<void>; emitUpdate?: () => void; lastEmittedUpdate?: string; unsubscribe?: () => void;}
interface RetainedRlmChild { session: AgentSession; run?: RlmChildRun;}
interface RlmSubagentModelSelection { model: Model<Api>;}
/** Birth routing of an RLM child: the role decides it, and a follow-up never changes it. */
const KERNEL_STATE_LISTING_TIMEOUT_MS = 5000;const RLM_MAX_DEPTH_STATE_CUSTOM_TYPE = "rlm_max_depth_state";/** Minimum spacing between accepted progress notes from one child session. */const RLM_PROGRESS_NOTE_MIN_INTERVAL_MS = 10_000;/** Bounded ring of progress notes kept per child run; the snapshot exposes the newest. */const RLM_CHILD_PROGRESS_NOTE_RING_MAX = 5;/** A running child with no tracked activity for this long reports activityStaleMs. */const RLM_CHILD_STALE_ACTIVITY_THRESHOLD_MS = 10 * 60_000;/** Hard cap for answer previews carried into kernel roster entries. */const RLM_REGISTRY_ANSWER_PREVIEW_MAX_LENGTH = 200;/** Hard cap for labels carried into kernel roster entries (snapshots keep the full prompt). */const RLM_REGISTRY_LABEL_MAX_LENGTH = 200;const COMPACTION_THRESHOLD_STATE_CUSTOM_TYPE = "compaction_threshold_state";
interface PersistedCompactionThresholdState { threshold: number | null;}
function isPersistedCompactionThresholdState(value: unknown): value is PersistedCompactionThresholdState { if (typeof value !== "object" || value === null || Array.isArray(value)) return false; const candidate = value as Record<string, unknown>; return ( candidate.threshold === null || (typeof candidate.threshold === "number" && Number.isSafeInteger(candidate.threshold) && candidate.threshold > 0) );}
function noopRlmChildAbort(): void {}function noopRlmChildEventUnsubscribe(): void {}
function autoRefineInstructions(reason: AutoRefineReason, review: AutoRefineReview): string { const detail = review.instructions ? `Reviewer instructions: ${review.instructions}` : ""; return `Automatic refine review triggered by ${reason}. Only create/update/delete local harness entries if there is clear evidence that should help this session continue. Prefer an empty edits array over speculative or one-off memories. Do not promote anything global unless explicitly requested. Reviewer rationale: ${review.rationale}${detail}`;}
function isNonNegativeInteger(value: unknown): value is number { return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;}
function parseDepth(value: string | undefined, fallback: number, name: string): number { if (value === undefined || value === "") { return fallback; } if (!/^\d+$/.test(value)) { throw new Error(`${name} must be a non-negative integer`); } const parsed = Number(value); if (!isNonNegativeInteger(parsed)) { throw new Error(`${name} must be a non-negative integer`); } return parsed;}
function isPersistedRlmMaxDepthState(value: unknown): value is PersistedRlmMaxDepthState { return ( typeof value === "object" && value !== null && isNonNegativeInteger((value as PersistedRlmMaxDepthState).maxDepth) );}
function parseGoalBudgetValue(value: string): number { if (!/^[1-9]\d*$/.test(value)) { throw new Error("Goal token budget must be a positive integer."); } const budget = validateGoalBudget(Number(value)); if (budget === undefined) { throw new Error("Goal token budget must be a positive integer."); } return budget;}
const AUTONOMOUS_STATUS_NUMBER_FORMAT = new Intl.NumberFormat("en-US");
const AUTONOMOUS_BUDGET_USAGE = "Usage: /autonomous [status|off] or /autonomous on [--max-continuations <n|unlimited>] [--max-turns <n|unlimited>] [--max-tokens <n|unlimited>] [--timeout-ms <n|unlimited>] [--gate <command>] [--gate-retries <n>] [--gate-timeout-ms <n>] [--subagent-keep-alive-ms <n>]";
// `/autonomous` budget flags mirror the `--autonomous-*` CLI options. The CLI// spelling (`--autonomous-max-continuations`) is accepted as an alias so the// exact CLI budget flags also work from the slash command.const AUTONOMOUS_BUDGET_FLAGS: ReadonlySet<string> = new Set([ "max-continuations", "max-turns", "max-tokens", "timeout-ms", "gate", "gate-retries", "gate-timeout-ms", "subagent-keep-alive-ms",]);
/** Keep-alive windows accept 0 (disable the valve) or a positive integer. */function parseSubagentKeepAliveMs(value: string): number { const digits = value.replace(/[,_]/g, ""); if (digits === "0" || /^[1-9]\d*$/.test(digits)) { const parsed = Number(digits); if (parsed <= MAX_SUBAGENT_KEEP_ALIVE_MS) { return parsed; } } throw new Error( `--subagent-keep-alive-ms must be 0 or a positive integer up to ${MAX_SUBAGENT_KEEP_ALIVE_MS}. ${AUTONOMOUS_BUDGET_USAGE}`, );}
function parseAutonomousBudgetInt(flag: string, value: string, allowUnlimited = false): number { if (allowUnlimited && value.toLowerCase() === "unlimited") { return UNLIMITED_AUTONOMOUS_LIMIT; } // Commas and underscores are accepted as digit separators (100,000,000). const digits = value.replace(/[,_]/g, ""); if (!/^[1-9]\d*$/.test(digits)) { throw new Error( `--${flag} must be a positive integer${allowUnlimited ? ' or "unlimited"' : ""}. ${AUTONOMOUS_BUDGET_USAGE}`, ); } return Number(digits);}
function parseAutonomousBudgetOptions(tokens: string[]): AgentAutonomousConfig { const config: AgentAutonomousConfig = {}; const gateCommands: string[] = []; for (let i = 0; i < tokens.length; i++) { const token = tokens[i]!; if (!token.startsWith("--")) { throw new Error(`Unexpected autonomous argument: ${token}. ${AUTONOMOUS_BUDGET_USAGE}`); } const equalsIndex = token.indexOf("="); const rawFlag = equalsIndex === -1 ? token : token.slice(0, equalsIndex); const inlineValue = equalsIndex === -1 ? undefined : token.slice(equalsIndex + 1); const flag = rawFlag.startsWith("--autonomous-") ? rawFlag.slice("--autonomous-".length) : rawFlag.slice(2); if (!AUTONOMOUS_BUDGET_FLAGS.has(flag)) { throw new Error(`Unknown autonomous budget flag: ${rawFlag}. ${AUTONOMOUS_BUDGET_USAGE}`); } let value = inlineValue; if (value === undefined) { const next = tokens[i + 1]; if (next === undefined || next.startsWith("--")) { throw new Error(`Missing value for ${rawFlag}. ${AUTONOMOUS_BUDGET_USAGE}`); } value = next; i++; } if (value === "") { throw new Error(`Missing value for ${rawFlag}. ${AUTONOMOUS_BUDGET_USAGE}`); } switch (flag) { case "gate": gateCommands.push(value); break; case "gate-retries": config.gates = config.gates ?? {}; config.gates.maxRetries = parseAutonomousBudgetInt(flag, value); break; case "gate-timeout-ms": config.gates = config.gates ?? {}; config.gates.timeoutMs = parseAutonomousBudgetInt(flag, value); break; case "max-continuations": config.maxContinuations = parseAutonomousBudgetInt(flag, value, true); break; case "max-turns": config.maxTurns = parseAutonomousBudgetInt(flag, value, true); break; case "max-tokens": config.maxTokens = parseAutonomousBudgetInt(flag, value, true); break; case "timeout-ms": config.timeoutMs = parseAutonomousBudgetInt(flag, value, true); break; case "subagent-keep-alive-ms": config.subagentKeepAliveMs = parseSubagentKeepAliveMs(value); break; } } if (gateCommands.length > 0) { config.gates = { ...config.gates, commands: gateCommands }; } // Named budget flags define the whole budget: any limit the user did not // name stops cutting the run short. With no budget flags at all, the // configured or default limits still apply. if ( config.maxContinuations !== undefined || config.maxTurns !== undefined || config.maxTokens !== undefined || config.timeoutMs !== undefined ) { config.maxContinuations ??= UNLIMITED_AUTONOMOUS_LIMIT; config.maxTurns ??= UNLIMITED_AUTONOMOUS_LIMIT; config.maxTokens ??= UNLIMITED_AUTONOMOUS_LIMIT; config.timeoutMs ??= UNLIMITED_AUTONOMOUS_LIMIT; } return config;}
export function compactRlmText(text: string, maxLength = 160): string { const compact = text.replace(/\s+/g, " ").trim(); if (compact.length <= maxLength) { return compact; } return `${compact.slice(0, Math.max(0, maxLength - 3)).trimEnd()}...`;}
// Child-agent label: collapse to one line but keep the full prompt — the TUI// truncates to the visible width and elides shared prefixes, so capping here// would only hide the divergence between near-identical sibling prompts.export function rlmChildLabel(prompt: string): string { return prompt.replace(/\s+/g, " ").trim() || "child agent";}
/** * Record a tracked child activity on both clocks: lastActivityAt stays * wall-clock ms for snapshots, and its monotonic twin bounds staleness so a * host sleep cannot inflate it. */function touchRlmChildActivity(run: RlmChildRun): void { run.lastActivityAt = Date.now(); run.lastActivityMonotonicAt = performance.now();}
/** * Lazily computed staleness for a running child: how long since the last * tracked activity, once past the threshold. Computed at snapshot build time * only — no background timers update it. * * A tool call in flight (activity "executing") is legitimately quiet for its * whole duration — a minutes-long bash() run emits no events while it works — * so an executing child never reports stale. Staleness measures active time: * the wall clock alone would mark every running child stale after a laptop * sleep, so the smaller of the wall and monotonic clock deltas bounds it to * time the host was actually awake. */function rlmActivityStaleMs( status: RlmChildAgentStatus, activity: RlmChildAgentActivity | undefined, lastActivityAt: number | undefined, lastActivityMonotonicAt: number | undefined,): number | undefined { if (status !== "running" || lastActivityAt === undefined) return undefined; if (activity?.kind === "executing") return undefined; const wallStaleMs = Date.now() - lastActivityAt; const monotonicStaleMs = lastActivityMonotonicAt === undefined ? wallStaleMs : performance.now() - lastActivityMonotonicAt; // Integer ms like every other roster wire field: performance.now() deltas // are fractional, and the kernel parser rejects non-int activity_stale_ms. const staleMs = Math.floor(Math.min(wallStaleMs, monotonicStaleMs)); return staleMs >= RLM_CHILD_STALE_ACTIVITY_THRESHOLD_MS ? staleMs : undefined;}
function readAssistantText(message: AssistantMessage): string { return message.content .filter((block) => block.type === "text") .map((block) => block.text) .join("");}
function waitForPromiseOrAbort<T>( promise: Promise<T>, signal: AbortSignal | undefined, abortMessage: string,): Promise<T> { if (!signal) return promise; if (signal.aborted) return Promise.reject(new Error(abortMessage)); return new Promise<T>((resolve, reject) => { const onAbort = () => { cleanup(); reject(new Error(abortMessage)); }; const cleanup = () => signal.removeEventListener("abort", onAbort); signal.addEventListener("abort", onAbort, { once: true }); // Close the listener-registration race before observing the awaited work. if (signal.aborted) return onAbort(); promise.then( (value) => { cleanup(); resolve(value); }, (error: unknown) => { cleanup(); reject(error); }, ); });}
// Bounds how much accumulated child usage a parent process crash can lose.const RLM_CHILD_USAGE_FLUSH_MAX_PENDING_MS = 60_000;
/** Label a child completion's usage by the nearest preceding prompt that triggered it. */function rlmChildUsageOrigin( messages: readonly AgentMessage[], assistant: AssistantMessage,): ChildUsageAttributionEntry["origin"] { for (let index = messages.lastIndexOf(assistant) - 1; index >= 0; index--) { const message = messages[index]; if (message.role !== "user" && message.role !== "custom") continue; return message.role === "custom" && isAgentSessionMessage(message) ? message.details.id.startsWith("spawn:") ? "spawn_task" : "agent_message" : "direct_user"; } return "direct_user";}
function attributeChildUsage(parentUsage: Usage, childUsage: Usage): void { const parentContextTokens = parentUsage.totalTokens || parentUsage.input + parentUsage.output + parentUsage.cacheRead + parentUsage.cacheWrite; // Recursive children are launched from an assistant tool call, so the parent assistant // message carries their billable usage for session-level cost totals. addAssistantUsage(parentUsage, childUsage); // Child work affects session-level billable totals, not the parent's model-facing context size. parentUsage.totalTokens = parentContextTokens;}
export class AgentSession { readonly agent: Agent; readonly sessionManager: SessionManager; readonly settingsManager: SettingsManager; private _serviceTierPreference: ServiceTier;
private _scopedModels: Array<{ model: Model<any>; thinkingLevel?: ThinkingLevel; }>;
private _unsubscribeAgent?: () => void; private _eventListeners: AgentSessionEventListener[] = []; private _lastSessionActionSnapshot: SessionActionSnapshot = { queuedCount: 0, steering: [], followUps: [], }; private _agentEventQueue: Promise<void> = Promise.resolve();
/** Session-owned actions. Items are never fed into Agent.steer/followUp. */ private readonly _actionStore = new ActionStore<QueuedSessionAction>(); private _sessionInputPump: Promise<void> = Promise.resolve(); private _sessionInputPumpRequested = false; // Invalidates preparation when a branch pause starts and finishes before its next await resumes. private _sessionInputPumpEpoch = 0; private _sessionInputArrivalEpoch = 0; // Persists abort/restart suspension after the initiating call returns. private _sessionInputPumpSuspended = false; private _sessionInputSuspendedForUpdateRestart = false; // Branch mutation pause leases can overlap and must all release before dispatch resumes. private readonly _queuedWorkPauses = new Set<symbol>(); private readonly _sessionInputAdmissionPauses = new Set<symbol>(); private readonly _durableRlmTerminalNoticeActionIds = new Set<string>(); private _sessionActionCommitTail: Promise<void> = Promise.resolve(); private _sessionActionCommitOwner: symbol | undefined; private _pendingSessionActionFenceWaiters = 0; private readonly _sessionActionCommitContext = new AsyncLocalStorage<symbol>(); private readonly _sessionActionCommitDisposeAbortController = new AbortController(); // Checkpoint, handoff, and activity waiters share lifecycle-edge notifications to avoid polling. private readonly _sessionInputCheckpointWaiters = new Set<() => void>(); private _pendingNextTurnMessages: CustomMessage[] = []; private _assistantTurnCount = 0; private _languageDriftNotices = 0; private _lastLanguageDriftTurn = Number.NEGATIVE_INFINITY;
private _goalState: GoalState = emptyGoalState(); private _goalAccountingStartedAt: number | undefined = undefined; private _goalContinuationAwaitsRlmWork = false; private _goalAccountedAssistantMessages = new WeakSet<AssistantMessage>(); private _goalAbortInProgress = false; private _autonomousState: AutonomousRuntimeState; private _autonomousContinuationSuppressionDepth = 0; private _autonomousContinuationSuppressedMessages = new WeakSet<AgentMessage>(); // Held autonomous continuation owed while descendant work runs; mirrors // _goalContinuationAwaitsRlmWork. Child replies and exit notices are the // real wake-up signals, so timer-driven continuations pause instead of // re-prompting a waiting parent (and pause without consuming budget). private _autonomousContinuationAwaitsRlmWork = false; private _autonomousSubagentKeepAliveTimer: ReturnType<typeof setTimeout> | undefined = undefined; // In-flight gate evaluation for an owed continuation; holds the promise so // settlement sites never double-fire the resume. private _autonomousContinuationResumeTask: Promise<void> | undefined = undefined; // Monotonic count of admitted RLM child terminal notices; differencing // against the arrival epoch separates sibling notices (benign for the // owed continuation) from user-driven admissions. private _rlmTerminalNoticeAdmissionCount = 0;
private _compactionThresholdTokens: number | undefined = undefined; private _compactionAbortController: AbortController | undefined = undefined; private _autoCompactionAbortController: AbortController | undefined = undefined; private _compactionOperation: Promise<void> | undefined = undefined; /** One recovery attempt per overflow; "reported" dedups the failure notice. */ private _overflowRecovery: "idle" | "attempted" | "reported" = "idle"; private _continueAfterThresholdCompaction = false; private _pendingRequestedCompaction: { customInstructions?: string } | undefined; private _pendingRequestedRefine: { instructions?: string; global?: boolean } | undefined;
private _branchSummaryAbortController: AbortController | undefined = undefined; private _branchSummaryOperation: Promise<void> | undefined = undefined;
private _retryAbortController: AbortController | undefined = undefined; private _retryAttempt = 0; /** Bumped by every retry resolution; stale scheduled-continue callbacks check it before touching retry state. */ private _retryGeneration = 0; private _retryPromise: Promise<void> | undefined = undefined; private _retryResolve: (() => void) | undefined = undefined; private _retryAuthFailureSources: AuthSourceToken[] = []; /** Ongoing wait-for-recovery state: pings issued and when the wait started. */ private _providerWait: { attempts: number; startedAtMs: number } | undefined = undefined; /** Set while turns are routed to the user-configured backup model. */ private _backupModel: | { backup: Model<any>; primary: Model<any>; thinkingLevel: ThinkingLevel; serviceTier: ServiceTier; } | undefined = undefined; private _agentMessageClearEpoch = 0; private _agentMessageOutcomes = new Map<string, AgentMessageOutcome>(); private _lateIpythonSentAgentMessages = new Map<string, KernelSentAgentMessage[]>(); /** Outcome disclosures whose session-file append failed; retained for context rebuilds. */ private readonly _unpersistedOutcomes: CustomMessage[] = []; /** Fresh/empty contexts defer digest injection to the first committed turn so untouched sessions stay empty. */ private _harnessDigestPending = false;
private _bashAbortControllers = new Set<AbortController>(); private _userBashRunning = false; private _userBashAbortRequested = false; private _pendingBashMessages: BashExecutionMessage[] = [];
private _extensionRunner!: ExtensionRunner; private _execEnvProvider?: () => Record<string, string | undefined> | undefined; private _turnIndex = 0; private _modelSelectEmitQueue: Promise<void> = Promise.resolve(); private _modelSelectEmitQueueIdle = true; private _modelSelectEmitContext = new AsyncLocalStorage<boolean>();
private _resourceLoader: ResourceLoader; private _customTools: ToolDefinition[]; private _acpMcpTools: ToolDefinition[] = []; private _baseToolDefinitions: Map<string, ToolDefinition> = new Map(); private _cwd: string; private _agentDir?: string; private _extensionRunnerRef?: { current?: ExtensionRunner }; private _initialActiveToolNames?: string[]; private _allowedToolNames?: Set<string>; private _includeGoals: boolean; private _includeCompactSkill: boolean; private _rlmHeartbeatController?: AgentRlmHeartbeatController; private _agentMessageController?: AgentSessionMessageController; private _agentObserveController?: AgentObserveController; private _agentSessionController?: AgentSessionController; private _mcpManager?: McpManager; private _baseToolsOverride?: Record<string, AgentTool>; private _sessionStartEvent: SessionStartEvent; private _extensionUIContext?: ExtensionUIContext; private _extensionCommandContextActions?: ExtensionCommandContextActions; private _extensionShutdownHandler?: ShutdownHandler; private _extensionErrorListener?: ExtensionErrorListener; private _extensionErrorUnsubscriber?: () => void; private _disposed = false; private readonly _disposeCallbacks = new Set<() => void | Promise<void>>(); private _disposeCallbacksPromise?: Promise<void>; // Set at the start of async teardown so a child finishing mid-disposeAsync doesn't // re-populate the retained map after it's been cleared. private _disposing = false; private _disposeAsyncPromise?: Promise<void>; private _ipythonKernelProvisioner?: IpythonKernelProvisioner; /** Artifact dir backing the current provisioner's kernel snapshot, if any. */ private _ipythonKernelSnapshotDir?: string; /** True once the runtime has been built once; later builds are in-process rebuilds (/reload). */ private _ipythonRuntimeBuilt = false; private readonly _prewarmIpythonKernel: boolean; private _rlmDepth: number; private readonly _configuredRlmMaxDepth: number | undefined; private _rlmMaxDepth: number; private _rlmMaxDepthSource: RlmMaxDepthSource; private _rlmSessionDir?: string; private readonly _rlmAllowPush: boolean; private readonly _semanticEdges: SemanticEdgeRecorder; private _rlmParentNodeId?: string; private _rlmParentAgent?: string; private readonly _remoteHost?: string; private _repliedToParentSinceTask: boolean | undefined; private _parentReplyCount = 0; private _subagentRuntimeHost?: SubagentRuntimeHost; // Shared by children charged to the same assistant; excludes usage not yet attributed on disk. private _rlmDurableParentUsage = new WeakMap<AssistantMessage, Usage>(); // Child usage not yet represented by an indexed attribution, including a delayed parent entry. private _rlmUnindexedChildUsage = new WeakMap<AssistantMessage, Usage>(); private _activeRlmChildRuns = new Map<string, RlmChildRun>(); /** Wall-clock ms of the last accepted progress note; throttles rlm.progress.note. */ private _lastRlmProgressNoteAt: number | undefined; private _unsettledRlmChildRuns = new Set<RlmChildRun>(); private _abandonedRlmQuiescenceChildIds = new Set<string>(); private _rlmQuiescenceWaitAborts = new Set<AbortController>(); private _pendingRlmSubagentSessionNames = new Set<string>(); // Inline mode keeps finished child sessions so the inspector can still read them; // the daemon does the same by leaving the child session resident in its registry. private _rlmChildSessions = new Map<string, RetainedRlmChild>(); private _rlmModelsStore: RlmModelsStore; private _hasCalledRlmModels = false; private _rlmModelsGateResetAt = 0; private _deletedRlmChildIds = new Set<string>(); // Failed explicit deletes stay hidden from listings but retain their original // selector so a later delete can retry cleanup without orphaning the runtime. private _rlmChildCleanupFailures = new Map<string, RlmSubagentRegistryEntry>(); private _deletingRlmChildren = new Map< string, { subagent: RlmSubagentRegistryEntry; promise: Promise<RlmDeleteSubagentResult>; } >(); // Kept alive for retained children so nested updates (e.g. a grandchild cancel) // still forward to root; torn down when the retained child is disposed. private _rlmChildUnsubscribes = new Map<string, () => void>(); /** Latest recap for this session, written by the daemon summarizer; read by a parent to label its child snapshots. */ private _currentRecap?: string;
private _modelRegistry: ModelRegistry;
private _toolRegistry: Map<string, AgentTool> = new Map(); private _toolDefinitions: Map<string, ToolDefinitionEntry> = new Map(); private _toolPromptSnippets: Map<string, string> = new Map(); private _toolPromptGuidelines: Map<string, string[]> = new Map();
private _baseSystemPrompt = ""; private _baseSystemPromptOptions!: BuildSystemPromptOptions; private _assistantTurnsSinceAutoRefine = 0; private _lastAutoRefineReviewAt = 0; private _autoRefineInProgress = false; private readonly _autoRefineOperations = new Set<Promise<void>>(); private readonly _scheduledAutoRefineTimers = new Set<ReturnType<typeof setTimeout>>(); private _compactAutoRefinePending = false; private _turnIntervalAutoRefinePending = false; private _postCompactionContinuationScheduled = false; private _postCompactionContinuationSettlement: PostCompactionContinuationSettlement | undefined; private _postCompactionContinuationMessages: AgentMessage[] = []; private _scheduledPostCompactionContinuationMessages: AgentMessage[] = []; private _queuedAutonomousThresholdContinuations = new WeakMap<AssistantMessage, AgentMessage>(); private _queuedAutonomousContinuationSnapshots = new WeakMap<AgentMessage, AutonomousRuntimeSnapshot>(); private _pendingThresholdCompactionAutonomousMessages: AgentMessage[] = []; private _queuedGoalThresholdContinuation: AgentMessage | undefined; private _pendingAutoRefineReview: { reason: AutoRefineReason; review: AutoRefineReview } | undefined; private _autoRefineBranchVersion = 0; private _autoRefineReviewAbort?: AbortController; private _refineAbortController?: AbortController; private readonly _autoRefineReviewer?: AutoRefineReviewer; private readonly _serializedRefine: boolean; private _refineInFlight?: Promise<void>; private _refinePlanInFlight?: Promise<void>; private _serializedPlanInFlight?: Promise<SerializedBackgroundPlanResult | undefined>; private _serializedPlanClaim?: Promise<void>; private _serializedExplicitRefineOptions?: { instructions?: string; global?: boolean; };
constructor(config: AgentSessionConfig) { this.agent = config.agent; this.sessionManager = config.sessionManager; this.settingsManager = config.settingsManager; this._serviceTierPreference = config.serviceTierPreference ?? config.agent.state.serviceTier; this._scopedModels = config.scopedModels ?? []; this._resourceLoader = config.resourceLoader; this._customTools = config.customTools ?? []; this._cwd = config.cwd; this._agentDir = config.agentDir; this._modelRegistry = config.modelRegistry; this._extensionRunnerRef = config.extensionRunnerRef; this._initialActiveToolNames = config.initialActiveToolNames; this._allowedToolNames = config.allowedToolNames ? new Set(config.allowedToolNames) : undefined; this._includeGoals = config.includeGoals ?? true; this._includeCompactSkill = config.includeCompactSkill ?? this.settingsManager.getCompactionAgentCallable(); this._rlmHeartbeatController = config.rlmHeartbeatController; this._agentMessageController = config.agentMessageController; this._agentObserveController = config.agentObserveController; this._agentSessionController = config.agentSessionController; this._mcpManager = config.mcpManager; this._baseToolsOverride = config.baseToolsOverride; this._sessionStartEvent = config.sessionStartEvent ?? { type: "session_start", reason: "startup" }; const headerRlmDepth = this.sessionManager.getHeader()?.rlmDepth; this._rlmDepth = config.rlmDepth ?? (isNonNegativeInteger(headerRlmDepth) ? headerRlmDepth : parseDepth(process.env.RLM_DEPTH, 0, "RLM_DEPTH")); this._configuredRlmMaxDepth = config.rlmMaxDepth; if (this._configuredRlmMaxDepth !== undefined && !isNonNegativeInteger(this._configuredRlmMaxDepth)) { throw new Error("rlmMaxDepth must be a non-negative integer"); } const resolvedRlmMaxDepth = this._resolveRlmMaxDepth(); this._rlmMaxDepth = resolvedRlmMaxDepth.maxDepth; this._rlmMaxDepthSource = resolvedRlmMaxDepth.source; this._compactionThresholdTokens = this._resolveCompactionThreshold(); this._prewarmIpythonKernel = (config.prewarmIpythonKernel ?? false) && this._rlmDepth === 0; this._autoRefineReviewer = config.autoRefineReviewer; this._serializedRefine = config.serializedRefine ?? false; this._rlmModelsStore = new RlmModelsStore(config.rlmModelsFilePath); this._hasCalledRlmModels = false; this._rlmSessionDir = config.rlmSessionDir; this._rlmAllowPush = config.rlmAllowPush ?? false; this._rlmParentNodeId = config.rlmParentNodeId; this._rlmParentAgent = config.rlmParentAgent; this._semanticEdges = new SemanticEdgeRecorder({ ledgerPath: semanticEdgeLedgerPath({ rlmSessionDir: this._rlmSessionDir, sessionArtifactDir: this.sessionManager.getSessionArtifactDir(), }), sessionId: this.sessionManager.getSessionId(), parentSessionId: config.semanticParentSessionId, spawnedByRequestId: config.semanticSpawnedByRequestId, }); this.agent.streamFn = wrapStreamFnWithSemanticEdges(this.agent.streamFn, this._semanticEdges); this._remoteHost = config.remoteHost; // A resumed child may have replied before this process started; false would // claim knowledge that is not present in the session transcript. this._repliedToParentSinceTask = this._rlmDepth > 0 && this.sessionManager.getBranch().some((entry) => entry.type === "message") ? undefined : false; this._subagentRuntimeHost = config.subagentRuntimeHost; this._autonomousState = createAutonomousRuntimeState(config.autonomous, { cwd: this._cwd, defaultLimits: this.settingsManager.getAutonomousLimits(), }); this._goalState = this._loadPersistedGoalState(); // Seed initial goal from CLI --goal flag, but only for top-level sessions // and only when the branch contains only bootstrap entry types (model_change, // thinking_level_change, service_tier_change) and no persisted // thread_goal_state. This prevents reseeding after clear/complete/error // or restart/rehydration of a session that already has messages or a goal. if (this._rlmDepth === 0 && config.initialGoal && this._isBranchSeedable()) { this._goalState = this._startGoal(config.initialGoal.objective, config.initialGoal.tokenBudget); // Goal context is the model's only source of goal visibility; action // admission is unavailable mid-construction, so ride the next turn. this._pendingNextTurnMessages.push(createGoalContextMessage(this._goalState, "continuation")); } this._restoreLateIpythonSentAgentMessages(); if (this._goalState.status === "active") { this._goalAccountingStartedAt = Date.now(); }
this._unsubscribeAgent = this.agent.subscribe(this._handleAgentEvent); this._installAgentToolHooks(); this._installAgentTurnHook(); this._installAgentContinuationHook();
this._buildRuntime({ activeToolNames: this._initialActiveToolNames, includeAllExtensionTools: true, }); this._ensureHarnessDigestContext(); }
/** Refreshes MCP provider registrations without rebuilding the session runtime. */ refreshMcpProviders(): void { this._mcpManager?.refresh(); }
/** * Set the RLM heartbeat controller after construction. Used by * print/headless mode to attach an in-process heartbeat scheduler * when the session is created outside the daemon. */ setRlmHeartbeatController(controller: AgentRlmHeartbeatController): void { if (this._rlmHeartbeatController === controller) { return; } this._rlmHeartbeatController = controller; this._buildRuntime({ activeToolNames: this.getActiveToolNames(), includeAllExtensionTools: true, }); this._baseSystemPrompt = this._rebuildSystemPrompt(this.getActiveToolNames()); this.agent.state.systemPrompt = this._baseSystemPrompt; }
replaceAcpMcpServers(servers: readonly AcpMcpServerConfig[], ownerId: string): void { if (this.isStreaming) throw new Error("Cannot replace ACP MCP servers while the agent is running"); if (!this._mcpManager) { if (servers.length > 0) throw new Error("MCP is unavailable in this session"); return; } if (servers.length > 0 && !this._ipythonKernelProvisioner) { throw new Error("ACP MCP servers require the built-in cpython tool"); } this._assertAcpMcpToolNamesAvailable(acpMcpToolNames(servers)); if (!this._mcpManager.replaceAcpServers(servers, ownerId)) return; this._rebuildRuntimeForAcpMcpServers(); }
async releaseAcpMcpServers(ownerId: string, serverNames: readonly string[]): Promise<void> { if (!this._mcpManager?.canReleaseAcpServers(ownerId)) return; if (this._mcpManager.replaceAcpServers([], ownerId)) { const removedToolNames = new Set(this._acpMcpTools.map((tool) => tool.name)); const activeToolNames = this.getActiveToolNames().filter((name) => !removedToolNames.has(name)); for (const name of removedToolNames) this._allowedToolNames?.delete(name); this._acpMcpTools = []; this._refreshToolRegistry({ activeToolNames, includeAllExtensionTools: true }); this._baseSystemPrompt = this._rebuildSystemPrompt(this.getActiveToolNames()); this.agent.state.systemPrompt = this._baseSystemPrompt; } const names = [...new Set(serverNames)]; if (names.length === 0) return;
const inputPause = this.acquireSessionInputPause(); try { // Do not rebuild or kill the notebook. Wait for the current turn, then ask // the kernel-owned MCP registry to close only these cached transports. await this.agent.waitForIdle(); await this._agentEventQueue; const manager = this._ipythonKernelProvisioner?.manager; if (!manager?.isRunning) return; const code = [ "import importlib as _prime_importlib", '_prime_mcp = _prime_importlib.import_module("rlm.mcp")', `_prime_mcp_names = ${JSON.stringify(names)}`, "_prime_mcp_errors = []", "for _prime_mcp_name in _prime_mcp_names:", " try:", " await _prime_mcp.reload(_prime_mcp_name)", " except BaseException as _prime_mcp_error:", " _prime_mcp_errors.append(_prime_mcp_error)", "if _prime_mcp_errors:", " raise _prime_mcp_errors[0]", "del _prime_mcp, _prime_importlib, _prime_mcp_names, _prime_mcp_errors, _prime_mcp_name", ].join("\n"); const result = await manager.execute(code); if (result.status !== "ok") { throw new Error(`Failed to close ACP MCP kernel transports: ${result.stderr || "kernel error"}`); } } finally { inputPause.release(); } }
private _assertAcpMcpToolNamesAvailable(names: readonly string[]): void { const occupiedNames = new Set([ ...this._baseToolDefinitions.keys(), ...this._customTools.map((tool) => tool.name), ...this._extensionRunner.getAllRegisteredTools().map((tool) => tool.definition.name), ]); for (const name of names) { if (occupiedNames.has(name)) { throw new Error(`ACP MCP tool name conflicts with an existing tool: ${name}`); } } }
private _rebuildRuntimeForAcpMcpServers(): void { const previousToolNames = new Set(this._acpMcpTools.map((tool) => tool.name)); const nextToolNames = acpMcpToolNames(this._mcpManager?.getAcpServers() ?? []); this._assertAcpMcpToolNamesAvailable(nextToolNames); const activeToolNames = this.getActiveToolNames().filter((name) => !previousToolNames.has(name)); activeToolNames.push(...nextToolNames); this._buildRuntime({ activeToolNames, includeAllExtensionTools: true, }); this._baseSystemPrompt = this._rebuildSystemPrompt(this.getActiveToolNames()); this.agent.state.systemPrompt = this._baseSystemPrompt; }
get modelRegistry(): ModelRegistry { return this._modelRegistry; }
setSubagentRuntimeHost(host?: SubagentRuntimeHost): void { this._subagentRuntimeHost = host; }
private async _getRequiredRequestAuth(model: Model<any>): Promise<{ apiKey: string; headers?: Record<string, string>; requestModel: Model<Api>; }> { const result = await this._modelRegistry.getApiKeyAndHeaders(model, undefined, this.sessionId); if (!result.ok) { if (result.error.startsWith("No API key found")) { throw new Error(formatNoApiKeyFoundMessage(model.provider)); } throw new Error(result.error); } if (result.apiKey) { return { apiKey: result.apiKey, headers: result.headers, requestModel: result.requestModel ?? model }; }
const isOAuth = this._modelRegistry.isUsingOAuth(model); if (isOAuth) { throw new Error(formatAuthenticationFailedMessage(model.provider)); } throw new Error(formatNoApiKeyFoundMessage(model.provider)); }
/** * Install tool hooks once on the Agent instance. * * The callbacks read `this._extensionRunner` at execution time, so extension reload swaps in the * new runner without reinstalling hooks. Extension-specific tool wrappers are still used to adapt * registered tool execution to the extension context. Tool call and tool result interception now * happens here instead of in wrappers. */ private _installAgentToolHooks(): void { this.agent.beforeToolCall = async ({ toolCall, args }) => { const runner = this._extensionRunner; if (!runner.hasHandlers("tool_call")) { return undefined; }
await this._agentEventQueue;
try { return await runner.emitToolCall({ type: "tool_call", toolName: toolCall.name, toolCallId: toolCall.id, input: args as Record<string, unknown>, }); } catch (err) { if (err instanceof Error) { throw err; } throw new Error(`Extension failed, blocking execution: ${String(err)}`); } };
this.agent.afterToolCall = async ({ toolCall, args, result, isError }) => { const runner = this._extensionRunner; if (!runner.hasHandlers("tool_result")) { return undefined; }
const hookResult = await runner.emitToolResult({ type: "tool_result", toolName: toolCall.name, toolCallId: toolCall.id, input: args as Record<string, unknown>, content: result.content, details: result.details, isError, });
if (!hookResult) { return undefined; }
return { content: hookResult.content, details: hookResult.details, isError: hookResult.isError ?? isError, }; }; }
private _installAgentContinuationHook(): void { this.agent.getContinuationMessages = (context, signal) => this._getContinuationMessages(context, signal); }
private _installAgentTurnHook(): void { this.agent.shouldStopBeforeTurn = () => this._shouldStopBeforeTurn(); this.agent.shouldStopAfterTurn = (context) => this._shouldStopAfterTurn(context); }
private _emit(event: AgentSessionEvent): void { for (const l of this._eventListeners) { try { l(event); } catch { // A failing observer must not prevent other subscribers from // receiving lifecycle and persistence events. } } }
private _emitQueueUpdate(): void { const actions = this.getSessionActionSnapshot(); if (JSON.stringify(actions) === JSON.stringify(this._lastSessionActionSnapshot)) return; this._lastSessionActionSnapshot = actions; this._emit({ type: "session_action_update", actions }); }
private _restoreLateIpythonSentAgentMessages(): void { this._lateIpythonSentAgentMessages.clear(); for (const entry of this.sessionManager.branchEntries("custom")) { if (entry.type !== "custom" || entry.customType !== IPYTHON_SENT_AGENT_MESSAGE_CUSTOM_ENTRY) { continue; } const persisted = parsePersistedIpythonSentAgentMessage(entry.data); if (persisted) { this._rememberLateIpythonSentAgentMessage(persisted.toolCallId, persisted.message); } } }
private _rememberLateIpythonSentAgentMessage(toolCallId: string, message: KernelSentAgentMessage): boolean { const messages = this._lateIpythonSentAgentMessages.get(toolCallId) ?? []; const isNew = !messages.some((entry) => entry.id === message.id); if (isNew) { messages.push(message); this._lateIpythonSentAgentMessages.set(toolCallId, messages); } for (let index = this.agent.state.messages.length - 1; index >= 0; index -= 1) { if (appendSentAgentMessageToToolResult(this.agent.state.messages[index], toolCallId, message)) { break; } } return isNew; }
private _applyLateIpythonSentAgentMessages(message: AgentMessage): void { if (message.role !== "toolResult" || message.toolName !== "ipython") { return; } for (const sentMessage of this._lateIpythonSentAgentMessages.get(message.toolCallId) ?? []) { appendSentAgentMessageToToolResult(message, message.toolCallId, sentMessage); } }
private _recordLateIpythonSentAgentMessage(toolCallId: string, message: KernelSentAgentMessage): void { const record = () => { if (this._disposed || !this._rememberLateIpythonSentAgentMessage(toolCallId, message)) { return; } this.sessionManager.appendCustomEntry(IPYTHON_SENT_AGENT_MESSAGE_CUSTOM_ENTRY, { toolCallId, message }); this._emit({ type: "ipython_sent_agent_message", toolCallId, message }); }; this._agentEventQueue = this._agentEventQueue.then(record, record); this._agentEventQueue.catch(() => {}); }
private _emitGoalUpdate(): void { this._emit({ type: "goal_update", goal: this.goalState }); }
private _loadPersistedRlmMaxDepthState(): PersistedRlmMaxDepthState | undefined { const entry = this.sessionManager.findLastBranchEntry( "custom", (candidate) => candidate.type === "custom" && candidate.customType === RLM_MAX_DEPTH_STATE_CUSTOM_TYPE && isPersistedRlmMaxDepthState(candidate.data), ); return entry?.type === "custom" ? (entry.data as PersistedRlmMaxDepthState) : undefined; }
private _resolveRlmMaxDepth(): { maxDepth: number; source: RlmMaxDepthSource; } { const persisted = this._loadPersistedRlmMaxDepthState(); if (persisted) { return { maxDepth: persisted.maxDepth, source: "chat" }; } if (this._configuredRlmMaxDepth !== undefined) { return { maxDepth: this._configuredRlmMaxDepth, source: "inherited" }; } const global = this.settingsManager.getRlmMaxDepth(); if (global !== undefined && isNonNegativeInteger(global)) { return { maxDepth: global, source: "global" }; } const env = process.env.RLM_MAX_DEPTH; if (env !== undefined && env !== "") { return { maxDepth: parseDepth(env, 1, "RLM_MAX_DEPTH"), source: "env" }; } return { maxDepth: 2, source: "default" }; }
private _loadPersistedGoalState(): GoalState { const entry = this.sessionManager.findLastBranchEntry( "custom", (candidate) => candidate.type === "custom" && candidate.customType === GOAL_STATE_CUSTOM_TYPE && isPersistedGoalState(candidate.data), ); return entry?.type === "custom" && isPersistedGoalState(entry.data) ? normalizeGoalState(entry.data) : emptyGoalState(); }
/** * Whether the session branch is seedable for an initial goal. Returns true * only when the branch contains exclusively bootstrap entry types * (model_change, thinking_level_change, service_tier_change) and no * thread_goal_state custom entry. Any message, custom entry, or persisted * goal (including cleared/complete/error) means the session has been used * and should not be reseeded. */ private _isBranchSeedable(): boolean { for (const type of this.sessionManager.branchEntryTypes()) { switch (type) { case "model_change": case "thinking_level_change": case "service_tier_change": continue; default: return false; } } return true; }
private _reloadGoalStateFromBranch(options: { monotonicTokens?: boolean } = {}): void { const previous = this._goalState; const reloaded = this._loadPersistedGoalState(); if (options.monotonicTokens && reloaded.goalId !== undefined && reloaded.goalId === previous.goalId) { // A context rebuild continues the same timeline, but the rebuilt branch's // last persisted goal entry can lag the in-memory state (queue/flush // races; child-usage attribution landing late). Neither the accounting // counters nor an already-fired gate (budget limit, pause, completion) // for the same logical goal may regress across the cold boundary. Tree // navigation keeps faithful branch semantics by calling without the flag. this._goalState = { ...previous, tokensUsed: Math.max(previous.tokensUsed, reloaded.tokensUsed), continuationsUsed: Math.max(previous.continuationsUsed, reloaded.continuationsUsed), timeUsedSeconds: Math.max(previous.timeUsedSeconds, reloaded.timeUsedSeconds), }; } else { this._goalState = reloaded; } this._goalAccountingStartedAt = this._goalState.status === "active" ? Date.now() : undefined; this._emitGoalUpdate(); }
private _reloadRlmMaxDepthFromBranch(): void { const previousMaxDepth = this._rlmMaxDepth; const resolved = this._resolveRlmMaxDepth(); this._rlmMaxDepth = resolved.maxDepth; this._rlmMaxDepthSource = resolved.source; if (resolved.maxDepth !== previousMaxDepth) { this._baseSystemPrompt = this._rebuildSystemPrompt(this.getActiveToolNames()); this.agent.state.systemPrompt = this._baseSystemPrompt; } }
private _loadPersistedCompactionThresholdState(): PersistedCompactionThresholdState | undefined { const entry = this.sessionManager.findLastBranchEntry( "custom", (candidate) => candidate.type === "custom" && candidate.customType === COMPACTION_THRESHOLD_STATE_CUSTOM_TYPE && isPersistedCompactionThresholdState(candidate.data), ); return entry?.type === "custom" ? (entry.data as PersistedCompactionThresholdState) : undefined; }
private _resolveCompactionThreshold(): number | undefined { const persisted = this._loadPersistedCompactionThresholdState(); if (persisted) { return persisted.threshold ?? undefined; } return this.settingsManager.getCompactionMaxContextTokens(); }
private _reloadCompactionThresholdFromBranch(): void { this._compactionThresholdTokens = this._resolveCompactionThreshold(); }
private _getEffectiveCompactionSettings(): CompactionSettings { const settings = this.settingsManager.getCompactionSettings(); return { ...settings, ...(this._compactionThresholdTokens !== undefined ? { maxContextTokens: this._compactionThresholdTokens } : {}), }; }
private _persistGoalState(goal: GoalState): void { this.sessionManager.appendCustomEntry(GOAL_STATE_CUSTOM_TYPE, goal); // Force flush so the goal state is durable on disk immediately, // even before the first assistant response. This ensures idempotent // restart/rehydration can detect the persisted goal. this.sessionManager.flushNow(); }
private _setGoalState(next: GoalState, options: { persist?: boolean } = {}): void { const normalized = normalizeGoalState({ ...next, updatedAt: Date.now(), }); this._goalState = normalized; if (normalized.status === "active") { this._goalAccountingStartedAt ??= Date.now(); } else { this._goalAccountingStartedAt = undefined; } if (options.persist !== false) { this._persistGoalState(normalized); } this._emitGoalUpdate(); }
private _goalWithCurrentWallClock(now = Date.now()): GoalState { if (this._goalState.status !== "active" || !this._goalAccountingStartedAt) { return this._goalState; } const elapsedSeconds = Math.floor((now - this._goalAccountingStartedAt) / 1000); if (elapsedSeconds <= 0) { return this._goalState; } return { ...this._goalState, timeUsedSeconds: this._goalState.timeUsedSeconds + elapsedSeconds, }; }
private _goalWithAccountedWallClock(): GoalState { const now = Date.now(); const goal = this._goalWithCurrentWallClock(now); if (goal !== this._goalState) { this._goalAccountingStartedAt = now; } return goal; }
private _cancelSessionActions( predicate: (action: QueuedSessionAction) => boolean, error: Error, candidates = this._actionStore.clearableActions(), ): QueuedSessionAction[] { const matching = candidates.filter(predicate); const previousStates = new Map(matching.map((action) => [action.id, action.lifecycle.state])); const preparing = this._actionStore .activeActions() .filter( (action): action is SessionAction<PreparedTurnPayload> => action.payload.kind === "turn" && action.lifecycle.state === "preparing", ); const previousAnchor = preparing.at(-1); const actions = this._actionStore.remove(predicate, candidates); const restorableMessages: CustomMessage[] = []; const removed = new Set(actions); if (previousAnchor && removed.has(previousAnchor)) { for (const action of preparing) { if (!removed.has(action)) action.payload.prepared = undefined; } } for (const action of actions) { const ticket = this._actionStore.ticketFor(action); if ( action.payload.kind === "turn" && (action.payload.acceptedAgentMessage || !action.payload.queueVisible || previousStates.get(action.id) !== "queued") ) { ticket.rejectDelivered(error); } else { ticket.settleDelivered({ status: "not_applicable" }); } ticket.settleCompleted(error); const dispatched = previousStates.get(action.id) === "committing" && action.payload.kind === "turn"; if (action.payload.kind === "turn") { const payload = action.payload; const restorable = payload.records .filter( (record): record is DeliveryRecord & { message: CustomMessage } => (record.role === "next_turn" || (payload.acceptedAgentMessage && record.role === "prefix")) && record.message.role === "custom" && record.message.customType !== HARNESS_DIGEST_CUSTOM_TYPE && !record.durable, ) .map((record) => cloneCustomMessage(record.message)); restorableMessages.push(...restorable); // Lazy injection owns digest delivery: a cancelled turn re-arms it // instead of restoring a possibly stale digest message. if ( payload.records.some( (record) => record.message.role === "custom" && record.message.customType === HARNESS_DIGEST_CUSTOM_TYPE, ) ) { this._harnessDigestPending = true; } if (dispatched) { payload.captureRunMessages = new Set(payload.records.map((record) => record.message)); this.agent.state.messages = this.agent.state.messages.filter( (message) => !payload.captureRunMessages?.has(message), ); } } if (!dispatched) { this._actionStore.releaseTerminal(action); } } this._pendingNextTurnMessages.unshift(...restorableMessages); if (actions.length > 0) this._notifySessionInputCheckpointChange(); return actions; }
private _clearQueuedGoalContexts(): void { this._goalContinuationAwaitsRlmWork = false; this._pendingNextTurnMessages = this._pendingNextTurnMessages.filter( (message) => message.customType !== GOAL_CONTEXT_CUSTOM_TYPE, ); this.agent.removeQueuedMessages( (message) => message.role === "custom" && message.customType === GOAL_CONTEXT_CUSTOM_TYPE, ); this._cancelSessionActions( (action) => action.payload.kind === "turn" && action.payload.customMessage?.customType === GOAL_CONTEXT_CUSTOM_TYPE, new Error("Queued goal context was cleared before delivery."), ); this._emitQueueUpdate(); }
private _startGoal(objectiveText: string, tokenBudget: number | undefined): GoalState { const objective = validateGoalObjective(objectiveText); const budget = validateGoalBudget(tokenBudget); const now = Date.now(); const goal: GoalState = { active: true, status: "active", goalId: randomUUID(), objective, tokenBudget: budget, tokensUsed: 0, timeUsedSeconds: 0, continuationsUsed: 0, createdAt: now, updatedAt: now, }; this._goalAccountingStartedAt = now; this._goalContinuationAwaitsRlmWork = false; this._setGoalState(goal); return this._goalState; }
private _clearGoal(): void { this._clearQueuedGoalContexts(); this._setGoalState(emptyGoalState()); }
private _pauseGoal(reason = "Paused by user"): void { this._clearQueuedGoalContexts(); if (this._goalState.status !== "active") { this._emitGoalUpdate(); return; } const goal = this._goalWithAccountedWallClock(); this._setGoalState({ ...goal, active: false, status: "paused", lastReason: reason, lastError: undefined, }); }
private async _resumeGoal(): Promise<void> { if (!this._goalState.objective) { this._emitGoalUpdate(); return; } if (this._goalState.status !== "paused" && this._goalState.status !== "budget_limited") { this._emitGoalUpdate(); return; } const exhausted = this._goalState.tokenBudget !== undefined && this._goalState.tokensUsed >= this._goalState.tokenBudget; const nextStatus: GoalStatus = exhausted ? "budget_limited" : "active"; this._setGoalState({ ...this._goalState, active: nextStatus === "active", status: nextStatus, lastReason: exhausted ? "Goal token budget already reached" : undefined, lastError: undefined, }); if (nextStatus === "active") { await this._runOrQueueGoalContext("continuation"); } }
private _finishGoalWithError(errorMessage: string): void { if (!this._goalState.objective || this._goalState.status !== "active") { return; } const goal = this._goalWithAccountedWallClock(); this._setGoalState({ ...goal, active: false, status: "error", lastReason: errorMessage, lastError: errorMessage, }); }
private _finishGoalForTerminalAssistantMessage(message: AssistantMessage): void { if (this._goalState.status !== "active") { return; }
if (message.stopReason === "aborted") { this._goalAbortInProgress = false; return; }
if (message.stopReason === "error") { if (this._goalAbortInProgress) { this._goalAbortInProgress = false; return; } this._finishGoalWithError(message.errorMessage || "Assistant response failed"); } }
private _stopGoalContinuationForTerminalMessage(message: AssistantMessage): boolean { if (message.stopReason !== "error" && message.stopReason !== "aborted") { return false; } try { this._finishGoalForTerminalAssistantMessage(message); } catch { // Goal hooks must not reject; listener failures should not crash the agent loop. } return true; }
private _parseGoalSlashCommand(text: string): GoalSlashCommand | undefined { const command = parseSessionSlashCommand(text); if (command?.name !== "goal") return undefined;
const rest = command.args; const normalized = rest.toLowerCase(); if (!rest || normalized === "status") { return { kind: "status" }; } if (normalized === "clear" || normalized === "stop") { return { kind: "clear" }; } if (normalized === "pause") { return { kind: "pause" }; } if (normalized === "resume") { return { kind: "resume" }; }
let tokenBudget: number | undefined; let objective = rest; const firstToken = rest.split(/\s+/, 1)[0] ?? ""; if ( firstToken === "--budget" || firstToken === "--token-budget" || firstToken.startsWith("--budget=") || firstToken.startsWith("--token-budget=") ) { let valueText: string; if (firstToken === "--budget" || firstToken === "--token-budget") { const withoutFlag = rest.slice(firstToken.length).trimStart(); const nextSpace = withoutFlag.search(/\s/); if (nextSpace < 0) { throw new Error("Usage: /goal [--budget <tokens>] <objective>"); } valueText = withoutFlag.slice(0, nextSpace); objective = withoutFlag.slice(nextSpace + 1).trim(); } else { const separator = firstToken.indexOf("="); valueText = firstToken.slice(separator + 1); objective = rest.slice(firstToken.length).trim(); } tokenBudget = parseGoalBudgetValue(valueText); }
return { kind: "start", objective: validateGoalObjective(objective), tokenBudget, }; }
private _parseAutonomousSlashCommand(text: string): AutonomousSlashCommand | undefined { const command = parseSessionSlashCommand(text); if (command?.name !== "autonomous") return undefined; const tokens = parseCommandArgs(command.args); if (tokens.length === 0 || tokens[0]!.toLowerCase() === "status") { if (tokens.length > 1) { throw new Error(`Unexpected autonomous argument: ${tokens[1]}. ${AUTONOMOUS_BUDGET_USAGE}`); } return { kind: "status" }; } const subcommand = tokens[0]!.toLowerCase(); if (subcommand === "on" || subcommand === "enable" || subcommand === "enabled") { return { kind: "on", config: parseAutonomousBudgetOptions(tokens.slice(1)) }; } if (subcommand === "off" || subcommand === "disable" || subcommand === "disabled") { if (tokens.length > 1) { throw new Error(`Unexpected autonomous argument: ${tokens[1]}. ${AUTONOMOUS_BUDGET_USAGE}`); } return { kind: "off" }; } throw new Error(AUTONOMOUS_BUDGET_USAGE); }
private _formatAutonomousStatus(): string { const status = this.getAutonomousStatus(); const state = status.enabled ? "on" : "off"; const elapsedSeconds = status.startedAt ? Math.round((Date.now() - status.startedAt) / 1000) : 0; const gateSummary = status.gates.commands.length > 0 ? status.gates.commands.map((command) => `"${command}"`).join(", ") : "none"; const formatCount = (value: number): string => isUnlimitedAutonomousLimit(value) ? "unlimited" : AUTONOMOUS_STATUS_NUMBER_FORMAT.format(value); const timeBudget = isUnlimitedAutonomousLimit(status.limits.timeoutMs) ? "unlimited" : `${AUTONOMOUS_STATUS_NUMBER_FORMAT.format(Math.round(status.limits.timeoutMs / 1000))}s`; const subagentKeepAliveMs = status.subagentKeepAliveMs ?? 0; const keepAlive = subagentKeepAliveMs > 0 ? subagentKeepAliveMs >= 60_000 ? `${AUTONOMOUS_STATUS_NUMBER_FORMAT.format(Math.round(subagentKeepAliveMs / 60_000))}m` : `${AUTONOMOUS_STATUS_NUMBER_FORMAT.format(subagentKeepAliveMs)}ms` : "off"; return `[autonomous-status: ${state}]\n\nContinuations: ${formatCount(status.continuationsUsed)}/${formatCount(status.limits.maxContinuations)}. Turns: ${formatCount(status.turnsUsed)}/${formatCount(status.limits.maxTurns)}. Tokens: ${formatCount(status.tokensUsed)}/${formatCount(status.limits.maxTokens)}. Time: ${elapsedSeconds}s/${timeBudget}. Gates: ${gateSummary}. Subagent keep-alive: ${keepAlive}.`; }
private _emitAutonomousStatus(): void { const message = { role: "custom" as const, customType: "autonomous_status", content: this._formatAutonomousStatus(), display: true, details: this.getAutonomousStatus(), timestamp: Date.now(), } satisfies CustomMessage<AgentAutonomousStatus>; this.agent.state.messages.push(message); this.sessionManager.appendCustomMessageEntry( message.customType, message.content, message.display, message.details, ); this._emit({ type: "message_start", message }); this._emit({ type: "message_end", message }); }
private async _handleAutonomousSlashCommand(text: string): Promise<boolean> { const command = this._parseAutonomousSlashCommand(text); if (!command) { return false; } if (command.kind === "on") { setAutonomousEnabled(this._autonomousState, true, { cwd: this._cwd }); setAutonomousLimits(this._autonomousState, command.config); // Re-sync the keep-alive with the new window: a 0 setting must // disarm an already-armed timer, and a shortened window must not // keep the old one pending. this._disarmAutonomousSubagentKeepAlive(); if (this._autonomousContinuationAwaitsRlmWork) { this._armAutonomousSubagentKeepAlive(); } } else if (command.kind === "off") { setAutonomousEnabled(this._autonomousState, false); this._clearQueuedAutonomousContinuations(); this._clearAutonomousContinuationAwait(); } this._emitAutonomousStatus(); return true; }
private _appendBeforeAgentStartMessages( messages: AgentMessage[], result: Awaited<ReturnType<ExtensionRunner["emitBeforeAgentStart"]>>, ): void { if (!result?.messages) return; for (const message of result.messages) { messages.push({ role: "custom", customType: message.customType, content: message.content, display: message.display, details: message.details, timestamp: Date.now(), }); } }
private async _validateCanStartAgentRun(): Promise<void> { if (!this.model) { throw new Error(formatNoModelSelectedMessage()); } if (!this._modelRegistry.hasConfiguredAuth(this.model)) { const isOAuth = this._modelRegistry.isUsingOAuth(this.model); if (isOAuth) { throw new Error(formatAuthenticationFailedMessage(this.model.provider)); } throw new Error(formatNoApiKeyFoundMessage(this.model.provider)); } }
/** * Goals are pursued through the kernel goal skill, so the only tool the * model needs is ipython. Force-activate it (including into a live * continuation context) so the model can always reach `goal.complete()`. */ private _ensureGoalRuntimeActive(context?: AgentContext): void { if (!this._includeGoals) { throw new Error("Goals are disabled. Enable goals before using /goal."); } const ipythonTool = this._toolRegistry.get("ipython"); if (!ipythonTool) { throw new Error("Goals require the ipython tool, which is not available in this session."); } const activeToolNames = new Set(this.getActiveToolNames()); if (!activeToolNames.has("ipython")) { activeToolNames.add("ipython"); this.setActiveToolsByName([...activeToolNames]); } if (context) { const contextTools = [...(context.tools ?? [])]; if (!contextTools.some((tool) => tool.name === "ipython")) { contextTools.push(ipythonTool); context.tools = contextTools; } } }
private _maybeResumeGoalContinuationAfterRlmWork(): void { if (!this._goalContinuationAwaitsRlmWork) return; if (this._disposed || this._disposing || this._hasUnsettledRlmQuiescenceWork()) return; if (this._goalState.status !== "active" || !this._goalState.objective) { this._goalContinuationAwaitsRlmWork = false; return; } // Keep the deferral while admission is paused or the pump is suspended // (post-abort); the pause release and resumeQueuedWork retry. if (this._sessionInputAdmissionPauses.size > 0 || this._sessionInputPumpSuspended) return; const goalBeforeResume = this._goalState; try { this._ensureGoalRuntimeActive(); this._setGoalState({ ...this._goalState, continuationsUsed: this._goalState.continuationsUsed + 1, lastReason: undefined, lastError: undefined, }); const message = createGoalContextMessage(this._goalState, "continuation"); const normalized = normalizeMessageContent(message.content); // No front: a settling child's terminal notice must be read first. this._admitSessionInput( this._createPreparedTurnAction("followUp", normalized.text, normalized.images, { message, resumeIfIdle: true, }), ); this._goalContinuationAwaitsRlmWork = false; } catch { // Admission can race a new pause; roll back so the retry re-counts. this._setGoalState(goalBeforeResume); } }
/** * Hold the timer-driven autonomous continuation while descendant work is * unsettled, mirroring the goal gate: delegating and ending the turn is * correct behavior, and child replies and exit notices are the real * wake-up signals. The owed continuation is delivered when descendants * settle without consuming the continuation budget while it waits. An * active goal holds its own continuation, so the held continuation is * not double-queued behind it. */ private _holdAutonomousContinuationForRlmWork(message: AssistantMessage): boolean { if (!this._autonomousState.enabled) { return false; } if (message.stopReason === "error" || message.stopReason === "aborted") { return false; } if (autonomousLimitReason(this._autonomousState)) { // The run is over: hold nothing so the hook can apply the limit. return false; } if (!this._hasUnsettledRlmQuiescenceWork()) { return false; } // An active goal's own continuation gate owns the wake-up discipline; // drop any owed continuation so both are never queued. if (this._goalOwnsContinuationWakeup()) { this._clearAutonomousContinuationAwait(); return true; } this._autonomousContinuationAwaitsRlmWork = true; this._armAutonomousSubagentKeepAlive(); return true; }
/** True while an active goal's continuation loop owns the session wake-ups. */ private _goalOwnsContinuationWakeup(): boolean { return this._goalState.status === "active" && !!this._goalState.objective; }
/** Deliver the owed continuation once descendant work settles. */ private _maybeResumeAutonomousContinuationAfterRlmWork(): void { if (!this._autonomousContinuationAwaitsRlmWork) return; if (this._disposed || this._disposing || this._hasUnsettledRlmQuiescenceWork()) return; if (!this._autonomousState.enabled || this._goalOwnsContinuationWakeup()) { this._clearAutonomousContinuationAwait(); return; } // Keep the deferral while admission is paused or the pump is suspended // (post-abort); the pause release and resumeQueuedWork retry. if (this._sessionInputAdmissionPauses.size > 0 || this._sessionInputPumpSuspended) return; if (this._autonomousContinuationResumeTask) return; this._autonomousContinuationResumeTask = this._resumeOwedAutonomousContinuation().finally(() => { this._autonomousContinuationResumeTask = undefined; }); }
/** * Deliver the owed continuation, evaluating configured quality gates first * so a settlement never spends a turn when the gates already pass. Counted * at delivery like a goal continuation; a failed admission rolls the count * back and keeps the deferral for the pause-release retry. */ private async _resumeOwedAutonomousContinuation(): Promise<void> { const snapshot = this._snapshotAutonomousRuntimeState(); const beforeGates = { arrivalEpoch: this._sessionInputArrivalEpoch, noticeAdmissions: this._rlmTerminalNoticeAdmissionCount, }; try { // agent_end clears the live field, so fall back to the transcript; // either way the session's last assistant turn decides the gate run. const lastAssistantMessage = this._lastAssistantMessage ?? this._findLastAssistantInMessages(this.agent.state.messages); if (!lastAssistantMessage) { if (autonomousLimitReason(this._autonomousState)) { // The run is over; no continuation is owed anymore. this._clearAutonomousContinuationAwait(); return; } addAutonomousContinuation(this._autonomousState); this._admitOwedAutonomousContinuation(createAutonomousContinuationMessage(this._autonomousState)); return; } // Configured quality gates decide whether the run is already done. // The decision runs before any accounting is written so a stale // drop never spends a continuation or clobbers a user reset. const decision = await shouldAutonomouslyContinue(this._autonomousState, lastAssistantMessage, { cwd: this._cwd, signal: this.agent.signal, }); if (!decision.shouldContinue) { this._clearAutonomousContinuationAwait(); return; } // Re-validate after the gate await: mode-off, a goal takeover, or // any user-driven admission (finished or queued) must not be // bypassed by a stale continuation. Sibling terminal notices are // the exception: this owed continuation is exactly the wake that // reads them. const admissions = this._sessionInputArrivalEpoch - beforeGates.arrivalEpoch; const noticeAdmissions = this._rlmTerminalNoticeAdmissionCount - beforeGates.noticeAdmissions; const userDrivenAdmissions = admissions > noticeAdmissions; if ( this._disposed || this._disposing || !this._autonomousState.enabled || this._goalOwnsContinuationWakeup() || userDrivenAdmissions ) { this._clearAutonomousContinuationAwait(); return; } addAutonomousContinuation(this._autonomousState); const message = (decision.reason === "gate_failed" ? createAutonomousGateFailureContinuationMessage(this._autonomousState) : undefined) ?? createAutonomousContinuationMessage(this._autonomousState); this._admitOwedAutonomousContinuation(message); } catch { // Admission can race a new pause; roll back so the retry re-counts. this._restoreAutonomousRuntimeSnapshot(snapshot); } }
/** Admit an already-built owed continuation behind pending notices. */ private _admitOwedAutonomousContinuation(message: UserMessage): void { const normalized = normalizeMessageContent(message.content); // No front: a settling child's terminal notice must be read first. this._admitSessionInput( this._createPreparedTurnAction("followUp", normalized.text, normalized.images, { message, resumeIfIdle: true, }), ); this._clearAutonomousContinuationAwait(); }
/** * Deliver the keep-alive continuation while subagents are still active. * Returns false when admission raced a pause so the window can re-arm. */ private _deliverAutonomousSubagentKeepAlive(): boolean { if (autonomousLimitReason(this._autonomousState)) { // The run is over; no keep-alive is owed anymore. this._clearAutonomousContinuationAwait(); return true; } const snapshot = this._snapshotAutonomousRuntimeState(); try { addAutonomousContinuation(this._autonomousState); this._admitOwedAutonomousContinuation(createAutonomousSubagentKeepAliveMessage(this._autonomousState)); return true; } catch { // Admission can race a new pause; roll back so the retry re-counts. this._restoreAutonomousRuntimeSnapshot(snapshot); return false; } }
/** One keep-alive continuation per window of continuous subagent activity. */ private _armAutonomousSubagentKeepAlive(): void { if (this._autonomousSubagentKeepAliveTimer !== undefined) return; const keepAliveMs = this._autonomousState.subagentKeepAliveMs; if (!keepAliveMs || keepAliveMs <= 0) return; const timer = setTimeout(() => { this._autonomousSubagentKeepAliveTimer = undefined; this._fireAutonomousSubagentKeepAlive(); }, keepAliveMs); // A pending keep-alive must not hold the event loop open on its own. timer.unref(); this._autonomousSubagentKeepAliveTimer = timer; }
private _disarmAutonomousSubagentKeepAlive(): void { if (this._autonomousSubagentKeepAliveTimer === undefined) return; clearTimeout(this._autonomousSubagentKeepAliveTimer); this._autonomousSubagentKeepAliveTimer = undefined; }
private _clearAutonomousContinuationAwait(): void { this._autonomousContinuationAwaitsRlmWork = false; this._disarmAutonomousSubagentKeepAlive(); }
/** * Safety valve for hung children: while subagents stay active past the * keep-alive window, wake the parent so it can inspect and unblock them * (a stopped SIGTTIN child never delivers its exit notice). */ private _fireAutonomousSubagentKeepAlive(): void { if (!this._autonomousContinuationAwaitsRlmWork) return; if (this._disposed || this._disposing) return; if (!this._hasUnsettledRlmQuiescenceWork()) { // Descendants settled while the keep-alive was pending; the normal // resume path owns delivery. this._maybeResumeAutonomousContinuationAfterRlmWork(); return; } if (!this._autonomousState.enabled || this._goalOwnsContinuationWakeup()) { this._clearAutonomousContinuationAwait(); return; } // Keep the deferral while admission is paused or the pump is suspended // (post-abort); the pause release and resumeQueuedWork retry. if (this._sessionInputAdmissionPauses.size > 0 || this._sessionInputPumpSuspended) { this._armAutonomousSubagentKeepAlive(); return; } if (!this._deliverAutonomousSubagentKeepAlive()) { // Admission raced a pause; retry after another window. this._armAutonomousSubagentKeepAlive(); } }
/** * Queued goal continuations snapshot goal accounting at queue time, but * delivery can lag arbitrarily (a threshold compaction, admission pauses, * other queued work) while usage keeps accruing. A continuation's whole * purpose is "here is the current goal state — keep working", so stale * numbers misreport the budget and can mislead the model into stopping * early or ignoring limits. Refresh the frozen content in place at * delivery, preserving the message identity every queue/dedup marker * (post-compaction continuation tracking, threshold dedup) matches on. * Budget-limit steers are excluded: they are created immediately after the * accounting event they report, and `objective_updated` carries the * objective snapshot that was the event. */ private _refreshGoalContextMessageAtDelivery(message: AgentMessage): void { if (message.role !== "custom" || message.customType !== GOAL_CONTEXT_CUSTOM_TYPE) return; const custom = message as CustomMessage<GoalContextDetails | undefined>; const details = custom.details; if (details?.kind !== "continuation" || details.goalId === undefined) return; if ( this._goalState.status !== "active" || this._goalState.goalId !== details.goalId || !this._goalState.objective ) { return; } const images = Array.isArray(custom.content) ? custom.content.filter((block): block is ImageContent => block.type === "image") : undefined; const fresh = createGoalContextMessage(this._goalState, "continuation", images); custom.content = fresh.content; custom.details = fresh.details; custom.timestamp = fresh.timestamp; }
private _runOrQueueGoalContext(kind: "continuation" | "objective_updated", images?: ImageContent[]): void { if (!this._goalState.objective) return; this._ensureGoalRuntimeActive(); const message = createGoalContextMessage(this._goalState, kind, images); const normalized = normalizeMessageContent(message.content); const action = this._createPreparedTurnAction("followUp", normalized.text, normalized.images, { message, resumeIfIdle: true, // Front insertion only holds while nothing can be admitted ahead of it. priority: "pinned", }); this._admitSessionInput(action, { front: true, wake: false }); }
private async _handleGoalSlashCommand(text: string, images: ImageContent[] | undefined): Promise<boolean> { const command = this._parseGoalSlashCommand(text); if (!command) { return false; }
if (command.kind === "status") { this._emitGoalUpdate(); return true; }
if (command.kind === "clear") { this._clearGoal(); return true; }
if (command.kind === "pause") { this._pauseGoal(); return true; }
if (command.kind === "resume") { await this._resumeGoal(); return true; }
const previousWasActive = this._goalState.status === "active"; if (!this.isStreaming) { await this._validateCanStartAgentRun(); } this._ensureGoalRuntimeActive(); this._clearQueuedGoalContexts(); this._startGoal(command.objective, command.tokenBudget); await this._runOrQueueGoalContext(previousWasActive ? "objective_updated" : "continuation", images); return true; }
private async _handleCompactSlashCommand(args?: string): Promise<{ text?: string; display: boolean }> { const parsed = parseCompactCommandArgs(args ?? "", this.model?.contextWindow);
if (parsed.kind === "status") { const status = this.getCompactionThresholdStatus(); return { text: status, display: true }; }
if (parsed.kind === "reset") { const result = await this.setCompactionThreshold(undefined, { global: parsed.global }); const contextWindowStr = this.model?.contextWindow ? this.model.contextWindow.toLocaleString("en-US") : "unknown"; let text = `Auto-compaction threshold reset to default (model context window: ${contextWindowStr} tokens).`; if (parsed.global && result.globalSaved) { text += " Saved to global settings."; } return { text, display: true }; }
if (parsed.kind === "set_threshold") { const result = await this.setCompactionThreshold(parsed.threshold, { global: parsed.global }); const formattedThreshold = parsed.threshold.toLocaleString("en-US"); const contextWindowStr = this.model?.contextWindow ? this.model.contextWindow.toLocaleString("en-US") : "unknown";
const usage = this.getContextUsage(); const currentTokens = usage?.tokens; const contextWindow = this.model?.contextWindow ?? 0; const effectiveSettings = this._getEffectiveCompactionSettings(); const exceeds = currentTokens !== null && currentTokens !== undefined && contextWindow > 0 ? shouldCompact(currentTokens, contextWindow, effectiveSettings) : false;
let text = `Auto-compaction threshold set to ${formattedThreshold} tokens (model context window: ${contextWindowStr} tokens).`; if (parsed.global && result.globalSaved) { text += " Saved to global settings."; }
if (exceeds) { text += ` Current context (${currentTokens!.toLocaleString("en-US")} tokens) exceeds threshold; compacting now...`; await this.compact(parsed.instructions, { skipAbort: true }); } return { text, display: true }; }
await this.compact(parsed.instructions, { skipAbort: true }); return { text: undefined, display: false }; }
getCompactionThresholdStatus(): string { const usage = this.getContextUsage(); const tokensStr = usage?.tokens !== null && usage?.tokens !== undefined ? `${usage.tokens.toLocaleString("en-US")} tokens` : "unknown"; const percentStr = usage?.percent !== null && usage?.percent !== undefined ? ` (${usage.percent.toFixed(1)}%)` : ""; const contextWindowStr = this.model?.contextWindow ? `${this.model.contextWindow.toLocaleString("en-US")} tokens` : "unknown";
if (this._compactionThresholdTokens !== undefined) { return `Auto-compaction threshold: ${this._compactionThresholdTokens.toLocaleString("en-US")} tokens (model context window: ${contextWindowStr}, current context: ${tokensStr}${percentStr}).`; } return `Auto-compaction threshold: default (model context window: ${contextWindowStr}, current context: ${tokensStr}${percentStr}).`; }
async setCompactionThreshold( threshold: number | undefined, options: { global?: boolean } = {}, ): Promise<{ threshold?: number; globalSaved?: boolean; globalError?: string }> { if (threshold !== undefined && (!Number.isSafeInteger(threshold) || threshold <= 0)) { throw new Error("Compaction threshold must be a positive integer."); }
this.sessionManager.appendCustomEntryWithRollback(COMPACTION_THRESHOLD_STATE_CUSTOM_TYPE, { threshold: threshold ?? null, }); this._compactionThresholdTokens = threshold;
let globalError: string | undefined; if (options.global) { await this.settingsManager.flush(); const staleErrors = this.settingsManager.drainErrors("global"); for (const { error } of staleErrors) { console.warn(`Warning: Earlier global settings write failed: ${error.message}`); } this.settingsManager.setCompactionMaxContextTokens(threshold); await this.settingsManager.flush(); const errors = this.settingsManager.drainErrors("global"); globalError = errors.map(({ error }) => error.message).join("; ") || undefined; }
return { threshold, globalSaved: options.global === true && globalError === undefined, ...(globalError ? { globalError } : {}), }; }
getCompactionThreshold(): number | undefined { return this._compactionThresholdTokens; }
private _accountGoalUsageForAssistantMessage(message: AssistantMessage): boolean { if (!this._goalState.objective) { return false; } if (message.stopReason === "error" || message.stopReason === "aborted") { return false; } if (this._goalAccountedAssistantMessages.has(message)) { return false; } // Usage is attributed at the assistant message's message_end, which fires // before that turn's ipython cell runs. goal.complete() only arrives later // over the kernel host bridge, so the completing turn is always accounted // while the goal is still active. Only count turns spent pursuing the goal; // post-completion turns (e.g. a closing summary) must not be attributed. if (this._goalState.status !== "active") { return false; } this._goalAccountedAssistantMessages.add(message); const tokenDelta = goalTokenDeltaForUsage(message.usage); const goal = this._goalWithAccountedWallClock(); const nextGoal: GoalState = { ...goal, tokensUsed: goal.tokensUsed + tokenDelta, }; const budgetReached = nextGoal.tokenBudget !== undefined && nextGoal.tokensUsed >= nextGoal.tokenBudget; if (!budgetReached) { this._setGoalState(nextGoal); return false; } this._setGoalState({ ...nextGoal, active: false, status: "budget_limited", lastReason: `Reached ${nextGoal.tokenBudget} token goal budget`, lastError: undefined, }); return true; }
private get _steeringStopPending(): boolean { return ( this._actionStore.queuedActions("next_turn_boundary").length > 0 || this._actionStore .activeActions("next_turn_boundary") .some( (action) => action.payload.kind === "turn" && (action.lifecycle.state === "selected" || action.lifecycle.state === "preparing"), ) ); }
private _shouldStopBeforeTurn(): boolean { return this._steeringStopPending; }
private async _shouldStopAfterTurn(context: ShouldStopAfterTurnContext): Promise<boolean> { if (this._stopGoalContinuationForTerminalMessage(context.message)) { return true; } try { if (this._accountGoalUsageForAssistantMessage(context.message)) { const message = createGoalContextMessage(this._goalState, "budget_limit"); const normalized = normalizeMessageContent(message.content); await this._queuePreparedPrompt("steer", normalized.text, normalized.images, { message, resumeIfIdle: true, }); } } catch { // Goal accounting must not interrupt the core agent loop. } // Serialized refine checkpoint: in print/headless mode, run refinement // planning+apply synchronously here — the quiescent boundary between // turns — so it never overlaps the primary model request. // This MUST run BEFORE threshold compaction to prevent the // compaction model call from overlapping an in-flight refine // plan/apply that was started at message_end. if (this._serializedRefine) { // Ensure the preceding message_end processing (counter increment, // background plan kickoff) has completed before the checkpoint. await this._agentEventQueue; await this._runSerializedRefineCheckpoint(); } if (await this._shouldStopForThresholdCompaction(context)) { return true; } // Steering stops continuation only after mandatory serialized checkpoints. // Returning true here still prevents the agent loop from starting another turn. return this._steeringStopPending; }
private async _shouldStopForThresholdCompaction(context: ShouldStopAfterTurnContext): Promise<boolean> { this._continueAfterThresholdCompaction = false; if (this._pendingRequestedCompaction === undefined && !(await this._thresholdCompactionNeeded(context))) { return false; }
const lastMessage = this.agent.state.messages[this.agent.state.messages.length - 1]; // A queued continuation disproves the assistant-last "task finished" heuristic, so preserve a true set above. this._continueAfterThresholdCompaction ||= lastMessage !== undefined && lastMessage.role !== "assistant"; return true; }
/** * Serialized-mode auto-refine checkpoint called from _shouldStopAfterTurn. * Runs the review, planning, and application phases inline between turns * at the quiescent shouldStopAfterTurn boundary. This path NEVER calls * _maybeAutoRefine, _runApprovedRefine, public refine(), agent.abort(), * or agent.waitForIdle — all of which would deadlock or defer because * the agent loop still owns activeRun at this point. Instead it calls * _reviewAutoRefine, _planRefine, and _applyRefine directly with proper * in-flight guards and counter resets. */ private async _runSerializedRefineCheckpoint(): Promise<void> { if (this._disposed || this._disposing) { return; }
// 1. Await any background plan that was started at message_end // (either for a pending refine.run or for interval-triggered // auto-refine). This must be checked BEFORE the pending and // interval checks because background planning may have consumed // the pending request at message_end. const branchVersion = this._autoRefineBranchVersion; const bgConsumption = await this._consumeSerializedBackgroundPlan(async (bgResult) => { if (this._disposed || this._disposing) { return true; }
if (bgResult?.status === "plan") { if (bgResult.branchVersion !== this._autoRefineBranchVersion) { if (!this._pendingRequestedRefine) { this._lastAutoRefineReviewAt = Date.now(); this._assistantTurnsSinceAutoRefine = 0; return true; } } else { // Apply the EXACT background plan directly via _applyRefine // (no second _planRefine call). try { await this._applySerializedPlan(bgResult); } catch (error) { this._emitRefineFailed(error); } this._lastAutoRefineReviewAt = Date.now(); this._assistantTurnsSinceAutoRefine = 0; if (!this._pendingRequestedRefine) { return true; } } }
if (bgResult?.status === "skip") { // Reviewer declined or an extension skipped during background planning. // Reset exactly once. Never retry the interval review; only fall through for a separate pending refine.run. if (bgResult.explicit) { this._emitRefineFailed(new RefineSkippedError("Refinement skipped by extension")); } this._lastAutoRefineReviewAt = Date.now(); this._assistantTurnsSinceAutoRefine = 0; if (!this._pendingRequestedRefine) { return true; } }
if (bgResult?.status === "failure") { // Background review or planning failure stamps cooldown without a synchronous retry. // A separately queued refine.run may still be serviced below. if (branchVersion === this._autoRefineBranchVersion) { this._lastAutoRefineReviewAt = Date.now(); } // Re-queue an explicit refine.run whose background plan failed, // but only when branchVersion is still current and no newer // pending request has arrived since the background plan consumed // the original one. A newer request retains priority; interval // failures keep existing no-retry cooldown semantics. if ( bgResult.explicit && bgResult.branchVersion === this._autoRefineBranchVersion && !this._pendingRequestedRefine ) { this._pendingRequestedRefine = bgResult.options; } if (!this._pendingRequestedRefine) { return true; } }
if (bgResult?.status === "invalidated" && !this._pendingRequestedRefine) { this._lastAutoRefineReviewAt = Date.now(); this._assistantTurnsSinceAutoRefine = 0; return true; }
await this._runSerializedRefineCheckpointAfterBackground(branchVersion); return true; }); if (this._disposed || this._disposing || bgConsumption !== "none") { return; } await this._runSerializedRefineCheckpointAfterBackground(branchVersion); }
private async _runSerializedRefineCheckpointAfterBackground(branchVersion: number): Promise<void> { // No background result, or a refine.run arrived while the background result was // in flight. Fall through so an explicit pending request is serviced at this boundary.
// 2. Agent-callable refine.run requests that were NOT consumed by // background planning (e.g. interval not reached at message_end, // or cooldown was active). Service them synchronously. const pending = this._pendingRequestedRefine; if (pending) { this._pendingRequestedRefine = undefined; try { await this._runSerializedRefine(pending, "self"); } catch (error) { this._emitRefineFailed(error); } this._lastAutoRefineReviewAt = Date.now(); this._assistantTurnsSinceAutoRefine = 0; return; }
// 3. Post-compaction auto-refine. Serialized sessions defer the // compaction trigger to this boundary instead of entering the interactive // path, which waits for agent idle and can never run inside a tool loop. if (!this._autoRefineAllowedForSession()) { this._compactAutoRefinePending = false; return; } const settings = this.settingsManager.getAutoRefineSettings(); if (!settings.enabled) { this._compactAutoRefinePending = false; return; } if (this._compactAutoRefinePending) { if (!settings.compact) { this._compactAutoRefinePending = false; } else { const nowMs = Date.now(); const underCooldown = this._lastAutoRefineReviewAt > 0 && nowMs - this._lastAutoRefineReviewAt < settings.cooldownMs; if (underCooldown) { // Preserve the compact trigger for a later boundary, matching the // interactive path's pending behavior while the cooldown is active. return; } this._compactAutoRefinePending = false; await this._runSerializedAutoRefineReview("compact", branchVersion); return; } }
// 4. Interval-triggered auto-refine (no background plan was started). if (this._assistantTurnsSinceAutoRefine < settings.turnInterval) { return; } const nowMs = Date.now(); const underCooldown = this._lastAutoRefineReviewAt > 0 && nowMs - this._lastAutoRefineReviewAt < settings.cooldownMs; if (underCooldown) { return; } await this._runSerializedAutoRefineReview("turn_interval", branchVersion); }
private async _runSerializedAutoRefineReview( reason: "compact" | "turn_interval", branchVersion: number, ): Promise<void> { const reviewAbort = new AbortController(); this._autoRefineReviewAbort = reviewAbort; this._autoRefineInProgress = true; try { const review = await this._reviewAutoRefine( { reason, turnsSinceLastReview: this._assistantTurnsSinceAutoRefine }, reviewAbort.signal, ); if (this._disposed || this._disposing || branchVersion !== this._autoRefineBranchVersion) { return; } if (!review.shouldRefine) { this._lastAutoRefineReviewAt = Date.now(); this._assistantTurnsSinceAutoRefine = 0; return; } await this._runSerializedRefine({ instructions: autoRefineInstructions(reason, review) }, "auto"); if (this._disposed || this._disposing || branchVersion !== this._autoRefineBranchVersion) { return; } this._lastAutoRefineReviewAt = Date.now(); this._assistantTurnsSinceAutoRefine = 0; } catch (error) { if (branchVersion === this._autoRefineBranchVersion) { this._lastAutoRefineReviewAt = Date.now(); // An extension skip is an intentional non-round, not a failure. if (error instanceof RefineSkippedError) { this._assistantTurnsSinceAutoRefine = 0; } else { this._emitRefineFailed(error); } } } finally { if (this._autoRefineReviewAbort === reviewAbort) { this._autoRefineReviewAbort = undefined; } this._autoRefineInProgress = false; } }
/** * Claim and process the serialized background plan if one is in flight. * A concurrent caller waits for the claim holder's full processing callback * instead of resuming as soon as planning settles. */ private async _consumeSerializedBackgroundPlan( consume: (result: SerializedBackgroundPlanResult | undefined) => Promise<boolean>, ): Promise<"none" | "waited" | "continue" | "stop"> { if (this._serializedPlanClaim) { await this._serializedPlanClaim.catch(() => undefined); return "waited"; } const planInFlight = this._serializedPlanInFlight; if (!planInFlight) { return "none"; }
let releaseClaim: () => void = () => {}; const claim = new Promise<void>((resolve) => { releaseClaim = resolve; }); this._serializedPlanClaim = claim; try { const result = await planInFlight.catch(() => undefined); if (this._serializedPlanInFlight === planInFlight) { this._serializedPlanInFlight = undefined; this._serializedExplicitRefineOptions = undefined; } return (await consume(result)) ? "stop" : "continue"; } finally { releaseClaim(); if (this._serializedPlanClaim === claim) { this._serializedPlanClaim = undefined; } } }
/** * Apply an exact background plan directly via _applyRefine without * calling _planRefine again. Sets _refineInFlight for safety. */ private async _applySerializedPlan( bgResult: Extract<SerializedBackgroundPlanResult, { status: "plan" }>, ): Promise<void> { let resolveApplySettled: () => void = () => {}; const applySettled = new Promise<void>((resolve) => { resolveApplySettled = resolve; }); this._refineInFlight = applySettled; try { await this._applyRefine(bgResult.plan, bgResult.options, bgResult.abort, bgResult.source); } finally { resolveApplySettled(); if (this._refineInFlight === applySettled) { this._refineInFlight = undefined; } this._notifySessionInputCheckpointChange(); this._scheduleSessionInputPump(); } }
/** * Start background refinement planning at assistant message_end, while * tools are still executing. The plan (if any) is awaited at the * shouldStopAfterTurn boundary before applying. Planning overlaps tool * execution only — never another model request. */ private _maybeStartSerializedBackgroundPlan(): void { if (!this._serializedRefine || this._disposed || this._disposing) { return; } // Don't start if a plan is already in flight. if (this._serializedPlanInFlight || this._refineInFlight || this._refinePlanInFlight) { return; }
// Start background planning for a pending agent-callable // refine.run request, so its plan is ready at the shouldStopAfterTurn // boundary. The pending request is consumed (cleared) here so the // boundary doesn't re-plan it. Explicit refine.run skips the review gate. const pending = this._pendingRequestedRefine; if (pending) { this._pendingRequestedRefine = undefined; this._serializedExplicitRefineOptions = pending; const refineAbort = new AbortController(); this._refineAbortController = refineAbort; const branchVersion = this._autoRefineBranchVersion; this._serializedPlanInFlight = this._runBackgroundPlan(pending, refineAbort, branchVersion, true); return; }
// Interval-triggered auto-refine background planning. if (!this._autoRefineAllowedForSession()) { return; } const settings = this.settingsManager.getAutoRefineSettings(); if (!settings.enabled) { return; } if (this._assistantTurnsSinceAutoRefine < settings.turnInterval) { return; } const nowMs = Date.now(); const underCooldown = this._lastAutoRefineReviewAt > 0 && nowMs - this._lastAutoRefineReviewAt < settings.cooldownMs; if (underCooldown) { return; }
const refineAbort = new AbortController(); this._refineAbortController = refineAbort; const branchVersion = this._autoRefineBranchVersion; // Pass empty options — _runBackgroundPlan derives instructions from // the review result for interval-triggered auto-refine. this._serializedPlanInFlight = this._runBackgroundPlan({}, refineAbort, branchVersion); }
/** * Shared background planning coroutine. Runs review + planRefine and * returns a discriminated result so the boundary can distinguish * reviewer-declined ("skip") from failure ("failure") from a ready * plan ("plan") and apply that exact plan without re-planning. */ private async _runBackgroundPlan( options: { instructions?: string; rollbackId?: string; global?: boolean }, refineAbort: AbortController, branchVersion: number, skipReview = false, ): Promise<SerializedBackgroundPlanResult | undefined> { try { let planOptions = options; if (!skipReview) { // Interval-triggered: run the review gate first, then derive // instructions from the review result (not prepopulated). const review = await this._reviewAutoRefine( { reason: "turn_interval", turnsSinceLastReview: this._assistantTurnsSinceAutoRefine, }, refineAbort.signal, ); if (this._disposed || this._disposing || branchVersion !== this._autoRefineBranchVersion) { return { status: "invalidated", branchVersion }; } if (!review.shouldRefine) { return { status: "skip" }; } planOptions = { instructions: autoRefineInstructions("turn_interval", review), }; } // For explicit refine.run (skipReview=true), plan directly with // the user-provided options — no auto-review gate. const plan = await this._planRefine(planOptions, refineAbort.signal, skipReview ? "manual" : "auto"); if (this._disposed || this._disposing || branchVersion !== this._autoRefineBranchVersion) { return { status: "invalidated", branchVersion }; } return { status: "plan", plan, options: planOptions, abort: refineAbort, branchVersion, source: skipReview ? "self" : "auto", }; } catch (error) { if (this._disposed || this._disposing || branchVersion !== this._autoRefineBranchVersion) { return { status: "invalidated", branchVersion }; } if (error instanceof RefineSkippedError) { return { status: "skip", explicit: skipReview }; } return { status: "failure", explicit: skipReview, options, branchVersion, }; } finally { if (this._refineAbortController === refineAbort) { this._refineAbortController = undefined; } } }
/** * Direct serialized plan+apply. Calls _planRefine and _applyRefine with * proper in-flight guards but NEVER agent.waitForIdle or agent.abort. * The caller (shouldStopAfterTurn) is already at the quiescent boundary, * so the agent is between turns and _applyRefine's disconnect/reconnect * is safe. */ private async _runSerializedRefine( options: { instructions?: string; rollbackId?: string; global?: boolean; }, source: Exclude<RefinementSource, "user">, ): Promise<void> { if (this._disposed || this._disposing) { return; } // Guard: serialize against concurrent _runSerializedRefine calls. // _serializedPlanInFlight covers background planning; _refineInFlight // covers the apply phase. Both must be settled before starting a new // plan+apply cycle. while (this._serializedPlanInFlight || this._refineInFlight || this._refinePlanInFlight) { if (this._serializedPlanInFlight) { await this._consumeSerializedBackgroundPlan(async () => false); } else if (this._refineInFlight) { await this._refineInFlight; } else { await this._refinePlanInFlight; } } if (this._disposed || this._disposing) { return; }
const refineAbort = new AbortController(); this._refineAbortController = refineAbort;
const planRun = this._planRefine(options, refineAbort.signal, source === "auto" ? "auto" : "manual"); const planSettled = planRun.then( () => undefined, () => undefined, ); this._refinePlanInFlight = planSettled; let plan: RefinementPlan; try { plan = await planRun; } catch (error) { if (this._refineAbortController === refineAbort) { this._refineAbortController = undefined; } this._scheduleSessionInputPump(); throw error; } finally { if (this._refinePlanInFlight === planSettled) { this._refinePlanInFlight = undefined; } }
if (this._disposed || refineAbort.signal.aborted) { if (this._refineAbortController === refineAbort) { this._refineAbortController = undefined; } this._scheduleSessionInputPump(); return; }
// Do NOT call agent.waitForIdle() — we are at the quiescent boundary // already (shouldStopAfterTurn). _applyRefine handles disconnect/reconnect internally. let resolveApplySettled: () => void = () => {}; const applySettled = new Promise<void>((resolve) => { resolveApplySettled = resolve; }); this._refineInFlight = applySettled; try { await this._applyRefine(plan, options, refineAbort, source); } finally { resolveApplySettled(); if (this._refineInFlight === applySettled) { this._refineInFlight = undefined; } this._notifySessionInputCheckpointChange(); this._scheduleSessionInputPump(); } }
private async _thresholdCompactionNeeded(context: ShouldStopAfterTurnContext): Promise<boolean> { const settings = this._getEffectiveCompactionSettings(); if (!settings.enabled) return false;
const contextWindow = this.model?.contextWindow ?? 0; const compactionEntry = getLatestCompactionEntry(this.sessionManager.getBranch()); const compactionTimestamp = compactionEntry ? new Date(compactionEntry.timestamp).getTime() : undefined; if (compactionTimestamp !== undefined && context.message.timestamp <= compactionTimestamp) { return false; }
const contextTokens = this._getThresholdContextTokens(context.message, compactionTimestamp); if (contextTokens === undefined || !shouldCompact(contextTokens, contextWindow, settings)) { return false; }
// Goal continuation takes exclusive priority over autonomous continuation, matching _getContinuationMessages. if (this._queueGoalContinuationForThresholdCompaction(context.message)) { this._continueAfterThresholdCompaction = true; } else if (await this._queueAutonomousContinuationForThresholdCompaction(context.message)) { this._continueAfterThresholdCompaction = true; } return true; }
private _snapshotAutonomousRuntimeState(): AutonomousRuntimeSnapshot { return { continuationsUsed: this._autonomousState.continuationsUsed, gateAttempts: { ...this._autonomousState.gateAttempts }, lastGateFailure: this._autonomousState.lastGateFailure ? { ...this._autonomousState.lastGateFailure } : undefined, lastGateFailureSnapshot: this._autonomousState.lastGateFailureSnapshot ? { ...this._autonomousState.lastGateFailureSnapshot } : undefined, }; }
private _restoreAutonomousRuntimeSnapshot(snapshot: AutonomousRuntimeSnapshot): void { this._autonomousState.continuationsUsed = snapshot.continuationsUsed; this._autonomousState.gateAttempts = { ...snapshot.gateAttempts }; this._autonomousState.lastGateFailure = snapshot.lastGateFailure ? { ...snapshot.lastGateFailure } : undefined; this._autonomousState.lastGateFailureSnapshot = snapshot.lastGateFailureSnapshot ? { ...snapshot.lastGateFailureSnapshot } : undefined; }
private async _queueAutonomousContinuationForThresholdCompaction( message: AssistantMessage, ): Promise<AgentMessage | undefined> { const queuedMessage = this._queuedAutonomousThresholdContinuations.get(message); if (queuedMessage && this._postCompactionContinuationMessages.includes(queuedMessage)) { return queuedMessage; } // Hold the post-compaction continuation while descendants are unsettled; // the owed continuation is delivered when they settle. if (this._holdAutonomousContinuationForRlmWork(message)) { return undefined; } const snapshot = this._snapshotAutonomousRuntimeState(); const arrivalEpoch = this._sessionInputArrivalEpoch; const autonomousMessage = await nextAutonomousContinuation(this._autonomousState, message, { cwd: this._cwd, signal: this.agent.signal, }); if (!autonomousMessage) { return undefined; } if (this._sessionInputArrivalEpoch !== arrivalEpoch) { this._restoreAutonomousRuntimeSnapshot(snapshot); return undefined; } this._queuedAutonomousThresholdContinuations.set(message, autonomousMessage); this._queuedAutonomousContinuationSnapshots.set(autonomousMessage, snapshot); this._postCompactionContinuationMessages.push(autonomousMessage); this._pendingThresholdCompactionAutonomousMessages.push(autonomousMessage); const text = typeof autonomousMessage.content === "string" ? autonomousMessage.content : autonomousMessage.content.map((block) => (block.type === "text" ? block.text : "")).join("\n"); this._admitSessionInput( this._createPreparedTurnAction("followUp", text, undefined, { message: autonomousMessage, }), ); return autonomousMessage; }
// The role heuristic reads an assistant-last threshold stop as "task finished" and // agent.continue() cannot resume from it, so the goal continuation is queued as a session input. private _queueGoalContinuationForThresholdCompaction(message: AssistantMessage): boolean { if (message.stopReason === "error" || message.stopReason === "aborted") { return false; } if (this._goalState.status !== "active" || !this._goalState.objective) { return false; } const alreadyQueued = this._queuedGoalThresholdContinuation; if ( alreadyQueued !== undefined && this._actionStore.unfinishedActions().some((action) => { if (action.payload.kind !== "turn" || primaryDeliveryRecord(action).message !== alreadyQueued) return false; // A running continuation may already need a successor; only undelivered actions deduplicate. return ( action.lifecycle.state === "queued" || action.lifecycle.state === "selected" || action.lifecycle.state === "preparing" || action.lifecycle.state === "committing" ); }) ) { return true; } try { this._ensureGoalRuntimeActive(); this._setGoalState({ ...this._goalState, continuationsUsed: this._goalState.continuationsUsed + 1, lastReason: undefined, lastError: undefined, }); const goalMessage = createGoalContextMessage(this._goalState, "continuation"); const normalized = normalizeMessageContent(goalMessage.content); this._admitSessionInput( this._createPreparedTurnAction("followUp", normalized.text, normalized.images, { message: goalMessage, }), ); this._queuedGoalThresholdContinuation = goalMessage; return true; } catch { return false; } }
// Withdraws a goal continuation queued for a threshold compaction the user cancelled, // rolling back the continuationsUsed increment so the next natural stop re-queues it. private _clearQueuedGoalContinuationAfterCancelledThresholdCompaction( queuedGoalContinuation: AgentMessage | undefined, ): void { if (queuedGoalContinuation === undefined) return; const cancelled = this._cancelSessionActions( (action) => action.payload.kind === "turn" && primaryDeliveryRecord(action).message === queuedGoalContinuation, new Error("Queued goal continuation was cleared before delivery."), ); this._queuedGoalThresholdContinuation = undefined; // A stale marker (continuation already consumed) matches no action; only an // actual cancellation may roll back its queue-time continuationsUsed increment. if (cancelled.length === 0) return; this._setGoalState({ ...this._goalState, continuationsUsed: this._goalState.continuationsUsed - 1 }); this._emitQueueUpdate(); }
private _clearQueuedAutonomousContinuations( options: { restoreAutonomousState?: boolean; messages?: AgentMessage[] } = {}, ): void { const requestedMessages = options.messages ?? [...this._postCompactionContinuationMessages]; const requestedMessageSet = new Set(requestedMessages); const queuedMessages = this._postCompactionContinuationMessages.filter((message) => requestedMessageSet.has(message), ); if (queuedMessages.length === 0) { return; } const queuedMessageSet = new Set(queuedMessages); this._postCompactionContinuationMessages = this._postCompactionContinuationMessages.filter( (message) => !queuedMessageSet.has(message), ); this.agent.removeQueuedMessages((message) => queuedMessageSet.has(message)); this._cancelSessionActions( (action) => action.payload.kind === "turn" && queuedMessageSet.has(primaryDeliveryRecord(action).message), new Error("Queued autonomous continuation was cleared before delivery."), ); this._emitQueueUpdate(); if (options.restoreAutonomousState) { for (const queuedMessage of queuedMessages) { const snapshot = this._queuedAutonomousContinuationSnapshots.get(queuedMessage); if (snapshot) { this._restoreAutonomousRuntimeSnapshot(snapshot); break; } } } for (const queuedMessage of queuedMessages) { this._queuedAutonomousContinuationSnapshots.delete(queuedMessage); } this._pendingThresholdCompactionAutonomousMessages = this._pendingThresholdCompactionAutonomousMessages.filter( (message) => !queuedMessageSet.has(message), ); if (options.messages === undefined) { this._continueAfterThresholdCompaction = false; } if (!this.agent.hasQueuedMessages() && this.unfinishedActionCount === 0) { this._cancelPostCompactionContinue(); } }
private _clearQueuedAutonomousContinuationsAfterSkippedThresholdCompaction( shouldContinueAfterThreshold: boolean, queuedMessages: AgentMessage[], ): void { if (shouldContinueAfterThreshold) { this._clearQueuedAutonomousContinuations({ restoreAutonomousState: true, messages: queuedMessages, }); } }
/** * Handle a goal.* request from the Python kernel host bridge (the bundled * goal skill). All goal state stays host-side; the kernel only sees the * serialized snake_case response. */ handleGoalHostRequest(type: string, payload: Record<string, unknown> = {}): GoalHostResponse { if (!this._includeGoals) { throw new Error("goals are disabled in this session"); } switch (type) { case "goal.get": return goalHostResponse(this.goalState); case "goal.create": { if (typeof payload.objective !== "string") { throw new Error("goal.create objective must be a string"); } if (payload.token_budget !== undefined && typeof payload.token_budget !== "number") { throw new Error("goal.create token_budget must be an integer when provided"); } return goalHostResponse(this._createGoalFromHost(payload.objective, payload.token_budget)); } case "goal.complete": return goalHostResponse(this._completeGoalFromHost()); default: throw new Error(`unknown goal request type "${type}"`); } }
/** * Handle a compact.* request from the kernel host bridge. Compaction would * abort the run executing the requesting cell, so compact.run only schedules * it; _checkCompaction consumes the request at the turn boundary. */ handleCompactHostRequest(type: string, payload: Record<string, unknown> = {}): Record<string, unknown> { if (!this._includeCompactSkill) { throw new Error("the compact skill is disabled in this session"); } switch (type) { case "compact.status": { const usage = this.getContextUsage(); const contextWindow = usage?.contextWindow ?? null; return { tokens: usage?.tokens ?? null, context_window: contextWindow, threshold: this._compactionThresholdTokens ?? null, // The ceiling the harness will refuse to build past; asked for, never pushed. payload_ceiling: contextWindow ? calculatePromptPayloadCeiling(contextWindow) : null, percent: usage?.percent ?? null, scheduled: this._pendingRequestedCompaction !== undefined, }; } case "compact.threshold": { const value = payload.value; if (value === undefined || value === null) { return { threshold: this._compactionThresholdTokens ?? null, context_window: this.model?.contextWindow ?? null, }; } if (typeof value === "string") { const parsed = parseCompactCommandArgs(`at ${value}`, this.model?.contextWindow); if (parsed.kind === "set_threshold") { this.setCompactionThreshold(parsed.threshold).catch(() => {}); return { threshold: parsed.threshold, raw: parsed.rawValue, }; } else if (parsed.kind === "reset") { this.setCompactionThreshold(undefined).catch(() => {}); return { threshold: null, }; } throw new Error(`invalid threshold value: ${value}`); } if (typeof value === "number" && Number.isSafeInteger(value) && value > 0) { this.setCompactionThreshold(value).catch(() => {}); return { threshold: value, }; } throw new Error("threshold value must be a positive integer or threshold string"); } case "compact.run": { const instructions = payload.instructions; if (instructions !== undefined && typeof instructions !== "string") { throw new Error("compact.run instructions must be a string when provided"); } if (!this.isStreaming) { return { scheduled: false, reason: "no active turn; compaction can only be requested while a turn is running", }; } const preparation = prepareCompaction( this.sessionManager.getBranch(), this._getEffectiveCompactionSettings(), ); if (!preparation) { const lastEntry = this.sessionManager.getBranch().at(-1); return { scheduled: false, reason: lastEntry?.type === "compaction" ? "already compacted" : "session is too short to compact", }; } this._pendingRequestedCompaction = { customInstructions: instructions }; return { scheduled: true, note: "Compaction runs when the current turn ends; you resume automatically afterwards. Continue working normally.", }; } default: throw new Error(`unknown compact request type "${type}"`); } }
/** * Handle a refine.* request from the kernel host bridge. Like compact, * refinement waits for the current turn to become idle before applying * changes, so refine.run only schedules it; _consumePendingRequestedRefine * fires it at the turn boundary. This prevents a deadlock that would occur * if refine() awaited agent idle from within the active tool call. */ handleRefineHostRequest(type: string, payload: Record<string, unknown> = {}): Record<string, unknown> { switch (type) { case "refine.status": { return { pending: this._pendingRequestedRefine !== undefined, in_flight: this._refineInFlight !== undefined || this._refinePlanInFlight !== undefined || this._serializedPlanInFlight !== undefined, }; } case "refine.run": { const instructions = payload.instructions; if (instructions !== undefined && typeof instructions !== "string") { throw new Error("refine.run instructions must be a string when provided"); } const globalFlag = payload.global; if (globalFlag !== undefined && typeof globalFlag !== "boolean") { throw new Error("refine.run global must be a boolean when provided"); } if (!this.isStreaming) { return { scheduled: false, reason: "no active turn; refine can only be requested while a turn is running", }; } const previous = this._pendingRequestedRefine ?? this._serializedExplicitRefineOptions; this._pendingRequestedRefine = { instructions: instructions ?? previous?.instructions, global: globalFlag ?? previous?.global, }; // In serialized mode, kick off background planning immediately // (the primary response ended at message_end, tools are active). // This lets planning overlap tool execution rather than waiting // for the shouldStopAfterTurn boundary. if (this._serializedRefine) { if (this._serializedPlanInFlight) { this._autoRefineBranchVersion++; if (this._refineAbortController) { this._refineAbortController.abort(); } else { this._serializedPlanInFlight = Promise.resolve({ status: "invalidated", branchVersion: this._autoRefineBranchVersion, }); } } else { this._maybeStartSerializedBackgroundPlan(); } } return { scheduled: true, note: "Refinement runs when the current turn ends; applied edits are appended to your context as a refinement notice and you resume automatically. Continue working normally.", }; } default: throw new Error(`unknown refine request type "${type}"`); } }
/** * Handle an rlm_heartbeat.* request from the bundled rlm-heartbeat skill. * These heartbeats are internal to this active session and never read or * mutate the user-level /heartbeat. */ handleRlmHeartbeatHostRequest(type: string, payload: Record<string, unknown> = {}): Record<string, unknown> { const controller = this._rlmHeartbeatController; if (!controller) { throw new Error("RLM heartbeat skill is not available in this session"); } switch (type) { case "rlm_heartbeat.list": { const includeInactive = payload.include_inactive === true || payload.includeInactive === true; return { heartbeats: controller .listRlmHeartbeats({ includeInactive }) .map((heartbeat) => rlmHeartbeatHostResponse(heartbeat)), }; } case "rlm_heartbeat.create": { if (typeof payload.instruction !== "string") { throw new Error("rlm_heartbeat.create instruction must be a string"); } if (payload.interval !== undefined && typeof payload.interval !== "string") { throw new Error("rlm_heartbeat.create interval must be a string when provided"); } if (payload.label !== undefined && typeof payload.label !== "string") { throw new Error("rlm_heartbeat.create label must be a string when provided"); } const deliveryMode = normalizeHeartbeatDeliveryMode(payload.delivery_mode ?? payload.deliveryMode); return { heartbeat: rlmHeartbeatHostResponse( controller.createRlmHeartbeat({ instruction: payload.instruction, interval: payload.interval, label: payload.label, deliveryMode, }), ), }; } case "rlm_heartbeat.update": { if (typeof payload.id !== "string") { throw new Error("rlm_heartbeat.update id must be a string"); } if (payload.instruction !== undefined && typeof payload.instruction !== "string") { throw new Error("rlm_heartbeat.update instruction must be a string when provided"); } if (payload.interval !== undefined && typeof payload.interval !== "string") { throw new Error("rlm_heartbeat.update interval must be a string when provided"); } if (payload.label !== undefined && typeof payload.label !== "string") { throw new Error("rlm_heartbeat.update label must be a string when provided"); } if (payload.status !== undefined && !isRlmHeartbeatStatusUpdate(payload.status)) { throw new Error('rlm_heartbeat.update status must be "pause" or "resume" when provided'); } const rawDeliveryMode = payload.delivery_mode ?? payload.deliveryMode; const deliveryMode = normalizeHeartbeatDeliveryMode(rawDeliveryMode); if ( payload.instruction === undefined && payload.interval === undefined && payload.label === undefined && payload.status === undefined && rawDeliveryMode === undefined ) { throw new Error("rlm_heartbeat.update requires at least one field to update"); } const heartbeat = controller.updateRlmHeartbeat({ id: payload.id, instruction: payload.instruction, interval: payload.interval, label: payload.label, status: payload.status, deliveryMode, }); return { heartbeat: heartbeat ? rlmHeartbeatHostResponse(heartbeat) : null, }; } case "rlm_heartbeat.delete": { if (typeof payload.id !== "string") { throw new Error("rlm_heartbeat.delete id must be a string"); } const heartbeat = controller.deleteRlmHeartbeat(payload.id); return { heartbeat: heartbeat ? rlmHeartbeatHostResponse(heartbeat) : null, }; } default: throw new Error(`unknown RLM heartbeat request type "${type}"`); } }
async handleAgentMessageHostRequest( type: string, payload: Record<string, unknown> = {}, ): Promise<AgentSessionMessageReceipt> { if (!this._agentMessageController) { throw new Error("agent messaging is not available in this session"); } switch (type) { case "agent_message.send": { if (typeof payload.target !== "string") { throw new Error("agent_message.send target must be a string"); } if (typeof payload.message !== "string") { throw new Error("agent_message.send message must be a string"); } let message = normalizeAgentSessionMessage(payload.message); let role = payload.receiverRole as AgentFamilyRelationship | undefined; if (!role && this._agentMessageController.family) { try { const members = await this._agentMessageController.family(); const member = members.find( (item) => item.entry.id === payload.target || item.entry.name === payload.target, ); if (member) role = member.relationship; } catch { // ignore } } if (this._extensionRunner.hasHandlers("before_agent_message_send")) { const result = await this._extensionRunner.emitBeforeAgentMessageSend({ type: "before_agent_message_send", target: payload.target, message, receiverRole: role, receiverName: typeof payload.receiverName === "string" ? payload.receiverName : undefined, }); if (result?.cancel) { throw new Error(result.reason ?? "Agent message cancelled by extension"); } if (typeof result?.message === "string") { message = normalizeAgentSessionMessage(result.message); } } return this._agentMessageController.sendAgentMessage({ target: assertDirectAgentMessageTarget(payload.target), message, receiverRole: role, receiverName: typeof payload.receiverName === "string" ? payload.receiverName : undefined, }); } default: throw new Error(`unknown agent message request type "${type}"`); } }
handleAgentObserveHostRequest( type: string, payload: Record<string, unknown> = {}, ): | AgentObserveListResult | AgentObserveAgentSnapshot | AgentObserveRecentMessagesResult | Promise<AgentObserveListResult | AgentObserveAgentSnapshot | AgentObserveRecentMessagesResult> { const controller = this._agentObserveController; if (!controller) { throw new Error("agent observation is not available in this session"); } switch (type) { case "agent_observe.list": return controller.listAgents(); case "agent_observe.get": { if (typeof payload.target !== "string") { throw new Error("agent_observe.get target must be a string"); } return controller.getAgent(payload.target); } case "agent_observe.recent": { if (typeof payload.target !== "string") { throw new Error("agent_observe.recent target must be a string"); } return controller.recentMessages({ target: payload.target, limit: normalizeObserveLimit(payload.limit as number | undefined), maxChars: normalizeObserveMaxChars((payload.max_chars ?? payload.maxChars) as number | undefined), }); } default: throw new Error(`unknown agent observe request type "${type}"`); } }
private _createGoalFromHost(objective: string, tokenBudget: number | undefined): GoalState { switch (this._goalState.status) { case "active": throw new Error( "cannot create a new goal because this thread already has an active goal; run `await goal.complete()` when it is achieved, or ask the user to clear it with /goal clear", ); case "paused": throw new Error( "cannot create a new goal because a paused goal exists; ask the user to resume it with /goal resume or clear it with /goal clear", ); case "budget_limited": throw new Error( "cannot create a new goal because a budget-limited goal exists; ask the user to resume it with /goal resume or clear it with /goal clear", ); default: // idle, or a terminal record (complete / error): nothing pending, start fresh. return this._startGoal(objective, tokenBudget); } }
private _completeGoalFromHost(): GoalState { if (!this._goalState.objective || this._goalState.status === "idle") { throw new Error("cannot complete goal because this thread has no goal"); } const goal = this._goalWithAccountedWallClock(); // A turn can cross the budget and complete the goal at once: accounting // runs at message_end, before the completing ipython cell executes, so a // budget-limit context may already be steered. It is stale now — drop it. this._clearQueuedGoalContexts(); this._setGoalState({ ...goal, active: false, status: "complete", lastReason: "Goal achieved", lastError: undefined, }); return this._goalState; }
private async _getGoalContinuationMessages( context: GetContinuationMessagesContext, signal?: AbortSignal, ): Promise<AgentMessage[]> { if (this._stopGoalContinuationForTerminalMessage(context.message)) { return []; } if (signal?.aborted || this._goalState.status !== "active" || !this._goalState.objective) { return []; } // Delegating and ending the turn is correct behavior; hold the continuation // until descendants settle instead of re-prompting a waiting parent. if (this._hasUnsettledRlmQuiescenceWork()) { this._goalContinuationAwaitsRlmWork = true; return []; } this._goalContinuationAwaitsRlmWork = false; try { this._ensureGoalRuntimeActive(context.context); const nextGoal = { ...this._goalState, continuationsUsed: this._goalState.continuationsUsed + 1, lastReason: undefined, lastError: undefined, }; this._setGoalState(nextGoal); return [createGoalContextMessage(this._goalState, "continuation")]; } catch (error) { const message = error instanceof Error ? error.message : String(error); try { this._finishGoalWithError(message); } catch { // The continuation hook must not reject; listener failures should not crash the agent loop. } return []; } }
private async _getContinuationMessages( context: GetContinuationMessagesContext, signal?: AbortSignal, ): Promise<AgentMessage[]> { if (this.queuedActionCount > 0) { return []; } const arrivalEpoch = this._sessionInputArrivalEpoch; const goalSnapshot = this._goalState; const goalAccountingStartedAt = this._goalAccountingStartedAt; const goalMessages = await this._getGoalContinuationMessages(context, signal); if (goalMessages.length > 0 || signal?.aborted) { if (goalMessages.length > 0 && this._sessionInputArrivalEpoch !== arrivalEpoch) { this._setGoalState(goalSnapshot); this._goalAccountingStartedAt = goalAccountingStartedAt; return []; } return goalMessages; } if ( this._autonomousContinuationSuppressionDepth > 0 || context.newMessages.some((message) => this._autonomousContinuationSuppressedMessages.has(message)) ) { return []; } // Delegating and ending the turn is correct behavior; hold the // continuation until descendants settle instead of re-prompting a // waiting parent, mirroring the goal gate above. if (this._holdAutonomousContinuationForRlmWork(context.message)) { return []; } const autonomousSnapshot = this._snapshotAutonomousRuntimeState(); const autonomousMessage = await nextAutonomousContinuation(this._autonomousState, context.message, { cwd: this._cwd, signal, }); if (autonomousMessage && this._sessionInputArrivalEpoch !== arrivalEpoch) { this._restoreAutonomousRuntimeSnapshot(autonomousSnapshot); return []; } return autonomousMessage ? [autonomousMessage] : []; }
private _lastAssistantMessage: AssistantMessage | undefined = undefined;
private _agentMessageOutcome(agentMessageId: string): AgentMessageOutcome { let outcome = this._agentMessageOutcomes.get(agentMessageId); if (!outcome) { outcome = {}; this._agentMessageOutcomes.set(agentMessageId, outcome); } return outcome; }
/** * Register a delivery waiter before submitting the prompt. Delivery outcomes are not retained * for late lookup, so callers that register after admission may wait for a future use of the id. */ waitForAgentMessagePromptDelivery(agentMessageId: string): Promise<void> { const outcome = this._agentMessageOutcome(agentMessageId); outcome.delivery ??= createAgentMessageDeferred(); return outcome.delivery.promise; }
private _settleAgentMessage( agentMessageId: string | undefined, leg: "delivery" | "completion", error?: Error, ): void { if (agentMessageId === undefined) return; const outcome = this._agentMessageOutcomes.get(agentMessageId); if (!outcome) return; const deferred = outcome[leg]; if (!deferred) return; outcome[leg] = undefined; if (!outcome.delivery && !outcome.completion) { this._agentMessageOutcomes.delete(agentMessageId); } if (error) deferred.reject(error); else deferred.resolve(); }
private _rejectAgentMessage(agentMessageId: string | undefined, error: Error): void { if (agentMessageId === undefined) return; this._settleAgentMessage(agentMessageId, "delivery", error); this._settleAgentMessage(agentMessageId, "completion", error); }
private _rejectQueuedAgentMessageDeliveries(deliveryError: Error, completionError = deliveryError): void { for (const action of this._actionStore.unfinishedActions()) { this._settleAgentMessage(action.agentMessageId, "delivery", deliveryError); this._settleAgentMessage(action.agentMessageId, "completion", completionError); } }
private _capturingCancelledAction(message: AgentMessage): QueuedSessionAction | undefined { return this._actionStore .ownedActions() .find( (action) => action.lifecycle.state === "cancelled" && action.payload.kind === "turn" && action.payload.captureRunMessages?.has(message) === true, ); }
private _hasCancelledDispatchCapture(): boolean { return this._actionStore .ownedActions() .some( (action) => action.lifecycle.state === "cancelled" && action.payload.kind === "turn" && action.payload.captureRunMessages !== undefined, ); }
private _handleAgentEvent = (event: AgentEvent): void => { this._createRetryPromiseForAgentEnd(event); if (event.type === "message_start" || event.type === "message_end") { for (const action of this._actionStore.ownedActions()) { if ( action.payload.kind !== "turn" || !action.payload.captureRunMessages || action.payload.cancelledDispatchEnded ) { continue; } const primary = primaryDeliveryRecord(action); if (event.message === primary.message || primary.started) { action.payload.captureRunMessages.add(event.message); } } } else if (event.type === "agent_end") { const captured = new Set<AgentMessage>(); for (const action of this._actionStore.ownedActions()) { if (action.payload.kind === "turn" && action.payload.captureRunMessages) { for (const message of action.payload.captureRunMessages) captured.add(message); action.payload.cancelledDispatchEnded = true; } } if (captured.size > 0) { this.agent.state.messages = this.agent.state.messages.filter((message) => !captured.has(message)); } } if (event.type === "message_start" && (event.message.role === "user" || event.message.role === "custom")) { for (const action of this._actionStore.actionsForMessage(event.message)) { const record = action.payload.kind === "turn" ? action.payload.records.find((candidate) => candidate.message === event.message) : undefined; if (record) record.started = true; if (record?.role === "primary") { this._actionStore.ticketFor(action).settleDelivered({ status: "delivered" }); this._settleAgentMessage(action.agentMessageId, "delivery"); } } } else if (event.type === "message_end" && (event.message.role === "user" || event.message.role === "custom")) { for (const action of this._actionStore.actionsForMessage(event.message)) { const record = action.payload.kind === "turn" ? action.payload.records.find((candidate) => candidate.message === event.message) : undefined; if (record) record.durable = true; if (record?.role === "primary" && action.lifecycle.state === "committing") { transitionSessionAction(action, { state: "running", execution: "agent_turn", }); this._notifySessionInputCheckpointChange(); this._emitQueueUpdate(); } } } this._agentEventQueue = this._agentEventQueue.then( () => this._processAgentEvent(event), () => this._processAgentEvent(event), ); this._agentEventQueue.catch(() => {}); };
private _createRetryPromiseForAgentEnd(event: AgentEvent): void { if (event.type !== "agent_end" || this._retryPromise) { return; }
const settings = this.settingsManager.getRetrySettings(); if (!settings.enabled) { return; }
const lastAssistant = this._findLastAssistantInMessages(event.messages); const concreteAuthFailure = lastAssistant ? this._isConcreteProviderAuthFailure(lastAssistant) : false; if (!lastAssistant || (!this._isRetryableError(lastAssistant) && !concreteAuthFailure)) { return; } if (concreteAuthFailure) { this._captureRetryAuthFailureSource(lastAssistant); }
this._retryPromise = new Promise((resolve) => { this._retryResolve = resolve; }); }
private _findLastAssistantInMessages(messages: AgentMessage[]): AssistantMessage | undefined { for (let i = messages.length - 1; i >= 0; i--) { const message = messages[i]; if (message.role === "assistant") { return message as AssistantMessage; } } return undefined; }
/** * Advisory, throttled language-drift check: a reply that drifted out of the user's * script gets one notice in the transcript, at most three times per session and never * twice within twenty assistant turns. It never blocks a turn and never asks the model * to summarize, shorten, or re-plan anything. */ private _maybeReportLanguageDrift(messages: AssistantMessage | AssistantMessage[]): void { const list = Array.isArray(messages) ? messages : [messages]; const assistantMessages = list.filter((m) => m.role === "assistant" && m.content); if (assistantMessages.length === 0) return; this._assistantTurnCount += 1; if (this._languageDriftNotices >= 3) return; if (this._assistantTurnCount - this._lastLanguageDriftTurn < 20) return; const lastUser = [...this.messages] .reverse() .find((entry) => entry.role === "user" || (entry.role === "custom" && entry.display)); const referenceContent: unknown = lastUser && "content" in lastUser ? lastUser.content : undefined; const reference = typeof referenceContent === "string" ? referenceContent : Array.isArray(referenceContent) ? referenceContent .map((part) => part && typeof part === "object" && "text" in part ? String((part as { text?: unknown }).text ?? "") : "", ) .join("\n") : "";
for (const message of assistantMessages) { const reply = message.content .filter((part): part is { type: "text"; text: string } => part.type === "text") .map((part) => part.text) .join("\n"); if (!reply.trim()) continue; const detected = detectLanguageDriftScript(reply, reference); if (!detected) continue; this._languageDriftNotices += 1; this._lastLanguageDriftTurn = this._assistantTurnCount; const notice = createLanguageDriftNoticeMessage({ detected, expected: "the user's" }); try { this.sessionManager.appendCustomMessageEntryWithRollback( notice.customType, notice.content, notice.display, notice.details, ); } catch { this.sessionManager.appendCustomMessageEntry( notice.customType, notice.content, notice.display, notice.details, ); } this.agent.state.messages.push(notice); this._emit({ type: "message_start", message: notice }); this._emit({ type: "message_end", message: notice }); break; } }
private _addLoginGuidanceToAuthError(event: AgentEvent): void { const message = event.type === "message_end" && event.message.role === "assistant" ? (event.message as AssistantMessage) : event.type === "agent_end" ? this._findLastAssistantInMessages(event.messages) : undefined; if (!message || message.stopReason !== "error" || !message.errorMessage) { return; } if (!isLikelyAuthenticationError(message.errorMessage)) { return; } message.errorMessage = addLoginGuidanceToAuthError(message.errorMessage); }
private async _processAgentEvent(event: AgentEvent): Promise<void> { let clearedDispatchEnded = false; if ((event.type === "message_start" || event.type === "message_end") && event.message.role === "toolResult") { this._applyLateIpythonSentAgentMessages(event.message); } if (event.type === "message_start" || event.type === "message_end") { const cleared = this._capturingCancelledAction(event.message); if (cleared?.payload.kind === "turn" && cleared.payload.captureRunMessages) { const captured = cleared.payload.captureRunMessages; this.agent.state.messages = this.agent.state.messages.filter((message) => !captured.has(message)); return; } } if (event.type === "agent_end") { const cleared = this._actionStore .ownedActions() .filter( (action) => action.lifecycle.state === "cancelled" && action.payload.kind === "turn" && action.payload.captureRunMessages !== undefined, ); if (cleared.length > 0) { clearedDispatchEnded = true; const removed = new Set( cleared.flatMap((action) => action.payload.kind === "turn" ? [...(action.payload.captureRunMessages ?? [])] : [], ), ); this.agent.state.messages = this.agent.state.messages.filter((message) => !removed.has(message)); (this.agent.state as { errorMessage?: string }).errorMessage = undefined; this._lastAssistantMessage = undefined; for (const action of cleared) this._actionStore.releaseTerminal(action); this._notifySessionInputCheckpointChange(); this._resolveRetry(); } }
if (event.type === "message_start" && startsAgentRun(event.message)) { this._overflowRecovery = "idle"; }
await this._emitExtensionEvent(event); if (event.type === "message_start" || event.type === "message_end") { const cleared = this._capturingCancelledAction(event.message); if (cleared?.payload.kind === "turn" && cleared.payload.captureRunMessages) { const captured = cleared.payload.captureRunMessages; this.agent.state.messages = this.agent.state.messages.filter((message) => !captured.has(message)); return; } }
this._addLoginGuidanceToAuthError(event);
this._emit(event);
if (event.type === "message_end") { if (event.message.role === "custom") { this.sessionManager.appendCustomMessageEntry( event.message.customType, event.message.content, event.message.display, event.message.details, ); } else if ( event.message.role === "user" || event.message.role === "assistant" || event.message.role === "toolResult" ) { this.sessionManager.appendMessage(event.message); }
if (event.message.role === "assistant") { this._lastAssistantMessage = event.message;
const assistantMsg = event.message as AssistantMessage; if (assistantMsg.stopReason !== "error") { addAutonomousUsage(this._autonomousState, assistantMsg.usage); } if (assistantMsg.stopReason !== "error" && assistantMsg.stopReason !== "aborted") { this._assistantTurnsSinceAutoRefine++; // In serialized mode, kick off background refinement planning // immediately after the primary stream finishes, while tools // are still executing. The plan is awaited at shouldStopAfterTurn // before applying, so planning overlaps tools only — never another // model request. this._maybeStartSerializedBackgroundPlan(); } if (assistantMsg.stopReason !== "error") { this._overflowRecovery = "idle"; } if (this._isConcreteProviderAuthFailure(assistantMsg)) { this._captureRetryAuthFailureSource(assistantMsg); }
// Reset retry counter immediately on successful assistant response // This prevents accumulation across multiple LLM calls within a turn if (assistantMsg.stopReason !== "error" && this._retryAttempt > 0) { const restoredModel = this._restorePrimaryModelAfterBackup(); this._emit({ type: "auto_retry_end", success: true, attempt: this._retryAttempt, ...(restoredModel ? { restoredModel } : {}), }); this._retryAttempt = 0; this._providerWait = undefined; this._retryAuthFailureSources = []; } if (this._accountGoalUsageForAssistantMessage(assistantMsg)) { const message = createGoalContextMessage(this._goalState, "budget_limit"); const normalized = normalizeMessageContent(message.content); await this._queuePreparedPrompt("steer", normalized.text, normalized.images, { message, resumeIfIdle: true, }); } } }
if (clearedDispatchEnded) { return; }
if (event.type === "agent_end") { const msg = this._lastAssistantMessage ?? (this._retryPromise ? this._findLastAssistantInMessages(event.messages) : undefined); this._lastAssistantMessage = undefined; if (!msg) { this._resolveRetry(); return; }
// Advisory only, and before the retry decision so a drifting reply is still // reported when the turn ended cleanly. const turnAssistantMessages = event.messages ? (event.messages.filter((m): m is AssistantMessage => m.role === "assistant") as AssistantMessage[]) : []; this._maybeReportLanguageDrift(turnAssistantMessages.length > 0 ? turnAssistantMessages : [msg]); const concreteAuthFailure = this._isConcreteProviderAuthFailure(msg); const retryConcreteAuthFailure = concreteAuthFailure && !this._isStructuredPermanentProviderRetryExhausted(msg); const resetMs = providerStreamFailureRetryAfterMs(msg) ?? parseProviderResetMs(msg.errorMessage);
// A depleted account is a wall, not a burst: trip the local breaker before the // retry decision (a quota failure never retries), so later spawns are refused // instantly instead of burning turns against an account that cannot answer. if (this._isProviderQuotaExhaustedFailure(msg)) { this._modelRegistry.tripProviderQuotaBreaker( msg.provider, resetMs !== undefined ? { retryAfterMs: resetMs } : undefined, ); if (msg.model) { this._modelRegistry.tripProviderQuotaBreaker( `${msg.provider}/${msg.model}`, resetMs !== undefined ? { retryAfterMs: resetMs } : undefined, ); } } if (this._isRetryableError(msg) || retryConcreteAuthFailure) { if (retryConcreteAuthFailure) { this._captureRetryAuthFailureSource(msg); } // A usage-limit response blocks the current account so the retry // (and any other request) rotates to a sibling account when one exists. let retryWithSibling = false; if (this._isProviderRateLimitFailure(msg)) { const blockedUntilMs = resetMs !== undefined ? Date.now() + resetMs : undefined; const blocked = this._modelRegistry.markProviderRateLimited( msg.provider, blockedUntilMs, this.sessionId, ); retryWithSibling = blocked && this._modelRegistry.hasUnblockedProviderCredential(msg.provider); if (resetMs !== undefined && !retryWithSibling) { this._modelRegistry.tripProviderQuotaBreaker(msg.provider, { blockedUntilMs, reason: "rate_limit", }); if (msg.model) { this._modelRegistry.tripProviderQuotaBreaker(`${msg.provider}/${msg.model}`, { blockedUntilMs, reason: "rate_limit", }); } } } const didRetry = await this._handleRetryableError(msg, { markAuthStaleOnFailure: retryConcreteAuthFailure, authSourceTokens: retryConcreteAuthFailure ? this._retryAuthFailureSources : undefined, retryWithSibling, }); if (didRetry) return; // Retry was initiated, don't proceed to compaction }
const compactionWillRetry = await this._checkCompaction(msg); if (compactionWillRetry && this._retryAttempt > 0) { return; } this._finishActiveRetryWithFailure(msg); this._resolveRetry(); if (!compactionWillRetry) { this._finishGoalForTerminalAssistantMessage(msg); // In serialized mode, agent-callable refine.run is serviced // at the shouldStopAfterTurn boundary, not here at agent_end. if (!this._serializedRefine) { const consumedRequestedRefine = this._consumePendingRequestedRefine(); if (!consumedRequestedRefine) { this._scheduleAutoRefineAfterAgentEnd(); } } } } }
private _resolveRetry(): void { this._retryGeneration += 1; this._semanticEdges.clearTurnRetry(); if (this._retryResolve) { this._retryResolve(); this._retryResolve = undefined; this._retryPromise = undefined; this._notifySessionInputCheckpointChange(); this._scheduleSessionInputPump(); } }
private _findLastAssistantMessage(): AssistantMessage | undefined { const messages = this.agent.state.messages; for (let i = messages.length - 1; i >= 0; i--) { const msg = messages[i]; if (msg.role === "assistant") { return msg as AssistantMessage; } } return undefined; }
private _replaceMessageInPlace(target: AgentMessage, replacement: AgentMessage): void { // Agent-core stores the finalized message object in its state before emitting message_end. // SessionManager persistence happens later in _processAgentEvent() with event.message. // Mutating this object in place keeps agent state, later turn/agent events, listeners, // and the eventual SessionManager.appendMessage(event.message) persistence in sync. if (target === replacement) { return; }
const targetRecord = target as unknown as Record<string, unknown>; for (const key of Object.keys(targetRecord)) { delete targetRecord[key]; } Object.assign(targetRecord, replacement); }
private async _emitExtensionEvent(event: AgentEvent): Promise<void> { if (event.type === "agent_start") { this._turnIndex = 0; this.sessionManager.recordGitStateIfChanged(); await this._extensionRunner.emit({ type: "agent_start" }); } else if (event.type === "agent_end") { // Also capture at end of turn so commits made during the run (e.g. via a bash tool) land. this.sessionManager.recordGitStateIfChanged(); await this._extensionRunner.emit({ type: "agent_end", messages: event.messages, }); } else if (event.type === "turn_start") { const extensionEvent: TurnStartEvent = { type: "turn_start", turnIndex: this._turnIndex, timestamp: Date.now(), }; await this._extensionRunner.emit(extensionEvent); } else if (event.type === "turn_end") { const extensionEvent: TurnEndEvent = { type: "turn_end", turnIndex: this._turnIndex, message: event.message, toolResults: event.toolResults, }; await this._extensionRunner.emit(extensionEvent); this._turnIndex++; } else if (event.type === "message_start") { const extensionEvent: MessageStartEvent = { type: "message_start", message: event.message, }; await this._extensionRunner.emit(extensionEvent); } else if (event.type === "message_update") { const extensionEvent: MessageUpdateEvent = { type: "message_update", message: event.message, assistantMessageEvent: event.assistantMessageEvent, }; await this._extensionRunner.emit(extensionEvent); } else if (event.type === "message_end") { const extensionEvent: MessageEndEvent = { type: "message_end", message: event.message, }; const replacement = await this._extensionRunner.emitMessageEnd(extensionEvent); if (replacement) { this._replaceMessageInPlace(event.message, replacement); } } else if (event.type === "tool_execution_start") { const extensionEvent: ToolExecutionStartEvent = { type: "tool_execution_start", toolCallId: event.toolCallId, toolName: event.toolName, args: event.args, }; await this._extensionRunner.emit(extensionEvent); } else if (event.type === "tool_execution_update") { const extensionEvent: ToolExecutionUpdateEvent = { type: "tool_execution_update", toolCallId: event.toolCallId, toolName: event.toolName, args: event.args, partialResult: event.partialResult, }; await this._extensionRunner.emit(extensionEvent); } else if (event.type === "tool_execution_end") { const extensionEvent: ToolExecutionEndEvent = { type: "tool_execution_end", toolCallId: event.toolCallId, toolName: event.toolName, result: event.result, isError: event.isError, }; await this._extensionRunner.emit(extensionEvent); } }
/** * Subscribe to agent events. * Session persistence is handled internally (saves messages on message_end). * Multiple listeners can be added. Returns unsubscribe function for this listener. */ subscribe(listener: AgentSessionEventListener): () => void { this._eventListeners.push(listener);
return () => { const index = this._eventListeners.indexOf(listener); if (index !== -1) { this._eventListeners.splice(index, 1); } }; }
/** * Temporarily disconnect from agent events. * User listeners are preserved and will receive events again after resubscribe(). * Used internally during operations that need to pause event processing. */ private _disconnectFromAgent(): void { if (this._unsubscribeAgent) { this._unsubscribeAgent(); this._unsubscribeAgent = undefined; } }
/** * Reconnect to agent events after _disconnectFromAgent(). * Preserves all existing listeners. */ private _reconnectToAgent(): void { if (this._unsubscribeAgent) return; // Already connected this._unsubscribeAgent = this.agent.subscribe(this._handleAgentEvent); }
/** * Remove all listeners and disconnect from agent. * Call this when completely done with the session. */ /** * Async teardown for graceful quit/switch: await the Python kernel's dispose * (which flushes a final namespace snapshot) before the synchronous dispose, so * the latest state reaches disk instead of racing process exit. */ async disposeAsync(options?: { kernelSnapshot?: boolean }): Promise<void> { if (this._disposed) { return this._disposeCallbacksPromise; } // Concurrent callers await the same in-flight teardown so none resolves before // the kernel snapshot flush finishes. if (this._disposeAsyncPromise) { return this._disposeAsyncPromise; } const kernelSnapshot = options?.kernelSnapshot ?? true; this._disposeAsyncPromise = (async () => { // Drain before marking _disposing so a refine triggered at the final // agent_end completes instead of being aborted by dispose(). await this._drainPendingRefinementForDisposal(); if (this._disposed) { return this._disposeCallbacksPromise; } this._disposing = true; this._sessionActionCommitDisposeAbortController.abort(); await this._disposeAsyncOnce(kernelSnapshot); })(); return this._disposeAsyncPromise; }
/** * Await any in-flight refinement (planning or application) and run a * pending auto-refine that was scheduled but not yet started. Called * from disposeAsync before _disposing is set so refinement completes * before disposal. */ private async _drainPendingRefinementForDisposal(): Promise<void> { for (const timer of this._scheduledAutoRefineTimers) { clearTimeout(timer); } this._scheduledAutoRefineTimers.clear(); await Promise.allSettled([...this._autoRefineOperations]); for (const timer of this._scheduledAutoRefineTimers) { clearTimeout(timer); } this._scheduledAutoRefineTimers.clear(); // Wait for in-flight refinement (including serialized background plan) to settle. while (this._refineInFlight || this._refinePlanInFlight || this._serializedPlanInFlight) { if (this._refineInFlight) { await this._refineInFlight; } else if (this._refinePlanInFlight) { await this._refinePlanInFlight; } else if (this._serializedPlanInFlight) { // Await the background plan and apply a ready "plan" result before teardown. await this._consumeSerializedBackgroundPlan(async (bgResult) => { if (bgResult?.status === "plan" && bgResult.branchVersion === this._autoRefineBranchVersion) { try { await this._applySerializedPlan(bgResult); } catch (error) { this._emitRefineFailed(error); } // Stamp cooldown and reset counter so the interval // check below does not trigger a duplicate refine. this._lastAutoRefineReviewAt = Date.now(); this._assistantTurnsSinceAutoRefine = 0; } // Preserve a consumed explicit request when its background plan failed, // matching the turn-boundary recovery path. The pending drain below // retries it once before disposal. if ( bgResult?.status === "failure" && bgResult.explicit && bgResult.branchVersion === this._autoRefineBranchVersion && !this._pendingRequestedRefine ) { this._pendingRequestedRefine = bgResult.options; } if (bgResult?.status === "skip" && bgResult.explicit) { this._emitRefineFailed(new RefineSkippedError("Refinement skipped by extension")); } // For "skip" or "failure", stamp cooldown and reset counter // so the interval check below does not trigger a duplicate // terminal retry. if ( bgResult?.status === "skip" || bgResult?.status === "failure" || bgResult?.status === "invalidated" ) { this._lastAutoRefineReviewAt = Date.now(); this._assistantTurnsSinceAutoRefine = 0; } return false; }); } else { await new Promise<void>((resolve) => setTimeout(resolve, 0)); } } // Drain an agent-callable refine.run request that was scheduled but // not yet consumed. Use the direct serialized path (no waitForIdle) // since the agent may still own activeRun at the final agent_end. if (this._pendingRequestedRefine) { const pending = this._pendingRequestedRefine; this._pendingRequestedRefine = undefined; try { await this._runSerializedRefine(pending, "self"); } catch { // Best-effort drain; refinement errors must not block disposal. } // Stamp cooldown and reset counter so the interval check below // does not trigger a duplicate refine after the explicit drain. this._lastAutoRefineReviewAt = Date.now(); this._assistantTurnsSinceAutoRefine = 0; } // A serialized compaction can finish without another model turn. Drain its // pending review here so disposal does not silently lose the trigger. if (this._serializedRefine && this._compactAutoRefinePending && this._autoRefineAllowedForSession()) { const compactSettings = this.settingsManager.getAutoRefineSettings(); if (!compactSettings.enabled || !compactSettings.compact) { this._compactAutoRefinePending = false; } else { const nowMs = Date.now(); const underCooldown = this._lastAutoRefineReviewAt > 0 && nowMs - this._lastAutoRefineReviewAt < compactSettings.cooldownMs; this._compactAutoRefinePending = false; if (!underCooldown) { try { await this._runSerializedAutoRefineReview("compact", this._autoRefineBranchVersion); } catch { // Best-effort drain; refinement errors must not block disposal. } return; } } }
// If auto-refine is due but has not started yet, run it now so the // refinement is persisted before disposal. Use the direct serialized // path in serialized mode, or _maybeAutoRefine in interactive mode // (where the agent is idle at this point). if (this._disposed || !this._autoRefineAllowedForSession()) { return; } const settings = this.settingsManager.getAutoRefineSettings(); if (!settings.enabled) { return; } if (this._assistantTurnsSinceAutoRefine < settings.turnInterval) { return; } const nowMs = Date.now(); const underCooldown = this._lastAutoRefineReviewAt > 0 && nowMs - this._lastAutoRefineReviewAt < settings.cooldownMs; if (underCooldown) { return; } if (this._serializedRefine) { await this._runSerializedRefineCheckpoint(); } else { await this._maybeAutoRefine("turn_interval"); } }
private async _disposeAsyncOnce(kernelSnapshot: boolean): Promise<void> { // Flush kernels/traces for both still-running and retained children; the sync // dispose() below only tears them down synchronously. for (const run of [...this._activeRlmChildRuns.values()]) { const childSession = run.session; if (!childSession) continue; if (run.detachedDeletion) { run.suppressTerminalNotice = true; if (run.deletionCleanupObserver) { await run.deletionCleanupObserver.catch(() => false); } else if (run.deletionCleanup) { await run.deletionCleanup.catch(() => childSession.disposeAsync().catch(() => undefined)); } else { // Cleanup already failed and was exposed for retry before disposal. await childSession.disposeAsync().catch(() => undefined); } if (!run.settled) await this._finishRlmRunDeletion(run); } else { await childSession.disposeAsync().catch(() => undefined); } } for (const unsubscribe of this._rlmChildUnsubscribes.values()) { unsubscribe(); } this._rlmChildUnsubscribes.clear(); for (const { session } of this._rlmChildSessions.values()) { await session.disposeAsync().catch(() => undefined); } this._rlmChildSessions.clear(); this._rlmChildCleanupFailures.clear(); this._deletedRlmChildIds.clear(); try { await this._ipythonKernelProvisioner?.dispose({ snapshot: kernelSnapshot }); } catch { // a failed kernel startup already cleaned up after itself } this.dispose(); await this._disposeCallbacksPromise; }
private _startDisposeCallbacks(): Promise<void> { if (this._disposeCallbacksPromise) { return this._disposeCallbacksPromise; } const pending: Promise<void>[] = []; for (const callback of this._disposeCallbacks) { try { const result = callback(); if (result) { pending.push(result.catch(() => undefined)); } } catch { // Disposal remains best-effort; one owner must not block the rest. } } this._disposeCallbacks.clear(); this._disposeCallbacksPromise = Promise.all(pending).then(() => undefined); return this._disposeCallbacksPromise; }
dispose(): void { if (this._disposed) { return; } this._disposed = true; for (const run of this._unsettledRlmChildRuns) run.suppressTerminalNotice = true; for (const controller of this._rlmQuiescenceWaitAborts) controller.abort(); this._sessionActionCommitDisposeAbortController.abort(); try { // Invalidate scheduled timers and abort any in-flight review so a late // resolution cannot write harness state or re-subscribe handlers. this._autoRefineReviewAbort?.abort(); this._refineAbortController?.abort(); for (const timer of this._scheduledAutoRefineTimers) { clearTimeout(timer); } this._scheduledAutoRefineTimers.clear(); this._disarmAutonomousSubagentKeepAlive(); this._serializedPlanInFlight = undefined; this._serializedExplicitRefineOptions = undefined; this._pendingRequestedRefine = undefined; this._discardPendingAutoRefine({ cancelPostCompactionContinue: true }); this._autoRefineBranchVersion++; this._cancelActiveRlmChildRuns("Parent session disposed"); for (const unsubscribe of this._rlmChildUnsubscribes.values()) { unsubscribe(); } this._rlmChildUnsubscribes.clear(); for (const { session } of this._rlmChildSessions.values()) { session.dispose(); } this._rlmChildSessions.clear(); this._rlmChildCleanupFailures.clear(); this._deletedRlmChildIds.clear(); this._pendingNextTurnMessages = []; const deliveryError = new Error("Session disposed before prompt delivery."); const completionError = new Error("Session disposed before prompt completion."); this._rejectQueuedAgentMessageDeliveries(deliveryError, completionError); for (const [agentMessageId, outcome] of this._agentMessageOutcomes) { if (outcome.delivery) this._settleAgentMessage(agentMessageId, "delivery", deliveryError); if (outcome.completion) this._settleAgentMessage(agentMessageId, "completion", completionError); } this._cancelSessionActions(() => true, deliveryError); this.agent.clearAllQueues(); this._extensionRunner.invalidate( "This extension ctx is stale after session replacement or reload. Do not use a captured pi or command ctx after ctx.newSession(), ctx.fork(), ctx.switchSession(), or ctx.reload(). For newSession, fork, and switchSession, move post-replacement work into withSession and use the ctx passed to withSession. For reload, do not use the old ctx after await ctx.reload().", ); this._disconnectFromAgent(); this._eventListeners = []; cleanupSessionResources(this.sessionId); } finally { void this._startDisposeCallbacks(); } }
registerDisposeCallback(callback: () => void | Promise<void>): void { if (this._disposed) { try { const result = callback(); if (result) void result.catch(() => undefined); } catch { // Late registration follows the same best-effort disposal contract. } return; } this._disposeCallbacks.add(callback); }
get state(): AgentState { return this.agent.state; }
get model(): Model<any> | undefined { return this.agent.state.model; }
get thinkingLevel(): ThinkingLevel { return this.agent.state.thinkingLevel; }
get serviceTier(): ServiceTier { return this.agent.state.serviceTier; }
get isStreaming(): boolean { return this.agent.state.isStreaming; }
get systemPrompt(): string { return this.agent.state.systemPrompt; }
get retryAttempt(): number { return this._retryAttempt; }
getActiveToolNames(): string[] { return this.agent.state.tools.map((t) => t.name); }
getAllTools(): ToolInfo[] { return Array.from(this._toolDefinitions.values()).map(({ definition, sourceInfo }) => ({ name: definition.name, description: definition.description, parameters: definition.parameters, sourceInfo, })); }
getToolDefinition(name: string): ToolDefinition | undefined { return this._toolDefinitions.get(name)?.definition; }
setActiveToolsByName(toolNames: string[]): void { const tools: AgentTool[] = []; const validToolNames: string[] = []; const seenToolNames = new Set<string>(); for (const name of toolNames) { if (seenToolNames.has(name)) { continue; } const tool = this._toolRegistry.get(name); if (tool) { seenToolNames.add(name); tools.push(tool); validToolNames.push(name); } } this.agent.state.tools = tools;
this._baseSystemPrompt = this._rebuildSystemPrompt(validToolNames); this.agent.state.systemPrompt = this._baseSystemPrompt; }
get isCompacting(): boolean { return ( this._autoCompactionAbortController !== undefined || this._compactionAbortController !== undefined || this._branchSummaryAbortController !== undefined ); }
get messages(): AgentMessage[] { return this.agent.state.messages; }
buildSessionContext(): SessionContext { const context = this.sessionManager.buildSessionContext(); for (const message of context.messages) { this._applyLateIpythonSentAgentMessages(message); } this._mergeUnpersistedOutcomes(context.messages); return context; }
private _mergeUnpersistedOutcomes(messages: AgentMessage[]): void { for (const outcome of this._unpersistedOutcomes) { let insertAt = messages.length; while (insertAt > 0 && messages[insertAt - 1]!.timestamp > outcome.timestamp) { insertAt -= 1; } messages.splice(insertAt, 0, outcome); } }
get steeringMode(): "all" | "one-at-a-time" { return this.agent.steeringMode; }
get followUpMode(): "all" | "one-at-a-time" { return this.agent.followUpMode; }
get sessionFile(): string | undefined { return this.sessionManager.getSessionFile(); }
get sessionId(): string { return this.sessionManager.getSessionId(); }
get rlmDepth(): number { return this._rlmDepth; }
get semanticEdges(): SemanticEdgeRecorder { return this._semanticEdges; }
get rlmMaxDepth(): number { return this._rlmMaxDepth; }
get remoteHost(): string | undefined { return this._remoteHost; }
get sessionName(): string | undefined { return this.sessionManager.getSessionName(); }
get goalState(): GoalState { return { ...this._goalWithCurrentWallClock() }; }
getAutonomousStatus(): AgentAutonomousStatus { return autonomousStatus(this._autonomousState); }
recordHostAutonomousContinuation(): void { addAutonomousContinuation(this._autonomousState); }
async refreshAutonomousGates(): Promise<void> { await refreshAutonomousQualityGates(this._autonomousState, { cwd: this._cwd, }); }
private async _runWithAutonomousContinuationSuppressed<T>(fn: () => Promise<T>): Promise<T> { this._autonomousContinuationSuppressionDepth++; try { return await fn(); } finally { this._autonomousContinuationSuppressionDepth--; } }
private _markAutonomousContinuationSuppressed(message: AgentMessage): void { this._autonomousContinuationSuppressedMessages.add(message); }
get scopedModels(): ReadonlyArray<{ model: Model<any>; thinkingLevel?: ThinkingLevel; }> { return this._scopedModels; }
setScopedModels(scopedModels: Array<{ model: Model<any>; thinkingLevel?: ThinkingLevel }>): void { this._scopedModels = scopedModels; }
get promptTemplates(): ReadonlyArray<PromptTemplate> { return this._resourceLoader.getPrompts().prompts; }
private _normalizePromptSnippet(text: string | undefined): string | undefined { if (!text) return undefined; const oneLine = text .replace(/[\r\n]+/g, " ") .replace(/\s+/g, " ") .trim(); return oneLine.length > 0 ? oneLine : undefined; }
private _normalizePromptGuidelines(guidelines: string[] | undefined): string[] { if (!guidelines || guidelines.length === 0) { return []; }
const unique = new Set<string>(); for (const guideline of guidelines) { const normalized = guideline.trim(); if (normalized.length > 0) { unique.add(normalized); } } return Array.from(unique); }
private _rebuildSystemPrompt(toolNames: string[]): string { const validToolNames = toolNames.filter((name) => this._toolRegistry.has(name)); const toolSnippets: Record<string, string> = {}; const promptGuidelines: string[] = []; for (const name of validToolNames) { const snippet = this._toolPromptSnippets.get(name); if (snippet) { toolSnippets[name] = snippet; }
const toolGuidelines = this._toolPromptGuidelines.get(name); if (toolGuidelines) { promptGuidelines.push(...toolGuidelines); } }
const loaderSystemPrompt = this._resourceLoader.getSystemPrompt(); const loaderAppendSystemPrompt = this._resourceLoader.getAppendSystemPrompt(); const appendSystemPrompt = loaderAppendSystemPrompt.length > 0 ? loaderAppendSystemPrompt.join("\n\n") : undefined; const loadedSkills = this._modelVisibleSkills(); const loadedContextFiles = this._resourceLoader.getAgentsFiles().agentsFiles;
this._baseSystemPromptOptions = { cwd: this._cwd, skills: loadedSkills, contextFiles: loadedContextFiles, customPrompt: loaderSystemPrompt, appendSystemPrompt, messagesPath: this.sessionManager.getSessionFile(), selectedTools: validToolNames, toolSnippets, promptGuidelines, allowRecursion: this._rlmDepth < this._rlmMaxDepth, rlmDepth: this._rlmDepth, rlmParentAgent: this._rlmParentAgent, genericMcpServers: this._mcpManager?.getEnabledPersistentGenericServers(), remoteHost: this._remoteHost, skillDigest: this._ipythonKernelProvisioner?.skillDigest, }; return buildSystemPrompt(this._baseSystemPromptOptions); }
private _refreshExtensionSystemPrompt(extensionPrompt: string, baseSnapshot: string): string { if (this._baseSystemPrompt === baseSnapshot) { return extensionPrompt; } if (!extensionPrompt.includes(baseSnapshot)) { return extensionPrompt; } return extensionPrompt.replace(baseSnapshot, () => this._baseSystemPrompt); }
private _finishSubmissionNormalization( text: string, images: ImageContent[] | undefined, policy: SubmissionNormalizationPolicy, ): NormalizedSubmission { if (policy.expandPromptTemplates) this._throwIfUnknownSlashCommand(text); let expandedText = text; if (policy.expandSkills) expandedText = this._expandSkillCommand(expandedText); if (policy.expandPromptTemplates) { expandedText = expandPromptTemplate(expandedText, [...this.promptTemplates]); } return { kind: "prompt", text: expandedText, images }; }
/** * Reject slash-command typos before they burn a model round trip and * pollute the transcript. Only fires when a registered command name is * close enough to be the intended one; anything else passes through so * genuine prompts that merely start with "/" keep working. */ private _throwIfUnknownSlashCommand(text: string): void { const parsed = parseSlashCommand(text); if (!parsed) return; // No registered command name is anywhere near this long; oversized // /-prefixed inputs are prompts, and fuzzy-matching them would be // quadratic work on the event loop. if (parsed.name.length > 64) return; if (isBuiltinSlashCommandName(parsed.name)) return; if (this.promptTemplates.some((template) => template.name === parsed.name)) return; if ( parsed.name.startsWith("skill:") && this.resourceLoader.getSkills().skills.some((skill) => skill.name === parsed.name.slice("skill:".length)) ) { return; } const skills = this.resourceLoader.getSkills().skills.map((skill) => skill.name); const candidates = [ ...BUILTIN_SLASH_COMMANDS.flatMap((command) => [command.name, ...(command.aliases ?? [])]), ...SESSION_SLASH_COMMAND_NAMES, ...this.promptTemplates.map((template) => template.name), ...this._extensionRunner.getRegisteredCommands().map((command) => command.invocationName), ...skills.map((skill) => `skill:${skill}`), ]; const suggestion = findSlashCommandSuggestion(parsed.name, candidates); if (!suggestion) return; throw new Error(`Unknown command: /${parsed.name}. Did you mean /${suggestion}?`); }
private _normalizeSubmission( text: string, images: ImageContent[] | undefined, policy: SubmissionNormalizationPolicy, ): NormalizedSubmission | Promise<NormalizedSubmission> { if (policy.parseSessionCommands) { const command = parseSessionSlashCommand(text); if (command) return { kind: "sessionCommand", text, images, command }; }
if (text.startsWith("/")) { if (policy.extensionCommands === "execute") { const completion = this._executeExtensionCommand(text); if (completion) return { kind: "extensionCommand", completion }; } else if (policy.extensionCommands === "reject") { this._throwIfExtensionCommand(text); } }
if (policy.inputSource !== undefined && this._extensionRunner.hasHandlers("input")) { return this._extensionRunner.emitInput(text, images, policy.inputSource).then((result) => { if (result.action === "handled") return { kind: "handled" }; if (result.action === "transform") { return this._finishSubmissionNormalization(result.text, result.images ?? images, policy); } return this._finishSubmissionNormalization(text, images, policy); }); }
return this._finishSubmissionNormalization(text, images, policy); }
private async _runPreTurnCompaction(): Promise<void> { const lastAssistant = this._findLastAssistantMessage(); if (lastAssistant) await this._checkCompaction(lastAssistant, false, false); }
private async _prepareForCommit<TPrepared, TCommitted>( policy: CommitPreparationPolicy, steps: CommitPreparationSteps<TPrepared, TCommitted>, ): Promise<TCommitted | undefined> { if ( policy.initialRefineBarrier === "always" || (policy.initialRefineBarrier === "ifInFlight" && this._refineInFlight) ) { await this._waitForRefineIdle(); } if (policy.flushPendingBashBeforeValidation) this._flushPendingBashMessages(); if (policy.validateModelAndAuth) await this._validateCanStartAgentRun(); steps.afterValidation?.(); if (!policy.flushPendingBashBeforeValidation) this._flushPendingBashMessages();
if (policy.preTurnCompaction === "beforeModelSelection") await this._runPreTurnCompaction(); if (policy.awaitPendingModelSelection) { const pendingModelSelectEmit = this._pendingModelSelectEmit(); if (pendingModelSelectEmit) await pendingModelSelectEmit; } if (policy.preTurnCompaction === "afterModelSelection") await this._runPreTurnCompaction();
const prepared = await steps.prepare(); if (steps.shouldCommit && !steps.shouldCommit(prepared)) return undefined; steps.beforeFinalRefineBarrier?.(prepared); let passedFinalRefineBarrier = false; if ( policy.finalRefineBarrier === "always" || (policy.finalRefineBarrier === "ifInFlight" && this._refineInFlight) ) { await this._waitForRefineIdle(); passedFinalRefineBarrier = true; } return steps.commit(prepared, passedFinalRefineBarrier); }
private _applyPreparedSystemPrompt( preparation: PreparedPromptPreparation | undefined, preserveEmptyExtensionPrompt: boolean, ): void { const extensionPrompt = preparation?.result?.systemPrompt; const hasExtensionPrompt = preserveEmptyExtensionPrompt ? extensionPrompt !== undefined : Boolean(extensionPrompt); this.agent.state.systemPrompt = hasExtensionPrompt && extensionPrompt !== undefined && preparation !== undefined ? this._refreshExtensionSystemPrompt(extensionPrompt, preparation.basePromptSnapshot) : this._baseSystemPrompt; }
private _canStartSessionActionImmediately(): boolean { return ( !this.isStreaming && !this.isCompacting && !this.isRetrying && !this.isBashRunning && !this._sessionInputPumpSuspended && this._queuedWorkPauses.size === 0 && !this._disposed && !this._disposing ); }
/** * Send a prompt to the agent. * - Handles extension commands (registered via pi.registerCommand) immediately, even during streaming * - Expands file-based prompt templates by default * - During streaming, queues via steer() or followUp() based on streamingBehavior option * - Validates model and API key before sending (when not streaming) * @throws Error if streaming and no streamingBehavior specified * @throws Error if no model selected or no API key available (when not streaming) */ async prompt(text: string, options?: PromptOptions): Promise<void> { return this._prompt(text, options); }
async promptUntilAccepted(text: string, options?: PromptOptions): Promise<void> { return this._prompt(text, { ...options, returnAfterAccepted: true }); }
async promptAndWait(text: string, options?: PromptOptions): Promise<void> { const agentMessageId = options?.agentMessageId ?? `prompt-wait:${randomUUID()}`; if (this._agentMessageOutcomes.get(agentMessageId)?.completion) { throw new Error(`Prompt completion id is already in use: ${agentMessageId}`); } const outcome = this._agentMessageOutcome(agentMessageId); outcome.completion = createAgentMessageDeferred(); const completion = outcome.completion.promise; const signal = options?.signal; let cancelQueuedPrompt: (() => void) | undefined; try { await this.promptUntilAccepted(text, { ...options, agentMessageId }); if (signal) { cancelQueuedPrompt = () => { const error = new Error("Prompt was cancelled before it started."); const cancelled = this._cancelSessionActions( (action) => action.agentMessageId === agentMessageId && action.payload.kind === "turn", error, ); if (cancelled.length > 0) { this._settleAgentMessage(agentMessageId, "completion", error); } }; signal.addEventListener("abort", cancelQueuedPrompt, { once: true }); if (signal.aborted) cancelQueuedPrompt(); } await completion; } catch (error) { this._settleAgentMessage(agentMessageId, "completion", this._asError(error)); throw error; } finally { if (signal && cancelQueuedPrompt) { signal.removeEventListener("abort", cancelQueuedPrompt); } } }
async acceptAgentMessagePrompt(text: string, options?: PromptOptions): Promise<void> { const customMessage = options?.customMessage && isAgentSessionMessage(options.customMessage) ? options.customMessage : undefined; const clearEpoch = this._agentMessageClearEpoch; const admissionCommitted = () => { options?.admissionCommitted?.(); if (clearEpoch !== this._agentMessageClearEpoch) { throw new Error("Agent message was cleared before admission"); } }; if ( this._sessionInputPumpSuspended && this._isBusyForSessionInput("preflight") && options?.queueIfBusy === true && options.streamingBehavior ) { admissionCommitted(); const queued = await this.queueAgentMessagePrompt(text, options.streamingBehavior, customMessage); options.preflightResult?.(queued, queued); return; } await this._prompt(text, { ...options, resumeIfIdle: false, expandPromptTemplates: false, skipInputHandlers: true, skipPrePromptWork: true, returnAfterAccepted: true, agentMessageId: options?.agentMessageId ?? customMessage?.details.id ?? parseAgentSessionMessagePromptId(text), customMessage, admissionCommitted, }); if (customMessage?.details.fromRelationship === "parent") this._repliedToParentSinceTask = false; }
async queueAgentMessagePrompt( text: string, streamingBehavior: "steer" | "followUp", customMessage?: AgentSessionMessage, ): Promise<boolean> { const agentMessageId = customMessage?.details.id ?? parseAgentSessionMessagePromptId(text); if (streamingBehavior === "steer") { await this._queuePreparedPrompt("steer", text, undefined, { agentMessageId, message: customMessage, }); if (customMessage?.details.fromRelationship === "parent") this._repliedToParentSinceTask = false; return true; } const queued = await this._queuePreparedPrompt("followUp", text, undefined, { agentMessageId, message: customMessage, }); if (queued && customMessage?.details.fromRelationship === "parent") this._repliedToParentSinceTask = false; return queued; }
async promptHeartbeat(job: AgentCronJob, options?: PromptOptions): Promise<void> { const message = createHeartbeatPromptMessage(job); await this._promptInjectedMessage(message.content, message, { ...options, followUpQueueKey: options?.followUpQueueKey ?? `heartbeat:${job.id}`, resumeIfIdle: true, }); }
private _isRlmTerminalNotice(message: CustomMessage): boolean { return ( message.customType === RLM_CHILD_TERMINAL_NOTICE_CUSTOM_TYPE || message.customType === RLM_CHILD_FAILURE_CUSTOM_TYPE ); }
private _assertRlmTerminalNotice(message: CustomMessage): void { if (!this._isRlmTerminalNotice(message)) { throw new Error("Deferred terminal admission only accepts RLM child terminal notices."); } }
private _isRlmTerminalNoticeAction(action: QueuedSessionAction): boolean { if (action.payload.kind !== "turn") return false; const message = primaryDeliveryRecord(action).message; return message.role === "custom" && this._isRlmTerminalNotice(message); }
private _hasDeferredCompletionNotices(): boolean { return this._pendingNextTurnMessages.some((message) => this._isRlmTerminalNotice(message)); }
/** * A child that died, or that finished without answering, is something the parent has to * act on; a notice that only surfaces on the next external resume strands the parent. * Parent-initiated cancellations stay on the quiet path, because waking on a cancel * would restart the turn the parent just ended. */ private _shouldWakeForRlmTerminalNotice(message: CustomMessage): boolean { if (message.customType === RLM_CHILD_FAILURE_CUSTOM_TYPE) return true; if (message.customType !== RLM_CHILD_TERMINAL_NOTICE_CUSTOM_TYPE) return false; const details = message.details as RlmChildTerminalNoticeDetails | undefined; return details?.kind === "completed_without_reply" || details?.kind === "harvested_fallback"; }
private _enqueueRlmTerminalNoticeAction(message: CustomMessage): void { this._assertRlmTerminalNotice(message); // `resumeIfIdle` marks the action as an immediate wake and `wake: true` schedules the // pump, so an idle parent reads the notice now instead of at its next resume. The // action stays queued rather than immediately eligible, which lets the pump batch a // sibling swarm into one turn (followUpMode "all"). const wake = this._shouldWakeForRlmTerminalNotice(message); const action = this._createPreparedTurnAction("followUp", message.content as string, undefined, { message, suppressAutonomousContinuation: true, resumeIfIdle: wake, source: "internal", executionPolicy: this._turnExecutionPolicy("injected"), queueVisible: false, }); this._durableRlmTerminalNoticeActionIds.add(action.id); try { const result = this._admitSessionInput(action, { wake }); if (!result.accepted) throw new Error("RLM child terminal notice was not admitted."); this._rlmTerminalNoticeAdmissionCount++; } catch (error) { this._durableRlmTerminalNoticeActionIds.delete(action.id); throw error; } }
private _flushDeferredCompletionNotices(): void { if (this._queuedWorkPauses.size > 0 || this._disposed || this._disposing) return; const admissionPaused = this._sessionInputAdmissionPauses.size > 0; const pumpSuspended = this._sessionInputPumpSuspended; while (true) { const index = this._pendingNextTurnMessages.findIndex((message) => this._isRlmTerminalNotice(message)); if (index < 0) break; const message = this._pendingNextTurnMessages[index]; // A wake notice is what resumes a suspended pump, so it is not held back by the // suspension that parked it. A quiet notice still waits for an explicit resume, and // nothing is admitted during an owned admission pause or an update-restart teardown: // the release path re-flushes those. const mayFlush = !admissionPaused && !this._sessionInputSuspendedForUpdateRestart && (!pumpSuspended || this._shouldWakeForRlmTerminalNotice(message)); if (!mayFlush) return; // Remove it first: admission can re-enter this flush (a wake resumes the pump), and // a still-queued message would then be admitted twice. this._pendingNextTurnMessages.splice(index, 1); try { this._enqueueRlmTerminalNoticeAction(message); } catch { this._pendingNextTurnMessages.splice(index, 0, message); return; } } this._scheduleSessionInputPump(); }
private async _acquireRlmTerminalNoticeRetentionFence(): Promise<{ owner: symbol; release(): void } | undefined> { const disposeSignal = this._sessionActionCommitDisposeAbortController.signal; while (!this._disposed && !this._disposing && !disposeSignal.aborted) { if (this._queuedWorkPauses.size > 0) { let wake = () => {}; const pauseReleased = new Promise<void>((resolve) => { wake = resolve; this._sessionInputCheckpointWaiters.add(resolve); }); try { await waitForPromiseOrAbort(pauseReleased, disposeSignal, "Terminal notice retention cancelled"); } catch { return undefined; } finally { this._sessionInputCheckpointWaiters.delete(wake); } continue; } let fence: { owner: symbol; release(): void }; try { fence = await this._acquireSessionActionCommitFence(disposeSignal); } catch { return undefined; } if (this._queuedWorkPauses.size === 0 && !this._disposed && !this._disposing) return fence; fence.release(); } return undefined; }
private async _deferRlmTerminalNotice(message: CustomMessage): Promise<void> { this._assertRlmTerminalNotice(message); const fence = await this._acquireRlmTerminalNoticeRetentionFence(); if (!fence) return; try { if (this._disposed || this._disposing) return; this._pendingNextTurnMessages.push(cloneCustomMessage(message)); this._flushDeferredCompletionNotices(); } finally { fence.release(); } }
private _demoteDeferredCompletionActions(): void { const actions = this._actionStore .clearableActions() .filter((action) => this._durableRlmTerminalNoticeActionIds.has(action.id)); if (actions.length === 0) return; for (const action of actions) { if (!this._isRlmTerminalNoticeAction(action)) continue; const message = primaryDeliveryRecord(action).message; if (message.role === "custom") this._pendingNextTurnMessages.push(cloneCustomMessage(message)); } const ids = new Set(actions.map((action) => action.id)); this._cancelSessionActions( (action) => ids.has(action.id), new Error("Internal completion notice deferred across session input suspension."), actions, ); for (const id of ids) { this._durableRlmTerminalNoticeActionIds.delete(id); } }
/** * The kernel read the command's result before the notice reached the model, so * the notice has nothing left to report: drop it while it is still queued. * Delivered notices are no longer clearable, which makes this a no-op. */ private _withdrawAsyncBashCompletionNotice(details: { pid: number; command: string }): void { // One read withdraws one notice: pid reuse can queue an identical key twice, // and the read belongs to the older handle, which is the earlier notice. const notice = this._actionStore .clearableActions() .find((action) => this._isAsyncBashCompletionActionFor(action, details)); if (!notice) return; this._cancelSessionActions( (action) => action === notice, new Error("Background command completion notice withdrawn: the kernel read the result first."), ); this._emitQueueUpdate(); }
private _isAsyncBashCompletionActionFor( action: QueuedSessionAction, details: { pid: number; command: string }, ): boolean { if (action.payload.kind !== "turn") return false; const message = primaryDeliveryRecord(action).message; if (message.role !== "custom" || message.customType !== ASYNC_BASH_COMPLETION_CUSTOM_TYPE) return false; // pids are reused across handles, so the command has to match too. const completion = message.details as AsyncBashCompletionDetails | undefined; return completion?.pid === details.pid && completion.command === details.command; }
private async _promptInjectedMessage( text: string, message: CustomMessage, options?: InternalPromptOptions & { executionPolicy?: TurnExecutionPolicy }, ): Promise<void> { if (!this.isStreaming && options?.resumeIfIdle) this._resumeSessionInputAdmission(); const admissionEpoch = this._sessionInputPumpEpoch; const admissionFence = await this._acquireDirectTurnAdmissionFence(options?.signal).catch((error: unknown) => { throwIfPromptAdmissionCancelled(options?.signal); throw error; }); const reportPreflight = oncePreflight(options?.preflightResult); try { throwIfPromptAdmissionCancelled(options?.signal); if (admissionEpoch !== this._sessionInputPumpEpoch) { throw new Error("Injected session input was invalidated before admission"); } options?.admissionCommitted?.(); const queueForStreaming = this.isStreaming; const queueForBusy = options?.queueIfBusy === true && this._isBusyForSessionInput("preflight"); const visibleQueued = queueForStreaming || queueForBusy; if (visibleQueued && !options?.streamingBehavior) { const stateDescription = queueForStreaming ? "Agent is already processing" : "Agent has queued work"; throw new Error( `${stateDescription}. Specify streamingBehavior ('steer' or 'followUp') to queue the message.`, ); } const schedule = options?.streamingBehavior ?? "followUp"; const prefixMessages = visibleQueued ? this._takePendingNextTurnMessages() : undefined; const action = this._createPreparedTurnAction(schedule, text, undefined, { message, prefixMessages, queueKey: options?.followUpQueueKey, previewLabel: injectedMessagePreviewLabel(message), suppressAutonomousContinuation: options?.suppressAutonomousContinuation, resumeIfIdle: !visibleQueued || options?.resumeIfIdle || (options?.queueIfBusy === true && canSelectSessionAction(this._runtimeActivity())), source: options?.source ?? "internal", priority: options?.priority, executionPolicy: options?.executionPolicy ?? (visibleQueued ? this._turnExecutionPolicy("queued") : this._turnExecutionPolicy("injected")), queueVisible: visibleQueued, }); const result = this._admitSessionInput(action, { immediatelyEligible: !visibleQueued, }); admissionFence.release(); if (!result.accepted || !result.ticket) { if (prefixMessages) this._pendingNextTurnMessages.unshift(...prefixMessages); reportPreflight(false, false); return; } if (result.disposition === "queued") { reportPreflight(true, true); } else { void result.ticket.delivered.then( () => reportPreflight(true), () => reportPreflight(false), ); } if (options?.returnAfterAccepted) { if (result.disposition === "starts_when_admitted") await result.ticket.delivered; return; } if (visibleQueued) return; await result.ticket.completed; } catch (error) { reportPreflight(false); throw error; } finally { admissionFence.release(); } }
private async _prompt(text: string, options?: InternalPromptOptions): Promise<void> { const resumeSuspendedInput = options?.resumeIfIdle !== false; if (!this.isStreaming) { if (resumeSuspendedInput) this._resumeSessionInputAdmission(); this._assertSessionActionAdmissionAvailable(); } const admissionEpoch = this._sessionInputPumpEpoch; const commitFence = this.isStreaming ? undefined : await this._acquireDirectTurnAdmissionFence(options?.signal).catch((error: unknown) => { throwIfPromptAdmissionCancelled(options?.signal); throw error; }); const reportPreflight = oncePreflight(options?.preflightResult); const run = async () => { try { throwIfPromptAdmissionCancelled(options?.signal); if (!resumeSuspendedInput && admissionEpoch !== this._sessionInputPumpEpoch) { throw new Error("Session input was invalidated before admission"); } options?.admissionCommitted?.(); const isInternalPrompt = options?.internalPrompt === true; const expandPromptTemplates = isInternalPrompt ? false : (options?.expandPromptTemplates ?? true); const normalizationResult = this._normalizeSubmission(text, options?.images, { parseSessionCommands: !isInternalPrompt && !options?.skipPrePromptWork, extensionCommands: expandPromptTemplates ? "execute" : "ignore", inputSource: !isInternalPrompt && !options?.skipInputHandlers ? (options?.source ?? "interactive") : undefined, expandSkills: expandPromptTemplates, expandPromptTemplates, }); const normalized = normalizationResult instanceof Promise ? await normalizationResult : normalizationResult; // Async input handlers ran between the admission check above and // admission itself; re-check so content invalidated during that // await (e.g. a cron job cancelled or updated) is not admitted. if (normalizationResult instanceof Promise) options?.admissionCommitted?.(); if (normalized.kind === "extensionCommand") { commitFence?.release(); reportPreflight(true); void normalized.completion.then( () => this._settleAgentMessage(options?.agentMessageId, "completion"), (error) => this._settleAgentMessage(options?.agentMessageId, "completion", error), ); void normalized.completion.catch(() => undefined); if (!options?.returnAfterAccepted) await normalized.completion.catch(() => undefined); return; } if (normalized.kind === "handled") { commitFence?.release(); reportPreflight(true); this._settleAgentMessage(options?.agentMessageId, "completion"); return; }
const pendingOwnedWork = this._actionStore.unfinishedActions().length > 0; const wasRuntimeBusy = this.isStreaming || this.isCompacting || this.isRetrying || this.isBashRunning; const wasBusy = wasRuntimeBusy || pendingOwnedWork; if (normalized.kind === "sessionCommand") { const schedule = options?.streamingBehavior ?? (this.isStreaming ? "steer" : "followUp"); const action = this._createSessionCommandAction( normalized.text, normalized.command, normalized.images, schedule, { agentMessageId: options?.agentMessageId, source: isInternalPrompt ? "internal" : (options?.source ?? "interactive"), priority: options?.priority, }, ); const result = this._admitSessionInput(action, { immediatelyEligible: !wasBusy && this._canStartSessionActionImmediately(), }); commitFence?.release(); reportPreflight(result.accepted, result.disposition === "queued"); if (!result.accepted || !result.ticket) return; if (options?.returnAfterAccepted) { if (result.disposition === "starts_when_admitted") await result.ticket.delivered; return; } if (result.disposition === "queued") return; await this.waitForSessionInputIdle(); return; }
const queueForStreaming = this.isStreaming; const queueForBusy = options?.queueIfBusy === true && this._isBusyForSessionInput("preflight"); const visibleQueued = queueForStreaming || queueForBusy; if (visibleQueued && !options?.streamingBehavior) { const stateDescription = queueForStreaming ? "Agent is already processing" : "Agent has queued work"; throw new Error( `${stateDescription}. Specify streamingBehavior ('steer' or 'followUp') to queue the message.`, ); } const schedule = options?.streamingBehavior ?? "followUp"; const prefixMessages = visibleQueued ? this._takePendingNextTurnMessages() : undefined; const content = options?.content ? options.content.map((block) => ({ ...block })) : this._buildPromptContent(normalized.text, normalized.images); const suppliedMessage = options?.customMessage; const primaryMessage = suppliedMessage ? visibleQueued ? suppliedMessage : cloneCustomMessage(suppliedMessage) : ({ role: "user", content: content.map((block) => ({ ...block })), timestamp: Date.now(), } satisfies UserMessage); const acceptedAgentMessage = options?.skipPrePromptWork === true && options.returnAfterAccepted === true; const action = this._createPreparedTurnAction(schedule, normalized.text, normalized.images, { agentMessageId: options?.agentMessageId, queueKey: options?.followUpQueueKey, content, message: primaryMessage, prefixMessages, suppressAutonomousContinuation: options?.suppressAutonomousContinuation, resumeIfIdle: !visibleQueued || options?.resumeIfIdle || (options?.queueIfBusy === true && canSelectSessionAction(this._runtimeActivity())), source: isInternalPrompt ? "internal" : (options?.source ?? "interactive"), priority: options?.priority, executionPolicy: visibleQueued ? this._turnExecutionPolicy("queued") : this._turnExecutionPolicy("directPrompt", { returnAfterAccepted: options?.returnAfterAccepted, skipPrePromptWork: options?.skipPrePromptWork, }), queueVisible: visibleQueued, acceptedAgentMessage, acceptedBeforeCompletion: options?.returnAfterAccepted === true, }); if (action.suppressAutonomousContinuation) { this._markAutonomousContinuationSuppressed(primaryDeliveryRecord(action).message); } const result = this._admitSessionInput(action, { immediatelyEligible: !visibleQueued && this._canStartSessionActionImmediately(), }); commitFence?.release(); if (!result.accepted || !result.ticket) { if (prefixMessages) this._pendingNextTurnMessages.unshift(...prefixMessages); reportPreflight(false, false); return; } if (result.disposition === "queued") { reportPreflight(true, true); } else { void result.ticket.delivered.then( () => reportPreflight(true), () => reportPreflight(false), ); } const deferralObserver = acceptedAgentMessage && options?.queueIfBusy === true && !options.streamingBehavior && result.disposition === "starts_when_admitted" ? this._observeSessionActionDeferral(action) : undefined; if (acceptedAgentMessage && !queueForStreaming && !queueForBusy && !options?.streamingBehavior) { try { const outcome = deferralObserver ? await Promise.race([ result.ticket.delivered.then(() => "delivered" as const), deferralObserver.deferred.then(() => "deferred" as const), ]) : await result.ticket.delivered.then(() => "delivered" as const); if (outcome === "deferred" && !options?.streamingBehavior) { const error = new Error( "Agent became busy before prompt delivery. Specify streamingBehavior ('steer' or 'followUp') to queue the message.", ); this._rejectAgentMessage(action.agentMessageId, error); this._cancelSessionActions((candidate) => candidate === action, error); this._emitQueueUpdate(); throw error; } return; } finally { deferralObserver?.stop(); } } if (options?.returnAfterAccepted) { if (result.disposition === "starts_when_admitted" || (acceptedAgentMessage && !visibleQueued)) { await result.ticket.delivered; } return; } if (visibleQueued) return; await result.ticket.completed; await this.waitForSessionInputIdle(); } catch (error) { reportPreflight(false); throw error; } finally { commitFence?.release(); } }; return commitFence ? this._sessionActionCommitContext.run(commitFence.owner, run) : run(); }
private _executeExtensionCommand(text: string): Promise<void> | undefined { const parsed = parseSlashCommand(text); if (!parsed) return undefined; const commandName = parsed.name; const args = parsed.args;
const command = this._extensionRunner.getCommand(commandName); if (!command) return undefined; const context = this._extensionRunner.createCommandContext(command.sourceInfo.path); return Promise.resolve() .then(() => command.handler(args, context))
.catch((error: unknown) => { const commandError = error instanceof Error ? error : new Error(String(error)); this._extensionRunner.emitError({ extensionPath: `command:${commandName}`, event: "command", error: commandError.message, }); throw commandError; }); }
/** * Expand skill commands (/skill:name args) to their full content. * Returns the expanded text, or the original text if not a skill command or skill not found. * Emits errors via extension runner if file read fails. */ private _expandSkillCommand(text: string): string { if (!text.startsWith("/skill:")) return text;
const parsed = parseSlashCommand(text); if (!parsed?.name.startsWith("skill:")) return text; const skillName = parsed.name.slice("skill:".length); const args = parsed.args;
const skill = this.resourceLoader.getSkills().skills.find((s) => s.name === skillName); if (!skill) return text; // Unknown skill, pass through
try { const content = readFileSync(skill.filePath, "utf-8"); const body = stripFrontmatter(content).trim(); const skillBlock = `<skill name="${skill.name}" location="${skill.filePath}">\nReferences are relative to ${skill.baseDir}.\n\n${body}\n</skill>`; return args ? `${skillBlock}\n\n${args}` : skillBlock; } catch (err) { this._extensionRunner.emitError({ extensionPath: skill.filePath, event: "skill_expansion", error: err instanceof Error ? err.message : String(err), }); return text; // Return original on error } }
/** * Queue a steering message while the agent is running. * Delivered after the current assistant turn finishes executing its tool calls, * before the next LLM call. * Expands skill commands and prompt templates. Errors on extension commands. * @param images Optional image attachments to include with the message * @throws Error if text is an extension command */ async steer( text: string, images?: ImageContent[], options: { queueKey?: string; agentMessageId?: string; resumeIfIdle?: boolean; priority?: SessionActionPriority; } = {}, ): Promise<void> { const normalized = this._normalizeSubmission(text, images, { parseSessionCommands: false, extensionCommands: "reject", expandSkills: true, expandPromptTemplates: true, }); if (normalized instanceof Promise || normalized.kind !== "prompt") { throw new Error("Queued prompt normalization did not produce a prompt"); }
await this._queuePreparedPrompt("steer", normalized.text, normalized.images, { queueKey: options.queueKey, agentMessageId: options.agentMessageId, resumeIfIdle: options.resumeIfIdle, priority: options.priority ?? sessionActionPriorityFor(true, undefined, options.agentMessageId), }); }
/** * Queue a follow-up message to be processed after the agent finishes. * Delivered only when agent has no more tool calls or steering messages. * Expands skill commands and prompt templates. Errors on extension commands. * @param images Optional image attachments to include with the message * @throws Error if text is an extension command */ async followUp( text: string, images?: ImageContent[], options: { queueKey?: string; agentMessageId?: string; resumeIfIdle?: boolean; priority?: SessionActionPriority; } = {}, ): Promise<boolean> { const normalized = this._normalizeSubmission(text, images, { parseSessionCommands: false, extensionCommands: "reject", expandSkills: true, expandPromptTemplates: true, }); if (normalized instanceof Promise || normalized.kind !== "prompt") { throw new Error("Queued prompt normalization did not produce a prompt"); }
return this._queuePreparedPrompt("followUp", normalized.text, normalized.images, { queueKey: options.queueKey, agentMessageId: options.agentMessageId, resumeIfIdle: options.resumeIfIdle, priority: options.priority ?? sessionActionPriorityFor(true, undefined, options.agentMessageId), }); }
async restoreSessionActions(snapshot: SessionActionRecoverySnapshot): Promise<number> { if (snapshot.formatVersion !== SESSION_ACTION_RECOVERY_FORMAT_VERSION) { throw new Error(`Unsupported session action recovery format version: ${snapshot.formatVersion}`); } const actionIds = new Set(this._actionStore.ownedActions().map((action) => action.id)); const actions = snapshot.actions.map((recovered): QueuedSessionAction => { if (actionIds.has(recovered.id)) throw new Error(`Duplicate session action id: ${recovered.id}`); actionIds.add(recovered.id); if ( recovered.payload.kind === "turn" && recovered.payload.records.some((record) => record.ownerActionId !== recovered.id) ) { throw new Error(`Session action ${recovered.id} has invalid delivery correlation`); } const payload: PreparedTurnPayload | PreparedCommandPayload = recovered.payload.kind === "turn" ? { kind: "turn", text: recovered.payload.text, ...(recovered.payload.preview ? { preview: recovered.payload.preview } : {}), records: recovered.payload.records.map((record) => ({ id: record.id, role: record.role, message: cloneQueuedAgentMessage(record.message), started: false, durable: false, ownerActionId: record.ownerActionId, })), ...(recovered.payload.images ? { images: recovered.payload.images.map((image) => ({ ...image, })), } : {}), ...(recovered.payload.content ? { content: recovered.payload.content.map((block) => ({ ...block, })), } : {}), ...(recovered.payload.customMessage ? { customMessage: cloneCustomMessage(recovered.payload.customMessage), } : {}), executionPolicy: { ...recovered.payload.executionPolicy, preparation: { ...recovered.payload.executionPolicy.preparation, }, }, queueVisible: recovered.payload.queueVisible, acceptedAgentMessage: recovered.payload.acceptedAgentMessage, acceptedBeforeCompletion: recovered.payload.acceptedBeforeCompletion, } : { kind: "session_command", text: recovered.payload.text, command: { ...recovered.payload.command }, ...(recovered.payload.images ? { images: recovered.payload.images.map((image) => ({ ...image, })), } : {}), }; const primaryMessage = payload.kind === "turn" ? payload.records.find((record) => record.role === "primary")?.message : undefined; return { id: recovered.id, source: recovered.source, delivery: recovered.delivery, priority: recovered.priority ?? sessionActionPriorityFor(isHumanInputSource(recovered.source), primaryMessage, recovered.agentMessageId), wake: recovered.wake, payload, lifecycle: { state: "queued" }, ...(recovered.queueKey ? { queueKey: recovered.queueKey } : {}), ...(recovered.agentMessageId ? { agentMessageId: recovered.agentMessageId } : {}), ...(recovered.suppressAutonomousContinuation ? { suppressAutonomousContinuation: true } : {}), }; }); for (const action of actions) { const durableTerminalNotice = this._isRlmTerminalNoticeAction(action); if (durableTerminalNotice) this._durableRlmTerminalNoticeActionIds.add(action.id); try { this._admitSessionInput(action, { restore: true }); } catch (error) { if (durableTerminalNotice) this._durableRlmTerminalNoticeActionIds.delete(action.id); throw error; } } return actions.length; }
private _restoreSessionCommand( text: string, customMessage: CustomMessage | undefined, images: ImageContent[] | undefined, schedule: SessionInputSchedule, agentMessageId: string | undefined, ): boolean | undefined { if (!isSessionSlashCommandMessage(customMessage) || text !== customMessage.details.command.text) { return undefined; } return this._admitSessionInput( this._createSessionCommandAction(text, customMessage.details.command, images, schedule, { agentMessageId, source: "internal", priority: sessionActionPriorityFor(true, undefined, agentMessageId), }), { restore: true }, ).accepted; }
private _restorePromptInput(schedule: SessionInputSchedule, snapshot: RestoredPromptInput): Promise<boolean> { return this._queuePreparedPrompt(schedule, snapshot.text, snapshot.images, { queueKey: snapshot.queueKey, agentMessageId: snapshot.agentMessageId, content: snapshot.content, message: snapshot.customMessage, prefixMessages: snapshot.prefixMessages, source: "internal", priority: sessionActionPriorityFor(true, snapshot.customMessage, snapshot.agentMessageId), // Restored input replays the order it was admitted in. preserveOrder: true, }); }
async restoreSteeringMessage( text: string, images?: ImageContent[], options: { queueKey?: string; agentMessageId?: string; content?: (TextContent | ImageContent)[]; customMessage?: CustomMessage; prefixMessages?: CustomMessage[]; } = {}, ): Promise<void> { if ( this._restoreSessionCommand(text, options.customMessage, images, "steer", options.agentMessageId) !== undefined ) return;
await this._restorePromptInput("steer", { text, images, queueKey: options.queueKey, agentMessageId: options.agentMessageId, content: options.content, customMessage: options.customMessage, prefixMessages: options.prefixMessages, }); }
async restoreFollowUpMessage( text: string, images?: ImageContent[], options: { queueKey?: string; agentMessageId?: string; content?: (TextContent | ImageContent)[]; customMessage?: CustomMessage; prefixMessages?: CustomMessage[]; } = {}, ): Promise<boolean> { const restoredCommand = this._restoreSessionCommand( text, options.customMessage, images, "followUp", options.agentMessageId, ); if (restoredCommand !== undefined) return restoredCommand;
return this._restorePromptInput("followUp", { text, images, queueKey: options.queueKey, agentMessageId: options.agentMessageId, content: options.content, customMessage: options.customMessage, prefixMessages: options.prefixMessages, }); }
private _buildPromptContent(text: string, images?: ImageContent[]): (TextContent | ImageContent)[] { const content: (TextContent | ImageContent)[] = []; content.push({ type: "text", text }); if (images) content.push(...images); return content; }
private _takePendingNextTurnMessages(): CustomMessage[] { const messages = this._pendingNextTurnMessages; this._pendingNextTurnMessages = []; return messages; }
private _deliveryPolicy(schedule: SessionInputSchedule): DeliveryPolicy { return schedule === "steer" ? "next_turn_boundary" : "when_run_idle"; }
private _createDeliveryRecord( actionId: string, role: DeliveryRecord["role"], message: QueuedAgentMessage, ): DeliveryRecord { return { id: randomUUID(), role, message, started: false, durable: false, ownerActionId: actionId, }; }
private _turnExecutionPolicy( kind: "queued" | "directPrompt" | "injected" | "customTrigger", options: { returnAfterAccepted?: boolean; skipPrePromptWork?: boolean; } = {}, ): TurnExecutionPolicy { if (kind === "queued") { return { preparation: { initialRefineBarrier: "skip", flushPendingBashBeforeValidation: false, validateModelAndAuth: true, awaitPendingModelSelection: true, preTurnCompaction: "beforeModelSelection", finalRefineBarrier: "always", }, runBeforeAgentStart: true, nextTurnContextTiming: "commit", preserveEmptyExtensionPrompt: true, completionIncludesRetryChain: true, }; } if (kind === "directPrompt") { return { preparation: { initialRefineBarrier: options.returnAfterAccepted ? "skip" : "always", flushPendingBashBeforeValidation: true, validateModelAndAuth: true, awaitPendingModelSelection: true, preTurnCompaction: options.skipPrePromptWork ? "skip" : "afterModelSelection", finalRefineBarrier: "ifInFlight", }, runBeforeAgentStart: !options.skipPrePromptWork, nextTurnContextTiming: "preparation", preserveEmptyExtensionPrompt: false, completionIncludesRetryChain: true, }; } if (kind === "injected") { return { preparation: { initialRefineBarrier: "always", flushPendingBashBeforeValidation: true, validateModelAndAuth: true, awaitPendingModelSelection: true, preTurnCompaction: "beforeModelSelection", finalRefineBarrier: "ifInFlight", }, runBeforeAgentStart: true, nextTurnContextTiming: "preparation", preserveEmptyExtensionPrompt: true, completionIncludesRetryChain: true, }; } return { preparation: { initialRefineBarrier: "always", flushPendingBashBeforeValidation: false, validateModelAndAuth: false, awaitPendingModelSelection: false, preTurnCompaction: "skip", finalRefineBarrier: "skip", }, runBeforeAgentStart: false, nextTurnContextTiming: "skip", preserveEmptyExtensionPrompt: false, completionIncludesRetryChain: false, }; }
private _createPreparedTurnAction( schedule: SessionInputSchedule, text: string, images: ImageContent[] | undefined, options: { agentMessageId?: string; queueKey?: string; content?: (TextContent | ImageContent)[]; message?: QueuedAgentMessage; prefixMessages?: CustomMessage[]; previewLabel?: string; suppressAutonomousContinuation?: boolean; resumeIfIdle?: boolean; source?: InputSource | "internal"; priority?: SessionActionPriority; executionPolicy?: TurnExecutionPolicy; queueVisible?: boolean; acceptedAgentMessage?: boolean; acceptedBeforeCompletion?: boolean; }, ): QueuedSessionAction { const id = randomUUID(); const content = options.content ?? this._buildPromptContent(text, images); const message = options.message ?? ({ role: "user", content: content.map((block) => ({ ...block })), timestamp: Date.now(), } satisfies UserMessage); const prefixMessages = options.prefixMessages?.map((prefix) => cloneCustomMessage(prefix)) ?? []; const preview = options.previewLabel ? `${options.previewLabel}: ${text}` : undefined; const payload: PreparedTurnPayload = { kind: "turn", text, records: [ ...prefixMessages.map((prefix) => this._createDeliveryRecord(id, "prefix", prefix)), this._createDeliveryRecord(id, "primary", message), ], preview, images: images?.map((image) => ({ ...image })), content: content.map((block) => ({ ...block })), customMessage: options.message?.role === "custom" ? cloneCustomMessage(options.message) : undefined, executionPolicy: options.executionPolicy ?? this._turnExecutionPolicy("queued"), queueVisible: options.queueVisible ?? true, acceptedAgentMessage: options.acceptedAgentMessage ?? false, acceptedBeforeCompletion: options.acceptedBeforeCompletion ?? false, }; const source = options.source ?? "internal"; return { id, source, delivery: this._deliveryPolicy(schedule), priority: options.priority ?? sessionActionPriorityFor(isHumanInputSource(source), message, options.agentMessageId), wake: options.resumeIfIdle === true ? "immediate" : schedule === "steer" ? "on_lower_boundary" : "external_resume", payload, lifecycle: { state: "queued" }, queueKey: options.queueKey, agentMessageId: options.agentMessageId, suppressAutonomousContinuation: options.suppressAutonomousContinuation, }; }
private _createSessionCommandAction( text: string, command: SessionSlashCommand, images: ImageContent[] | undefined, schedule: SessionInputSchedule, options: { agentMessageId?: string; source?: InputSource | "internal"; priority?: SessionActionPriority; } = {}, ): QueuedSessionAction { const source = options.source ?? "internal"; return { id: randomUUID(), source, delivery: this._deliveryPolicy(schedule), priority: options.priority ?? sessionActionPriorityFor(isHumanInputSource(source), undefined, options.agentMessageId), wake: "immediate", payload: { kind: "session_command", text, command, images }, lifecycle: { state: "queued" }, agentMessageId: options.agentMessageId, }; }
private _coalescedFollowUpOwner(action: QueuedSessionAction): QueuedSessionAction | undefined { if (action.delivery !== "when_run_idle" || action.payload.kind !== "turn" || !action.queueKey) return undefined; return this._actionStore .unfinishedActions() .find( (candidate) => candidate.queueKey === action.queueKey && (candidate.lifecycle.state === "queued" || candidate.lifecycle.state === "selected" || candidate.lifecycle.state === "preparing"), ); }
private _assertSessionActionAdmissionAvailable(): void { if (this._disposed || this._disposing) { throw new Error("Cannot admit a session action because the session is disposing or disposed."); } if (this._sessionInputAdmissionPauses.size > 0) { throw new SessionInputAdmissionPausedError( "Cannot admit a session action while session input admission is paused.", ); } if (this._sessionInputPumpSuspended) { throw new Error("Cannot admit a session action while queued session input is suspended."); } }
private _admitSessionInput( action: QueuedSessionAction, options: { restore?: boolean; front?: boolean; preserveOrder?: boolean; wake?: boolean; immediatelyEligible?: boolean; } = {}, ): { accepted: boolean; disposition: "starts_when_admitted" | "queued"; ticket?: ActionTicket; } { if (this._disposed || this._disposing) { throw new Error("Cannot admit a session action because the session is disposing or disposed."); } if (this._sessionInputAdmissionPauses.size > 0) { throw new SessionInputAdmissionPausedError( "Cannot admit a session action while session input admission is paused.", ); } // A message from another agent is admitted even when this session already carries a // long queue: the ceiling is enforced by sender-side backpressure, and refusing here // failed exactly when fan-in results were arriving. const coalescedOwner = options.restore ? undefined : this._coalescedFollowUpOwner(action); if (coalescedOwner) { if (action.agentMessageId !== coalescedOwner.agentMessageId) { this._rejectAgentMessage( action.agentMessageId, new Error("Prompt was not queued because an equivalent follow-up is already pending."), ); } return { accepted: false, disposition: "queued" }; } const canStartImmediately = options.immediatelyEligible === true && (this._actionStore.unfinishedActions().length === 0 || options.front === true); const placement: SessionActionPlacement = options.front ? "front" : options.restore || options.preserveOrder ? "tail" : "priority"; this._actionStore.enqueue(action, placement); let disposition: "starts_when_admitted" | "queued" = "queued"; if (canStartImmediately && this._actionStore.selectFirst() === action) disposition = "starts_when_admitted"; const controller = this._actionStore.ticketFor(action); controller.settleAccepted({ status: "accepted", actionId: action.id, disposition, }); this._sessionInputArrivalEpoch++; this._emitQueueUpdate(); if ( !options.restore && options.wake !== false && (disposition === "starts_when_admitted" || (action.delivery === "next_turn_boundary" && this.isStreaming) || action.payload.kind === "session_command" || action.wake === "immediate") ) { if ( action.payload.kind === "turn" && action.wake === "immediate" && !this._sessionInputSuspendedForUpdateRestart ) { this._resumeSessionInputAdmission(); } this._scheduleSessionInputPump(); } return { accepted: true, disposition, ticket: controller.ticket }; }
private async _queuePreparedPrompt( schedule: SessionInputSchedule, text: string, images?: ImageContent[], options: { agentMessageId?: string; queueKey?: string; content?: (TextContent | ImageContent)[]; message?: QueuedAgentMessage; prefixMessages?: CustomMessage[]; previewLabel?: string; suppressAutonomousContinuation?: boolean; resumeIfIdle?: boolean; source?: InputSource | "internal"; priority?: SessionActionPriority; preserveOrder?: boolean; } = {}, ): Promise<boolean> { const action = this._createPreparedTurnAction(schedule, text, images, options); if (action.suppressAutonomousContinuation) { this._markAutonomousContinuationSuppressed(primaryDeliveryRecord(action).message); } return this._admitSessionInput(action, { preserveOrder: options.preserveOrder }).accepted; }
private _runtimeActivity(): RuntimeActivity { return { lowerAgentRun: this.isStreaming, compaction: this.isCompacting, retry: this.isRetrying, bash: this.isBashRunning, refinementApply: this._refineInFlight !== undefined, branchMutation: this._branchSummaryOperation !== undefined, schedulerPauseCount: this._queuedWorkPauses.size + (this._sessionInputPumpSuspended ? 1 : 0), disposing: this._disposed || this._disposing, }; }
private _hasSelectableSessionInput(): boolean { return ( this._actionStore.queuedActions().length > 0 || this._actionStore.activeActions().some((action) => action.lifecycle.state === "selected") ); }
get hasPendingSessionWork(): boolean { return this._actionStore.unfinishedActions().some((action) => { const state = action.lifecycle.state; return ( state === "queued" || state === "selected" || state === "preparing" || (state === "committing" && action.payload.kind === "turn" && !primaryDeliveryRecord(action).durable) ); }); }
get hasPendingAdmissionWaiters(): boolean { return ( this._sessionActionCommitOwner !== undefined || this._pendingSessionActionFenceWaiters > 0 || this._sessionInputCheckpointWaiters.size > 0 ); }
private _scheduleSessionInputPump(): void { if (this._sessionInputPumpSuspended || this._queuedWorkPauses.size > 0) return; if (this._disposed || this._disposing || this._sessionInputPumpRequested || !this._hasSelectableSessionInput()) { return; } this._sessionInputPumpRequested = true; const epoch = this._sessionInputPumpEpoch; const pump = async () => { this._sessionInputPumpRequested = false; await this._pumpSessionInputs(epoch); }; this._sessionInputPump = this._sessionInputPump.then(pump, pump); this._sessionInputPump.catch(() => {}); }
private async _pumpSessionInputs(epoch: number): Promise<void> { let blocked = false; try { while (!this._disposed && !this._disposing && this._hasSelectableSessionInput()) { await this.agent.waitForIdle(); const preselected = this._actionStore .activeActions() .find((action) => action.lifecycle.state === "selected"); if (epoch !== this._sessionInputPumpEpoch) { if (preselected) { this._actionStore.rollback(preselected); this._notifySessionInputCheckpointChange(); this._emitQueueUpdate(); } return; } if (!this._hasCancelledDispatchCapture()) await this._agentEventQueue; if (!preselected || preselected.payload.kind === "session_command") await this._waitForRefineIdle(); const activity = this._runtimeActivity(); const canSelectPreselectedTurn = preselected?.payload.kind === "turn" && canSelectSessionAction({ ...activity, refinementApply: false }); if ( this._isSessionInputHandoffDeferred(epoch) || (!canSelectPreselectedTurn && !canSelectSessionAction(activity)) ) { blocked = true; this._notifySessionInputCheckpointChange(); return; } const first = preselected ?? this._actionStore.selectFirst(); if (!first) return; if (first.payload.kind === "session_command") { await this._executeSelectedSessionCommand(first, epoch); return; }
const mode = first.delivery === "next_turn_boundary" ? this.steeringMode : this.followUpMode; const actions: QueuedSessionAction[] = [first]; while (!preselected && mode === "all") { const next = this._actionStore.queuedActions(first.delivery)[0]; if ( !next || next.payload.kind !== "turn" || !turnExecutionPoliciesEqual(first.payload.executionPolicy, next.payload.executionPolicy) ) { break; } this._actionStore.selectFirst(); actions.push(next); } if (epoch !== this._sessionInputPumpEpoch) { for (const action of actions) this._actionStore.rollback(action); return; } for (const action of actions) transitionSessionAction(action, { state: "preparing" }); this._notifySessionInputCheckpointChange(); this._emitQueueUpdate(); try { await this._startPreparedTurnActions(actions, epoch); for (const action of actions) { if (action.lifecycle.state === "committing") { const primary = primaryDeliveryRecord(action); if (this.agent.state.messages.includes(primary.message)) { primary.durable = true; transitionSessionAction(action, { state: "running", execution: "agent_turn", }); } } if (action.lifecycle.state === "running") { transitionSessionAction(action, { state: "completed" }); this._actionStore.ticketFor(action).settleCompleted(); this._settleAgentMessage(action.agentMessageId, "completion"); } } } catch (error) { const transcript = this.agent.state.messages; const delivered = new Set(transcript); const undelivered: QueuedSessionAction[] = []; for (const action of actions) { if (action.payload.kind !== "turn" || action.lifecycle.state === "cancelled") continue; for (const record of action.payload.records) record.durable ||= delivered.has(record.message); action.payload.records = action.payload.records.filter((record) => { if (record.role === "prefix") return !record.durable; if (record.role === "next_turn") return record.durable; return true; }); if (!primaryDeliveryRecord(action).durable) undelivered.push(action); } if (this._isDeferredSessionInputError(error, epoch)) { for (const action of undelivered) { if (action.lifecycle.state === "committing") { this._actionStore.rollback(action, { dispatchSettled: true, transcript, }); } else if (action.lifecycle.state === "preparing" || action.lifecycle.state === "selected") { this._actionStore.rollback(action); } } if (undelivered.length > 0) this._emitQueueUpdate(); blocked = epoch !== this._sessionInputPumpEpoch || this._isBusyForSessionInput("pump"); if (blocked) return; continue; } const terminalError = this._asError(error); for (const action of actions) { if (action.lifecycle.state === "cancelled") continue; if (action.lifecycle.state !== "completed" && action.lifecycle.state !== "failed") { transitionSessionAction(action, { state: "failed", error: terminalError, }); } const ticket = this._actionStore.ticketFor(action); if (undelivered.includes(action)) { ticket.rejectDelivered(terminalError); this._settleAgentMessage(action.agentMessageId, "delivery", terminalError); } this._settleAgentMessage(action.agentMessageId, "completion", terminalError); ticket.settleCompleted(terminalError); } if (actions.some((action) => action.payload.kind !== "turn" || action.payload.queueVisible)) { this._surfaceSessionInputError(error); } } finally { for (const action of actions) { const retainedCancelledDispatch = action.lifecycle.state === "cancelled" && action.payload.kind === "turn" && action.payload.captureRunMessages !== undefined; if ( !retainedCancelledDispatch && (action.lifecycle.state === "completed" || action.lifecycle.state === "failed" || action.lifecycle.state === "cancelled") ) { this._durableRlmTerminalNoticeActionIds.delete(action.id); this._actionStore.releaseTerminal(action); } } this._notifySessionInputCheckpointChange(); this._emitQueueUpdate(); } if (epoch !== this._sessionInputPumpEpoch || blocked) return; } } finally { if (!blocked && epoch === this._sessionInputPumpEpoch && this._hasSelectableSessionInput()) { this._scheduleSessionInputPump(); } } }
private async _executeSelectedSessionCommand(action: QueuedSessionAction, epoch: number): Promise<void> { if (action.payload.kind !== "session_command") throw new Error("Expected a selected session command"); const input = action.payload; const commitFence = await this._acquireSessionActionCommitFence(); try { await this._sessionActionCommitContext.run(commitFence.owner, async () => { const isCancelled = () => action.lifecycle.state === "cancelled"; if (isCancelled()) return; await this._waitForRefineIdle(); if (isCancelled()) return; if (this._isSessionInputHandoffDeferred(epoch) || !canSelectSessionAction(this._runtimeActivity())) { this._actionStore.rollback(action); this._notifySessionInputCheckpointChange(); this._emitQueueUpdate(); return; } transitionSessionAction(action, { state: "running", execution: "session_command", }); this._notifySessionInputCheckpointChange(); this._emitQueueUpdate(); try { this._appendDurableSessionCommandMessage(input.text, input.command, false); this._actionStore.ticketFor(action).settleDelivered({ status: "not_applicable" }); this._settleAgentMessage(action.agentMessageId, "delivery"); await this._executeQueuedSessionCommand(action); transitionSessionAction(action, { state: "completed" }); this._actionStore.ticketFor(action).settleCompleted(); this._settleAgentMessage(action.agentMessageId, "completion"); } catch (error) { const commandError = this._asError(error); transitionSessionAction(action, { state: "failed", error: commandError, }); const ticket = this._actionStore.ticketFor(action); ticket.rejectDelivered(commandError); ticket.settleCompleted(commandError); this._rejectAgentMessage(action.agentMessageId, commandError); } finally { this._actionStore.releaseTerminal(action); this._notifySessionInputCheckpointChange(); this._emitQueueUpdate(); } }); } finally { commitFence.release(); } }
/** * Busy inputs: compaction, retry, bash, plus (for "pump") disposal, * suspension, queued-work pauses, and branch-summary mutation. Waiters * parked on this predicate rely on every clear site notifying the * session-input checkpoint waiters. */ private _isBusyForSessionInput(point: "preflight" | "pump"): boolean { const externalBusy = this.isCompacting || this.isRetrying || this.isBashRunning; if (point === "pump") { return ( externalBusy || this._disposed || this._disposing || this._sessionInputPumpSuspended || this._queuedWorkPauses.size > 0 || this._branchSummaryOperation !== undefined ); } return externalBusy || this._actionStore.unfinishedActions().length > 0; }
private _isSessionInputHandoffDeferred(epoch: number): boolean { return epoch !== this._sessionInputPumpEpoch || this._isBusyForSessionInput("pump"); }
private _asError(error: unknown): Error { return error instanceof Error ? error : new Error(String(error)); }
private _isDeferredSessionInputError(error: unknown, epoch: number): boolean { if (error instanceof DeferredSessionInputError) return true; if (epoch !== this._sessionInputPumpEpoch) return true; if (this._isBusyForSessionInput("pump")) { this._surfaceSessionInputError(error); return true; } return false; }
private _surfaceSessionInputError(error: unknown): void { const normalized = this._asError(error); try { this._extensionRunner.emitError({ extensionPath: "<session-input>", event: "session_input", error: normalized.message, stack: normalized.stack, }); } catch { // Best-effort: a throwing error listener must not break the pump's requeue path. } }
private async _startPreparedTurnActions(actions: QueuedSessionAction[], epoch: number): Promise<void> { let nextTurnMessages: CustomMessage[] = []; const activeTurns = () => actions.filter( (action): action is SessionAction<PreparedTurnPayload> => action.payload.kind === "turn" && action.lifecycle.state === "preparing", ); const firstTurn = activeTurns()[0]; if (!firstTurn) return; const executionPolicy = firstTurn.payload.executionPolicy; // The digest is never parked as pending context; lazy injection re-arms instead. const parkNextTurnMessages = (messages: CustomMessage[]) => { const parked = messages.filter((message) => message.customType !== HARNESS_DIGEST_CUSTOM_TYPE); if (parked.length !== messages.length) this._harnessDigestPending = true; this._pendingNextTurnMessages.unshift(...parked); }; const restoreNextTurnContext = () => { parkNextTurnMessages(nextTurnMessages); nextTurnMessages = []; }; try { const preparedTurn = await this._prepareForCommit(executionPolicy.preparation, { afterValidation: () => { if (this._isSessionInputHandoffDeferred(epoch)) { throw new DeferredSessionInputError("Session input paused before preflight"); } }, prepare: async () => { if (executionPolicy.nextTurnContextTiming === "preparation") { nextTurnMessages = this._takePendingNextTurnMessages(); } if (!executionPolicy.runBeforeAgentStart) return undefined; while (activeTurns().some((action) => action.payload.prepared === undefined)) { if (this._isSessionInputHandoffDeferred(epoch)) { throw new DeferredSessionInputError("Session input paused before preparation"); } const preparationAction = activeTurns().at(-1); if (!preparationAction) return undefined; const basePromptSnapshot = this._baseSystemPrompt; const result = await this._extensionRunner.emitBeforeAgentStart( preparationAction.payload.text, preparationAction.payload.images, basePromptSnapshot, this._baseSystemPromptOptions, preparationAction.source, ); if (activeTurns().at(-1) !== preparationAction) continue; const prepared = { result, basePromptSnapshot }; for (const action of activeTurns()) action.payload.prepared = prepared; } if (this._isSessionInputHandoffDeferred(epoch)) { throw new DeferredSessionInputError("Session input paused before handoff"); } return activeTurns()[0]?.payload.prepared; }, shouldCommit: () => activeTurns().length > 0, commit: (prepared) => { if (this._isSessionInputHandoffDeferred(epoch)) { throw new DeferredSessionInputError("Session input paused before handoff"); } const turns = activeTurns(); if (turns.length === 0) return undefined; return { prepared, turns }; }, }); if (!preparedTurn) { restoreNextTurnContext(); return; } const { prepared, turns } = preparedTurn; const commitFence = await this._acquireSessionActionCommitFence(); let promptPromise: Promise<void>; try { promptPromise = this._sessionActionCommitContext.run(commitFence.owner, () => { if ( this._isSessionInputHandoffDeferred(epoch) || this.isStreaming || turns.some((action) => action.lifecycle.state !== "preparing") ) { throw new DeferredSessionInputError("Agent became active before session input handoff"); } if (executionPolicy.nextTurnContextTiming === "commit") { nextTurnMessages = this._takePendingNextTurnMessages(); } if (this._harnessDigestPending) { // The first-turn digest rides the turn's delivery records so a // cancelled first turn strips it with the rest of the turn. this._harnessDigestPending = false; const digest = this._harnessDigest(); if (this._latestContextHarnessDigest() !== digest) { nextTurnMessages = [createHarnessDigestMessage(digest), ...nextTurnMessages]; } } const contextRecords = nextTurnMessages.map((message) => this._createDeliveryRecord(turns[0].id, "next_turn", message), ); const firstPrimaryIndex = turns[0].payload.records.indexOf(primaryDeliveryRecord(turns[0])); turns[0].payload.records.splice(firstPrimaryIndex, 0, ...contextRecords); for (const action of turns) { // Queued continuations must report goal accounting as of delivery, // not as of queue time (see refresh rationale). this._refreshGoalContextMessageAtDelivery(primaryDeliveryRecord(action).message); } const preparedMessages: AgentMessage[] = turns.flatMap((action) => action.payload.records.map((record) => record.message), ); for (const action of turns) { if (action.suppressAutonomousContinuation) { this._markAutonomousContinuationSuppressed(primaryDeliveryRecord(action).message); } } if (executionPolicy.runBeforeAgentStart) { this._appendBeforeAgentStartMessages(preparedMessages, prepared?.result); this._applyPreparedSystemPrompt(prepared, executionPolicy.preserveEmptyExtensionPrompt); } else if (executionPolicy.nextTurnContextTiming !== "skip") { this.agent.state.systemPrompt = this._baseSystemPrompt; } for (const action of turns) transitionSessionAction(action, { state: "committing" }); this._notifySessionInputCheckpointChange(); this._emitQueueUpdate(); return turns.some((action) => action.suppressAutonomousContinuation) ? this._runWithAutonomousContinuationSuppressed(() => this.agent.prompt(preparedMessages)) : this.agent.prompt(preparedMessages); }); } finally { commitFence.release(); } await promptPromise; if (executionPolicy.completionIncludesRetryChain) await this.waitForRetry(); if (!this._hasCancelledDispatchCapture()) await this._agentEventQueue; if ( turns.some( (action) => action.lifecycle.state !== "cancelled" && !primaryDeliveryRecord(action).durable && !this.agent.state.messages.includes(primaryDeliveryRecord(action).message), ) ) { throw new Error("Session input dispatch settled without durable delivery"); } this._forgetConsumedPostCompactionContinuations(turns.map((action) => primaryDeliveryRecord(action).message)); } catch (error) { const delivered = new Set(this.agent.state.messages); parkNextTurnMessages(nextTurnMessages.filter((message) => !delivered.has(message))); for (const action of actions) { if (action.payload.kind === "turn") { action.payload.records = action.payload.records.filter((record) => record.role !== "next_turn"); } } throw error; } }
private async _executeQueuedSessionCommand(action: QueuedSessionAction): Promise<void> { if (action.payload.kind !== "session_command") throw new Error("Expected a session command action"); const input = action.payload; try { let resultText: string | undefined; let displayResult = true; switch (input.command.name) { case "compact": { const result = await this._handleCompactSlashCommand(input.command.args); resultText = result.text; displayResult = result.display; break; } case "refine": { let result: RefinementResult; try { const options = parseRefineCommandOptions(input.command.args); result = await this.refine(options, { skipAbort: true }); } catch (error) { // Only a failure of the refinement itself is a refine failure; a later // result-row persist error must not report a completed refinement as failed. this._emitRefineFailed(this._asError(error)); throw error; } const applied = result.appliedEdits.filter((edit) => edit.applied).length; resultText = `Refined continual harness state: ${applied} edit${applied === 1 ? "" : "s"} applied.`; displayResult = false; break; } case "goal": await this._handleGoalSlashCommand(input.text, input.images); resultText = this._goalState.objective ? `Goal ${this._goalState.status}: ${this._goalState.objective}` : "No active goal."; break; case "autonomous": await this._handleAutonomousSlashCommand(input.text); break; } if (resultText) { this._appendDurableSessionCommandMessage(resultText, input.command, true, false, displayResult); } } catch (error) { if (error instanceof CompactionSkippedError) return; const commandError = error instanceof Error ? error : new Error(String(error)); try { this._appendDurableSessionCommandMessage( `Command failed: ${commandError.message}`, input.command, true, true, ); } catch { // The result row is also the command-correlated UI settle edge. const message = createSessionSlashCommandResultMessage(`Command failed: ${commandError.message}`, { command: input.command, success: false, severity: "error", error: commandError.message, }); this._emit({ type: "message_start", message }); this._emit({ type: "message_end", message }); } throw commandError; } }
private _appendDurableSessionCommandMessage( content: string, command: SessionSlashCommand, isResult: boolean, isError = false, display = true, ): void { const message: CustomMessage = isResult ? createSessionSlashCommandResultMessage( content, { command, success: !isError, severity: isError ? "error" : "info", ...(isError ? { error: content.replace(/^Command failed:\s*/, "") } : {}), }, display, ) : createSessionSlashCommandMessage(command); // Persist before touching live state so a failed write cannot leave an // unsaved leaf that the next entry would silently parent onto. this.sessionManager.appendCustomMessageEntryWithRollback( message.customType, message.content, message.display, message.details, ); this.agent.state.messages.push(message); this._emit({ type: "message_start", message }); this._emit({ type: "message_end", message }); }
private _throwIfExtensionCommand(text: string): void { const commandName = parseSlashCommand(text)?.name ?? ""; const command = this._extensionRunner.getCommand(commandName);
if (command) { throw new Error( `Extension command "/${commandName}" cannot be queued. Use prompt() or execute the command when not streaming.`, ); } }
/** * Send a custom message to the session. Creates a CustomMessageEntry. * * Handles three cases: * - Streaming: queues message, processed when loop pulls from queue * - Not streaming + triggerTurn: appends to state/session, starts new turn * - Not streaming + no trigger: appends to state/session, no turn * * @param message Custom message with customType, content, display, details * @param options.triggerTurn If true and not streaming, triggers a new LLM turn * @param options.deliverAs Delivery mode: "steer", "followUp", or "nextTurn" */ async sendCustomMessage<T = unknown>( message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details">, options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn"; }, ): Promise<void> { const appMessage = { role: "custom" as const, customType: message.customType, content: message.content, display: message.display, details: message.details, timestamp: Date.now(), } satisfies CustomMessage<T>; if (options?.deliverAs === "nextTurn") { this._pendingNextTurnMessages.push(appMessage); } else if (this.isStreaming) { const normalized = normalizeMessageContent(message.content); if (options?.deliverAs === "followUp") { await this._queuePreparedPrompt("followUp", normalized.text, normalized.images, { message: appMessage, resumeIfIdle: true, }); } else { await this._queuePreparedPrompt("steer", normalized.text, normalized.images, { message: appMessage, resumeIfIdle: true, }); } } else if (options?.triggerTurn) { if (!this._sessionInputSuspendedForUpdateRestart) this._resumeSessionInputAdmission(); const admissionFence = await this._acquireDirectTurnAdmissionFence(); try { const normalized = normalizeMessageContent(message.content); const immediatelyEligible = this._canStartSessionActionImmediately(); const action = this._createPreparedTurnAction("followUp", normalized.text, normalized.images, { message: appMessage, resumeIfIdle: true, executionPolicy: this._turnExecutionPolicy("customTrigger"), queueVisible: false, }); const result = this._admitSessionInput(action, { immediatelyEligible }); admissionFence.release(); if (!result.ticket) return; await result.ticket.completed; } finally { admissionFence.release(); } } else { this.agent.state.messages.push(appMessage); this.sessionManager.appendCustomMessageEntry( message.customType, message.content, message.display, message.details, ); this._emit({ type: "message_start", message: appMessage }); this._emit({ type: "message_end", message: appMessage }); } }
/** * Send a user message to the agent. Always triggers a turn. * When the agent is streaming, use deliverAs to specify how to queue the message. * * @param content User message content (string or content array) * @param options.deliverAs Delivery mode when streaming: "steer" or "followUp" */ async sendUserMessage( content: string | (TextContent | ImageContent)[], options?: { deliverAs?: "steer" | "followUp" }, ): Promise<void> { let text: string; let images: ImageContent[] | undefined;
if (typeof content === "string") { text = content; } else { const textParts: string[] = []; images = []; for (const part of content) { if (part.type === "text") { textParts.push(part.text); } else { images.push(part); } } text = textParts.join("\n"); if (images.length === 0) images = undefined; }
await this._prompt(text, { expandPromptTemplates: false, streamingBehavior: options?.deliverAs, images, source: "extension", resumeIfIdle: true, }); }
clearQueue(): { steering: string[]; followUp: string[] } { const clearable = this._actionStore .clearableActions() .filter((action) => action.payload.kind === "session_command" || action.payload.queueVisible); if (clearable.some((action) => action.payload.kind === "turn" && action.lifecycle.state === "preparing")) { this._sessionInputPumpEpoch++; } const steering = clearable .filter((action) => action.delivery === "next_turn_boundary") .map((action) => action.payload.text); const followUp = clearable .filter((action) => action.delivery === "when_run_idle") .map((action) => action.payload.text); const promptError = new Error("Queued prompt was cleared before delivery."); const agentMessageError = new Error("Queued agent message was cleared before delivery."); for (const action of clearable) { const error = action.payload.kind === "turn" && action.lifecycle.state === "preparing" ? promptError : agentMessageError; this._settleAgentMessage(action.agentMessageId, "delivery", error); this._settleAgentMessage(action.agentMessageId, "completion", error); } const clearableIds = new Set(clearable.map((action) => action.id)); this._cancelSessionActions((action) => clearableIds.has(action.id), agentMessageError); this.agent.clearAllQueues(); this._emitQueueUpdate(); return { steering, followUp }; }
private _invalidateQueuedPromptPreparation(): void { for (const action of this._actionStore.clearableActions()) { if (action.payload.kind === "turn") action.payload.prepared = undefined; } }
clearQueuedAgentMessages(): { steering: string[]; followUp: string[] } { this._agentMessageClearEpoch++; // customType identifies agent messages; the text parser covers persisted pre-grammar prompts. return this._clearQueuedTurnActionsMatching( (action) => isAgentSessionMessage(primaryDeliveryRecord(action).message) || isAgentSessionMessagePrompt(action.payload.text), ); }
clearQueuedUserMessagesMatching(predicate: (text: string) => boolean): { steering: string[]; followUp: string[] } { return this._clearQueuedTurnActionsMatching((action) => predicate(action.payload.text)); }
private _clearQueuedTurnActionsMatching(matches: (action: QueuedSessionAction) => boolean): { steering: string[]; followUp: string[]; } { const ownedActions = this._actionStore.ownedActions(); const dispatchedTurnCount = ownedActions.filter( (action) => action.payload.kind === "turn" && (action.lifecycle.state === "committing" || action.lifecycle.state === "running"), ).length; const matching = ownedActions.filter( (action) => action.payload.kind === "turn" && action.agentMessageId !== undefined && matches(action) && (action.lifecycle.state === "queued" || action.lifecycle.state === "selected" || action.lifecycle.state === "preparing" || (action.lifecycle.state === "committing" && dispatchedTurnCount === 1 && !primaryDeliveryRecord(action).started)), ); if (matching.length === 0) return { steering: [], followUp: [] }; const removedTexts = (delivery: DeliveryPolicy) => [ ...matching.filter((action) => action.delivery === delivery && action.lifecycle.state === "queued"), ...matching.filter((action) => action.delivery === delivery && action.lifecycle.state !== "queued"), ].map((action) => action.payload.text); const removedSteering = removedTexts("next_turn_boundary"); const removedFollowUp = removedTexts("when_run_idle"); const acceptedError = new Error("Accepted agent message was cleared before delivery."); const queuedError = new Error("Queued agent message was cleared before delivery."); for (const action of matching) { const error = action.payload.kind === "turn" && action.payload.acceptedAgentMessage ? acceptedError : queuedError; this._rejectAgentMessage(action.agentMessageId, error); } for (const [accepted, error] of [ [true, acceptedError], [false, queuedError], ] as const) { const ids = new Set( matching .filter((action) => action.payload.kind === "turn" && action.payload.acceptedAgentMessage === accepted) .map((action) => action.id), ); if (ids.size > 0) this._cancelSessionActions((action) => ids.has(action.id), error, matching); } if ( matching.some( (action) => action.lifecycle.state === "cancelled" && action.payload.kind === "turn" && action.payload.captureRunMessages, ) ) { this.agent.abort(); } this._emitQueueUpdate(); return { steering: removedSteering, followUp: removedFollowUp }; }
/** * Mutate a single visible queued message, addressed by its position in the same * projection the session-action snapshot publishes. expectedText must match the * item's current preview so clients never edit a shifted queue by accident. */ mutateQueuedMessage( lane: QueuedMessageLane, index: number, expectedText: string, mutation: QueuedMessageMutation, ): QueuedMessageMutationStatus { const policy = queuedMessageLaneDeliveryPolicy(lane); const projection = visibleSessionActionProjection(this._actionStore.queuedActions(policy)); const item = projection[index]; if (!item || queuedAgentMessagePreview(item) !== expectedText) return "rejected"; if (mutation.type === "delete") { const error = new Error("Queued prompt was deleted before delivery."); this._rejectAgentMessage(item.agentMessageId, error); this._cancelSessionActions((candidate) => candidate === item, error); this._emitQueueUpdate(); this.resumeQueuedWork(); return "applied"; } if (mutation.type === "move") { const neighbor = projection[index + mutation.direction]; if (!neighbor) return "rejected"; this._actionStore.swapQueued(item, neighbor); this._emitQueueUpdate(); return "applied"; } if ( item.payload.kind === "turn" && (item.payload.acceptedAgentMessage || item.payload.records.some((record) => record.role === "primary" && record.message.role !== "user")) ) { return "rejected"; } const images = mutation.images?.map((image) => ({ ...image })); if (item.payload.kind === "session_command") { const command = parseSessionSlashCommand(mutation.text); if (!command) return "invalid"; item.payload.text = mutation.text; item.payload.command = command; if (mutation.images !== undefined) item.payload.images = images?.length ? images : undefined; } else { item.payload.text = mutation.text; const text = { type: "text" as const, text: mutation.text }; if (mutation.images !== undefined) { item.payload.images = images?.length ? images : undefined; item.payload.content = [text, ...(images?.map((image) => ({ ...image })) ?? [])]; } else if (item.payload.content) { item.payload.content = [text, ...item.payload.content.filter((block) => block.type !== "text")]; } item.payload.preview = undefined; item.payload.prepared = undefined; for (const record of item.payload.records) { if (record.role === "primary" && record.message.role === "user") { record.message.content = item.payload.content?.map((block) => ({ ...block })) ?? mutation.text; } } } const targetPolicy = queuedMessageLaneDeliveryPolicy(mutation.lane); if (targetPolicy !== policy) { item.queueKey = undefined; item.wake = mutation.lane === "steering" ? "on_lower_boundary" : "external_resume"; this._actionStore.moveQueued(item, targetPolicy, this._actionStore.queuedActions(targetPolicy).length); } this.resumeQueuedWork(); this._emitQueueUpdate(); return "applied"; }
get queuedActionCount(): number { return visibleSessionActionProjection(this._actionStore.queuedActions()).length; }
get unfinishedActionCount(): number { return this._actionStore.unfinishedActions().length; }
get isQueuedWorkSuspended(): boolean { return this._sessionInputPumpSuspended; }
get isSessionActive(): boolean { return ( this._ipythonKernelProvisioner?.manager?.hasBackgroundWork === true || this.isStreaming || this.isCompacting || this.isRetrying || this.isBashRunning || this._refineInFlight !== undefined || this._branchSummaryOperation !== undefined || this._postCompactionContinuationSettlement !== undefined || this.unfinishedActionCount > 0 ); }
getSessionActionSnapshot(): SessionActionSnapshot { const steering = visibleSessionActionProjection(this._actionStore.queuedActions("next_turn_boundary")).map( queuedAgentMessagePreview, ); const followUps = visibleSessionActionProjection(this._actionStore.queuedActions("when_run_idle")).map( queuedAgentMessagePreview, ); const active = visibleSessionActionProjection(this._actionStore.activeActions())[0]; const activeState = active?.lifecycle.state; const phase = activeState === "selected" ? "preparing" : activeState === "preparing" || activeState === "committing" || activeState === "running" ? activeState : undefined; return { queuedCount: steering.length + followUps.length, steering, followUps, ...(active && phase ? { active: { kind: active.payload.kind, phase, label: compactRlmText(active.payload.text), }, } : {}), }; }
getSteeringMessages(): readonly string[] { return visibleSessionActionProjection(this._actionStore.queuedActions("next_turn_boundary")).map( (action) => action.payload.text, ); }
getSteeringMessagePreviews(): readonly string[] { return visibleSessionActionProjection(this._actionStore.queuedActions("next_turn_boundary")).map( queuedAgentMessagePreview, ); }
getFollowUpMessages(): readonly string[] { return visibleSessionActionProjection(this._actionStore.queuedActions("when_run_idle")).map( (action) => action.payload.text, ); }
getFollowUpMessagePreviews(): readonly string[] { return visibleSessionActionProjection(this._actionStore.queuedActions("when_run_idle")).map( queuedAgentMessagePreview, ); }
getSessionActionRecoverySnapshot(): SessionActionRecoverySnapshot { return { formatVersion: SESSION_ACTION_RECOVERY_FORMAT_VERSION, actions: this._actionStore.snapshotActions().map((action) => ({ id: action.id, source: action.source, delivery: action.delivery, priority: action.priority, wake: action.wake, ...(action.queueKey ? { queueKey: action.queueKey } : {}), ...(action.agentMessageId ? { agentMessageId: action.agentMessageId } : {}), ...(action.suppressAutonomousContinuation ? { suppressAutonomousContinuation: true } : {}), payload: action.payload.kind === "turn" ? { kind: "turn", text: action.payload.text, ...(action.payload.preview ? { preview: action.payload.preview } : {}), records: action.payload.records.map((record) => ({ id: record.id, role: record.role, message: cloneQueuedAgentMessage(record.message), ownerActionId: record.ownerActionId, })), ...(action.payload.images ? { images: action.payload.images.map((image) => ({ ...image, })), } : {}), ...(action.payload.content ? { content: action.payload.content.map((block) => ({ ...block, })), } : {}), ...(action.payload.customMessage ? { customMessage: cloneCustomMessage(action.payload.customMessage), } : {}), executionPolicy: { ...action.payload.executionPolicy, preparation: { ...action.payload.executionPolicy.preparation, }, }, queueVisible: action.payload.queueVisible, acceptedAgentMessage: action.payload.acceptedAgentMessage, acceptedBeforeCompletion: action.payload.acceptedBeforeCompletion, } : { kind: "session_command", text: action.payload.text, command: { ...action.payload.command }, ...(action.payload.images ? { images: action.payload.images.map((image) => ({ ...image, })), } : {}), }, })), }; }
private _notifySessionInputCheckpointChange(): void { const waiters = [...this._sessionInputCheckpointWaiters]; this._sessionInputCheckpointWaiters.clear(); for (const resolve of waiters) resolve(); }
private _waitForSessionActivityChange(signal: AbortSignal): Promise<void> { return new Promise<void>((resolve) => { const finish = () => { this._sessionInputCheckpointWaiters.delete(finish); signal.removeEventListener("abort", finish); resolve(); }; this._sessionInputCheckpointWaiters.add(finish); signal.addEventListener("abort", finish, { once: true }); if (signal.aborted) finish(); }); }
private _observeSessionActionDeferral(action: QueuedSessionAction): { deferred: Promise<void>; stop(): void; } { let resolveDeferral = () => {}; const deferred = new Promise<void>((resolve) => { resolveDeferral = resolve; }); const check = () => { if (action.lifecycle.state === "queued") resolveDeferral(); else this._sessionInputCheckpointWaiters.add(check); }; this._sessionInputCheckpointWaiters.add(check); return { deferred, stop: () => this._sessionInputCheckpointWaiters.delete(check), }; }
async waitForSessionInputCheckpoint(signal?: AbortSignal): Promise<void> { const blocksCheckpoint = () => this._actionStore.activeActions().some((action) => { if (action.payload.kind === "session_command") { return action.lifecycle.state === "selected" || action.lifecycle.state === "running"; } return ( action.lifecycle.state === "selected" || action.lifecycle.state === "preparing" || (action.lifecycle.state === "committing" && !primaryDeliveryRecord(action).durable) ); }); while (true) { while (blocksCheckpoint()) { if (signal?.aborted) throw new Error("Update restart preparation cancelled"); await new Promise<void>((resolve, reject) => { const onChange = () => { cleanup(); resolve(); }; const onAbort = () => { cleanup(); reject(new Error("Update restart preparation cancelled")); }; const cleanup = () => { this._sessionInputCheckpointWaiters.delete(onChange); signal?.removeEventListener("abort", onAbort); }; this._sessionInputCheckpointWaiters.add(onChange); signal?.addEventListener("abort", onAbort, { once: true }); if (signal?.aborted) onAbort(); }); } const commitFence = await this._acquireSessionActionCommitFence(signal); try { if (blocksCheckpoint()) continue; if (signal?.aborted) throw new Error("Update restart preparation cancelled"); await waitForPromiseOrAbort(this._agentEventQueue, signal, "Update restart preparation cancelled"); if (signal?.aborted) throw new Error("Update restart preparation cancelled"); this.sessionManager.flushNow(); return; } finally { commitFence.release(); } } }
acquireSessionInputPause(): { release(): void } { const token = Symbol("session-input-admission-pause"); this._sessionInputAdmissionPauses.add(token); this._sessionInputPumpRequested = false; this._sessionInputPumpEpoch++; let released = false; return { release: () => { if (released) return; released = true; this._sessionInputAdmissionPauses.delete(token); this._sessionInputPumpEpoch++; this._notifySessionInputCheckpointChange(); this._flushDeferredCompletionNotices(); this._maybeResumeGoalContinuationAfterRlmWork(); this._maybeResumeAutonomousContinuationAfterRlmWork(); this._scheduleSessionInputPump(); }, }; }
acquireQueuedWorkPause(): { release(): void } { const token = Symbol("queued-work-pause"); this._queuedWorkPauses.add(token); this._sessionInputPumpRequested = false; this._sessionInputPumpEpoch++; let released = false; return { release: () => { if (released) return; released = true; this._queuedWorkPauses.delete(token); this._notifySessionInputCheckpointChange(); this._flushDeferredCompletionNotices(); this._scheduleSessionInputPump(); }, }; }
private async _acquireDirectTurnAdmissionFence(signal?: AbortSignal): Promise<{ owner: symbol; release(): void }> { const inheritedOwner = this._sessionActionCommitContext.getStore(); if (inheritedOwner !== undefined && inheritedOwner === this._sessionActionCommitOwner) { this._assertSessionActionAdmissionAvailable(); return this._acquireSessionActionCommitFence(signal); } const disposeSignal = this._sessionActionCommitDisposeAbortController.signal; const waitSignal = signal ? AbortSignal.any([signal, disposeSignal]) : disposeSignal; while (true) { this._assertSessionActionAdmissionAvailable(); if (this._queuedWorkPauses.size > 0) { let wake = () => {}; const pauseReleased = new Promise<void>((resolve) => { wake = resolve; this._sessionInputCheckpointWaiters.add(resolve); }); try { await waitForPromiseOrAbort(pauseReleased, waitSignal, "Update restart preparation cancelled"); } catch (error) { if (disposeSignal.aborted) { throw new Error("Cannot admit a session action because the session is disposing or disposed."); } throw error; } finally { this._sessionInputCheckpointWaiters.delete(wake); } continue; } const fence = await this._acquireSessionActionCommitFence(signal); try { if (this._queuedWorkPauses.size === 0) { this._assertSessionActionAdmissionAvailable(); return fence; } } catch (error) { fence.release(); throw error; } fence.release(); } }
private async _acquireSessionActionCommitFence(signal?: AbortSignal): Promise<{ owner: symbol; release(): void }> { const inheritedOwner = this._sessionActionCommitContext.getStore(); if (inheritedOwner !== undefined && inheritedOwner === this._sessionActionCommitOwner) { return { owner: inheritedOwner, release: () => {} }; } const previous = this._sessionActionCommitTail; let resolve = () => {}; this._sessionActionCommitTail = new Promise<void>((release) => { resolve = release; }); const disposeSignal = this._sessionActionCommitDisposeAbortController.signal; const waitSignal = signal ? AbortSignal.any([signal, disposeSignal]) : disposeSignal; this._pendingSessionActionFenceWaiters++; try { await waitForPromiseOrAbort(previous, waitSignal, "Update restart preparation cancelled"); } catch (error) { this._pendingSessionActionFenceWaiters--; // A cancelled waiter remains in the FIFO chain until its predecessor releases. void previous.then(resolve, resolve); if (disposeSignal.aborted) { throw new Error("Cannot admit a session action because the session is disposing or disposed."); } throw error; } const owner = Symbol("session-action-commit"); this._sessionActionCommitOwner = owner; this._pendingSessionActionFenceWaiters--; let released = false; return { owner, release: () => { if (released) return; released = true; if (this._sessionActionCommitOwner === owner) this._sessionActionCommitOwner = undefined; resolve(); }, }; }
private _resumeSessionInputAdmission(): void { if (!this._sessionInputPumpSuspended) return; this._sessionInputPumpSuspended = false; this._sessionInputSuspendedForUpdateRestart = false; this._sessionInputPumpEpoch++; this._notifySessionInputCheckpointChange(); this._flushDeferredCompletionNotices(); }
/** Resume the scheduler after requestAbort/abortForUpdateRestart suspended it; owned pause leases are unaffected. */ resumeQueuedWork(): boolean { this._resumeSessionInputAdmission(); this._maybeResumeGoalContinuationAfterRlmWork(); this._maybeResumeAutonomousContinuationAfterRlmWork(); this._scheduleSessionInputPump(); return this._hasSelectableSessionInput(); }
async waitForSessionInputIdle(): Promise<void> { while (true) { const pump = this._sessionInputPump; await pump; if (pump === this._sessionInputPump && !this._sessionInputPumpRequested) return; } }
async waitForIdle(): Promise<void> { await this._waitForIdleOrSettlement(); }
/** * {@link waitForIdle} loop; with a settlement, returns once that settlement is * superseded so a cancelled post-compaction runner cannot keep a checkpoint * waiter registered (a leaked waiter holds hasPendingAdmissionWaiters true and * blocks daemon passivation). */ private async _waitForIdleOrSettlement(settlement?: PostCompactionContinuationSettlement): Promise<void> { while (settlement === undefined || this._postCompactionContinuationSettlement === settlement) { if (this._actionStore.queuedActions().length > 0) { // Park while the pump would refuse scheduling or selection: rescheduling // a blocked pump completes on already-resolved promises, so looping here // would spin on the microtask queue and starve the IO that ends the busy state. if (this._isBusyForSessionInput("pump")) { let wake = () => {}; const changed = new Promise<void>((resolve) => { wake = resolve; this._sessionInputCheckpointWaiters.add(resolve); }); try { await (settlement ? Promise.race([changed, settlement.promise]) : changed); } finally { this._sessionInputCheckpointWaiters.delete(wake); } continue; } this._scheduleSessionInputPump(); } const pump = this._sessionInputPump; await pump; await this.agent.waitForIdle(); const agentEventQueue = this._agentEventQueue; await agentEventQueue; if ( pump === this._sessionInputPump && agentEventQueue === this._agentEventQueue && !this._sessionInputPumpRequested && !this.agent.state.isStreaming && this.unfinishedActionCount === 0 ) { return; } } }
/** Waits out any owned post-compaction continuation and rejects when one cannot start; {@link waitForIdle} never rejects. */ async waitForHeadlessIdle(): Promise<void> { while (true) { await this.waitForIdle(); const postCompactionContinuation = this._postCompactionContinuationSettlement?.promise; if (!postCompactionContinuation) return; await postCompactionContinuation; } }
getPendingNextTurnMessageSnapshots(): readonly CustomMessage[] { const messages = this._pendingNextTurnMessages.map((message) => cloneCustomMessage(message)); for (const action of this._actionStore.unfinishedActions()) { if ( action.payload.kind !== "turn" || !action.payload.acceptedAgentMessage || !primaryDeliveryRecord(action).started ) { continue; } messages.push( ...action.payload.records .filter( (record): record is DeliveryRecord & { message: CustomMessage } => (record.role === "next_turn" || record.role === "prefix") && record.message.role === "custom" && !record.durable, ) .map((record) => cloneCustomMessage(record.message)), ); } return messages; }
restorePendingNextTurnMessages(messages: readonly CustomMessage[]): void { this._pendingNextTurnMessages.push(...messages.map((message) => cloneCustomMessage(message))); this._flushDeferredCompletionNotices(); }
removeQueuedFollowUp(queueKey: string): boolean { const matching = this._actionStore .clearableActions() .filter((action) => action.payload.kind === "turn" && action.queueKey === queueKey); if (matching.length === 0) return false; const error = new Error("Queued agent message was cleared before delivery."); for (const action of matching) this._rejectAgentMessage(action.agentMessageId, error); const ids = new Set(matching.map((action) => action.id)); this._cancelSessionActions((action) => ids.has(action.id), error); this._emitQueueUpdate(); return true; }
get resourceLoader(): ResourceLoader { return this._resourceLoader; }
requestAbort(): void { for (const run of [...this._unsettledRlmChildRuns]) { if (run.status === "cancelled") this._abandonRlmRunForQuiescence(run); } for (const controller of this._rlmQuiescenceWaitAborts) controller.abort(); this._sessionInputPumpRequested = false; this._sessionInputPumpEpoch++; this._sessionInputPumpSuspended = true; this._sessionInputSuspendedForUpdateRestart = false; this._demoteDeferredCompletionActions(); this._cancelSessionActions( (action) => action.payload.kind === "turn" && !action.payload.queueVisible && !this._durableRlmTerminalNoticeActionIds.has(action.id), new Error("Prompt aborted before delivery."), ); this._cancelPostCompactionContinue(); this.abortRetry(); this.abortCompaction(); this.abortBranchSummary(); this.abortBash(); this._pendingRequestedRefine = undefined; this._autoRefineBranchVersion++; this._autoRefineReviewAbort?.abort(); this._refineAbortController?.abort(); this.agent.abort(); }
async abort(): Promise<void> { const compactionOperation = this._compactionOperation; const branchSummaryOperation = this._branchSummaryOperation; this.requestAbort(); this._cancelActiveRlmChildRuns("Parent session aborted"); this._goalAbortInProgress = this._goalState.status === "active"; try { await Promise.allSettled([ this.agent.waitForIdle(), this._agentEventQueue, ...(compactionOperation ? [compactionOperation] : []), ...(branchSummaryOperation ? [branchSummaryOperation] : []), ]); } finally { this._goalAbortInProgress = false; } }
abortForUpdateRestart(): void { // Cancel scheduled pumps and suspend new ones: queued inputs must survive // into the restart manifest instead of starting a turn during teardown. this._sessionInputPumpRequested = false; this._sessionInputPumpEpoch++; this._sessionInputPumpSuspended = true; this._sessionInputSuspendedForUpdateRestart = true; this._cancelPostCompactionContinue(); this.abortRetry(); for (const controller of this._rlmQuiescenceWaitAborts) controller.abort(); this._cancelActiveRlmChildRuns("Parent session aborted for update restart"); this._goalAbortInProgress = this._goalState.status === "active"; this.agent.abort(); if (this._goalAbortInProgress) { void this.agent .waitForIdle() .then(() => this._agentEventQueue) .catch(() => undefined) .finally(() => { this._goalAbortInProgress = false; }); } }
private async _emitModelSelect( nextModel: Model<any>, previousModel: Model<any> | undefined, source: "set" | "cycle" | "restore", ): Promise<void> { if (modelsAreEqual(previousModel, nextModel)) return; await this._extensionRunner.emit({ type: "model_select", model: nextModel, previousModel, source, }); }
private _queueModelSelectEmit( nextModel: Model<any>, previousModel: Model<any> | undefined, source: "set" | "cycle" | "restore", ): Promise<void> { const emit = () => this._modelSelectEmitContext.run(true, () => this._emitModelSelect(nextModel, previousModel, source)); this._modelSelectEmitQueueIdle = false; const promise = this._modelSelectEmitQueue.then(emit, emit); const queued = promise.catch(() => {}); this._modelSelectEmitQueue = queued; void queued.finally(() => { if (this._modelSelectEmitQueue === queued) { this._modelSelectEmitQueueIdle = true; } }); return promise; }
async setModel(model: Model<any>, options: ModelSelectOptions = {}): Promise<void> { // Explicit selection recovers from a stale-auth lockout, but only a fully // validated switch commits the clear (single owner): failed selections never unlock. const staleOnly = !this._modelRegistry.hasConfiguredAuth(model) && this._modelRegistry.getProviderAuthStatus(model.provider).source === "stale"; if (!staleOnly && !this._modelRegistry.hasConfiguredAuth(model)) { throw new Error(`No API key for ${model.provider}/${model.id}`); } if (!(await this._modelRegistry.canUseModel(model, { assumeAuthConfigured: staleOnly }))) { throw new Error(`Model "${model.provider}/${model.id}" is not available for the current Prime team.`); } if (staleOnly) { this._modelRegistry.clearProviderAuthStale(model.provider); if (!this._modelRegistry.hasConfiguredAuth(model)) { throw new Error(`No API key for ${model.provider}/${model.id}`); } }
const previousModel = this.model; const thinkingLevel = this._getThinkingLevelForModelSwitch(); const serviceTier = this._getServiceTierForModelSwitch(); this.agent.state.model = model; this.sessionManager.appendModelChange(model.provider, model.id); this.settingsManager.setDefaultModelAndProvider(model.provider, model.id);
this.setThinkingLevel(thinkingLevel); this._clampServiceTierForModel(serviceTier);
const emitPromise = this._queueModelSelectEmit(model, previousModel, "set"); if (this._shouldWaitForModelSelectEmit(options)) { await emitPromise; } else { this._trackModelSelectEmitError(emitPromise); } }
private _trackModelSelectEmitError(emitPromise: Promise<void>): void { void emitPromise.catch((error) => { this._extensionRunner.emitError({ extensionPath: "<internal>", event: "model_select", error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, }); }); }
private _shouldWaitForModelSelectEmit(options: ModelSelectOptions): boolean { return options.waitForExtensions !== false && !this._modelSelectEmitContext.getStore(); }
private _pendingModelSelectEmit(): Promise<void> | undefined { if (!this._modelSelectEmitContext.getStore() && !this._modelSelectEmitQueueIdle) { return this._modelSelectEmitQueue; } return undefined; }
async cycleModel( direction: "forward" | "backward" = "forward", options: ModelSelectOptions = {}, ): Promise<ModelCycleResult | undefined> { if (this._scopedModels.length > 0) { return this._cycleScopedModel(direction, options); } return this._cycleAvailableModel(direction, options); }
private async _cycleScopedModel( direction: "forward" | "backward", options: ModelSelectOptions, ): Promise<ModelCycleResult | undefined> { const availableModels = await this._modelRegistry.refreshAvailableModels(); const scopedModels = this._scopedModels.filter((scoped) => availableModels.some((model) => modelsAreEqual(model, scoped.model)), ); if (scopedModels.length <= 1) return undefined;
const currentModel = this.model; let currentIndex = scopedModels.findIndex((sm) => modelsAreEqual(sm.model, currentModel));
if (currentIndex === -1) currentIndex = 0; const len = scopedModels.length; const nextIndex = direction === "forward" ? (currentIndex + 1) % len : (currentIndex - 1 + len) % len; const next = scopedModels[nextIndex]; const thinkingLevel = this._getThinkingLevelForModelSwitch(next.thinkingLevel); const serviceTier = this._getServiceTierForModelSwitch();
this.agent.state.model = next.model; this.sessionManager.appendModelChange(next.model.provider, next.model.id); this.settingsManager.setDefaultModelAndProvider(next.model.provider, next.model.id);
this.setThinkingLevel(thinkingLevel); this._clampServiceTierForModel(serviceTier);
const emitPromise = this._queueModelSelectEmit(next.model, currentModel, "cycle"); if (this._shouldWaitForModelSelectEmit(options)) { await emitPromise; } else { this._trackModelSelectEmitError(emitPromise); }
return { model: next.model, thinkingLevel: this.thinkingLevel, serviceTier: this.serviceTier, isScoped: true, }; }
private async _cycleAvailableModel( direction: "forward" | "backward", options: ModelSelectOptions, ): Promise<ModelCycleResult | undefined> { const availableModels = await this._modelRegistry.refreshAvailableModels(); if (availableModels.length <= 1) return undefined;
const currentModel = this.model; let currentIndex = availableModels.findIndex((m) => modelsAreEqual(m, currentModel));
if (currentIndex === -1) currentIndex = 0; const len = availableModels.length; const nextIndex = direction === "forward" ? (currentIndex + 1) % len : (currentIndex - 1 + len) % len; const nextModel = availableModels[nextIndex];
const thinkingLevel = this._getThinkingLevelForModelSwitch(); const serviceTier = this._getServiceTierForModelSwitch(); this.agent.state.model = nextModel; this.sessionManager.appendModelChange(nextModel.provider, nextModel.id); this.settingsManager.setDefaultModelAndProvider(nextModel.provider, nextModel.id);
this.setThinkingLevel(thinkingLevel); this._clampServiceTierForModel(serviceTier);
const emitPromise = this._queueModelSelectEmit(nextModel, currentModel, "cycle"); if (this._shouldWaitForModelSelectEmit(options)) { await emitPromise; } else { this._trackModelSelectEmitError(emitPromise); }
return { model: nextModel, thinkingLevel: this.thinkingLevel, serviceTier: this.serviceTier, isScoped: false, }; }
setThinkingLevel(level: ThinkingLevel): void { const availableLevels = this.getAvailableThinkingLevels(); const effectiveLevel = availableLevels.includes(level) ? level : this._clampThinkingLevel(level, availableLevels);
const previousLevel = this.agent.state.thinkingLevel; const isChanging = effectiveLevel !== previousLevel;
this.agent.state.thinkingLevel = effectiveLevel;
if (isChanging) { this.sessionManager.appendThinkingLevelChange(effectiveLevel); if (this.supportsThinking() || effectiveLevel !== "off") { this.settingsManager.setDefaultThinkingLevel(effectiveLevel); } this._emit({ type: "thinking_level_changed", level: effectiveLevel }); void this._extensionRunner.emit({ type: "thinking_level_select", level: effectiveLevel, previousLevel, }); } }
setServiceTier(serviceTier: ServiceTier): void { const effectiveServiceTier = this._getEffectiveServiceTier(serviceTier); const preferenceChanged = effectiveServiceTier !== this._serviceTierPreference; const effectiveTierChanged = effectiveServiceTier !== this.agent.state.serviceTier; if (!preferenceChanged && !effectiveTierChanged) { return; } this._serviceTierPreference = effectiveServiceTier; if (preferenceChanged) { this.sessionManager.appendServiceTierChange(effectiveServiceTier); if (this.model && supportsFastMode(this.model)) { this.settingsManager.setDefaultServiceTier(effectiveServiceTier); } } if (effectiveTierChanged) { this.agent.state.serviceTier = effectiveServiceTier; this._emit({ type: "service_tier_changed", serviceTier: effectiveServiceTier, }); } }
private _getEffectiveServiceTier(serviceTier: ServiceTier): ServiceTier { return serviceTier === "priority" && (!this.model || !supportsFastMode(this.model)) ? "default" : serviceTier; }
private _getServiceTierForModelSwitch(): ServiceTier { return this._serviceTierPreference; }
private _clampServiceTierForModel(serviceTier: ServiceTier = this.serviceTier): void { const effectiveServiceTier = this._getEffectiveServiceTier(serviceTier); if (effectiveServiceTier === this.agent.state.serviceTier) { return; } this.agent.state.serviceTier = effectiveServiceTier; this._emit({ type: "service_tier_changed", serviceTier: effectiveServiceTier, }); }
cycleThinkingLevel(): ThinkingLevel | undefined { if (!this.supportsThinking()) return undefined;
const levels = this.getAvailableThinkingLevels(); const currentIndex = levels.indexOf(this.thinkingLevel); const nextIndex = (currentIndex + 1) % levels.length; const nextLevel = levels[nextIndex];
this.setThinkingLevel(nextLevel); return nextLevel; }
getAvailableThinkingLevels(): ThinkingLevel[] { if (!this.model) return THINKING_LEVELS; return getSupportedThinkingLevels(this.model) as ThinkingLevel[]; }
supportsThinking(): boolean { return !!this.model?.reasoning; }
private _getThinkingLevelForModelSwitch(explicitLevel?: ThinkingLevel): ThinkingLevel { if (explicitLevel !== undefined) { return explicitLevel; } if (!this.supportsThinking()) { return this.settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL; } return this.thinkingLevel; }
private _clampThinkingLevel(level: ThinkingLevel, _availableLevels: ThinkingLevel[]): ThinkingLevel { return this.model ? (clampThinkingLevel(this.model, level) as ThinkingLevel) : "off"; }
private async _syncKernelStateAfterCompaction(): Promise<void> { const provisioner = this._ipythonKernelProvisioner; if (!provisioner?.hasRunningKernel) return; const pruned = await provisioner.pruneOversizedVariables().catch(() => null); const abort = new AbortController(); const timer = setTimeout(() => abort.abort(), KERNEL_STATE_LISTING_TIMEOUT_MS); if (typeof timer === "object" && "unref" in timer) timer.unref(); let names: string[] | null; try { names = await provisioner.listNamespaceNames(abort.signal).catch(() => null); } finally { clearTimeout(timer); } if (names === null && !provisioner.hasRunningKernel) return; const detail = names === null ? "" : names.length > 0 ? ` ${names.length} top-level name(s) remain defined in the kernel namespace.` : " You have not defined any names yet."; const prunedDetail = pruned && pruned.length > 0 ? ` Variables above the per-variable snapshot limit were removed: ${pruned.join(", ")}.` : ""; const content = [ "[python-state]", "", `Your Python kernel persisted through compaction; its remaining variables, imports, and helpers are still available.${prunedDetail}${detail}`, ].join("\n"); const message = { role: "custom" as const, customType: "ipython_state", content, display: false, timestamp: Date.now(), } satisfies CustomMessage; const messages = this.agent.state.messages; const last = messages[messages.length - 1]; const insertBeforeError = last?.role === "assistant" && (last as AssistantMessage).stopReason === "error"; if (insertBeforeError) { messages.splice(messages.length - 1, 0, message); } else { messages.push(message); } this.sessionManager.appendCustomMessageEntry(message.customType, message.content, message.display, undefined); this._emit({ type: "message_start", message }); this._emit({ type: "message_end", message }); }
/** * Answer a tool call whose result died with the previous process. A transcript can only * carry one when the process ended mid-call, and left alone the model reads a turn that * ends mid-call, guesses why, and re-derives work the notice would have spared it. */ private _repairInterruptedToolCalls(): void { for (const toolCall of unansweredToolCalls(this.agent.state.messages)) { const message = createInterruptedToolResultMessage(toolCall); this.agent.state.messages.push(message); this.sessionManager.appendMessage(message); } }
private _onIpythonStateRestored(result: RestoreResult): void { const lines = ["[python-state-restored]", ""]; if (result.restored.length > 0) { lines.push( `Your Python kernel state was revived from your previous session (${result.restored.length} top-level name(s) restored from ${result.path}).`, ); } else { lines.push( "Your previous Python kernel state could not be revived; the kernel is starting fresh, so re-create any variables, imports, or loaded data you need.", ); } if (result.failed.length > 0) { lines.push( `These could not be restored and must be recreated if needed: ${result.failed.map((f) => f.name).join(", ")}.`, ); } // The kernel is spawned fresh on resume, so a cell's os.chdir is lost while the // variables it was used with survive. Naming the directory keeps relative paths // from silently resolving somewhere else. if (result.cwdMissing) { lines.push( `The previous working directory ${result.cwdMissing} no longer exists; the kernel is in ${result.cwd ?? "its session directory"}, so relative paths resolve elsewhere now.`, ); } else if (result.cwd) { lines.push(`Working directory restored: ${result.cwd}`); } void this.sendCustomMessage( { customType: IPYTHON_STATE_RESTORED_CUSTOM_TYPE, content: lines.join("\n"), display: true, details: { restored: result.restored.length > 0 }, }, { deliverAs: "nextTurn" }, ).catch(() => {}); }
setSteeringMode(mode: "all" | "one-at-a-time"): void { this.agent.steeringMode = mode; this.settingsManager.setSteeringMode(mode); }
setFollowUpMode(mode: "all" | "one-at-a-time"): void { this.agent.followUpMode = mode; this.settingsManager.setFollowUpMode(mode); }
async compact(customInstructions?: string, options: { skipAbort?: boolean } = {}): Promise<CompactionResult> { if (options.skipAbort && this.isStreaming) { throw new Error("Cannot compact without aborting while the agent is running."); } const hadPostCompactionContinue = this._postCompactionContinuationScheduled; const continueAfterSessionInput = this._postCompactionContinuationSettlement?.continueAfterSessionInput ?? false; this._disconnectFromAgent(); if (!options.skipAbort) await this.abort(); let didCompact = false; const compactionAbort = new AbortController(); this._compactionAbortController = compactionAbort; let resolveCompactionOperation: () => void = () => {}; const compactionOperation = new Promise<void>((resolve) => { resolveCompactionOperation = resolve; }); this._compactionOperation = compactionOperation; this._emit({ type: "compaction_start", reason: "manual", customInstructions, });
try { if (!this.model) { throw new Error(formatNoModelSelectedMessage()); }
const { apiKey, headers, requestModel } = await this._getRequiredRequestAuth(this.model); const result = await this._performCompaction({ model: requestModel, apiKey, headers, customInstructions, signal: compactionAbort.signal, });
this._emit({ type: "compaction_end", reason: "manual", result, aborted: false, willRetry: false, customInstructions, }); didCompact = true; // A manual compaction satisfies any pending model request; on failure the // request stays scheduled for the next turn boundary. this._pendingRequestedCompaction = undefined; return result; } catch (error) { const message = error instanceof Error ? error.message : String(error); const aborted = message === "Compaction cancelled" || (error instanceof Error && error.name === "AbortError"); const skipped = error instanceof CompactionSkippedError; this._emit({ type: "compaction_end", reason: "manual", result: undefined, aborted, willRetry: false, errorMessage: aborted ? undefined : skipped ? message : `Compaction failed: ${message}`, errorSeverity: skipped ? "warning" : "error", customInstructions, }); throw error; } finally { this._compactionAbortController = undefined; this._reconnectToAgent(); if (this._compactionOperation === compactionOperation) { this._compactionOperation = undefined; } resolveCompactionOperation(); this._notifySessionInputCheckpointChange(); this._scheduleSessionInputPump(); if (didCompact) { this._discardPendingAutoRefine({ cancelPostCompactionContinue: true }); if (this._goalState.status === "active" && !compactionAbort.signal.aborted) { this._goalContinuationAwaitsRlmWork ||= !this.agent.hasQueuedMessages(); this.resumeQueuedWork(); if (this.agent.hasQueuedMessages()) this._schedulePostCompactionContinue(); } if (hadPostCompactionContinue) { this._schedulePostCompactionContinue(continueAfterSessionInput); } // Queued agent or session-owned inputs resume the loop; defer refine // behind them instead of interleaving it before their turns. this._scheduleAutoRefineAfterCompaction( this._goalContinuationAwaitsRlmWork || hadPostCompactionContinue || this.agent.hasQueuedMessages() || this.unfinishedActionCount > 0, ); } } }
/** * Shared compaction core behind /compact, auto-compaction, and the compact * skill. Throws CompactionSkippedError when there is nothing to compact and * Error("Compaction cancelled") on abort or extension cancel. */ private async _performCompaction(options: { model: Model<any>; apiKey: string; headers?: Record<string, string>; customInstructions?: string; signal: AbortSignal; }): Promise<CompactionResult> { const { model, apiKey, headers, customInstructions, signal } = options; const pathEntries = this.sessionManager.getBranch(); const settings = this._getEffectiveCompactionSettings();
const preparation = prepareCompaction(pathEntries, settings); if (!preparation) { const lastEntry = pathEntries[pathEntries.length - 1]; if (lastEntry?.type === "compaction") { throw new CompactionSkippedError("Already compacted"); } throw new CompactionSkippedError("Session is too short to compact — try again once it grows"); }
let extensionCompaction: CompactionResult | undefined; let fromExtension = false;
const semanticCompaction = this._semanticEdges.beginCompaction(); let compactionRecorded = false; const uncommittedSlices: string[] = []; let compactionSettled = false; let summary: string; let firstKeptEntryId: string; let tokensBefore: number; let details: CompactionResult["details"]; let usage: CompactionResult["usage"]; try { if (this._extensionRunner.hasHandlers("session_before_compact")) { const result = (await this._extensionRunner.emit({ type: "session_before_compact", preparation, branchEntries: pathEntries, customInstructions, signal, })) as SessionBeforeCompactResult | undefined;
if (result?.cancel) { throw new Error("Compaction cancelled"); }
if (result?.compaction) { extensionCompaction = result.compaction; fromExtension = true; } }
if (extensionCompaction) { ({ summary, firstKeptEntryId, tokensBefore, details, usage } = extensionCompaction); } else { // Each summary wire call gets its own request ID: split turns send two // different bodies, and one Idempotency-Key must never cover both. A slice // that succeeds on the wire stays uncommitted until the compaction itself // commits: a racing sibling's failure (or an abort) must leave no committed // summary request for the next turn's continuation edge to attach to. const summaryCall = async <T>( call: (callHeaders: Record<string, string> | undefined) => Promise<T>, ): Promise<T> => { const requestId = this._semanticEdges.startCompactionRequest(semanticCompaction.compactionId); if (requestId === undefined) { return call(headers); } try { const result = await call({ ...headers, ...modelRequestHeaders(requestId) }); // A slice resolving after a sibling's rejection already settled the // compaction would push into a drained list and stay in-flight forever. if (compactionSettled) { this._semanticEdges.failRequest(requestId); } else { uncommittedSlices.push(requestId); } return result; } catch (error) { this._semanticEdges.failRequest(requestId); throw error; } }; ({ summary, firstKeptEntryId, tokensBefore, details, usage } = await compact( preparation, model, apiKey, headers, customInstructions, signal, this.thinkingLevel, summaryCall, providerRetryPolicy(this.settingsManager), this.sessionId, )); }
if (signal.aborted) { throw new Error("Compaction cancelled"); }
// Ledger-before-effect: the compaction outcome is durable before the transcript // commits it. Marked first: the ID is consumed even when the write throws, and a // second finish attempt would mask the original I/O error. compactionRecorded = true; compactionSettled = true; for (const requestId of uncommittedSlices.splice(0)) { this._semanticEdges.finishRequest(requestId); } this._semanticEdges.finishCompaction(semanticCompaction.compactionId, "completed"); // Attached mechanically; the digest never flows through the summarizer LLM. this.sessionManager.appendCompaction( summary, firstKeptEntryId, tokensBefore, details, fromExtension, customInstructions, usage, this._harnessDigest(), ); } catch (error) { compactionSettled = true; for (const requestId of uncommittedSlices.splice(0)) { this._semanticEdges.failRequest(requestId); } if (!compactionRecorded) { const cancelled = error instanceof Error && (error.name === "AbortError" || error.message === "Compaction cancelled"); this._semanticEdges.finishCompaction(semanticCompaction.compactionId, cancelled ? "cancelled" : "failed"); } throw error; } const newEntries = this.sessionManager.getEntries(); this.agent.state.messages = this.sessionManager.buildSessionContext().messages; this._mergeUnpersistedOutcomes(this.agent.state.messages); this._restoreLateIpythonSentAgentMessages(); this._hasCalledRlmModels = false;
const savedCompactionEntry = newEntries.find((e) => e.type === "compaction" && e.summary === summary) as | CompactionEntry | undefined; if (savedCompactionEntry) { await this._extensionRunner.emit({ type: "session_compact", compactionEntry: savedCompactionEntry, fromExtension, }); } await this._syncKernelStateAfterCompaction(); await this._reapDeletedRlmSubagentRuntimesAfterCompaction();
return { summary, firstKeptEntryId, tokensBefore, details }; }
private async _reapDeletedRlmSubagentRuntimesAfterCompaction(): Promise<void> { const childIds = [...this._rlmChildCleanupFailures.keys()].filter( (childId) => !this._activeRlmChildRuns.get(childId)?.detachedDeletion, ); await Promise.allSettled(childIds.map((childId) => this.deleteRlmSubagent(childId))); }
abortCompaction(): void { this._compactionAbortController?.abort(); this._autoCompactionAbortController?.abort(); }
private _localHarnessStateDir(): string | undefined { return ( getLocalHarnessStateDir(this.sessionManager.getSessionArtifactDir()) ?? (this._rlmSessionDir ? getLocalHarnessStateDir(this._rlmSessionDir) : undefined) ); }
private _autoRefineAllowedForSession(): boolean { return this._rlmDepth === 0 && this._localHarnessStateDir() !== undefined; }
private _settlePostCompactionContinue(error?: Error): void { if (!error && this._postCompactionContinuationScheduled) return; const settlement = this._postCompactionContinuationSettlement; if (!settlement || settlement.settled) return; settlement.settled = true; this._postCompactionContinuationSettlement = undefined; if (error) settlement.reject(error); else settlement.resolve(); this._notifySessionInputCheckpointChange(); }
private _cancelPostCompactionContinue(): void { this._postCompactionContinuationScheduled = false; this._scheduledPostCompactionContinuationMessages = []; this._settlePostCompactionContinue(); }
private _discardPendingAutoRefine(options: { cancelPostCompactionContinue?: boolean } = {}): void { this._compactAutoRefinePending = false; this._turnIntervalAutoRefinePending = false; this._pendingAutoRefineReview = undefined; if (options.cancelPostCompactionContinue) { this._cancelPostCompactionContinue(); } }
private async _invalidatePendingAutoRefineForBranchChange(): Promise<void> { this._autoRefineReviewAbort?.abort(); this._discardPendingAutoRefine({ cancelPostCompactionContinue: true }); this._assistantTurnsSinceAutoRefine = 0; // Increment branch version BEFORE aborting/awaiting the serialized plan. // This invalidates the plan's branchVersion check at the boundary // so even if the plan completes, the boundary will reject it // (bgResult.branchVersion !== this._autoRefineBranchVersion). this._autoRefineBranchVersion++; // Abort the in-flight refine/bplan controller so any pending // _planRefine or _reviewAutoRefine call settles via signal abort // rather than hanging forever. this._refineAbortController?.abort(); if (this._serializedPlanInFlight) { await this._consumeSerializedBackgroundPlan(async () => false); } while (this._refinePlanInFlight) { await this._refinePlanInFlight; } await this._waitForRefineIdle(); }
/** * Consume a refine request that was scheduled by the agent-callable refine * skill (refine.run). Fire-and-forget: the refine() method handles its own * background planning, idle wait, application, and error recovery. Called * at the turn boundary after compaction checks and before auto-refine * scheduling so the manual request takes priority. */ private _emitRefineFailed(error: unknown): void { this._emit({ type: "refine_failed", error: error instanceof Error ? error.message : String(error), }); }
private _consumePendingRequestedRefine(): boolean { const pending = this._pendingRequestedRefine; if (!pending) return false; this._pendingRequestedRefine = undefined; void this.refine(pending, { source: "self" }).catch((error) => this._emitRefineFailed(error)); return true; }
private _scheduleAutoRefineAfterAgentEnd(): void { if (!this._autoRefineAllowedForSession()) { return; } if (this._pendingAutoRefineReview) { this._scheduleAutoRefine(this._pendingAutoRefineReview.reason); return; } if (this._compactAutoRefinePending) { if (this._postCompactionContinuationScheduled) { return; } this._scheduleAutoRefine("compact"); return; }
this._scheduleAutoRefine("turn_interval"); }
private _scheduleAutoRefineAfterCompaction(willContinueAfterCompaction: boolean): void { if (!this._autoRefineAllowedForSession()) { return; } if (this._serializedRefine) { // Serialized sessions must service compaction-triggered refinement at // shouldStopAfterTurn (or disposal), never through the interactive path. this._compactAutoRefinePending = true; return; } if (willContinueAfterCompaction) { this._compactAutoRefinePending = true; return; }
this._scheduleAutoRefine("compact"); }
private _schedulePostCompactionContinue(continueAfterSessionInput = false): void { if (!this._postCompactionContinuationSettlement || this._postCompactionContinuationSettlement.settled) { this._postCompactionContinuationSettlement = createPostCompactionContinuationSettlement(); } const settlement = this._postCompactionContinuationSettlement; settlement.continueAfterSessionInput ||= continueAfterSessionInput; if (this._postCompactionContinuationScheduled) { return; } this._postCompactionContinuationScheduled = true; this._scheduledPostCompactionContinuationMessages = [...this._postCompactionContinuationMessages]; void this._runScheduledPostCompactionContinue(settlement) .catch(() => undefined) .finally(() => { if (this._postCompactionContinuationSettlement === settlement) { this._settlePostCompactionContinue(); } }); }
private _sessionOwnsScheduledContinuations(continuationMessages: AgentMessage[]): boolean { return continuationMessages.some((message) => this._postCompactionContinuationMessages.includes(message)); }
private async _waitForQueuedWorkResume(settlement: PostCompactionContinuationSettlement): Promise<void> { while (this._queuedWorkPauses.size > 0 && this._postCompactionContinuationSettlement === settlement) { let resume = () => {}; const resumed = new Promise<void>((resolve) => { resume = resolve; this._sessionInputCheckpointWaiters.add(resolve); }); try { await Promise.race([resumed, settlement.promise]); } finally { this._sessionInputCheckpointWaiters.delete(resume); } } }
private async _runScheduledPostCompactionContinue(settlement: PostCompactionContinuationSettlement): Promise<void> { while (this._postCompactionContinuationScheduled && this._postCompactionContinuationSettlement === settlement) { await this.agent.waitForIdle(); await this.waitForRetry(); await this._waitForRefineIdle(); await this._waitForQueuedWorkResume(settlement); const compactionOperation = this._compactionOperation; if (compactionOperation) { await Promise.race([compactionOperation, settlement.promise]); continue; }
const commitFence = await this._acquireSessionActionCommitFence(); let continuation: Promise<void> | undefined; let continuationMessages: AgentMessage[] = []; let waitForSessionInput = false; try { await this.agent.waitForIdle(); if ( !this._postCompactionContinuationScheduled || this._postCompactionContinuationSettlement !== settlement ) { return; }
if (this._queuedWorkPauses.size > 0 || this._compactionOperation || this._refineInFlight) { continue; }
continuationMessages = [...this._scheduledPostCompactionContinuationMessages]; if (continuationMessages.length > 0 && !this._sessionOwnsScheduledContinuations(continuationMessages)) { this._cancelPostCompactionContinue(); this._scheduleAutoRefineAfterAgentEnd(); return; } if (this.unfinishedActionCount > 0 || this._sessionInputPumpRequested) { this._scheduleSessionInputPump(); waitForSessionInput = true; } else { this._postCompactionContinuationScheduled = false; continuation = this.agent.continue(); } } finally { commitFence.release(); }
if (waitForSessionInput) { await this._waitForIdleOrSettlement(settlement); if (this._postCompactionContinuationSettlement !== settlement) return; const shouldContinue = (settlement.continueAfterSessionInput && continuationMessages.length === 0) || this._sessionOwnsScheduledContinuations(continuationMessages); if (shouldContinue) { this._scheduledPostCompactionContinuationMessages = [...this._postCompactionContinuationMessages]; continue; } this._postCompactionContinuationScheduled = false; this._scheduledPostCompactionContinuationMessages = []; this._scheduleAutoRefineAfterAgentEnd(); return; }
try { await continuation; if (this._postCompactionContinuationSettlement === settlement) { this._forgetConsumedPostCompactionContinuations(continuationMessages); } return; } catch (error) { const code = error instanceof AgentContinueError ? error.code : undefined; if (code === "busy") { if (this._postCompactionContinuationSettlement === settlement) { this._postCompactionContinuationScheduled = true; this._scheduledPostCompactionContinuationMessages = [...this._postCompactionContinuationMessages]; } continue; } if (code !== "nothing-to-continue" && this._postCompactionContinuationSettlement === settlement) { this._settlePostCompactionContinue(this._asError(error)); } return; } } }
private _forgetConsumedPostCompactionContinuations(continuationMessages: AgentMessage[]): void { if (continuationMessages.length === 0) { return; } const continuationMessageSet = new Set(continuationMessages); const stillQueued = new Set(this.agent.removeQueuedMessages((message) => continuationMessageSet.has(message))); for (const message of stillQueued) { this.agent.followUp(message); } for (const message of continuationMessages) { if (!stillQueued.has(message)) { this._queuedAutonomousContinuationSnapshots.delete(message); } } this._postCompactionContinuationMessages = this._postCompactionContinuationMessages.filter( (message) => !continuationMessageSet.has(message) || stillQueued.has(message), ); }
private _shouldSkipAutoRefineForActiveAgent(): boolean { return this.isStreaming || this.isCompacting; }
private _scheduleDeferredAutoRefineIfIdle(): void { if (this._autoRefineInProgress || this._shouldSkipAutoRefineForActiveAgent() || this._pendingAutoRefineReview) { return; } if (this._turnIntervalAutoRefinePending) { this._turnIntervalAutoRefinePending = false; this._scheduleAutoRefine("turn_interval"); } }
private _scheduleAutoRefine(reason: AutoRefineReason, branchVersion = this._autoRefineBranchVersion): void { const timer = setTimeout(() => { this._scheduledAutoRefineTimers.delete(timer); if (branchVersion !== this._autoRefineBranchVersion) { return; } const operation = this._maybeAutoRefine(reason); this._autoRefineOperations.add(operation); void operation.finally(() => this._autoRefineOperations.delete(operation)).catch(() => undefined); }, 0); this._scheduledAutoRefineTimers.add(timer); }
private async _maybeAutoRefine(reason: AutoRefineReason): Promise<void> { if (this._disposed || this._disposing) { this._discardPendingAutoRefine(); return; } if (!this._autoRefineAllowedForSession()) { this._discardPendingAutoRefine(); return; }
const settings = this.settingsManager.getAutoRefineSettings(); if (!settings.enabled) { this._discardPendingAutoRefine(); return; } if (this._autoRefineInProgress || this._shouldSkipAutoRefineForActiveAgent()) { if (reason === "compact") { this._compactAutoRefinePending = true; } else { this._turnIntervalAutoRefinePending = true; } return; }
const nowMs = Date.now(); const underCooldown = this._lastAutoRefineReviewAt > 0 && nowMs - this._lastAutoRefineReviewAt < settings.cooldownMs;
const pendingReview = this._pendingAutoRefineReview; if (pendingReview) { // A failed refine stamps the cooldown; keep the pending review for later. if (underCooldown) { return; } await this._runApprovedRefine(pendingReview.reason, pendingReview.review); return; }
if (reason === "compact" && !settings.compact) { this._compactAutoRefinePending = false; reason = "turn_interval"; } if (reason === "turn_interval" && this._assistantTurnsSinceAutoRefine < settings.turnInterval) { return; } if (underCooldown) { if (reason === "compact") { this._compactAutoRefinePending = true; } else { this._turnIntervalAutoRefinePending = true; } return; } if (reason === "turn_interval") { this._turnIntervalAutoRefinePending = false; } if (!this.model) { if (reason === "compact") { this._compactAutoRefinePending = true; } return; } this._autoRefineInProgress = true; const turnsSinceLastReview = this._assistantTurnsSinceAutoRefine; const branchVersion = this._autoRefineBranchVersion; const reviewAbort = new AbortController(); this._autoRefineReviewAbort = reviewAbort; let approvedReview: AutoRefineReview | undefined; try { const review = await this._reviewAutoRefine({ reason, turnsSinceLastReview }, reviewAbort.signal); if (this._disposed || this._disposing || branchVersion !== this._autoRefineBranchVersion) { return; } if (!review.shouldRefine) { const preserveTurnIntervalReview = reason === "compact" && this._assistantTurnsSinceAutoRefine >= settings.turnInterval; if (preserveTurnIntervalReview) { this._turnIntervalAutoRefinePending = true; } else { this._lastAutoRefineReviewAt = nowMs; this._assistantTurnsSinceAutoRefine = 0; } if (reason === "compact") { this._compactAutoRefinePending = false; } return; } if (this._shouldSkipAutoRefineForActiveAgent()) { this._pendingAutoRefineReview = { reason, review }; return; } approvedReview = review; } catch { // Failed review: stamp the cooldown so a persistent failure (bad auth, // unparseable output) doesn't retry a full review on every agent end. if (branchVersion === this._autoRefineBranchVersion) { this._lastAutoRefineReviewAt = Date.now(); } } finally { if (this._autoRefineReviewAbort === reviewAbort) { this._autoRefineReviewAbort = undefined; } this._autoRefineInProgress = false; // When a refine follows, _runApprovedRefine schedules the deferred pass. if (!approvedReview) { this._scheduleDeferredAutoRefineIfIdle(); } } if (approvedReview) { await this._runApprovedRefine(reason, approvedReview); } }
private async _runApprovedRefine(reason: AutoRefineReason, review: AutoRefineReview): Promise<void> { this._autoRefineInProgress = true; try { await this.refine({ instructions: autoRefineInstructions(reason, review) }, { trigger: "auto" }); this._pendingAutoRefineReview = undefined; this._turnIntervalAutoRefinePending = false; this._lastAutoRefineReviewAt = Date.now(); this._assistantTurnsSinceAutoRefine = 0; if (reason === "compact") { this._compactAutoRefinePending = false; } } catch (error) { // Auto-refine is opportunistic; manual /refine remains available. // Stamp the cooldown so a persistently failing refine doesn't retry // (via a retained pending review) on every agent end. this._lastAutoRefineReviewAt = Date.now(); if (error instanceof RefineSkippedError) { // A skipped round is consumed like a reviewer decline, not retained for retry. this._pendingAutoRefineReview = undefined; this._turnIntervalAutoRefinePending = false; this._assistantTurnsSinceAutoRefine = 0; if (reason === "compact") this._compactAutoRefinePending = false; } } finally { this._autoRefineInProgress = false; this._scheduleDeferredAutoRefineIfIdle(); } }
/** * Refinement passes (review and planning) run with their own prompts, so * issuing them on the session model evicts the provider's prefix-cache entry * for the session and forces a full context re-read on the next session * request. Route them to the configured auxiliary model when it is set and * usable; fall back to the session model otherwise. */ private async _resolveRefinementModel(): Promise< { model: Model<Api>; apiKey: string; headers?: Record<string, string> } | undefined > { const sessionModel = this.model; if (!sessionModel) { return undefined; } const selector = this.settingsManager.getAuxiliaryModel()?.trim().toLowerCase(); if (!selector || `${sessionModel.provider}/${sessionModel.id}`.toLowerCase() === selector) { const { apiKey, headers, requestModel } = await this._getRequiredRequestAuth(sessionModel); return { model: requestModel, apiKey, headers }; } try { const model = (await this._authenticatedRlmModels()).find( (candidate) => `${candidate.provider}/${candidate.id}`.toLowerCase() === selector, ); if (!model) { throw new Error(`model "${selector}" is unavailable, unauthenticated, or expired`); } const { apiKey, headers, requestModel } = await this._getRequiredRequestAuth(model); return { model: requestModel, apiKey, headers }; } catch { // Error details from the auth stack can embed credential material, so only // the selector is logged (CodeQL js/clear-text-logging). console.warn(`Warning: auxiliaryModel "${selector}" unusable for refinement; using the session model.`); const { apiKey, headers, requestModel } = await this._getRequiredRequestAuth(sessionModel); return { model: requestModel, apiKey, headers }; } }
private async _reviewAutoRefine(context: AutoRefineReviewRequest, signal?: AbortSignal): Promise<AutoRefineReview> { if (this._autoRefineReviewer) { return this._autoRefineReviewer(context, signal); } const refinementModel = await this._resolveRefinementModel(); if (!refinementModel) { return { shouldRefine: false, rationale: "No model selected." }; } return reviewAutoRefine( this.agent.state.messages, this._loadMergedHarnessState(), this._loadRefinementHistory(), refinementModel.model, refinementModel.apiKey, context, refinementModel.headers, signal, this.thinkingLevel, providerRetryPolicy(this.settingsManager), this.sessionId, ); }
/** The compact harness digest delivered at cold context boundaries (session start, resume, compaction head). */ private _harnessDigest(): string { const tools = this.getActiveToolNames(); const hasIpython = tools.includes("ipython"); const visibleSkills = this._modelVisibleSkills().filter((skill) => !skill.disableModelInvocation); const hasRefineSkill = visibleSkills.some((skill) => skill.name === REFINE_SKILL_NAME); return formatHarnessStateForPrompt(this._loadMergedHarnessState(), { includeIpythonExamples: hasIpython, includeShellExamples: tools.includes("bash"), includeRefineExamples: hasIpython && hasRefineSkill, queryTerms: this._buildHarnessDigestQueryTerms(), }); }
/** * Relevance signal for the harness digest: terms from the active goal * objective (strongest) and the last few user/assistant messages, * newest first. Term overlap is cheap and capped so scoring stays trivial. */ private _buildHarnessDigestQueryTerms(): HarnessQueryTerms { const terms = new Map<string, number>(); const addText = (text: string | undefined, weight: number) => { if (!text) return; for (const raw of harnessQueryTerms(text)) { if (terms.size >= 48 && !terms.has(raw)) return; if (!terms.has(raw)) terms.set(raw, weight); } }; addText(this._goalState.objective, 3); const recent = this.agent.state.messages .filter( (message): message is UserMessage | AssistantMessage => message.role === "user" || message.role === "assistant", ) .slice(-4) .reverse(); let recencyWeight = 2; for (const message of recent) { const text = message.role === "assistant" ? readAssistantText(message) : typeof message.content === "string" ? message.content : message.content .filter((block): block is TextContent => block.type === "text") .map((block) => block.text) .join(" "); addText(text, recencyWeight); recencyWeight = Math.max(1, recencyWeight - 0.5); } return terms; }
/** Cold-boundary digest delivery: empty contexts defer to the first committed turn (untouched sessions must stay empty); non-empty contexts append only when the newest in-context digest mismatches disk. */ private _ensureHarnessDigestContext(): void { if (this.agent.state.messages.length === 0) { this._harnessDigestPending = true; return; } this._harnessDigestPending = false; this._appendHarnessDigestIfStale(); }
private _appendHarnessDigestIfStale(): void { const digest = this._harnessDigest(); if (this._latestContextHarnessDigest() === digest) return; const message = createHarnessDigestMessage(digest); try { this.sessionManager.appendCustomMessageEntryWithRollback( message.customType, message.content, message.display, message.details, ); } catch { // Unpersisted session: context-only injection. } this.agent.state.messages.push(message); }
private _latestContextHarnessDigest(): string | undefined { // Retained pre-compaction messages follow the compaction head, so recency is by timestamp, not position. let latest: { timestamp: number; digest: string } | undefined; for (const message of this.agent.state.messages) { let digest: string | undefined; if (message.role === "custom" && message.customType === HARNESS_DIGEST_CUSTOM_TYPE) { digest = (message.details as HarnessDigestDetails | undefined)?.digest; } else if (message.role === "compactionSummary") { digest = message.harnessDigest; } else { continue; } if (digest !== undefined && (!latest || message.timestamp >= latest.timestamp)) { latest = { timestamp: message.timestamp, digest }; } } return latest?.digest; }
/** Global harness state overlaid with this session's local state, when persisted. */ private _loadMergedHarnessState(): HarnessState { const localHarnessStateDir = this._localHarnessStateDir(); return mergeHarnessStates( loadHarnessState(getGlobalHarnessStateDir(), "global"), localHarnessStateDir ? loadHarnessState(localHarnessStateDir, "local") : undefined, ); }
private _loadRefinementHistory(): RefinementResult[] { return mergeRefinementHistory( loadGlobalRefinementHistory(getGlobalHarnessStateDir()), getRefinementHistory(this.sessionManager.getEntries().filter((entry) => entry.type === "custom")), ); }
/** * Refine editable continual harness state: prompt notes, memory, skills, and subagent specs. * The base system prompt is intentionally not editable through this path. * * Planning runs in the background and does NOT block turn entry points * (`_waitForRefineIdle` only waits for `_refineInFlight`). Only the fast * application phase (disk I/O + in-memory mutation) blocks turn entry points. */ async refine( options: { instructions?: string; rollbackId?: string; global?: boolean; } = {}, internal: { skipAbort?: boolean; trigger?: "manual" | "auto"; source?: RefinementSource } = {}, ): Promise<RefinementResult> { // Queued /refine executes from the session-input pump between turns; // refine never aborts the agent (planning is backgrounded and the apply // phase waits for quiescence), so skipAbort only asserts the pump's // idle invariant instead of changing abort behavior. if (internal.skipAbort && this.isStreaming) { throw new Error("Cannot refine without aborting while the agent is running."); } // Wait for any existing refine (both planning and application) before // starting a new run. This serializes concurrent /refine calls so two // planning phases cannot race into concurrent _applyRefine calls that // overwrite harness state. while (this._refineInFlight || this._refinePlanInFlight || this._serializedPlanInFlight) { if (this._refineInFlight) { await this._refineInFlight; } else if (this._refinePlanInFlight) { await this._refinePlanInFlight; } else { // A serialized background plan is in flight (started during an // active turn at message_end). Wait for planning and for the active // turn to settle so its normal checkpoint can consume the plan. const serializedPlanInFlight = this._serializedPlanInFlight; await serializedPlanInFlight; if (this._refineInFlight || this._refinePlanInFlight) { continue; } await this.agent.waitForIdle(); // Aborted turns skip shouldStopAfterTurn. Drop their settled plan // after idle so a later public refine cannot spin on it forever. if (this._serializedPlanInFlight === serializedPlanInFlight) { this._serializedPlanInFlight = undefined; this._serializedExplicitRefineOptions = undefined; } } }
const refineAbort = new AbortController(); this._refineAbortController = refineAbort;
const planRun = this._planRefine(options, refineAbort.signal, internal.trigger ?? "manual"); const planSettled = planRun.then( () => undefined, () => undefined, ); this._refinePlanInFlight = planSettled; let plan: RefinementPlan; try { plan = await planRun; } catch (e) { if (this._refineAbortController === refineAbort) { this._refineAbortController = undefined; } this._scheduleSessionInputPump(); throw e; } finally { if (this._refinePlanInFlight === planSettled) { this._refinePlanInFlight = undefined; } }
// Block new turns before waiting for the current turn to finish. One shared // settled promise covers the full transition and apply critical section. let resolveApplySettled: () => void = () => {}; const applySettled = new Promise<void>((resolve) => { resolveApplySettled = resolve; }); this._refineInFlight = applySettled; try { // Wait for the session to become quiescent before applying. Planning is // allowed to overlap active user work, but application must not disconnect // event handling until that work and its queued events have completed. await this.agent.waitForIdle(); while (true) { const eventQueue = this._agentEventQueue; const compactionOp = this._compactionOperation; const branchSummaryOp = this._branchSummaryOperation; await Promise.allSettled([ eventQueue, ...(compactionOp ? [compactionOp] : []), ...(branchSummaryOp ? [branchSummaryOp] : []), ]); if ( eventQueue === this._agentEventQueue && compactionOp === this._compactionOperation && branchSummaryOp === this._branchSummaryOperation ) { break; } } if (this._disposed || refineAbort.signal.aborted) { throw new Error("Refinement cancelled because the session was disposed."); } return await this._applyRefine( plan, options, refineAbort, internal.source ?? (internal.trigger === "auto" ? "auto" : "user"), ); } finally { resolveApplySettled(); if (this._refineInFlight === applySettled) { this._refineInFlight = undefined; } this._notifySessionInputCheckpointChange(); this._scheduleSessionInputPump(); } }
/** * Block a new agent turn until any in-flight refine application phase has * reattached event handling; otherwise the turn's messages are never * persisted or rendered. * * The idle-wait and application phase (`_refineInFlight`) block here. The * background planning phase (`_refinePlanInFlight`) does NOT block turns. * Refine failures surface to the refine caller, not here. */ private async _waitForRefineIdle(): Promise<void> { while (this._refineInFlight) { await this._refineInFlight; } }
/** * Background planning phase: runs the LLM planning call via `planRefinement`. * Does not disconnect from or abort the agent. Returns the plan without * applying anything. */ private async _planRefine( options: { instructions?: string; rollbackId?: string; global?: boolean }, signal: AbortSignal, trigger: "manual" | "auto" = "manual", ): Promise<RefinementPlan> { if (this._disposed) { throw new Error("Cannot refine a disposed session."); }
if (!this.model) { throw new Error(formatNoModelSelectedMessage()); }
const refinementModel = await this._resolveRefinementModel(); if (!refinementModel) { throw new Error(formatNoModelSelectedMessage()); } const globalHarnessStateDir = getGlobalHarnessStateDir(); const localHarnessStateDir = this._localHarnessStateDir(); const requestedScope = options.global ? "global" : "local"; if (!options.rollbackId && requestedScope === "local" && !localHarnessStateDir) { throw new Error("Local harness refinement requires a persisted session; use global refinement instead."); } const globalPlanningState = loadHarnessState(globalHarnessStateDir, "global"); const localPlanningState = localHarnessStateDir ? loadHarnessState(localHarnessStateDir, "local") : undefined; const planningState = requestedScope === "global" ? globalPlanningState : mergeHarnessStates(globalPlanningState, localPlanningState); const history = this._loadRefinementHistory(); const rollbackTarget = options.rollbackId ? history.find((item) => item.id === options.rollbackId) : undefined; let baselineScope = rollbackTarget ? (inferRefinementResultScope(rollbackTarget) ?? requestedScope) : requestedScope; let baselineHarnessStateDir = baselineScope === "global" ? globalHarnessStateDir : localHarnessStateDir; if (rollbackTarget?.harnessStatePath) { baselineHarnessStateDir = dirname(rollbackTarget.harnessStatePath); baselineScope = resolve(baselineHarnessStateDir) === resolve(globalHarnessStateDir) ? "global" : "local"; } if (!baselineHarnessStateDir) { throw new Error("Local harness refinement requires a persisted session; use global refinement instead."); } const baselineState = rollbackTarget ? loadHarnessState(baselineHarnessStateDir, baselineScope) : baselineScope === "global" ? globalPlanningState : localPlanningState!; if (!options.rollbackId && this._extensionRunner.hasHandlers("session_before_refine")) { const result = (await this._extensionRunner.emit({ type: "session_before_refine", preparation: { trigger, instructions: options.instructions, scope: requestedScope, planningState, history, conversationText: serializeConversation(convertToLlm(this.agent.state.messages)).slice(-80_000), }, signal, })) as SessionBeforeRefineResult | undefined; if (this._disposed || signal.aborted) { throw new Error("Refinement cancelled because the session was disposed."); } if (result?.skip) { throw new RefineSkippedError("Refinement skipped by extension"); } if (result?.proposal !== undefined) { return { proposal: normalizeRefinementProposal(result.proposal), id: generateRefinementId(), baselineState, }; } } const plan = await planRefinement( this.agent.state.messages, planningState, history, refinementModel.model, refinementModel.apiKey, { ...options, retry: providerRetryPolicy(this.settingsManager) }, refinementModel.headers, signal, this.thinkingLevel, this.sessionId, ); if (this._disposed || signal.aborted) { throw new Error("Refinement cancelled because the session was disposed."); } return { ...plan, baselineState }; }
private _recordRefinementOutcome(result: RefinementResult): void { this._appendDurableRefineMessage(createRefinementOutcomeMessage(result)); }
/** In-context notice for an applied refinement. Refinements with zero applied edits emit nothing. */ private _recordRefinementNotice(result: RefinementResult, source: RefinementSource): void { if (!result.appliedEdits.some((edit) => edit.applied)) return; this._appendDurableRefineMessage(createRefinementNoticeMessage(result, source)); }
private _appendDurableRefineMessage(message: CustomMessage): void { try { this.sessionManager.appendCustomMessageEntryWithRollback( message.customType, message.content, message.display, message.details, ); } catch { // Not in the session file, so context rebuilds would drop the outcome. this._unpersistedOutcomes.push(message); } this.agent.state.messages.push(message); this._emit({ type: "message_start", message }); this._emit({ type: "message_end", message }); }
/** * Synchronous application phase: disconnects from the agent, aborts any * in-flight agent run, applies the refinement plan to disk and memory, then * reconnects. This is the only phase that blocks turn entry points. */ private async _applyRefine( plan: RefinementPlan, options: { instructions?: string; rollbackId?: string; global?: boolean }, refineAbort: AbortController, source: RefinementSource, ): Promise<RefinementResult> { if (this._disposed) { throw new Error("Cannot refine a disposed session."); } // The caller has already set _refineInFlight and waited for agent idle. // Disconnect only for the brief apply + save + reconnect critical section. this._disconnectFromAgent();
try { const globalHarnessStateDir = getGlobalHarnessStateDir(); const localHarnessStateDir = this._localHarnessStateDir(); const requestedScope = options.global ? "global" : "local"; const history = this._loadRefinementHistory(); const rollbackTarget = options.rollbackId ? history.find((item) => item.id === options.rollbackId) : undefined; let targetScope = plan.rollbackScope ?? requestedScope; let targetHarnessStateDir = targetScope === "global" ? globalHarnessStateDir : localHarnessStateDir; if (targetScope === "local" && rollbackTarget?.harnessStatePath) { if (!existsSync(rollbackTarget.harnessStatePath)) { throw new Error( `Local refinement ${rollbackTarget.id} state file not found: ${rollbackTarget.harnessStatePath}`, ); } targetHarnessStateDir = dirname(rollbackTarget.harnessStatePath); // Legacy records predate scope fields and default to "local" but may point // at the global store; honor the recorded path so its entries stay global. if (resolve(targetHarnessStateDir) === resolve(globalHarnessStateDir)) { targetScope = "global"; } } if (!targetHarnessStateDir) { throw new Error("Local harness refinement requires a persisted session; use global refinement instead."); } // Re-read the target state immediately before applying so concurrent kernel // (`rlm.harness`) writes during the LLM pass are not clobbered. const state = loadHarnessState(targetHarnessStateDir, targetScope); const proposal = { ...plan.proposal, edits: plan.proposal.edits.map((edit) => { const localPrefix = "local:"; const globalPrefix = "global:"; return { ...edit, id: edit.id?.startsWith(localPrefix) ? edit.id.slice(localPrefix.length) : edit.id?.startsWith(globalPrefix) ? edit.id.slice(globalPrefix.length) : edit.id, }; }), }; if (this._disposed || refineAbort.signal.aborted) { throw new Error("Refinement cancelled because the session was disposed."); } const result = applyRefinementProposal(state, proposal, { id: plan.id, rollbackOf: plan.rollbackOf, scope: targetScope, baselineState: plan.baselineState, }); result.harnessStatePath = saveHarnessState(targetHarnessStateDir, state); if (targetScope === "global") { appendGlobalRefinement(globalHarnessStateDir, result); } let refinementAuditAppendError: { error: unknown } | undefined; try { this.sessionManager.appendCustomEntry("prime-agent.refinement", result); } catch (error) { refinementAuditAppendError = { error }; } try { this._recordRefinementOutcome(result); } catch (error) { if (!refinementAuditAppendError) throw error; } if (refinementAuditAppendError) throw refinementAuditAppendError.error; // The prompt stays byte-identical so the provider prefix cache survives; the notice carries the change. this._recordRefinementNotice(result, source); try { this._emit({ type: "refine_complete", result }); } catch { // Listener failures must not flip a successful refinement into // a reported failure — the refinement is already persisted. } try { await this._extensionRunner.emit({ type: "refine_complete", id: result.id, summary: result.summary, appliedEdits: result.appliedEdits.filter((edit) => edit.applied).length, scope: result.scope ?? "local", }); } catch { // Extension emit failures must not flip a successful refinement // into a reported failure — the refinement is already persisted. } return result; } finally { if (this._refineAbortController === refineAbort) { this._refineAbortController = undefined; } if (!this._disposed) { this._reconnectToAgent(); } } }
abortBranchSummary(): void { this._branchSummaryAbortController?.abort(); }
/** * Check if compaction is needed and run it. * Called after agent_end and before prompt submission. * * Two cases: * 1. Overflow: LLM returned context overflow error, remove error message from agent state, compact, auto-retry * 2. Threshold: Context over threshold, compact, and continue only for stopped in-progress loops or queued messages * * @param assistantMessage The assistant message to check * @param skipAbortedCheck If false, include aborted messages (for pre-prompt check). Default: true */ private _getThresholdContextTokens( assistantMessage: AssistantMessage, compactionTimestamp: number | undefined, ): number | undefined { const messages = this.agent.state.messages; const estimate = estimateContextTokens(messages); if (estimate.lastUsageIndex !== null) { // Verify the usage source is post-compaction. Kept pre-compaction messages // have stale usage reflecting the old (larger) context and would falsely // trigger compaction right after one just finished. const usageMsg = messages[estimate.lastUsageIndex]; if ( compactionTimestamp !== undefined && usageMsg.role === "assistant" && (usageMsg as AssistantMessage).timestamp <= compactionTimestamp ) { return undefined; } return estimate.tokens; } if (assistantMessage.stopReason === "error") return undefined; return calculateContextTokens(assistantMessage.usage); }
private async _checkCompaction( assistantMessage: AssistantMessage, skipAbortedCheck = true, queueAutonomousContinuation = true, ): Promise<boolean> { // An abort drops any compaction the model requested this turn, even on the // pre-prompt path (skipAbortedCheck=false) which continues to threshold checks. if (assistantMessage.stopReason === "aborted") { this._pendingRequestedCompaction = undefined; // An abort also drops any pending explicit refine.run request: the // turn that would service it (non-serialized: _consumePendingRequestedRefine // at agent_end; serialized: the shouldStopAfterTurn checkpoint) never // runs for an aborted turn, so a stale request would leak into the // next turn or checkpoint. this._pendingRequestedRefine = undefined; if (this._serializedPlanInFlight) { const serializedPlanInFlight = this._serializedPlanInFlight; this._autoRefineBranchVersion++; this._refineAbortController?.abort(); await serializedPlanInFlight.catch(() => undefined); if (this._serializedPlanInFlight === serializedPlanInFlight) { this._serializedPlanInFlight = undefined; this._serializedExplicitRefineOptions = undefined; } } if (skipAbortedCheck) return false; }
const settings = this._getEffectiveCompactionSettings(); const contextWindow = this.model?.contextWindow ?? 0;
// Skip overflow check if the message came from a different model. // This handles the case where user switched from a smaller-context model (e.g. opus) // to a larger-context model (e.g. codex) - the overflow error from the old model // shouldn't trigger compaction for the new model. const sameModel = this.model && assistantMessage.provider === this.model.provider && assistantMessage.model === this.model.id;
// Skip overflow/threshold checks if this assistant message is older than the // latest compaction boundary. This prevents a stale pre-compaction usage/error // from retriggering compaction on the first prompt after compaction. const compactionEntry = getLatestCompactionEntry(this.sessionManager.getBranch()); const compactionTimestamp = compactionEntry ? new Date(compactionEntry.timestamp).getTime() : undefined; const assistantIsFromBeforeCompaction = compactionTimestamp !== undefined && assistantMessage.timestamp <= compactionTimestamp;
// Case 1: Overflow - takes priority over a pending model request so the error // strip + retry still happen; the compaction it runs consumes the request. if ( !assistantIsFromBeforeCompaction && (settings.enabled || this._pendingRequestedCompaction !== undefined) && sameModel && isContextOverflow(assistantMessage, contextWindow) ) { if (this._overflowRecovery !== "idle") { if (this._overflowRecovery === "attempted") { this._overflowRecovery = "reported"; this._endCompactionUnsuccessfully( "overflow", "failed", "Context overflow recovery failed after one compact-and-retry attempt. The harness has already compacted once; report the failure and the last real blocker rather than trimming the work.", ); } return false; }
this._overflowRecovery = "attempted"; // Remove the error message from agent state (it IS saved to session for history, // but we don't want it in context for the retry) const messages = this.agent.state.messages; if (messages.length > 0 && messages[messages.length - 1].role === "assistant") { this.agent.state.messages = messages.slice(0, -1); } return await this._runAutoCompaction("overflow", true); }
if (this._pendingRequestedCompaction !== undefined) { return await this._runAutoCompaction("requested", false); }
if (!settings.enabled || assistantIsFromBeforeCompaction) return false;
// Case 3: Threshold - context is getting large. // Use the full-session estimate so messages appended after the last successful // assistant usage are included, matching the /usage context display. const contextTokens = this._getThresholdContextTokens(assistantMessage, compactionTimestamp); if (contextTokens === undefined) return false; if (shouldCompact(contextTokens, contextWindow, settings)) { if (queueAutonomousContinuation && this._queueGoalContinuationForThresholdCompaction(assistantMessage)) { this._continueAfterThresholdCompaction = true; } else if ( queueAutonomousContinuation && (await this._queueAutonomousContinuationForThresholdCompaction(assistantMessage)) ) { this._continueAfterThresholdCompaction = true; } return await this._runAutoCompaction("threshold", false); } return false; }
/** * Internal: Run automatic (threshold/overflow) or model-requested compaction * with events. */ private _endCompactionUnsuccessfully( reason: CompactionOutcomeReason, outcome: CompactionOutcome, message: string, options: { aborted?: boolean; errorSeverity?: "warning" | "error"; customInstructions?: string; } = {}, ): void { this._persistCompactionOutcome(reason, outcome, message); this._emit({ type: "compaction_end", reason, result: undefined, aborted: options.aborted ?? false, willRetry: false, // Aborts are user-initiated; they carry no error message on the event. errorMessage: options.aborted ? undefined : message, errorSeverity: options.errorSeverity, customInstructions: options.customInstructions, }); }
private _persistCompactionOutcome( reason: CompactionOutcomeReason, outcome: CompactionOutcome, message: string, ): void { let outcomeMessage = createCompactionOutcomeMessage(message, { reason, outcome, }); try { this.sessionManager.appendCustomMessageEntryWithRollback( outcomeMessage.customType, outcomeMessage.content, outcomeMessage.display, outcomeMessage.details, ); } catch (error) { const persistenceError = error instanceof Error ? error.message : String(error); outcomeMessage = createCompactionOutcomeMessage( `${message}\n\nThis compaction outcome could not be saved to session history: ${persistenceError}`, { reason, outcome }, ); // Not in the session file, so context rebuilds would drop the disclosure. this._unpersistedOutcomes.push(outcomeMessage); } this.agent.state.messages.push(outcomeMessage); this._emit({ type: "message_start", message: outcomeMessage }); this._emit({ type: "message_end", message: outcomeMessage }); }
private async _runAutoCompaction( reason: "overflow" | "threshold" | "requested", willRetry: boolean, ): Promise<boolean> { // Any compaction consumes a pending model request and honors its instructions // (overflow recovery can fire first and take the request with it). const pending = this._pendingRequestedCompaction; this._pendingRequestedCompaction = undefined; const customInstructions = pending?.customInstructions; const shouldContinueAfterCompaction = (reason === "threshold" || reason === "requested") && this._continueAfterThresholdCompaction; const queuedAutonomousContinuationsForThisCompaction = reason === "threshold" && shouldContinueAfterCompaction ? this._pendingThresholdCompactionAutonomousMessages.splice(0) : []; const queuedGoalContinuationForThisCompaction = reason === "threshold" && shouldContinueAfterCompaction ? this._queuedGoalThresholdContinuation : undefined; this._continueAfterThresholdCompaction = false;
// Requested/threshold stop the loop on purpose, so a failed or skipped compaction must not stall it. // Overflow stays excluded: a failed overflow recovery must not re-issue the overflowing request. const resumeAfterFailure = () => { if ( (reason === "requested" || reason === "threshold") && (shouldContinueAfterCompaction || this.agent.hasQueuedMessages() || this.hasPendingSessionWork) ) { this._schedulePostCompactionContinue(shouldContinueAfterCompaction); } };
this._emit({ type: "compaction_start", reason, customInstructions }); this._autoCompactionAbortController = new AbortController(); let resolveCompactionOperation: () => void = () => {}; const compactionOperation = new Promise<void>((resolve) => { resolveCompactionOperation = resolve; }); this._compactionOperation = compactionOperation;
try { const authResult = this.model ? await this._modelRegistry.getApiKeyAndHeaders(this.model, undefined, this.sessionId) : undefined; if (!this.model || !authResult || !authResult.ok || !authResult.apiKey) { const detail = !this.model || !authResult ? "no model is selected" : authResult.ok ? "no API key is available" : authResult.error; this._endCompactionUnsuccessfully(reason, "failed", `Compaction failed: ${detail}`); this._clearQueuedAutonomousContinuationsAfterSkippedThresholdCompaction( reason === "threshold" && shouldContinueAfterCompaction, queuedAutonomousContinuationsForThisCompaction, ); resumeAfterFailure(); return false; }
const result = await this._performCompaction({ model: authResult.requestModel ?? this.model, apiKey: authResult.apiKey, headers: authResult.headers, customInstructions, signal: this._autoCompactionAbortController.signal, });
this._emit({ type: "compaction_end", reason, result, aborted: false, willRetry, customInstructions, }); // Queued work lives in both the agent queues and the session-owned queues. const hasQueuedMessages = this.agent.hasQueuedMessages() || this.hasPendingSessionWork; const willContinueAfterCompaction = willRetry || shouldContinueAfterCompaction || hasQueuedMessages;
if (willRetry) { const messages = this.agent.state.messages; const lastMsg = messages[messages.length - 1]; if (lastMsg?.role === "assistant" && (lastMsg as AssistantMessage).stopReason === "error") { this.agent.state.messages = messages.slice(0, -1); }
this._schedulePostCompactionContinue(true); this._scheduleAutoRefineAfterCompaction(willContinueAfterCompaction); return true; } else if (shouldContinueAfterCompaction || hasQueuedMessages) { // Compaction can intentionally stop a tool loop between turns. // Queued follow-up/steering/custom messages can also be waiting. this._schedulePostCompactionContinue(shouldContinueAfterCompaction); this._scheduleAutoRefineAfterCompaction(willContinueAfterCompaction); } else { this._scheduleAutoRefineAfterCompaction(willContinueAfterCompaction); } return false; } catch (error) { this._clearQueuedAutonomousContinuationsAfterSkippedThresholdCompaction( reason === "threshold" && shouldContinueAfterCompaction, queuedAutonomousContinuationsForThisCompaction, ); const errorMessage = error instanceof Error ? error.message : "compaction failed"; const aborted = errorMessage === "Compaction cancelled" || (error instanceof Error && error.name === "AbortError"); if (aborted) { this._clearQueuedGoalContinuationAfterCancelledThresholdCompaction(queuedGoalContinuationForThisCompaction); this._endCompactionUnsuccessfully( reason, "cancelled", `${reason === "requested" ? "Requested c" : "C"}ompaction cancelled`, { aborted: true, customInstructions }, ); return false; } if (error instanceof CompactionSkippedError) { this._endCompactionUnsuccessfully( reason, "skipped", reason === "requested" ? `Requested compaction skipped: ${errorMessage}` : `Auto-compaction skipped: ${errorMessage}`, { errorSeverity: "warning", customInstructions }, ); resumeAfterFailure(); return false; } this._endCompactionUnsuccessfully( reason, "failed", reason === "overflow" ? `Context overflow recovery failed: ${errorMessage}` : reason === "requested" ? `Requested compaction failed: ${errorMessage}` : `Auto-compaction failed: ${errorMessage}`, { customInstructions }, ); resumeAfterFailure(); return false; } finally { this._autoCompactionAbortController = undefined; if (this._compactionOperation === compactionOperation) { this._compactionOperation = undefined; } resolveCompactionOperation(); this._notifySessionInputCheckpointChange(); this._scheduleSessionInputPump(); } }
setAutoCompactionEnabled(enabled: boolean): void { this.settingsManager.setCompactionEnabled(enabled); }
get autoCompactionEnabled(): boolean { return this.settingsManager.getCompactionEnabled(); }
/** * Set the provider for extra env vars merged over process.env in extension * pi.exec() subprocesses. The function is read at exec time, so a host (e.g. * the daemon) can update the underlying value per attach without rebinding. */ setExecEnvProvider(provider: (() => Record<string, string | undefined> | undefined) | undefined): void { this._execEnvProvider = provider; const extensions = this._resourceLoader.getExtensions(); extensions.runtime.getExecEnv = provider; }
async bindExtensions(bindings: ExtensionBindings): Promise<void> { if (bindings.uiContext !== undefined) { this._extensionUIContext = bindings.uiContext; } if (bindings.commandContextActions !== undefined) { this._extensionCommandContextActions = bindings.commandContextActions; } if (bindings.shutdownHandler !== undefined) { this._extensionShutdownHandler = bindings.shutdownHandler; } if (bindings.onError !== undefined) { this._extensionErrorListener = bindings.onError; }
this._applyExtensionBindings(this._extensionRunner); await this._extensionRunner.emit(this._sessionStartEvent); await this.extendResourcesFromExtensions(this._sessionStartEvent.reason === "reload" ? "reload" : "startup"); }
private async extendResourcesFromExtensions(reason: "startup" | "reload"): Promise<void> { if (!this._extensionRunner.hasHandlers("resources_discover")) { return; }
const { skillPaths, promptPaths, themePaths } = await this._extensionRunner.emitResourcesDiscover( this._cwd, reason, );
if (skillPaths.length === 0 && promptPaths.length === 0 && themePaths.length === 0) { return; }
const extensionPaths: ResourceExtensionPaths = { skillPaths: this.buildExtensionResourcePaths(skillPaths), promptPaths: this.buildExtensionResourcePaths(promptPaths), themePaths: this.buildExtensionResourcePaths(themePaths), };
this._resourceLoader.extendResources(extensionPaths); this._baseSystemPrompt = this._rebuildSystemPrompt(this.getActiveToolNames()); this.agent.state.systemPrompt = this._baseSystemPrompt; }
private buildExtensionResourcePaths(entries: Array<{ path: string; extensionPath: string }>): Array<{ path: string; metadata: { source: string; scope: "temporary"; origin: "top-level"; baseDir?: string; }; }> { return entries.map((entry) => { const source = this.getExtensionSourceLabel(entry.extensionPath); const baseDir = entry.extensionPath.startsWith("<") ? undefined : dirname(entry.extensionPath); return { path: entry.path, metadata: { source, scope: "temporary", origin: "top-level", baseDir, }, }; }); }
private getExtensionSourceLabel(extensionPath: string): string { if (extensionPath.startsWith("<")) { return `extension:${extensionPath.replace(/[<>]/g, "")}`; } const base = basename(extensionPath); const name = base.replace(/\.(ts|js)$/, ""); return `extension:${name}`; }
private _applyExtensionBindings(runner: ExtensionRunner): void { runner.setUIContext(this._extensionUIContext); runner.bindCommandContext(this._extensionCommandContextActions);
this._extensionErrorUnsubscriber?.(); this._extensionErrorUnsubscriber = this._extensionErrorListener ? runner.onError(this._extensionErrorListener) : undefined; }
refreshModelMetadata(): void { if (this.model?.provider === "xai") { this.agent.state.model = this._modelRegistry.getModelForCurrentAuth(this.model); this.setThinkingLevel(this.thinkingLevel); this._clampServiceTierForModel(); } this._scopedModels = this._scopedModels.map((scoped) => scoped.model.provider === "xai" ? { ...scoped, model: this._modelRegistry.getModelForCurrentAuth(scoped.model) } : scoped, ); }
private _refreshCurrentModelFromRegistry(): void { const currentModel = this.model; if (!currentModel) { return; }
const refreshedModel = this._modelRegistry.find(currentModel.provider, currentModel.id); if (!refreshedModel || refreshedModel === currentModel) { return; }
this.agent.state.model = refreshedModel; }
private _bindExtensionCore(runner: ExtensionRunner): void { const getCommands = (): SlashCommandInfo[] => { const extensionCommands: SlashCommandInfo[] = runner.getRegisteredCommands().map((command) => ({ name: command.invocationName, description: command.description, source: "extension", sourceInfo: command.sourceInfo, }));
const templates: SlashCommandInfo[] = this.promptTemplates.map((template) => ({ name: template.name, description: template.description, source: "prompt", sourceInfo: template.sourceInfo, }));
const skills: SlashCommandInfo[] = this._resourceLoader.getSkills().skills.map((skill) => ({ name: `skill:${skill.name}`, description: skill.description, source: "skill", sourceInfo: skill.sourceInfo, }));
return [...extensionCommands, ...templates, ...skills]; };
runner.bindCore( { sendMessage: (message, options) => { this.sendCustomMessage(message, options).catch((err) => { runner.emitError({ extensionPath: "<runtime>", event: "send_message", error: err instanceof Error ? err.message : String(err), }); }); }, sendUserMessage: (content, options) => { this.sendUserMessage(content, options).catch((err) => { runner.emitError({ extensionPath: "<runtime>", event: "send_user_message", error: err instanceof Error ? err.message : String(err), }); }); }, appendEntry: (customType, data) => { this.sessionManager.appendCustomEntry(customType, data); }, setSessionName: async (name) => { if (this._agentMessageController?.setSessionName) { await this._agentMessageController.setSessionName(name); return; } this.setSessionName(name); }, getSessionName: () => { return this.sessionManager.getSessionName(); }, setLabel: (entryId, label) => { this.sessionManager.appendLabelChange(entryId, label); }, getActiveTools: () => this.getActiveToolNames(), getAllTools: () => this.getAllTools(), setActiveTools: (toolNames) => this.setActiveToolsByName(toolNames), refreshTools: () => this._refreshToolRegistry(), getCommands, setModel: async (model) => { if (!this.modelRegistry.hasConfiguredAuth(model)) return false; await this.setModel(model); return true; }, getThinkingLevel: () => this.thinkingLevel, setThinkingLevel: (level) => this.setThinkingLevel(level), }, { getModel: () => this.model, isIdle: () => !this.isStreaming, getSignal: () => this.agent.signal, abort: () => this.abort(), hasPendingMessages: () => this.queuedActionCount > 0, shutdown: () => { this._extensionShutdownHandler?.(); }, getContextUsage: () => this.getContextUsage(), compact: (options) => { void (async () => { try { const result = await this.compact(options?.customInstructions); options?.onComplete?.(result); } catch (error) { const err = error instanceof Error ? error : new Error(String(error)); options?.onError?.(err); } })(); }, getSystemPrompt: () => this.systemPrompt, }, { registerProvider: (name, config) => { this._modelRegistry.registerProvider(name, config); this._refreshCurrentModelFromRegistry(); }, unregisterProvider: (name) => { this._modelRegistry.unregisterProvider(name); this._refreshCurrentModelFromRegistry(); }, }, ); }
private _refreshToolRegistry(options?: { activeToolNames?: string[]; includeAllExtensionTools?: boolean }): void { const previousRegistryNames = new Set(this._toolRegistry.keys()); const previousActiveToolNames = this.getActiveToolNames(); const allowedToolNames = this._allowedToolNames; const registeredTools = this._extensionRunner.getAllRegisteredTools(); const sdkToolEntry = (definition: ToolDefinition) => ({ definition, sourceInfo: createSyntheticSourceInfo(`<sdk:${definition.name}>`, { source: "sdk" as const, }), }); const allCustomTools = [ ...registeredTools, ...this._customTools.map(sdkToolEntry), ...this._acpMcpTools.map(sdkToolEntry), ]; const isAllowedTool = (name: string): boolean => !allowedToolNames || allowedToolNames.has(name); const allowedCustomTools = allCustomTools.filter((tool) => isAllowedTool(tool.definition.name)); const definitionRegistry = new Map<string, ToolDefinitionEntry>( Array.from(this._baseToolDefinitions.entries()) .filter(([name]) => isAllowedTool(name)) .map(([name, definition]) => [ name, { definition, sourceInfo: createSyntheticSourceInfo(`<builtin:${name}>`, { source: "builtin", }), }, ]), ); for (const tool of allowedCustomTools) { definitionRegistry.set(tool.definition.name, { definition: tool.definition, sourceInfo: tool.sourceInfo, }); } this._toolDefinitions = definitionRegistry; this._toolPromptSnippets = new Map( Array.from(definitionRegistry.values()) .map(({ definition }) => { const snippet = this._normalizePromptSnippet(definition.promptSnippet); return snippet ? ([definition.name, snippet] as const) : undefined; }) .filter((entry): entry is readonly [string, string] => entry !== undefined), ); this._toolPromptGuidelines = new Map( Array.from(definitionRegistry.values()) .map(({ definition }) => { const guidelines = this._normalizePromptGuidelines(definition.promptGuidelines); return guidelines.length > 0 ? ([definition.name, guidelines] as const) : undefined; }) .filter((entry): entry is readonly [string, string[]] => entry !== undefined), ); const runner = this._extensionRunner; const wrappedExtensionTools = wrapRegisteredTools(allowedCustomTools, runner); // Resolve the runner at call time so a rebuild/reload rebinds built-in tools to the // live runner instead of wedging them on the invalidated one's stale-ctx guard. const wrappedBuiltInTools = wrapRegisteredTools( Array.from(this._baseToolDefinitions.values()) .filter((definition) => isAllowedTool(definition.name)) .map((definition) => ({ definition, sourceInfo: createSyntheticSourceInfo(`<builtin:${definition.name}>`, { source: "builtin" }), })), () => this._extensionRunner, );
const toolRegistry = new Map(wrappedBuiltInTools.map((tool) => [tool.name, tool])); for (const tool of wrappedExtensionTools as AgentTool[]) { toolRegistry.set(tool.name, tool); } this._toolRegistry = toolRegistry;
const nextActiveToolNames = ( options?.activeToolNames ? [...options.activeToolNames] : [...previousActiveToolNames] ).filter((name) => isAllowedTool(name));
if (allowedToolNames) { for (const toolName of this._toolRegistry.keys()) { if (allowedToolNames.has(toolName)) { nextActiveToolNames.push(toolName); } } } else if (options?.includeAllExtensionTools) { for (const tool of wrappedExtensionTools) { nextActiveToolNames.push(tool.name); } } else if (!options?.activeToolNames) { for (const toolName of this._toolRegistry.keys()) { if (!previousRegistryNames.has(toolName)) { nextActiveToolNames.push(toolName); } } }
this.setActiveToolsByName([...new Set(nextActiveToolNames)]); }
private _buildRuntime(options: { activeToolNames?: string[]; flagValues?: Map<string, boolean | string>; includeAllExtensionTools?: boolean; }): void { // The first build in this process is a genuine resume: the transcript came from disk, // so a tool call whose result died with the previous process is still unanswered here. if (!this._ipythonRuntimeBuilt) this._repairInterruptedToolCalls(); const pythonSkills = getPythonSkillRuntimeInfo(this._modelVisibleSkills()); let configuredBaseToolDefinitions: Record<string, ToolDefinition>; if (this._baseToolsOverride) { configuredBaseToolDefinitions = Object.fromEntries( Object.entries(this._baseToolsOverride).map(([name, tool]) => [ name, createToolDefinitionFromAgentTool(tool), ]), ); } else { // Rebuilding (e.g. /reload) replaces the provisioner; drop the previous // kernel so the session never holds two live kernels. Gate the new kernel's // startup on the old one's dispose (which flushes a final snapshot), so a // reload can't restore from a snapshot the old kernel is still writing. const previousDispose = this._ipythonKernelProvisioner?.dispose(); this._ipythonKernelSnapshotDir = this.sessionManager.getSessionArtifactDir(); // Only surface the "revived from your previous session" notice on the first // build (a genuine resume). A later rebuild (/reload) restores state silently // for continuity — the conversation is unchanged, so there's nothing to flag. const notifyRestore = !this._ipythonRuntimeBuilt; this._ipythonKernelProvisioner = new IpythonKernelProvisioner(this._cwd, { env: this._rlmKernelEnv(), commandPrefix: this.settingsManager.getShellCommandPrefix(), shellPath: this.settingsManager.getShellPath(), sessionId: this.sessionId, hostHandlers: this._createKernelHostHandlers(), pythonSkills, snapshotDir: this._ipythonKernelSnapshotDir, readyGate: previousDispose, onRestore: notifyRestore ? (result) => this._onIpythonStateRestored(result) : undefined, // The digest exists only once the kernel has bootstrapped, so the base prompt is // rebuilt when it arrives: the next committed turn carries the generated surface. onSkillDigest: () => { this._baseSystemPrompt = this._rebuildSystemPrompt(this.getActiveToolNames()); }, remote: this._remoteHost ? { host: this._remoteHost } : undefined, }); configuredBaseToolDefinitions = createAllToolDefinitions(this._cwd, { ipython: { provisioner: this._ipythonKernelProvisioner, commandPrefix: this.settingsManager.getShellCommandPrefix(), shellPath: this.settingsManager.getShellPath(), cellTimeoutMs: this.settingsManager.getCellDeadlineMs(), onLateSentAgentMessage: (toolCallId, message) => this._recordLateIpythonSentAgentMessage(toolCallId, message), }, }); }
this._baseToolDefinitions = new Map( Object.entries(configuredBaseToolDefinitions).map(([name, tool]) => [name, tool as ToolDefinition]), );
const extensionsResult = this._resourceLoader.getExtensions(); if (options.flagValues) { for (const [name, value] of options.flagValues) { extensionsResult.runtime.flagValues.set(name, value); } } // Re-apply on (re)build so the provider survives /reload. Guarded: the // runtime object can be shared across sessions from one ResourceLoader // (RLM children), so a provider-less session must not wipe the owner's. if (this._execEnvProvider) { extensionsResult.runtime.getExecEnv = this._execEnvProvider; }
const previousRunner: ExtensionRunner | undefined = this._extensionRunner; this._extensionRunner = new ExtensionRunner( extensionsResult.extensions, extensionsResult.runtime, this._cwd, this.sessionManager, this._modelRegistry, ); // Retire only when the extension world restarts (reload); runtime-only rebuilds adopt the timer host instead, so session_start timers survive and unload still cancels them. if (previousRunner) { if (previousRunner.builtFromSameExtensions(extensionsResult.extensions)) { this._extensionRunner.adoptHostTimers(previousRunner); } else { previousRunner.retire(); } } if (this._extensionRunnerRef) { this._extensionRunnerRef.current = this._extensionRunner; } this._bindExtensionCore(this._extensionRunner); this._applyExtensionBindings(this._extensionRunner);
const previousAcpMcpToolNames = new Set(this._acpMcpTools.map((tool) => tool.name)); const acpServers = this._mcpManager?.getAcpServers() ?? []; if (acpServers.length > 0 && !this._ipythonKernelProvisioner) { throw new Error("ACP MCP servers require the built-in cpython tool"); } const acpMcpTools = this._ipythonKernelProvisioner ? createAcpMcpToolDefinitions(acpServers, this._ipythonKernelProvisioner) : []; this._assertAcpMcpToolNamesAvailable(acpMcpTools.map((tool) => tool.name)); for (const name of previousAcpMcpToolNames) this._allowedToolNames?.delete(name); for (const tool of acpMcpTools) this._allowedToolNames?.add(tool.name); this._acpMcpTools = acpMcpTools;
const defaultActiveToolNames = this._baseToolsOverride ? Object.keys(this._baseToolsOverride) : ["ipython"]; const baseActiveToolNames = [...(options.activeToolNames ?? defaultActiveToolNames)]; if (this._goalState.status === "active" && this._includeGoals) { // An active goal needs ipython so the model can reach the goal skill. baseActiveToolNames.push("ipython"); } this._refreshToolRegistry({ activeToolNames: [...new Set(baseActiveToolNames)], includeAllExtensionTools: options.includeAllExtensionTools, });
// Prewarm when configured, or whenever we're resuming a session that already // has a kernel snapshot — so its state is revived and the model is told what // came back before the first turn, rather than a turn later when the kernel // would otherwise lazily start on first use. const hasSnapshot = !!this._ipythonKernelSnapshotDir && existsSync(snapshotPathIn(this._ipythonKernelSnapshotDir)); if ((this._prewarmIpythonKernel || hasSnapshot) && this.getActiveToolNames().includes("ipython")) { this._ipythonKernelProvisioner?.prewarm(); }
// Subsequent builds are in-process rebuilds (/reload), not a fresh resume. this._ipythonRuntimeBuilt = true; }
/** * Skills exposed to the model (system prompt + kernel). The bundled goal * and compact skills are withheld when disabled for this session. */ private _modelVisibleSkills(): Skill[] { let skills = this._resourceLoader.getSkills().skills; if (!this._includeGoals) { skills = skills.filter((skill) => skill.name !== GOAL_SKILL_NAME); } if (!this._includeCompactSkill) { skills = skills.filter((skill) => skill.name !== COMPACT_SKILL_NAME); } if (!this._autoRefineAllowedForSession()) { skills = skills.filter((skill) => skill.name !== REFINE_SKILL_NAME); } if (!this._agentMessageController) { skills = skills.filter((skill) => skill.name !== AGENT_MESSAGE_SKILL_NAME); } if (!this._agentObserveController) { skills = skills.filter((skill) => skill.name !== AGENT_OBSERVE_SKILL_NAME); } if (!this._agentObserveController || !this._rlmHeartbeatController) { skills = skills.filter((skill) => skill.name !== ORCHESTRATION_HEARTBEAT_SKILL_NAME); } return skills; }
private _createKernelHostHandlers(): HostRequestHandlers { const handlers: HostRequestHandlers = { "rlm.run": createRlmRunHostHandler(async ({ prompt, kwargs, cellSourceCode }) => ({ ...(await this.runRlmChild(prompt, kwargs, cellSourceCode)), })), "rlm.create_session": createRlmCreateSessionHostHandler(async ({ prompt, kwargs }) => ({ ...(await this.createRlmSession(prompt, kwargs)), })), "bash.completed": createAsyncBashCompletionHostHandler(async (details) => { const message = createAsyncBashCompletionMessage(details); const disposeSignal = this._sessionActionCommitDisposeAbortController.signal; while (true) { let admissionCommitted = false; try { await this._promptInjectedMessage(message.content, message, { streamingBehavior: "steer", queueIfBusy: true, resumeIfIdle: true, returnAfterAccepted: true, suppressAutonomousContinuation: true, admissionCommitted: () => { admissionCommitted = true; }, }); return; } catch (error) { if (admissionCommitted || !(error instanceof SessionInputAdmissionPausedError)) throw error; while (this._sessionInputAdmissionPauses.size > 0 && !disposeSignal.aborted) { await this._waitForSessionActivityChange(disposeSignal); } } } }), "bash.consumed": createAsyncBashConsumedHostHandler((details) => { this._withdrawAsyncBashCompletionNotice(details); }), "rlm.models": createRlmModelsHostHandler(() => this.modelsRlm()), "rlm.list_subagents": createRlmListSubagentsHostHandler(() => this.listRlmSubagents()), "rlm.collect": createRlmCollectHostHandler((targets, timeoutMs) => this.collectRlmChildren(targets, timeoutMs), ), "rlm.progress.note": createRlmProgressNoteHostHandler((message) => this.noteRlmProgress(message)), "rlm.delete_subagent": createRlmDeleteSubagentHostHandler((target) => this.deleteRlmSubagent(target)), "rlm.context_usage": async () => { const usage = this.getContextUsage(); const contextWindow = usage?.contextWindow ?? null; return { usage: usage ? { tokens: usage.tokens, context_window: contextWindow, payload_ceiling: contextWindow ? calculatePromptPayloadCeiling(contextWindow) : null, compaction_count: usage.compactionCount ?? 0, percent: usage.percent, } : null, }; }, "rlm.get_context_usage": async () => { const usage = this.getContextUsage(); const contextWindow = usage?.contextWindow ?? null; return { usage: usage ? { tokens: usage.tokens, context_window: contextWindow, payload_ceiling: contextWindow ? calculatePromptPayloadCeiling(contextWindow) : null, compaction_count: usage.compactionCount ?? 0, percent: usage.percent, } : null, }; }, "model.info": async () => { const model = this.model; const contextWindow = model?.contextWindow ?? null; return { id: model?.id ?? null, provider: model?.provider ?? null, input: model?.input ?? [], contextWindow, payloadCeiling: contextWindow ? calculatePromptPayloadCeiling(contextWindow) : null, maxTokens: model?.maxTokens ?? null, reasoning: model?.reasoning ?? null, cost: model?.cost ?? null, }; }, "rlm.model_info": async (payload: { model?: string }) => { const modelId = typeof payload?.model === "string" ? payload.model : this.model?.id; if (!modelId) return { error: "No model specified or active" }; const info = this._modelRegistry.getStaticModelMetadata(modelId); if (!info) return { error: `Model '${modelId}' not found in catalog` }; return { id: info.id, provider: info.provider, contextWindow: info.contextWindow, payloadCeiling: info.payloadCeiling, maxTokens: info.maxTokens, reasoning: info.reasoning, cost: info.cost, }; }, "remote.ensure": async (payload) => { const host = typeof payload.host === "string" ? payload.host.trim() : ""; if (!host) { throw new Error("remote.ensure requires a string host"); } const pythonSkills = getPythonSkillRuntimeInfo(this._modelVisibleSkills()); const info = await ensureRemoteKernelRuntime({ hostConfig: { host, port: typeof payload.port === "number" ? payload.port : undefined, identityFile: typeof payload.identityFile === "string" ? payload.identityFile : undefined, sshOptions: Array.isArray(payload.sshOptions) ? payload.sshOptions.map(String) : undefined, cacheDir: typeof payload.cacheDir === "string" ? payload.cacheDir : undefined, pythonCmd: typeof payload.pythonCmd === "string" ? payload.pythonCmd : undefined, }, pythonSkills, }); return { remotePython: info.remotePython, remoteDir: info.remoteDir, runtimeHash: info.runtimeHash, cacheHit: info.cacheHit, }; }, }; if (this._includeGoals) { for (const type of ["goal.get", "goal.create", "goal.complete"]) { handlers[type] = async (payload) => this.handleGoalHostRequest(type, payload); } } if (this._includeCompactSkill) { for (const type of ["compact.run", "compact.status", "compact.threshold"]) { handlers[type] = async (payload) => this.handleCompactHostRequest(type, payload); } } if (this._autoRefineAllowedForSession()) { for (const type of ["refine.run", "refine.status"]) { handlers[type] = async (payload) => this.handleRefineHostRequest(type, payload); } } if (this._rlmHeartbeatController) { for (const type of [ "rlm_heartbeat.list", "rlm_heartbeat.create", "rlm_heartbeat.update", "rlm_heartbeat.delete", ]) { handlers[type] = async (payload) => this.handleRlmHeartbeatHostRequest(type, payload); } } const visibleKernelSkillNames = new Set( this._modelVisibleSkills() .filter((skill) => !skill.disableModelInvocation) .map((skill) => skill.name), ); const messageController = this._agentMessageController; if (messageController && visibleKernelSkillNames.has(AGENT_MESSAGE_SKILL_NAME)) { Object.assign( handlers, createAgentMessageHostHandlers({ family: async () => { if (!messageController.family) throw new Error("agent family roster is not available in this session"); return messageController.family(); }, awaitPendingChildPublication: (selector) => this._awaitPendingRlmChildPublication(selector), sendAgentMessage: async (input) => { const receipt = (await this.handleAgentMessageHostRequest("agent_message.send", { target: input.target, message: input.message, receiverRole: input.receiverRole, receiverName: input.receiverName, })) as AgentSessionMessageReceipt; if (this._rlmDepth > 0) { let addressedParent = input.receiverRole === "parent"; if (input.receiverRole === undefined && messageController.family) { try { addressedParent = (await messageController.family()).some( (member) => member.relationship === "parent" && (member.entry.id === input.target || agentFamilyMemberName(member.entry) === input.target), ); } catch { addressedParent = false; } } if (addressedParent) { this._repliedToParentSinceTask = true; this._parentReplyCount += 1; } } return receipt; }, }), ); } if (this._agentSessionController) { Object.assign(handlers, createAgentSessionHostHandlers(this._agentSessionController)); } if (this._agentObserveController) { Object.assign( handlers, createAgentObserveHostHandlers({ listAgents: () => this.handleAgentObserveHostRequest("agent_observe.list") as AgentObserveListResult, getAgent: (target) => this.handleAgentObserveHostRequest("agent_observe.get", { target, }) as AgentObserveAgentSnapshot, recentMessages: (input) => this.handleAgentObserveHostRequest("agent_observe.recent", { target: input.target, limit: input.limit, max_chars: input.maxChars, }) as AgentObserveRecentMessagesResult, }), ); } if (this._mcpManager) { Object.assign(handlers, this._mcpManager.hostHandlers()); } return handlers; }
async reload(): Promise<void> { const previousFlagValues = this._extensionRunner.getFlagValues(); await emitSessionShutdownEvent(this._extensionRunner, { type: "session_shutdown", reason: "reload", }); await this.settingsManager.reload(); // Re-read auth.json, models.json, and refresh live models from models.dev this._modelRegistry.refresh(); await this._modelRegistry.refreshModelsDev({ force: true }); resetApiProviders(); this._mcpManager?.refresh(); await this._resourceLoader.reload(); this._buildRuntime({ activeToolNames: this.getActiveToolNames(), flagValues: previousFlagValues, includeAllExtensionTools: true, });
const hasBindings = this._extensionUIContext || this._extensionCommandContextActions || this._extensionShutdownHandler || this._extensionErrorListener; if (hasBindings) { await this._extensionRunner.emit({ type: "session_start", reason: "reload", }); await this.extendResourcesFromExtensions("reload"); } }
private _rlmKernelEnv(): Record<string, string> { // Kernel env is provisioning-time only: RLM_MAX_DEPTH may be stale in an already-running kernel; // the TypeScript-side spawn check remains authoritative. const env: Record<string, string> = { RLM_DEPTH: String(this._rlmDepth), RLM_MAX_DEPTH: String(this._rlmMaxDepth), RLM_GLOBAL_HARNESS_STATE_DIR: getGlobalHarnessStateDir(), }; if (this._rlmAllowPush) { // Capability attenuation (R6): a child may publish only when its parent said so. env.RLM_ALLOW_PUSH = "1"; } const rlmSessionDir = this._ensureRlmSessionDir(); if (rlmSessionDir) { env.RLM_SESSION_DIR = rlmSessionDir; // Keep kernel writes and host reads (system prompt, review, /refine) on // the same local harness path. Subagents prefer their own artifact dir; // ephemeral sessions fall back to the RLM session dir once it exists. env.RLM_HARNESS_STATE_DIR = this._localHarnessStateDir() ?? getLocalHarnessStateDir(rlmSessionDir)!; } this._addWebsearchKeyEnv(env); return env; }
private _addWebsearchKeyEnv(env: Record<string, string>): void { if (this._agentDir) { env.PRIME_AGENT_CODING_AGENT_DIR = this._agentDir; }
if (process.env[SERPER_ENV_VAR]?.trim()) { return; } // Inject only when a websearch skill (bundled or custom) is actually loaded, // so the key isn't exposed to kernels that can't use it. if (!this._resourceLoader.getSkills().skills.some((skill) => skill.name === WEBSEARCH_SKILL_NAME)) { return; } const cred = this._modelRegistry.authStorage.get(SERPER_CREDENTIAL_ID); if (cred?.type !== "api_key") { return; } const resolved = resolveConfigValue(cred.key)?.trim(); if (resolved) { env[SERPER_ENV_VAR] = resolved; } }
// Undefined when there's no persistent artifact dir (e.g. the viewer client): // don't mkdtemp here, since this runs on every kernel build but a viewer never // does RLM work. The temp dir is created lazily in _createChildRlmSessionDir. private _ensureRlmSessionDir(): string | undefined { if (this._rlmSessionDir) { mkdirSync(this._rlmSessionDir, { recursive: true }); return this._rlmSessionDir; }
const sessionArtifactDir = this.sessionManager.getSessionArtifactDir(); if (sessionArtifactDir) { mkdirSync(sessionArtifactDir, { recursive: true }); this._rlmSessionDir = sessionArtifactDir; return sessionArtifactDir; }
return undefined; }
private _createChildRlmSessionDir(): string { const parentDir = this._ensureRlmSessionDir() ?? this._createEphemeralRlmSessionDir(); for (let i = 0; i < 100; i++) { const childDir = join(parentDir, `sub-${randomUUID().slice(0, 8)}`); try { mkdirSync(childDir); return childDir; } catch (error) { if (error instanceof Error && "code" in error && error.code === "EEXIST") { continue; } throw error; } } throw new Error("Unable to create unique RLM child session directory"); }
private _createEphemeralRlmSessionDir(): string { this._rlmSessionDir = mkdtempSync(join(tmpdir(), "prime-agent-rlm-")); return this._rlmSessionDir; }
_contextTokensForCurrentMessages(): number | undefined { const last = this._findLastAssistantMessage(); return last ? calculateContextTokens(last.usage) : undefined; }
setCurrentRecap(recap: string | undefined): void { if (this._currentRecap === recap) return; this._currentRecap = recap; this._emit({ type: "recap_update", recap }); }
get repliedToParentSinceTask(): boolean | undefined { return this._repliedToParentSinceTask; }
getCurrentRecap(): string | undefined { return this._currentRecap; }
private _findAssistantEntryForMessage(message: AssistantMessage): SessionMessageEntry | undefined { return this.sessionManager .getEntries() .find((entry): entry is SessionMessageEntry => entry.type === "message" && entry.message === message); }
private _createRlmSubagentRuntimeOptions(options: { id: string; prompt: string; sessionName: string; spawnCode?: string; sessionDir: string; model: Model<any>; thinkingLevel?: ThinkingLevel; spawnedByRequestId?: string; remoteHost?: string; allowPush?: boolean; cwd?: string; }): CreateRlmSubagentRuntimeOptions { return { parentSession: this, id: options.id, prompt: options.prompt, sessionName: options.sessionName, spawnCode: options.spawnCode, sessionDir: options.sessionDir, model: options.model, thinkingLevel: options.thinkingLevel ?? (clampThinkingLevel(options.model, this.thinkingLevel) as ThinkingLevel), serviceTier: this.serviceTier === "priority" && !supportsFastMode(options.model) ? "default" : this.serviceTier, scopedModels: [...this._scopedModels], activeToolNames: this.getActiveToolNames(), allowedToolNames: this._allowedToolNames ? [...this._allowedToolNames] : undefined, customTools: [...this._customTools], includeGoals: this._includeGoals, includeCompactSkill: this._includeCompactSkill, rlmDepth: this._rlmDepth + 1, rlmMaxDepth: this._rlmMaxDepth, rlmParentNodeId: options.id, spawnedByRequestId: options.spawnedByRequestId, remoteHost: options.remoteHost, allowPush: options.allowPush, // An isolated workspace wins; otherwise the parent session's own directory, never // whatever directory the daemon happened to be launched in. cwd: options.cwd ?? this.sessionManager.getCwd(), }; }
private async _createRlmSubagentRuntime(options: CreateRlmSubagentRuntimeOptions): Promise<RlmSubagentRuntime> { if (this._subagentRuntimeHost) { return await this._subagentRuntimeHost.createRlmSubagentRuntime(options); }
return this._createInlineRlmSubagentRuntime(options); }
private _createInlineRlmSubagentRuntime(options: CreateRlmSubagentRuntimeOptions): RlmSubagentRuntime { const childSessionManager = SessionManager.create(options.cwd ?? this._cwd, options.sessionDir); if (options.parentSession.sessionFile) { childSessionManager.newSession({ parentSession: options.parentSession.sessionFile, rlmDepth: options.rlmDepth, }); } childSessionManager.appendModelChange(options.model.provider, options.model.id); childSessionManager.appendThinkingLevelChange(options.thinkingLevel); childSessionManager.appendServiceTierChange(options.serviceTier);
const childAgent = new Agent({ initialState: { systemPrompt: "", model: options.model, thinkingLevel: options.thinkingLevel, serviceTier: options.serviceTier, tools: [], }, convertToLlm: this.agent.convertToLlm, transformContext: this.agent.transformContext, streamFn: this.agent.streamFn, getApiKey: this.agent.getApiKey, onPayload: this.agent.onPayload, onResponse: this.agent.onResponse, steeringMode: this.settingsManager.getSteeringMode(), followUpMode: this.settingsManager.getFollowUpMode(), sessionId: childSessionManager.getSessionId(), thinkingBudgets: this.settingsManager.getThinkingBudgets(), transport: this.settingsManager.getTransport(), toolExecution: this.agent.toolExecution, });
const child = new AgentSession({ agent: childAgent, sessionManager: childSessionManager, settingsManager: this.settingsManager, cwd: this._cwd, agentDir: this._agentDir, scopedModels: options.scopedModels, resourceLoader: this._resourceLoader, customTools: options.customTools, modelRegistry: this._modelRegistry, initialActiveToolNames: options.activeToolNames, allowedToolNames: options.allowedToolNames, includeGoals: options.includeGoals, includeCompactSkill: options.includeCompactSkill, rlmDepth: options.rlmDepth, rlmMaxDepth: options.rlmMaxDepth, rlmSessionDir: options.sessionDir, rlmParentNodeId: options.rlmParentNodeId, rlmParentAgent: options.parentSession.sessionName ?? options.parentSession.sessionId, semanticParentSessionId: options.parentSession.sessionId, semanticSpawnedByRequestId: options.spawnedByRequestId, remoteHost: options.remoteHost, sessionStartEvent: { type: "session_start", reason: "startup" }, }); if (child.sessionName !== options.sessionName) { try { child.setSessionName(options.sessionName); } catch (error) { child.dispose(); throw error; } } options.onSessionPublished?.(child);
return { session: child }; }
private _abandonRlmRunForQuiescence(run: RlmChildRun): void { run.suppressTerminalNotice = true; run.abandonedForQuiescence = true; this._abandonedRlmQuiescenceChildIds.add(run.id); this._unsettledRlmChildRuns.delete(run); run.settlement.resolve(); this._maybeResumeGoalContinuationAfterRlmWork(); this._maybeResumeAutonomousContinuationAfterRlmWork(); }
private _cancelActiveRlmChildRuns(reason: string): void { for (const run of this._activeRlmChildRuns.values()) { this._cancelRlmChildRun(run, reason); } }
private _cancelRlmChildRun(run: RlmChildRun, reason: string): boolean { if (run.status !== "running" && run.status !== "queued") { return false; } run.status = "cancelled"; if (this._sessionInputPumpSuspended) this._abandonRlmRunForQuiescence(run); run.error = reason; run.publication?.reject(new Error(reason)); run.abort(); // Surface the cancellation immediately; the run's own terminal update is // delayed indefinitely when the child is stuck mid-stream, which is // exactly when users reach for the kill. run.emitUpdate?.(); return true; }
getRlmChildRunStatus(childId: string): RlmChildAgentStatus | undefined { return this._activeRlmChildRuns.get(childId)?.status; }
private async _currentActiveSessionId(): Promise<string | undefined> { try { return (await this._agentMessageController?.listAgents())?.current?.activeSessionId; } catch { return undefined; } }
private async _awaitPendingRlmChildPublication(selector: string): Promise<string | undefined> { const run = [...this._activeRlmChildRuns.values()].find( (candidate) => (candidate.status === "queued" || candidate.status === "running" || candidate.status === "done") && !candidate.detachedDeletion && (candidate.id === selector || candidate.sessionName === selector), ); if (!run) return undefined; await run.publication.promise; return run.session?.sessionId; }
/** * Child-side progress note admission. The host wrapper validated shape and * length; this throttles per session (event-loop spam guard) and re-emits * an `rlm_progress_note` event for the parent's child-event subscription. * Pull-based only: rejected notes return a retry hint instead of an error. */ noteRlmProgress(message: string): RlmProgressNoteResult { const now = Date.now(); const lastAt = this._lastRlmProgressNoteAt; if (lastAt !== undefined && now - lastAt < RLM_PROGRESS_NOTE_MIN_INTERVAL_MS) { return { accepted: false, retry_after_ms: RLM_PROGRESS_NOTE_MIN_INTERVAL_MS - (now - lastAt) }; } this._lastRlmProgressNoteAt = now; this._emit({ type: "rlm_progress_note", message, timestamp: now }); return { accepted: true, retry_after_ms: undefined }; }
async listRlmSubagents(): Promise<RlmListSubagentsResult> { return this._buildRlmSubagentList(await this._agentMessageController?.listAgents()); }
/** * Registry extras a parent needs to decide reuse vs respawn: the birth routing, * and the child's own cumulative turns, tokens and parent follow-ups. */ private _rlmChildRegistryExtras( _childId: string, session?: AgentSession, ): Pick<RlmSubagentRegistryEntry, "model" | "turns" | "tokens" | "followups"> { const model = session?.model ? `${session.model.provider}/${session.model.id}` : undefined; const routing: Pick<RlmSubagentRegistryEntry, "model"> = model ? { model } : {}; if (!session) { return routing; } let turns = 0; let tokens = 0; let parentMessages = 0; for (const entry of session.sessionManager.getBranch()) { if (entry.type !== "message") continue; const message = entry.message as { role?: string; usage?: { totalTokens?: number }; details?: { fromRelationship?: string }; }; if (message.role === "assistant") { turns += 1; tokens += message.usage?.totalTokens ?? 0; } else if (message.role === "custom" && message.details?.fromRelationship === "parent") { parentMessages += 1; } } return { ...routing, turns, tokens, followups: Math.max(0, parentMessages - 1) }; }
/** * Project bounded snapshot fields onto kernel roster entries so the parent * model sees nearly as much as daemon clients do. Answer previews are * hard-capped here (compactRlmText already caps at 160). */ private _rlmRegistryExtrasFromSnapshot( snapshot: RlmChildAgentSnapshot, ): Pick< RlmSubagentRegistryEntry, | "activity" | "tool_use_count" | "duration_ms" | "answer_preview" | "replied_since_task" | "progress_note" | "label" | "last_activity_at" | "activity_stale_ms" > { const extras: Pick< RlmSubagentRegistryEntry, | "activity" | "tool_use_count" | "duration_ms" | "answer_preview" | "replied_since_task" | "progress_note" | "label" | "last_activity_at" | "activity_stale_ms" > = {}; if (snapshot.activity) { extras.activity = { kind: snapshot.activity.kind, ...(snapshot.activity.toolName ? { tool_name: snapshot.activity.toolName } : {}), }; } if (snapshot.toolUseCount !== undefined) extras.tool_use_count = snapshot.toolUseCount; if (snapshot.durationMs !== undefined) extras.duration_ms = snapshot.durationMs; if (snapshot.answerPreview !== undefined) { extras.answer_preview = snapshot.answerPreview.slice(0, RLM_REGISTRY_ANSWER_PREVIEW_MAX_LENGTH); } if (snapshot.repliedSinceTask !== undefined) extras.replied_since_task = snapshot.repliedSinceTask; if (snapshot.progressNote !== undefined) extras.progress_note = snapshot.progressNote; if (snapshot.label !== undefined) extras.label = snapshot.label.slice(0, RLM_REGISTRY_LABEL_MAX_LENGTH); if (snapshot.lastActivityAt !== undefined) extras.last_activity_at = snapshot.lastActivityAt; if (snapshot.activityStaleMs !== undefined) extras.activity_stale_ms = snapshot.activityStaleMs; return extras; }
private _buildRlmSubagentList(listedAgents?: AgentSessionMessageListResult): RlmListSubagentsResult { const daemonChildren = new Map<string, AgentSessionMessageAgentSummary>(); const parentActiveSessionId = listedAgents?.current?.activeSessionId; if (parentActiveSessionId) { for (const agent of listedAgents.agents) { if ( agent.runtimeKind === "subagent" && agent.parentActiveSessionId === parentActiveSessionId && agent.rlmChildId ) { daemonChildren.set(agent.rlmChildId, agent); } } }
const subagents: RlmListSubagentsResult["subagents"] = []; const recorded = new Set<string>(); for (const run of this._activeRlmChildRuns.values()) { if (this._deletingRlmChildren.has(run.id) || run.detachedDeletion || run.status === "cancelled") { continue; } const daemonChild = daemonChildren.get(run.id); subagents.push({ rlm_child_id: run.id, active_session_id: daemonChild?.activeSessionId ?? null, session_id: daemonChild?.sessionId ?? run.session?.sessionId ?? null, session_name: daemonChild?.sessionName ?? run.session?.sessionName ?? run.sessionName, session_dir: run.sessionDir, status: run.status === "done" ? "completed" : run.status === "error" ? "error" : "running", ...this._rlmRegistryExtrasFromSnapshot(this._rlmChildSnapshotForRun(run)), ...this._rlmChildRegistryExtras(run.id, run.session), }); recorded.add(run.id); } for (const [childId, { session: childSession, run: retainedRun }] of this._rlmChildSessions) { if ( this._deletingRlmChildren.has(childId) || recorded.has(childId) || this._rlmChildCleanupFailures.has(childId) ) { continue; } const daemonChild = daemonChildren.get(childId); const sessionDir = childSession._rlmSessionDir; if (!sessionDir) { continue; } const extrasSnapshot = retainedRun ? this._rlmChildSnapshotForRun(retainedRun, childSession) : this._rlmChildSnapshotForSession(childId, childSession); subagents.push({ rlm_child_id: childId, active_session_id: daemonChild?.activeSessionId ?? null, session_id: daemonChild?.sessionId ?? childSession.sessionId, session_name: daemonChild?.sessionName ?? childSession.sessionName ?? createDefaultRlmSubagentSessionName("", childId), session_dir: sessionDir, status: "completed", ...this._rlmRegistryExtrasFromSnapshot(extrasSnapshot), ...this._rlmChildRegistryExtras(childId, childSession), }); recorded.add(childId); } for (const [childId, daemonChild] of daemonChildren) { if ( recorded.has(childId) || this._deletingRlmChildren.has(childId) || this._deletedRlmChildIds.has(childId) || this._rlmChildCleanupFailures.has(childId) || !daemonChild.sessionDir ) { continue; } subagents.push({ rlm_child_id: childId, active_session_id: daemonChild.activeSessionId, session_id: daemonChild.sessionId, session_name: daemonChild.sessionName ?? createDefaultRlmSubagentSessionName("", childId), session_dir: daemonChild.sessionDir, status: daemonChild.rlmChildRegistryStatus === "completed" ? "completed" : "error", ...this._rlmChildRegistryExtras(childId, this._rlmChildSessions.get(childId)?.session), }); } return { subagents }; }
/** * Typed fan-in for direct RLM children: wait (bounded) for the selected * runs to settle and return result envelopes. Never steers the parent and * never rejects on timeout — a timeout returns the current snapshots so * the caller can end its turn, poll, or retry. `targets` are child ids or * session names; an empty list means every direct child that is not being * deleted. */ async collectRlmChildren(targets: string[], timeoutMs: number): Promise<RlmCollectResult> { const candidates = new Map<string, RlmChildRun>(); for (const run of this._activeRlmChildRuns.values()) { candidates.set(run.id, run); } // Terminal cleanup moves settled runs out of _activeRlmChildRuns while their // envelope stays retained in _rlmChildSessions until deleted; collect must see // both or a completed child can no longer be re-collected. for (const [childId, retained] of this._rlmChildSessions) { if (retained.run && !candidates.has(childId)) { candidates.set(childId, retained.run); } } const runs = new Map<string, RlmChildRun>(); if (targets.length === 0) { for (const [childId, run] of candidates) { if (!run.detachedDeletion && !this._deletingRlmChildren.has(run.id)) { runs.set(childId, run); } } } else { for (const target of targets) { const matches = [...candidates.values()].filter( (run) => !run.detachedDeletion && !this._deletingRlmChildren.has(run.id) && this._rlmChildRunMatchesTarget(run, target), ); if (matches.length === 0) { throw new Error(`No direct RLM child matches "${target}" in the current parent session`); } if (matches.length > 1) { throw new Error(`RLM child selector "${target}" is ambiguous in the current parent session`); } runs.set(matches[0].id, matches[0]); } } if (timeoutMs > 0) { const deadline = Date.now() + timeoutMs; await Promise.all( [...runs.values()].map((run) => { if (run.settled) return undefined; const remainingMs = deadline - Date.now(); if (remainingMs <= 0) return undefined; let timer: NodeJS.Timeout | undefined; const timeout = new Promise<void>((resolve) => { timer = setTimeout(resolve, remainingMs); timer.unref?.(); }); return Promise.race([ // A settled failure is terminal state, not a collect error. run.settlement.promise.then( () => undefined, () => undefined, ), timeout, ]).finally(() => { clearTimeout(timer); }); }), ); } return { results: [...runs.values()].map((run) => this._rlmCollectEntryForRun(run)) }; }
private _rlmChildRunMatchesTarget(run: RlmChildRun, target: string): boolean { const session = run.session ?? this._rlmChildSessions.get(run.id)?.session; return ( run.id === target || run.sessionName === target || session?.sessionId === target || session?.sessionName === target ); }
private _rlmCollectEntryForRun(run: RlmChildRun): RlmCollectResultEntry { const snapshot = this._rlmChildSnapshotForRun(run); return { rlm_child_id: snapshot.id, session_name: snapshot.sessionName, session_dir: snapshot.sessionDir, status: snapshot.status, settled: run.settled, answer_preview: snapshot.answerPreview, error: snapshot.error, duration_ms: snapshot.durationMs, tool_use_count: snapshot.toolUseCount, replied_since_task: snapshot.repliedSinceTask, }; }
private _rlmSubagentMatchesTarget(entry: RlmSubagentRegistryEntry, target: string): boolean { return ( entry.rlm_child_id === target || entry.active_session_id === target || entry.session_id === target || entry.session_name === target ); }
private async _resolveDirectRlmSubagent(target: string): Promise<RlmSubagentRegistryEntry> { const candidates = [...(await this.listRlmSubagents()).subagents, ...this._rlmChildCleanupFailures.values()]; const matches = candidates.filter((entry) => this._rlmSubagentMatchesTarget(entry, target)); if (matches.length === 0) { throw new Error(`No direct RLM subagent matches "${target}" in the current parent session`); } if (matches.length > 1) { throw new Error(`RLM subagent selector "${target}" is ambiguous in the current parent session`); } return matches[0]!; }
async deleteInactiveRlmSubagent( childId: string, isExternallyRunning: () => boolean = () => false, ): Promise<"deleted" | "not_found" | "running"> { for (const owner of this._rlmSubtreeSessions()) { const isRunning = (): boolean => { const status = owner._activeRlmChildRuns.get(childId)?.status; return status === "queued" || status === "running" || isExternallyRunning(); }; if (isRunning()) { return "running"; } const subagent = [ ...(await owner.listRlmSubagents()).subagents, ...owner._rlmChildCleanupFailures.values(), ].find((entry) => entry.rlm_child_id === childId); if (!subagent) continue; if (isRunning()) { return "running"; } const result = await owner._trackRlmSubagentDeletion(subagent, () => { if (isRunning()) { return Promise.resolve({ subagent, outcome: "skipped_running" }); } return owner._deleteResolvedRlmSubagent(subagent); }); return result.outcome === "skipped_running" ? "running" : "deleted"; } return "not_found"; }
async deleteRlmSubagent(target: string): Promise<RlmDeleteSubagentResult> { const inFlight = [...this._deletingRlmChildren.values()].filter(({ subagent }) => this._rlmSubagentMatchesTarget(subagent, target), ); if (inFlight.length > 1) { throw new Error(`RLM subagent selector "${target}" is ambiguous in the current parent session`); }
// Running and retained children can be reserved synchronously. This keeps // them hidden immediately while the async daemon listing checks for a // conflicting passive selector. const localMatches = [ ...this._buildRlmSubagentList().subagents, ...this._rlmChildCleanupFailures.values(), ].filter((entry) => this._rlmSubagentMatchesTarget(entry, target)); const matchingChildIds = new Set([ ...inFlight.map(({ subagent }) => subagent.rlm_child_id), ...localMatches.map((subagent) => subagent.rlm_child_id), ]); if (matchingChildIds.size > 1 || localMatches.length > 1) { throw new Error(`RLM subagent selector "${target}" is ambiguous in the current parent session`); } if (inFlight[0]) { return inFlight[0].promise; } if (localMatches[0]) { const subagent = localMatches[0]; return this._trackRlmSubagentDeletion(subagent, async () => { const listedAgents = await this._agentMessageController?.listAgents(); const listedSubagents = this._buildRlmSubagentList(listedAgents).subagents; const passiveMatches = listedSubagents.filter( (entry) => entry.rlm_child_id !== subagent.rlm_child_id && this._rlmSubagentMatchesTarget(entry, target), ); if (passiveMatches.length > 0) { throw new Error(`RLM subagent selector "${target}" is ambiguous in the current parent session`); } const parentActiveSessionId = listedAgents?.current?.activeSessionId; const daemonChild = listedAgents?.agents.find( (agent) => agent.rlmChildId === subagent.rlm_child_id && agent.parentActiveSessionId === parentActiveSessionId, ); const resolvedSubagent = daemonChild ? { ...subagent, active_session_id: daemonChild.activeSessionId, session_id: daemonChild.sessionId, session_name: daemonChild.sessionName ?? subagent.session_name, } : subagent; return this._deleteResolvedRlmSubagent(resolvedSubagent); }); }
const directMatches = [ ...(await this.listRlmSubagents()).subagents, ...this._rlmChildCleanupFailures.values(), ].filter((entry) => this._rlmSubagentMatchesTarget(entry, target)); const directChildIds = new Set(directMatches.map((subagent) => subagent.rlm_child_id)); if (directChildIds.size > 1) { throw new Error(`RLM subagent selector "${target}" is ambiguous in the current parent session`); } const subagent = directMatches[0] ?? (await this._resolveDirectRlmSubagent(target)); return this._trackRlmSubagentDeletion(subagent, () => this._deleteResolvedRlmSubagent(subagent)); }
private async _trackRlmSubagentDeletion( subagent: RlmSubagentRegistryEntry, startDeletion: () => Promise<RlmDeleteSubagentResult>, ): Promise<RlmDeleteSubagentResult> { const existing = this._deletingRlmChildren.get(subagent.rlm_child_id); if (existing) return existing.promise; const deletion = Promise.resolve().then(startDeletion); this._deletingRlmChildren.set(subagent.rlm_child_id, { subagent, promise: deletion, }); try { return await deletion; } finally { const clearReservation = () => { if (this._deletingRlmChildren.get(subagent.rlm_child_id)?.promise === deletion) { this._deletingRlmChildren.delete(subagent.rlm_child_id); } }; const run = this._activeRlmChildRuns.get(subagent.rlm_child_id); if (run?.detachedDeletion) { // Keep every selector reserved until the run settles, or until a failed // cleanup is exposed for an explicit retry. Repeated deletes before that // boundary return the same accepted result. void run.deletionReservation.promise.then(clearReservation, clearReservation); } else { clearReservation(); } } }
private _deleteRlmSubagentSession(childId: string, session?: AgentSession): Promise<void> { if (this._subagentRuntimeHost) { return this._subagentRuntimeHost.deleteRlmSubagentRuntime(childId, session); } return session?.disposeAsync() ?? Promise.resolve(); }
private _ensureRlmRunDeletionCleanup(run: RlmChildRun, session: AgentSession): Promise<void> { if (run.deletionCleanup) return run.deletionCleanup; const cleanup = Promise.resolve().then(() => this._deleteRlmSubagentSession(run.id, session)); run.deletionCleanup = cleanup; // Deletion admission is intentionally nonblocking. The detached run owner // joins this exact promise before settlement and records any failure. void cleanup.catch(() => undefined); return cleanup; }
private async _recordRlmRunDeletionCleanupFailure( run: RlmChildRun, subagent: RlmSubagentRegistryEntry, session: AgentSession, error: unknown, ): Promise<void> { if (this._disposed || this._disposing) { run.suppressTerminalNotice = true; await session.disposeAsync().catch(() => undefined); if (!run.settled) await this._finishRlmRunDeletion(run); return; } run.deletionCleanup = undefined; run.deletionCleanupObserver = undefined; run.deletionCleanupFailed = true; run.session = session; this._rlmChildCleanupFailures.set(run.id, subagent); // Make retry admission available before waking the parent model with the // retry-required notice. run.deletionReservation.resolve(); await Promise.resolve(); await run.reportDeletionCleanupFailure?.(error); }
private async _finishRlmRunDeletion(run: RlmChildRun): Promise<void> { await run.completeDeletion?.(); if (this._activeRlmChildRuns.get(run.id) === run) { this._removeRlmSubagentTracking(run.id, run); } run.settled = true; run.settlement.resolve(); run.deletionReservation.resolve(); this._unsettledRlmChildRuns.delete(run); this._maybeResumeGoalContinuationAfterRlmWork(); this._maybeResumeAutonomousContinuationAfterRlmWork(); }
private _observeRlmRunDeletionCleanup( run: RlmChildRun, subagent: RlmSubagentRegistryEntry, session: AgentSession, cleanup: Promise<void>, ): Promise<boolean> { if (run.deletionCleanupObserver) return run.deletionCleanupObserver; const observer = cleanup.then( () => true, async (error) => { await this._recordRlmRunDeletionCleanupFailure(run, subagent, session, error); return false; }, ); run.deletionCleanupObserver = observer; void observer.catch(() => undefined); return observer; }
private _continueFinishedRlmRunDeletion( run: RlmChildRun, subagent: RlmSubagentRegistryEntry, session: AgentSession, ): void { const cleanup = this._ensureRlmRunDeletionCleanup(run, session); const observer = this._observeRlmRunDeletionCleanup(run, subagent, session, cleanup); if (!run.deletionRunFinished) return; void observer .then(async (cleanupSucceeded) => { if (cleanupSucceeded) await this._finishRlmRunDeletion(run); }) .catch(() => undefined); }
private _removeRlmSubagentTracking(childId: string, run?: RlmChildRun): void { run?.unsubscribe?.(); this._rlmChildUnsubscribes.get(childId)?.(); this._rlmChildUnsubscribes.delete(childId); this._rlmChildSessions.delete(childId); this._rlmChildCleanupFailures.delete(childId); this._abandonedRlmQuiescenceChildIds.delete(childId); if (!run || this._activeRlmChildRuns.get(childId) === run) { this._activeRlmChildRuns.delete(childId); } if (run) { run.abort = noopRlmChildAbort; run.unsubscribe = undefined; run.session = undefined; } }
private _emitRlmSubagentRemoval(subagent: RlmSubagentRegistryEntry): void { this._emit({ type: "rlm_child_update", child: { id: subagent.rlm_child_id, parentId: this._rlmParentNodeId, activeSessionId: subagent.active_session_id ?? undefined, sessionName: subagent.session_name, label: subagent.session_name, status: "cancelled", sessionDir: subagent.session_dir, error: "Deleted by parent orchestrator", }, }); }
private async _deleteResolvedRlmSubagent(subagent: RlmSubagentRegistryEntry): Promise<RlmDeleteSubagentResult> { const childId = subagent.rlm_child_id; const run = this._activeRlmChildRuns.get(childId); if (run) { if (run.deletionCleanupFailed) { // Reset retry coordination only after selector preflight reaches the // resolved child. A failed preflight must leave the prior retry boundary // intact so a later call can acquire it. run.deletionCleanupFailed = false; run.deletionFailureNotice = undefined; run.deletionReservation = createAgentMessageDeferred(); } // The detached task remains the sole lifecycle owner. Mark deletion before // cancellation so its catch/finally path cannot race a normal release or // terminal notice against the physical delete. run.detachedDeletion = subagent; if (this._cancelRlmChildRun(run, "Deleted by parent orchestrator")) { run.deletionNeedsCompletionNotice = true; } else { this._emitRlmSubagentRemoval(subagent); } const liveSession = run.session; if (run.status === "error" && !liveSession && run.settled) { this._deletedRlmChildIds.add(childId); this._removeRlmSubagentTracking(childId, run); return { subagent }; } if (liveSession && run.settled) { run.deletionRunFinished = true; run.settlement = createAgentMessageDeferred(); run.settled = false; this._unsettledRlmChildRuns.add(run); } if (liveSession) this._continueFinishedRlmRunDeletion(run, subagent, liveSession);
// Return once deletion is accepted. The run stays hidden but unsettled until // abort-insensitive model/tool work unwinds and the shared cleanup finishes. this._deletedRlmChildIds.add(childId); return { subagent }; }
this._emitRlmSubagentRemoval(subagent); const retained = this._rlmChildSessions.get(childId)?.session; try { await this._deleteRlmSubagentSession(childId, retained); } catch (error) { if (this._disposed || this._disposing) { this._removeRlmSubagentTracking(childId); void retained?.disposeAsync().catch(() => undefined); } else { this._rlmChildCleanupFailures.set(childId, subagent); } throw error; } this._deletedRlmChildIds.add(childId); this._removeRlmSubagentTracking(childId); return { subagent }; }
/** * Retain a finished child session for the parent lifetime so inspectors and * daemon-hosted agent messaging can keep addressing it. Returns false (and disposes * the child) when the parent is already tearing down, so the caller can drop the * matching event forwarder too. */ registerRlmChildSession(childId: string, session: AgentSession, unsubscribe?: () => void): boolean { // A child can finish concurrently while the parent is (or has) torn down; don't // resurrect the map (it would never be disposed), just drop the child now. if (this._deletingRlmChildren.has(childId) || this._deletedRlmChildIds.has(childId)) { return false; } if (this._subagentRuntimeHost?.completeRlmSubagentRuntime?.(childId, session) === false) { return false; } if (this._disposed || this._disposing) { void session.disposeAsync().catch(() => undefined); return false; } this._rlmChildSessions.set(childId, { session, run: this._activeRlmChildRuns.get(childId) }); if (unsubscribe) { this._rlmChildUnsubscribes.set(childId, unsubscribe); } return true; }
releaseRlmChildSession(childId: string, session: AgentSession): (() => void) | false { const run = this._activeRlmChildRuns.get(childId); if (run?.session === session && run.status === "done") { const unsubscribe = run.unsubscribe ?? noopRlmChildEventUnsubscribe; return () => { run.unsubscribe = undefined; this._activeRlmChildRuns.delete(childId); unsubscribe(); }; } if (this._rlmChildSessions.get(childId)?.session !== session) return false; const unsubscribe = this._rlmChildUnsubscribes.get(childId) ?? noopRlmChildEventUnsubscribe; return () => { this._rlmChildUnsubscribes.delete(childId); this._rlmChildSessions.delete(childId); unsubscribe(); }; }
private _rlmChildSnapshotForRun( run: RlmChildRun, child = run.session ?? this._rlmChildSessions.get(run.id)?.session, ): RlmChildAgentSnapshot { const model = child?.model ?? run.model; return { id: run.id, parentId: this._rlmParentNodeId, sessionName: child?.sessionName ?? run.sessionName, model: `${model.provider}/${model.id}`, label: rlmChildLabel(run.prompt), status: run.status, durationMs: run.durationMs, answerPreview: run.answerPreview, toolUseCount: run.toolUseCount > 0 ? run.toolUseCount : undefined, tokenCount: child?._contextTokensForCurrentMessages(), recap: child?.getCurrentRecap(), sessionDir: run.sessionDir, activity: run.activity, repliedSinceTask: child?._repliedToParentSinceTask, progressNote: run.progressNotes?.at(-1), lastActivityAt: run.lastActivityAt, activityStaleMs: rlmActivityStaleMs(run.status, run.activity, run.lastActivityAt, run.lastActivityMonotonicAt), error: run.error, remoteHost: child?.remoteHost ?? run.remoteHost, }; }
private _rlmChildSnapshotForSession(childId: string, child: AgentSession): RlmChildAgentSnapshot { let answerPreview: string | undefined; let toolUseCount = 0; const messages = child.state.streamingMessage?.role === "assistant" ? [...child.messages, child.state.streamingMessage] : child.messages; for (const message of messages) { if (message.role !== "assistant") continue; const text = compactRlmText(readAssistantText(message)); if (text) answerPreview = text; toolUseCount += message.content.filter((block) => block.type === "toolCall").length; } return { id: childId, parentId: this._rlmParentNodeId, sessionName: child.sessionName, model: child.model ? `${child.model.provider}/${child.model.id}` : undefined, label: child.sessionName ?? "child agent", status: "done", answerPreview, toolUseCount: toolUseCount > 0 ? toolUseCount : undefined, tokenCount: child._contextTokensForCurrentMessages(), recap: child.getCurrentRecap(), sessionDir: child._rlmSessionDir ?? child.sessionManager.getSessionDir(), // No run exists (e.g. a child rehydrated after daemon recovery), so live // session state is the only source for in-flight follow-up work. Mirror // the run projection's convention: status stays "done" (the recorded task // finished) and current work surfaces through activity. activity: child.isSessionActive ? { kind: child.isStreaming ? "writing" : "waiting" } : undefined, repliedSinceTask: child._repliedToParentSinceTask, remoteHost: child.remoteHost, }; }
private _isUnboundTerminalRlmChildRun(run: RlmChildRun): boolean { if (run.session !== undefined || this._rlmChildSessions.has(run.id)) return false; return run.status === "done" || run.status === "error" || run.status === "cancelled"; }
/** Live recursive child roster from lifecycle state, including nested work under retained parents. */ getRlmChildSnapshots(): RlmChildAgentSnapshot[] { const snapshots: RlmChildAgentSnapshot[] = []; const recorded = new Set<string>(); const traversed = new Set<string>(); for (const run of this._activeRlmChildRuns.values()) { const hidden = run.detachedDeletion || this._deletingRlmChildren.has(run.id) || this._deletedRlmChildIds.has(run.id) || this._isUnboundTerminalRlmChildRun(run); const child = run.session; if (!hidden) { snapshots.push(this._rlmChildSnapshotForRun(run)); recorded.add(run.id); } if (child) { traversed.add(run.id); snapshots.push(...child.getRlmChildSnapshots()); } } for (const [childId, { session: child, run }] of this._rlmChildSessions) { if (recorded.has(childId) || traversed.has(childId)) continue; const hidden = this._deletingRlmChildren.has(childId) || this._deletedRlmChildIds.has(childId); if (!hidden) { const snapshot = run ? this._rlmChildSnapshotForRun(run, child) : this._rlmChildSnapshotForSession(childId, child); snapshots.push({ ...snapshot, status: this._rlmChildCleanupFailures.has(childId) ? "cancelled" : snapshot.status, }); } snapshots.push(...child.getRlmChildSnapshots()); } return snapshots; }
/** True when any direct or nested subagent is still running or queued. */ hasRunningRlmChildren(): boolean { for (const session of this._rlmSubtreeSessions()) { for (const run of session._activeRlmChildRuns.values()) { if (run.status === "running" || run.status === "queued") { return true; } } } return false; }
private _rlmChildSessionSnapshot(): AgentSession[] { const sessions = new Set<AgentSession>(); for (const [childId, { session }] of this._rlmChildSessions) { if (!this._abandonedRlmQuiescenceChildIds.has(childId)) sessions.add(session); } for (const run of this._activeRlmChildRuns.values()) { if (run.session && !run.abandonedForQuiescence) sessions.add(run.session); } return [...sessions]; }
private _hasUnsettledRlmQuiescenceWork(): boolean { if (this._hasDeferredCompletionNotices()) return true; if ([...this._unsettledRlmChildRuns].some((run) => !run.settled)) return true; return this._rlmChildSessionSnapshot().some( (child) => child.isSessionActive || child._hasUnsettledRlmQuiescenceWork(), ); }
/** * Wait for every admitted descendant run to publish its terminal parent * message and for the resulting parent turns to drain. Re-snapshotting after * each drain includes descendants spawned while earlier results were consumed. */ async waitForRlmQuiescence(externalSignal?: AbortSignal): Promise<void> { const cancellation = new AbortController(); const cancelFromParent = () => cancellation.abort(); if (externalSignal?.aborted) cancellation.abort(); else externalSignal?.addEventListener("abort", cancelFromParent, { once: true }); this._rlmQuiescenceWaitAborts.add(cancellation); let rejectCancelled = (_error: Error) => {}; const cancelled = new Promise<never>((_resolve, reject) => { rejectCancelled = reject; }); const onCancelled = () => rejectCancelled(new Error("RLM quiescence wait cancelled")); cancellation.signal.addEventListener("abort", onCancelled, { once: true }); if (cancellation.signal.aborted) onCancelled(); const wait = <T>(operation: Promise<T>): Promise<T> => Promise.race([operation, cancelled]); try { while (true) { await wait(this.waitForHeadlessIdle()); // Strong RLM quiescence also owns work that interactive waitForIdle ignores. if (this.isSessionActive || this._hasDeferredCompletionNotices()) { await wait(this._waitForSessionActivityChange(cancellation.signal)); continue; } const unsettledRuns = [...this._unsettledRlmChildRuns].filter((run) => !run.settled); const childSessions = this._rlmChildSessionSnapshot(); if (unsettledRuns.length === 0 && !this._hasUnsettledRlmQuiescenceWork()) return; await wait( Promise.all([ ...unsettledRuns.map((run) => run.settlement.promise), ...childSessions.map((child) => child.waitForRlmQuiescence(cancellation.signal)), ]), ); // Always loop through the self-active/deferred checks again. Work may // start at the child-settlement boundary. } } finally { // A local descendant error must cancel sibling recursive waits owned by // this barrier before their propagation listeners are removed. cancellation.abort(); externalSignal?.removeEventListener("abort", cancelFromParent); cancellation.signal.removeEventListener("abort", onCancelled); this._rlmQuiescenceWaitAborts.delete(cancellation); } }
// Inline (non-daemon) mode only; daemon clients attach to the child session directly. getRlmChildSession(childId: string): AgentSession | undefined { for (const session of this._rlmSubtreeSessions()) { const direct = session._activeRlmChildRuns.get(childId)?.session ?? session._rlmChildSessions.get(childId)?.session; if (direct) { return direct; } } return undefined; }
/** * Cancel a single RLM child run by id, searching nested child sessions. * * @returns true when a live run was cancelled or its unsettled terminal notice * was suppressed; false when the id is unknown or the run already settled. */ cancelRlmChildRun(childId: string, reason = "Cancelled by user"): boolean { for (const session of this._rlmSubtreeSessions()) { const run = session._activeRlmChildRuns.get(childId); if (run) { if (run.status !== "running" && run.status !== "queued" && !run.settled) { if (session._sessionInputPumpSuspended) session._abandonRlmRunForQuiescence(run); else run.suppressTerminalNotice = true; return true; } // The abort cascade never reaches running work retained under a settled descendant. const cancelled = session._cancelRlmChildRun(run, reason); const descendantsCancelled = run.session?.cancelRunningRlmDescendants(reason) ?? false; if (cancelled || descendantsCancelled) { return true; } } // A fruitless match keeps walking: child ids are only mkdir-unique among // siblings, so a colliding live run elsewhere must stay reachable. if (session._rlmChildSessions.get(childId)?.session.cancelRunningRlmDescendants(reason)) { return true; } } return false; }
// A done child sits in BOTH maps until passivation; the visited set keeps that dual membership from doubling the walk. private *_rlmSubtreeSessions(): Generator<AgentSession> { const visited = new Set<AgentSession>([this]); const stack: AgentSession[] = [this]; while (stack.length > 0) { const session = stack.pop()!; yield session; for (const run of session._activeRlmChildRuns.values()) { if (run.session && !visited.has(run.session)) { visited.add(run.session); stack.push(run.session); } } for (const { session: retained } of session._rlmChildSessions.values()) { if (!visited.has(retained)) { visited.add(retained); stack.push(retained); } } } }
/** Cancel every running or queued run in this session's subtree. */ cancelRunningRlmDescendants(reason = "Cancelled by user"): boolean { let cancelled = false; for (const session of this._rlmSubtreeSessions()) { for (const run of session._activeRlmChildRuns.values()) { if (session._cancelRlmChildRun(run, reason)) cancelled = true; } } return cancelled; }
private async _assertRlmSubagentSessionNameAvailable(name: string, ignorePendingReservation = false): Promise<void> { const depth = this._rlmDepth + 1; if (!ignorePendingReservation && this._pendingRlmSubagentSessionNames.has(name)) { throw new Error(formatAgentSessionNameUnavailable(name, depth)); } const localConflict = [...this._activeRlmChildRuns.values()].some( (run) => run.session?.sessionName === name || (!run.session && run.sessionName === name), ) || [...this._rlmChildSessions.values()].some(({ session }) => session.sessionName === name) || [...this._rlmChildCleanupFailures.values()].some((entry) => entry.session_name === name); if (localConflict) { throw new Error(formatAgentSessionNameUnavailable(name, depth)); } const controller = this._agentMessageController; if (!controller) return; const input = { name, depth, parentSessionId: this.sessionId, parentSessionPath: this.sessionFile, }; if (controller.assertSessionNameAvailable) { await controller.assertSessionNameAvailable(input); return; } const listed = await controller.listAgents(); const catalog = listed.agents.map( (agent): AgentFamilyCatalogEntry => ({ id: agent.sessionId, ...(agent.sessionName ? { name: agent.sessionName } : {}), depth: agent.rlmDepth ?? 0, status: agent.status ?? "idle", ...(agent.parentSessionId ? { parentSessionId: agent.parentSessionId } : {}), ...(agent.parentSessionPath ? { parentSessionPath: agent.parentSessionPath } : {}), ...(agent.sessionPath ? { sessionPath: agent.sessionPath } : {}), }), ); assertAgentSessionNameAvailable(catalog, input); }
private async _authenticatedRlmModels(): Promise<Model<Api>[]> { return (await this._modelRegistry.getExecutableModels()).filter((model) => { const status = this._modelRegistry.getProviderAuthStatus(model.provider); return status.source !== "stale" && status.label !== "expired"; }); }
/** * Horizon is a soft allowance. Exceeding it extends the allowance and tells the * parent; it never kills the child, because a child that ran long is usually a * child that was wanted. */ /** * Return the categorized available models from the TOML store. * Marks that the model has called .models() for this compaction window. */ async modelsRlm(): Promise<Record<string, string[]>> { const rawModels = this._rlmModelsStore.getModels(); const authenticated = await this._authenticatedRlmModels(); const authenticatedKeys = new Set(authenticated.map((m) => `${m.provider}/${m.id}`.toLowerCase())); const filtered: Record<string, string[]> = {}; for (const [category, list] of Object.entries(rawModels)) { filtered[category] = list.filter((id) => authenticatedKeys.has(id.toLowerCase())); } // Reserved key: a tripped provider stays listed in its categories so a forced spawn // stays possible, but it is named here so the caller can route around it. filtered.exhausted = this._modelRegistry.exhaustedProviders(); this._hasCalledRlmModels = true; try { this.sessionManager.appendCustomEntry("prime-agent.rlm-models-called", { timestamp: Date.now() }); } catch { // Best-effort append; ignore in lightweight harnesses without writable session storage } return filtered; }
get hasCalledRlmModels(): boolean { if (this._hasCalledRlmModels) return true; try { const branch = this.sessionManager.getBranch(); const compactionEntry = getLatestCompactionEntry(branch); const compactionIndex = compactionEntry ? branch.indexOf(compactionEntry) : -1; const relevant = compactionIndex >= 0 ? branch.slice(compactionIndex + 1) : branch; for (let i = relevant.length - 1; i >= 0; i--) { const entry = relevant[i]; if (entry && entry.type === "custom" && entry.customType === "prime-agent.rlm-models-called") { const ts = typeof (entry as any).data?.timestamp === "number" ? (entry as any).data.timestamp : 0; if (ts > this._rlmModelsGateResetAt) { this._hasCalledRlmModels = true; return true; } } } } catch { // Best-effort check } return false; }
set hasCalledRlmModels(value: boolean) { this._hasCalledRlmModels = value; if (!value) { this._rlmModelsGateResetAt = Date.now(); } } private async _resolveRlmSubagentModel( reference: string | undefined, target = "subagent", ): Promise<RlmSubagentModelSelection> { const parentModel = this.model; if (!parentModel) { throw new Error(formatNoModelSelectedMessage()); } if (!reference) { return { model: parentModel }; }
const normalizedReference = reference.toLowerCase(); if (`${parentModel.provider}/${parentModel.id}`.toLowerCase() === normalizedReference) { // The parent model is in active use, so a catalog miss must not // exclude it (e.g. an offline discovery refresh), but a stale or // expired provider has to fail the spawn here instead of starting // a child that fails its first model request. const status = this._modelRegistry.getProviderAuthStatus(parentModel.provider); if (status.source === "stale" || status.label === "expired") { throw new Error(`Requested ${target} model "${reference}" is unavailable, unauthenticated, or expired`); } const auth = await this._modelRegistry.getApiKeyAndHeaders(parentModel); if (!auth.ok) { throw new Error(`Requested ${target} model "${reference}" failed authentication preflight`); } return { model: parentModel }; } const candidates = await this._authenticatedRlmModels(); // The parent model can be missing from the authenticated catalog (offline // discovery or expired credentials) while staying selectable, so it backs // the short-form lookup when the catalog has no match. Several catalog // matches still leave the reference unresolved. const model = candidates.find( (candidate) => `${candidate.provider}/${candidate.id}`.toLowerCase() === normalizedReference, ) ?? findUniqueRlmShortFormModelMatch(reference, candidates, parentModel); if (!model) { throw new Error(formatRlmModelUnavailableError(reference, target, candidates)); }
const auth = await this._modelRegistry.getApiKeyAndHeaders(model, undefined, this.sessionId); if (!auth.ok) { throw new Error(`Requested ${target} model "${reference}" failed authentication preflight`); } return { model }; }
private async _startRlmChildRun( prompt: string, kwargs: Record<string, unknown> = {}, spawnCode?: string, ): Promise<RlmSpawnHandle> { // Snapshot before any await: the spawning request is the turn whose tool call is // executing now. A spawn arriving outside an active run (a detached kernel task // firing while the parent is idle) has no such turn; an absent edge beats a wrong one. const spawnedByRequestId = this.isStreaming ? this._semanticEdges.lastTurnRequestId : undefined; if (!this.hasCalledRlmModels) { throw new Error("rlm.spawn requires calling await rlm.models() first to discover available models"); } const { name: rawName, model: rawModel, thinking: rawThinking, host: rawHost, workspace: rawWorkspace, allow_push: rawAllowPush, cwd: rawCwd, ...unsupported } = kwargs; const unsupportedKwargs = Object.keys(unsupported); if (unsupportedKwargs.length > 0) { throw new Error(`Unsupported rlm.spawn kwargs: ${unsupportedKwargs.sort().join(", ")}`); } if (rawAllowPush !== undefined && typeof rawAllowPush !== "boolean") { throw new Error("allow_push must be a boolean"); } if (rawWorkspace !== undefined && rawWorkspace !== "isolated") { throw new Error('workspace must be "isolated" when provided'); } if (rawCwd !== undefined && (typeof rawCwd !== "string" || !rawCwd.trim())) { throw new Error("cwd must be a non-empty string"); } const requestedAllowPush = rawAllowPush === true; // The runtime creates the checkout and forwards it as `cwd`; a bare cwd is honoured too. const requestedCwd = typeof rawCwd === "string" && rawCwd.trim() ? rawCwd.trim() : undefined; if (rawModel === undefined) { throw new Error("rlm.spawn requires model="); } const requestedHost = typeof rawHost === "string" && rawHost.trim() ? rawHost.trim() : undefined; const requestedSessionName = normalizeRequestedRlmSubagentSessionName(rawName); if (requestedSessionName) assertDirectAgentMessageTarget(requestedSessionName); if (this._rlmDepth >= this._rlmMaxDepth) { throw new Error( `RLM recursion depth limit reached (RLM_DEPTH=${this._rlmDepth}, RLM_MAX_DEPTH=${this._rlmMaxDepth})`, ); } if (requestedSessionName) { if (this._pendingRlmSubagentSessionNames.has(requestedSessionName)) { throw new Error(formatAgentSessionNameUnavailable(requestedSessionName, this._rlmDepth + 1)); } this._pendingRlmSubagentSessionNames.add(requestedSessionName); } let modelSelection: RlmSubagentModelSelection; const requestedModel = normalizeRequestedRlmSubagentModel(rawModel); const requestedThinkingLevel = normalizeRequestedRlmSubagentThinkingLevel(rawThinking); try { if (requestedSessionName) await this._assertRlmSubagentSessionNameAvailable(requestedSessionName, true); modelSelection = await this._resolveRlmSubagentModel(requestedModel); } finally { if (requestedSessionName) this._pendingRlmSubagentSessionNames.delete(requestedSessionName); } if (requestedThinkingLevel !== undefined) { const supported = getSupportedThinkingLevels(modelSelection.model) as ThinkingLevel[]; if (!supported.includes(requestedThinkingLevel)) { throw new Error( `Requested thinking level "${requestedThinkingLevel}" is not supported by model "${modelSelection.model.provider}/${modelSelection.model.id}"; supported levels: ${supported.join(", ")}`, ); } } const thinkingLevel = requestedThinkingLevel ?? (clampThinkingLevel(modelSelection.model, this.thinkingLevel) as ThinkingLevel); if (this._disposed || this._disposing) throw new Error("Cannot spawn a subagent after its parent was disposed"); // Local admission gate: configured auth plus the quota breaker, checked before any // child directory, session, or provider request exists. Zero network requests. this._modelRegistry.assertRlmSpawnAdmission(modelSelection.model);
const childSessionDir = this._createChildRlmSessionDir(); const childNodeId = basename(childSessionDir); const sessionName = requestedSessionName ?? createDefaultRlmSubagentSessionName(prompt, childNodeId); if (!requestedSessionName) await this._assertRlmSubagentSessionNameAvailable(sessionName); const startedAt = Date.now(); const parentAssistantForUsage = this._findLastAssistantMessage(); if (parentAssistantForUsage && !this._rlmDurableParentUsage.has(parentAssistantForUsage)) { this._rlmDurableParentUsage.set(parentAssistantForUsage, cloneUsage(parentAssistantForUsage.usage)); } // Child completions accumulate per origin and flush one durable entry per // settle boundary (agent_end, settlement); the staleness checkpoints and // timer bound crash loss to one window of accumulated usage. const pendingChildUsage = new Map<ChildUsageAttributionEntry["origin"], Usage>(); let pendingChildUsageSince = 0; let pendingChildUsageTimer: ReturnType<typeof setTimeout> | undefined; let parentEntryDrainScheduled = false; const flushPendingChildUsageAttribution = (afterParentDrain = false) => { if (pendingChildUsageTimer !== undefined) { clearTimeout(pendingChildUsageTimer); pendingChildUsageTimer = undefined; } if (pendingChildUsage.size === 0 || !parentAssistantForUsage) return; const parentEntry = this._findAssistantEntryForMessage(parentAssistantForUsage); if (!parentEntry) { if (!afterParentDrain && !parentEntryDrainScheduled) { parentEntryDrainScheduled = true; const flushAfterParentDrain = () => { parentEntryDrainScheduled = false; flushPendingChildUsageAttribution(true); }; // A message_end extension may still be holding the parent assistant before its append. // The parent drain owns this retry; child settlement never waits for that queue. this._agentEventQueue = this._agentEventQueue.then(flushAfterParentDrain, flushAfterParentDrain); this._agentEventQueue.catch(() => {}); } return; } const batches = [...pendingChildUsage.entries()]; pendingChildUsage.clear(); for (const [origin, childUsage] of batches) { const aggregateUsage = cloneUsage(this._rlmDurableParentUsage.get(parentAssistantForUsage)!); attributeChildUsage(aggregateUsage, childUsage); const liveUsage = parentAssistantForUsage.usage; const entryCount = this.sessionManager.getEntries().length; try { this.sessionManager.appendChildUsageAttribution(parentEntry.id, childUsage, aggregateUsage, origin); this._rlmDurableParentUsage.set(parentAssistantForUsage, aggregateUsage); } catch { // Attribution is recoverable bookkeeping; a failed append must not break run settlement. } finally { // The manager updates this same message; retain siblings' still-pending live usage. parentAssistantForUsage.usage = liveUsage; const indexed = this.sessionManager.getEntries()[entryCount]; const unindexedUsage = this._rlmUnindexedChildUsage.get(parentAssistantForUsage); // _persist can throw after indexing. That row already participates in live own-usage subtraction. if ( indexed?.type === "child_usage_attributed" && indexed.targetId === parentEntry.id && unindexedUsage ) { subtractAssistantUsage(unindexedUsage, childUsage); } this._ownUsageMemo = undefined; } } }; const flushPendingChildUsageIfStale = () => { if ( pendingChildUsage.size > 0 && Date.now() - pendingChildUsageSince >= RLM_CHILD_USAGE_FLUSH_MAX_PENDING_MS ) { flushPendingChildUsageAttribution(); } }; let runningToolCount = 0; let childSession: AgentSession | undefined; const startedMonotonicAt = performance.now(); const targetHost = requestedHost === "local" ? undefined : (requestedHost ?? this._remoteHost); const run: RlmChildRun = { id: childNodeId, prompt, sessionName, sessionDir: childSessionDir, model: modelSelection.model, status: "queued", toolUseCount: 0, progressNotes: [], // Seed the staleness clock at admission: a child hung before its // first tracked event still crosses the threshold once running. lastActivityAt: startedAt, lastActivityMonotonicAt: startedMonotonicAt, settled: false, abort: noopRlmChildAbort, publication: createAgentMessageDeferred(), settlement: createAgentMessageDeferred(), deletionReservation: createAgentMessageDeferred(), remoteHost: targetHost, }; const throwIfCancelled = () => { if (run.status === "cancelled") throw new Error(run.error ?? "RLM child cancelled"); }; this._activeRlmChildRuns.set(run.id, run); this._unsettledRlmChildRuns.add(run); const emitChildUpdate = () => { const child = this._rlmChildSnapshotForRun(run); // Dedup compares observable child state, not clock-derived fields: // lastActivityAt advances on every streamed token delta and // activityStaleMs is recomputed on each snapshot build, so including // either would re-emit on every delta once answerPreview saturates its // cap. Emitted snapshots still carry both fields fresh. const { lastActivityAt: _lastActivityAt, activityStaleMs: _activityStaleMs, ...stable } = child; const serialized = JSON.stringify(stable); if (serialized === run.lastEmittedUpdate) return; run.lastEmittedUpdate = serialized; this._emit({ type: "rlm_child_update", child }); }; run.emitUpdate = emitChildUpdate; emitChildUpdate();
const publishChildSession = (child: AgentSession) => { childSession = child; if (this._activeRlmChildRuns.get(run.id) !== run) return; run.session = child; run.abort = () => void child.abort(); run.publication.resolve(); // Cancellation may have been admitted while runtime construction was // blocked and run.abort was still a no-op. if (run.status === "cancelled") run.abort(); }; const subagentOptions: CreateRlmSubagentRuntimeOptions = { ...this._createRlmSubagentRuntimeOptions({ id: childNodeId, prompt, sessionName, spawnCode, sessionDir: childSessionDir, model: modelSelection.model, thinkingLevel, spawnedByRequestId, remoteHost: targetHost, allowPush: requestedAllowPush, cwd: requestedCwd, }), onSessionPublished: publishChildSession, };
const deliverTerminalMessageToParent = async (message: CustomMessage): Promise<void> => { // Synthesized lifecycle notices always use the parent's private durable // path. Explicit child replies continue through agent_message separately. await this._deferRlmTerminalNotice(message); };
run.completeDeletion = () => { if (!run.deletionNeedsCompletionNotice || run.suppressTerminalNotice || this._disposed || this._disposing) { return Promise.resolve(); } if (run.deletionNotice) return run.deletionNotice; const notice = deliverTerminalMessageToParent( createRlmChildTerminalNoticeMessage({ kind: "cancelled", childId: run.id, sessionName, reason: run.error ?? "Deleted by parent orchestrator", }), ); run.deletionNotice = notice; return notice; };
run.reportDeletionCleanupFailure = (error) => { if (run.suppressTerminalNotice || this._disposed || this._disposing) return Promise.resolve(); if (run.deletionFailureNotice) return run.deletionFailureNotice; const cleanupError = error instanceof Error ? error.message : String(error); const notice = deliverTerminalMessageToParent( createRlmChildFailureMessage({ childId: run.id, sessionName, error: `Deletion cleanup failed; retry rlm.delete_subagent("${run.id}") before completion: ${cleanupError}`, }), ); run.deletionFailureNotice = notice; return notice; };
// Runtime startup and the task run are deliberately detached. The public // spawn resolves at admission, while this task owns live tracking, usage, // retention, cancellation, and late-startup cleanup. void (async () => { let childRuntime: RlmSubagentRuntime | undefined; try { childRuntime = await this._createRlmSubagentRuntime(subagentOptions); const child = childRuntime.session; if (run.status === "cancelled") throw new Error(run.error ?? "RLM child cancelled"); if (child.sessionName !== sessionName) child.setSessionName(sessionName); publishChildSession(child); throwIfCancelled(); run.status = "running"; emitChildUpdate(); const unsubscribeChildEvents = child.subscribe((event) => { if (event.type === "rlm_child_update") { this._emit(event); return; } if (event.type === "agent_start") { run.activity = { kind: "waiting" }; touchRlmChildActivity(run); emitChildUpdate(); } else if (event.type === "agent_end") { flushPendingChildUsageAttribution(); run.activity = undefined; touchRlmChildActivity(run); emitChildUpdate(); } else if (event.type === "rlm_progress_note") { run.progressNotes.push(event.message); if (run.progressNotes.length > RLM_CHILD_PROGRESS_NOTE_RING_MAX) { run.progressNotes.shift(); } touchRlmChildActivity(run); emitChildUpdate(); } else if (event.type === "message_end" && event.message.role === "assistant") { const assistant = event.message as AssistantMessage; if (assistant.stopReason !== "error" && assistant.stopReason !== "aborted") { // Flush before the fold: a persisted aggregate may only include // completions whose childUsage is durable with or before it. flushPendingChildUsageIfStale(); attributeChildUsage(parentAssistantForUsage?.usage ?? emptyUsage(), assistant.usage); if (parentAssistantForUsage) { const unindexedUsage = this._rlmUnindexedChildUsage.get(parentAssistantForUsage) ?? emptyUsage(); addAssistantUsage(unindexedUsage, assistant.usage); this._rlmUnindexedChildUsage.set(parentAssistantForUsage, unindexedUsage); this._ownUsageMemo = undefined; const origin = rlmChildUsageOrigin(child.messages, assistant); if (pendingChildUsage.size === 0) { pendingChildUsageSince = Date.now(); // Wall-clock backstop for long tool runs without checkpoints. pendingChildUsageTimer = setTimeout( flushPendingChildUsageAttribution, RLM_CHILD_USAGE_FLUSH_MAX_PENDING_MS, ); pendingChildUsageTimer.unref?.(); } const bucket = pendingChildUsage.get(origin) ?? emptyUsage(); addAssistantUsage(bucket, assistant.usage); pendingChildUsage.set(origin, bucket); } } const text = compactRlmText(readAssistantText(assistant)); if (text) run.answerPreview = text; touchRlmChildActivity(run); emitChildUpdate(); } else if (event.type === "message_start" || event.type === "message_update") { if (event.message.role === "assistant") { const text = compactRlmText(readAssistantText(event.message as AssistantMessage)); if (text) run.answerPreview = text; run.activity = { kind: "writing" }; touchRlmChildActivity(run); emitChildUpdate(); } } else if (event.type === "tool_execution_start") { flushPendingChildUsageIfStale(); run.toolUseCount += 1; runningToolCount += 1; run.activity = { kind: "executing", toolName: event.toolName }; touchRlmChildActivity(run); emitChildUpdate(); } else if (event.type === "tool_execution_end") { runningToolCount = Math.max(0, runningToolCount - 1); if (runningToolCount === 0) run.activity = { kind: "waiting" }; touchRlmChildActivity(run); emitChildUpdate(); } else if (event.type === "session_info_changed" || event.type === "recap_update") { emitChildUpdate(); } }); run.unsubscribe = unsubscribeChildEvents; // The task envelope stays exactly the task: the reply obligation lives in the // child doctrine (system prompt), so an echo of the prompt is still the task. const content = `[task from parent]\n\n${prompt}`; const spawnMessage: AgentSessionMessage = { role: "custom", customType: AGENT_MESSAGE_CUSTOM_TYPE, content, display: true, details: { id: `spawn:${run.id}`, message: prompt, from: { sessionId: this.sessionId, sessionName: this.sessionName, activeSessionId: await this._currentActiveSessionId(), }, fromRelationship: "parent", }, timestamp: Date.now(), }; throwIfCancelled(); const parentReplyCountBeforeRun = child._parentReplyCount; await child.promptAndWait(content, { expandPromptTemplates: false, source: "extension", customMessage: spawnMessage, }); await child.waitForRlmQuiescence(); if (run.error) throw new Error(run.error); // A turn that ended in a provider error resolves like any other, so without this // the child is recorded as done and the parent's only signal is a no-reply notice // that hides the cause (a depleted lane, a dead credential). Surfacing it as the // run's error routes it through the terminal failure notice instead. const terminalFailure = childRlmTerminalFailure(child); if (terminalFailure) throw new Error(terminalFailure); run.status = "done"; // Only successful completions return; the edge lands on the parent's next commit. const childLastCommitted = child.semanticEdges.lastCommittedRequestId; if (childLastCommitted !== undefined) { this._semanticEdges.recordChildReturned(child.sessionId, childLastCommitted); } run.durationMs = Date.now() - startedAt; run.activity = undefined; emitChildUpdate(); if ( !run.detachedDeletion && !run.suppressTerminalNotice && child._parentReplyCount === parentReplyCountBeforeRun ) { const lastAssistantText = child.getLastAssistantText(); // R11: a child that finished without replying must still hand over what it // produced, tagged so the parent can tell a harvest from an answer. An // empty child still gets the plain no-reply notice. await deliverTerminalMessageToParent( lastAssistantText && lastAssistantText.trim().length > 0 ? createRlmChildTerminalNoticeMessage({ kind: "harvested_fallback", childId: run.id, sessionName, text: lastAssistantText, }) : createRlmChildTerminalNoticeMessage({ kind: "completed_without_reply", childId: run.id, sessionName, }), ); } if (!this.registerRlmChildSession(run.id, child) && !run.detachedDeletion) { if (childRuntime && this._subagentRuntimeHost?.releaseRlmSubagentRuntime) { await this._subagentRuntimeHost .releaseRlmSubagentRuntime(childRuntime, subagentOptions, "error") .catch(() => void child.disposeAsync().catch(() => undefined)); } else { await child.disposeAsync().catch(() => undefined); } } } catch (error) { const runError = error instanceof Error ? error : new Error(String(error)); run.publication.reject(runError); if (run.status !== "cancelled") { run.status = "error"; run.error = runError.message; } // A failed child still returns an error outcome the parent consumes; // cancelled runs and zero-commit children return nothing. const failedChild = childSession ?? childRuntime?.session; const failedLastCommitted = failedChild?.semanticEdges.lastCommittedRequestId; if (run.status === "error" && failedChild && failedLastCommitted !== undefined) { this._semanticEdges.recordChildReturned(failedChild.sessionId, failedLastCommitted); } run.durationMs = Date.now() - startedAt; run.activity = undefined; if (run.status === "error" && childSession === undefined) { // A pre-bind failure leaves no row: "cancelled" is the wire's removal signal. this._emit({ type: "rlm_child_update", child: { ...this._rlmChildSnapshotForRun(run), status: "cancelled" }, }); } else { emitChildUpdate(); } if (!run.detachedDeletion && !run.suppressTerminalNotice) { if (run.status === "error") { await deliverTerminalMessageToParent( createRlmChildFailureMessage({ childId: run.id, sessionName, error: run.error ?? "unknown error", status: run.status, durationMs: run.durationMs, toolUseCount: run.toolUseCount, lastAssistantTextPreview: compactRlmText(childSession?.getLastAssistantText() ?? ""), hint: childSession === undefined ? "the child never bound a session: check the provider's quota and this session's credentials before respawning" : "read the child's transcript before respawning into the same failure", }), ); } else if (run.status === "cancelled") { await deliverTerminalMessageToParent( createRlmChildTerminalNoticeMessage({ kind: "cancelled", childId: run.id, sessionName, reason: run.error, }), ); } } if (!run.detachedDeletion && childSession && this._subagentRuntimeHost?.releaseRlmSubagentRuntime) { try { await this._subagentRuntimeHost.releaseRlmSubagentRuntime( childRuntime ?? { session: childSession }, subagentOptions, run.status === "cancelled" ? "cancelled" : "error", ); if (run.status === "cancelled" && !this._disposed && !this._disposing) { this._deletedRlmChildIds.add(run.id); this._removeRlmSubagentTracking(run.id); } } catch { await childSession?.disposeAsync().catch(() => undefined); } } else if (!run.detachedDeletion) { try { if (childRuntime && this._subagentRuntimeHost) { await this._subagentRuntimeHost.deleteRlmSubagentRuntime(run.id, childRuntime.session); } else if (childSession) { await childSession.disposeAsync(); } if (run.status === "cancelled" && !this._disposed && !this._disposing) { this._deletedRlmChildIds.add(run.id); this._removeRlmSubagentTracking(run.id); } } catch { // A failed best-effort retry remains available through the retained cleanup maps. } } } finally { flushPendingChildUsageAttribution(); if (run.detachedDeletion) { run.deletionRunFinished = true; if (!run.settled) { let cleanupSucceeded = !run.deletionCleanupFailed; if (childRuntime && cleanupSucceeded) { const cleanup = run.deletionCleanup ?? this._ensureRlmRunDeletionCleanup(run, childRuntime.session); cleanupSucceeded = await this._observeRlmRunDeletionCleanup( run, run.detachedDeletion, childRuntime.session, cleanup, ); } if (cleanupSucceeded) await this._finishRlmRunDeletion(run); } } else { if (this._activeRlmChildRuns.get(run.id) === run) { if (this._rlmChildSessions.has(run.id)) { this._activeRlmChildRuns.delete(run.id); if (run.unsubscribe) this._rlmChildUnsubscribes.set(run.id, run.unsubscribe); run.abort = noopRlmChildAbort; run.unsubscribe = undefined; run.session = undefined; } else if (run.status !== "error") { this._removeRlmSubagentTracking(run.id, run); } else { run.unsubscribe?.(); run.abort = noopRlmChildAbort; run.unsubscribe = undefined; } } run.settled = true; run.settlement.resolve(); this._unsettledRlmChildRuns.delete(run); this._maybeResumeGoalContinuationAfterRlmWork(); this._maybeResumeAutonomousContinuationAfterRlmWork(); } } })().catch(() => undefined);
return { rlm_child_id: childNodeId, name: sessionName, session_dir: childSessionDir, model: `${modelSelection.model.provider}/${modelSelection.model.id}`, }; }
async createRlmSession(prompt: string, kwargs: Record<string, unknown> = {}): Promise<RlmCreateSessionResult> { const { name: rawName, model: rawModel, thinking: rawThinking, cwd: rawCwd, ...unsupported } = kwargs; const unsupportedKeys = Object.keys(unsupported); if (unsupportedKeys.length > 0) { throw new Error(`Unsupported rlm.create_session kwargs: ${unsupportedKeys.sort().join(", ")}`); } if (!prompt.trim()) { throw new Error("rlm.create_session prompt must not be empty"); } if (this._rlmDepth !== 0) { throw new Error("rlm.create_session is available only from a depth-0 session"); } if (this._disposed || this._disposing) { throw new Error("Cannot create a top-level session after the current session was disposed"); } const host = this._subagentRuntimeHost; if (!host?.createRlmRootSession) { throw new Error("rlm.create_session requires a daemon-backed depth-0 session"); }
const operation = "rlm.create_session"; const sessionName = normalizeRequestedRlmSubagentSessionName(rawName, operation); const requestedModel = normalizeRequestedRlmSubagentModel(rawModel, operation); const requestedThinkingLevel = normalizeRequestedRlmSubagentThinkingLevel(rawThinking, operation); if (sessionName) { assertDirectAgentMessageTarget(sessionName); const controller = this._agentMessageController; if (controller?.assertSessionNameAvailable) { await controller.assertSessionNameAvailable({ name: sessionName, depth: 0 }); } } if (rawCwd !== undefined && (typeof rawCwd !== "string" || !rawCwd.trim())) { throw new Error("rlm.create_session cwd must be a non-empty string"); } const cwd = rawCwd === undefined ? this._cwd : resolve(this._cwd, rawCwd.trim()); const modelSelection = await this._resolveRlmSubagentModel(requestedModel, "top-level session"); if (requestedThinkingLevel !== undefined) { const supported = getSupportedThinkingLevels(modelSelection.model) as ThinkingLevel[]; if (!supported.includes(requestedThinkingLevel)) { throw new Error( `Requested thinking level "${requestedThinkingLevel}" is not supported by model "${modelSelection.model.provider}/${modelSelection.model.id}"; supported levels: ${supported.join(", ")}`, ); } } const thinkingLevel = requestedThinkingLevel ?? (clampThinkingLevel(modelSelection.model, this.thinkingLevel) as ThinkingLevel); if (this._disposed || this._disposing) { throw new Error("Cannot create a top-level session after the current session was disposed"); } return host.createRlmRootSession({ prompt, sessionName, cwd, model: modelSelection.model, thinkingLevel, }); }
async runRlmChild( prompt: string, kwargs: Record<string, unknown> = {}, spawnCode?: string, ): Promise<RlmSpawnHandle> { return this._startRlmChildRun(prompt, kwargs, spawnCode); }
private _isRetryableError(message: AssistantMessage): boolean { if (message.stopReason !== "error" || !message.errorMessage) return false;
const contextWindow = this.model?.contextWindow ?? 0; if (isContextOverflow(message, contextWindow)) return false;
if (this._isFauxProviderQueueExhausted(message)) { return false; }
if (this._isAgentLifecycleFailure(message)) { return false; }
if (this._isStructuredPermanentProviderRetryExhausted(message)) { return false; }
return true; }
private _isFauxProviderQueueExhausted(message: AssistantMessage): boolean { return isFauxProviderQueueExhausted(message); }
private _isAgentLifecycleFailure(message: AssistantMessage): boolean { return isAgentLifecycleFailure(message); }
private _getProviderStreamFailureKind(message: AssistantMessage): string | undefined { const kind = providerStreamFailureKind(message); if (kind) return kind; if (message.errorMessage) { const classified = classifyStreamFailure(undefined, undefined, message.errorMessage); if (classified !== "unknown") return classified; } return undefined; }
private _getProviderStreamFailureStatus(message: AssistantMessage): number | undefined { const status = providerStreamFailureStatus(message); if (status !== undefined) return status; const match = /\b(429|500|502|503|504|529|401|403|404)\b/.exec(message.errorMessage ?? ""); return match ? Number(match[1]) : undefined; }
private _isStructuredPermanentProviderRetryExhausted(message: AssistantMessage): boolean { return isPermanentProviderFailureKind( this._getProviderStreamFailureKind(message), this._retryAttempt, this._getProviderStreamFailureStatus(message), ); }
private _isProviderRateLimitFailure(message: AssistantMessage): boolean { return this._getProviderStreamFailureKind(message) === "rate_limit"; }
private _isProviderQuotaExhaustedFailure(message: AssistantMessage): boolean { return this._getProviderStreamFailureKind(message) === "quota_exhausted"; }
private _isConcreteProviderAuthFailure(message: AssistantMessage): boolean { if (message.stopReason !== "error" || !message.errorMessage) return false; // Only the provider's structured classification counts as an auth failure. return this._getProviderStreamFailureKind(message) === "auth"; }
private _captureRetryAuthFailureSource(message: AssistantMessage): AuthSourceToken | undefined { const token = this._modelRegistry.getCurrentProviderAuthSourceToken(message.provider, this.sessionId); if (!token) { return undefined; } if ( !this._retryAuthFailureSources.some( (existing) => existing.provider === token.provider && existing.source === token.source && existing.identityFingerprint === token.identityFingerprint && existing.valueFingerprint === token.valueFingerprint, ) ) { this._retryAuthFailureSources.push(token); } return token; }
private _markProviderAuthStale(message: AssistantMessage, authSourceTokens?: readonly AuthSourceToken[]): boolean { if (authSourceTokens && authSourceTokens.length > 0) { let marked = false; for (const token of authSourceTokens) { marked = this._modelRegistry.markProviderAuthSourceStale(token) || marked; } if (marked) { this._emit({ type: "auth_stale", provider: message.provider, sourceTokens: authSourceTokens, }); } return marked; } const marked = this._modelRegistry.markProviderAuthStale(message.provider, this.sessionId); if (marked) { this._emit({ type: "auth_stale", provider: message.provider }); } return marked; }
private _markProviderAuthStaleForRetryFailure( message: AssistantMessage, options?: { markAuthStaleOnFailure?: boolean; authSourceTokens?: readonly AuthSourceToken[]; }, ): boolean { const authSourceTokens = this._retryAuthFailureSources.length > 0 ? this._retryAuthFailureSources : options?.authSourceTokens; if ((authSourceTokens?.length ?? 0) > 0 || options?.markAuthStaleOnFailure) { const marked = this._markProviderAuthStale(message, authSourceTokens); if (marked && message.errorMessage) { message.errorMessage = addLoginGuidanceToAuthError(message.errorMessage); } return marked; } return false; }
private _finishActiveRetryWithFailure(message: AssistantMessage): void { if (this._retryAttempt === 0) { return; } this._markProviderAuthStaleForRetryFailure(message); this._restorePrimaryModelAfterBackup(); this._emit({ type: "auto_retry_end", success: false, attempt: this._retryAttempt, finalError: message.errorMessage, }); this._retryAttempt = 0; this._providerWait = undefined; this._retryAuthFailureSources = []; }
private async _handleRetryableError( message: AssistantMessage, options?: { markAuthStaleOnFailure?: boolean; authSourceTokens?: readonly AuthSourceToken[]; retryWithSibling?: boolean; }, ): Promise<boolean> { const settings = this.settingsManager.getRetrySettings(); if (!settings.enabled || settings.maxRetries <= 0) { this._markProviderAuthStaleForRetryFailure(message, options); this._retryAuthFailureSources = []; this._resolveRetry(); return false; }
if (!this._retryPromise) { this._retryPromise = new Promise((resolve) => { this._retryResolve = resolve; }); }
const waitClass = providerWaitClass( this._getProviderStreamFailureKind(message), this._getProviderStreamFailureStatus(message), );
// User-defined backup model (settings.providerBackupModel, default none): // route the failed turn to the backup instead of waiting while the // primary is quota-blocked or its provider is unavailable. if (waitClass !== "permanent") { const backupModel = this._resolveBackupModel(); if (backupModel && !modelsAreEqual(this.model, backupModel)) { return this._handleBackupModelRetry(message, options, backupModel); } }
this._retryAttempt++;
const waitPolicy = this.settingsManager.getProviderWaitSettings(); // Quota/subscription exhaustion: wait for usage to come back with bounded // exponential-backoff pings (and a scheduled resume when the provider // reports a reset time) instead of the quick-retry loop. if (waitClass === "quota" && waitPolicy.enabled && !options?.retryWithSibling) { return this._handleProviderWait(message, options, waitPolicy, "usage"); }
if (this._retryAttempt > settings.maxRetries) { // Quick retries exhausted on an unavailable provider: keep pinging // with bounded exponential backoff instead of giving up. if (waitClass === "transient" && waitPolicy.enabled) { return this._handleProviderWait(message, options, waitPolicy, "unavailable"); } this._markProviderAuthStaleForRetryFailure(message, options); const restoredModel = this._restorePrimaryModelAfterBackup(); this._emit({ type: "auto_retry_end", success: false, attempt: this._retryAttempt - 1, finalError: message.errorMessage, ...(restoredModel ? { restoredModel } : {}), }); this._retryAttempt = 0; this._retryAuthFailureSources = []; this._resolveRetry(); // Resolve so waitForRetry() completes return false; }
// Server-requested waits are honored, capped by retry.provider.maxRetryDelayMs (0 disables). const maxRetryDelayMs = this.settingsManager.getProviderRetrySettings().maxRetryDelayMs; const retryAfterMs = options?.retryWithSibling ? undefined : (providerStreamFailureRetryAfterMs(message) ?? parseProviderResetMs(message.errorMessage)); const delay = providerRetryDelay(this._retryAttempt, retryAfterMs, { baseDelayMs: settings.baseDelayMs, maxRetryDelayMs, }); if (delay.kind === "exceeds-cap") { this._modelRegistry.tripProviderQuotaBreaker(message.provider, { retryAfterMs: delay.retryAfterMs, reason: "rate_limit", }); if (message.model) { this._modelRegistry.tripProviderQuotaBreaker(`${message.provider}/${message.model}`, { retryAfterMs: delay.retryAfterMs, reason: "rate_limit", }); } this._markProviderAuthStaleForRetryFailure(message, options); const restoredModel = this._restorePrimaryModelAfterBackup(); this._emit({ type: "auto_retry_end", success: false, attempt: this._retryAttempt - 1, finalError: `Provider requested a ${Math.ceil(delay.retryAfterMs / 1000)}s wait before retrying (above retry.provider.maxRetryDelayMs=${maxRetryDelayMs}ms): ${message.errorMessage || "unknown error"}`, ...(restoredModel ? { restoredModel } : {}), }); this._retryAttempt = 0; this._retryAuthFailureSources = []; this._resolveRetry(); return false; }
return this._retryAfterDelay( message, options, { type: "auto_retry_start", attempt: this._retryAttempt, maxAttempts: settings.maxRetries, delayMs: delay.delayMs, errorMessage: message.errorMessage || "Unknown error", }, delay.delayMs, ); }
/** * Shared retry tail: park the failed turn, surface the retry attempt, wait, * and re-issue the turn. Returns false when the retry was aborted instead. */ private async _retryAfterDelay( message: AssistantMessage, options: | { markAuthStaleOnFailure?: boolean; authSourceTokens?: readonly AuthSourceToken[]; } | undefined, emitStart: Extract<AgentSessionEvent, { type: "auto_retry_start" }>, delayMs: number, ): Promise<boolean> { // Park now: the retry re-issues the failed call and must reuse its Idempotency-Key. // Payload hooks mutate the wire body after the hash point, so reuse is forfeited. if (!this._extensionRunner.hasHandlers("before_provider_request")) { this._semanticEdges.prepareTurnRetry(); }
this._emit(emitStart);
const messages = this.agent.state.messages; if (messages.length > 0 && messages[messages.length - 1].role === "assistant") { this.agent.state.messages = messages.slice(0, -1); }
this._retryAbortController = new AbortController(); try { await sleep(delayMs, this._retryAbortController.signal); } catch { const attempt = this._retryAttempt; this._markProviderAuthStaleForRetryFailure(message, options); this._retryAttempt = 0; this._retryAbortController = undefined; this._providerWait = undefined; const restoredModel = this._restorePrimaryModelAfterBackup(); this._emit({ type: "auto_retry_end", success: false, attempt, finalError: "Retry cancelled", ...(restoredModel ? { restoredModel } : {}), }); this._resolveRetry(); this._retryAuthFailureSources = []; return false; } this._retryAbortController = undefined;
const retryGeneration = this._retryGeneration; setTimeout(() => { // A retry aborted between the sleep and this scheduled start must not // re-issue the turn (e.g. onto a quota-blocked primary after a restore). if (this._retryGeneration !== retryGeneration || !this.isRetrying) return; this.agent.continue().catch((error: unknown) => { // A continue that never starts must still resolve the retry (else isRetrying // sticks forever) — unless a newer retry owns the state by now. if (this._retryGeneration !== retryGeneration || !this.isRetrying) return; this._markProviderAuthStaleForRetryFailure(message, options); const restoredModel = this._restorePrimaryModelAfterBackup(); const attempt = this._retryAttempt; this._retryAttempt = 0; this._providerWait = undefined; this._retryAuthFailureSources = []; this._emit({ type: "auto_retry_end", success: false, attempt, finalError: error instanceof Error ? error.message : String(error), ...(restoredModel ? { restoredModel } : {}), }); this._resolveRetry(); }); }, 0);
return true; }
/** * Resolve the user-configured backup model reference against the available * models. Unknown or unauthenticated references resolve to undefined: the * wait loop runs instead, and never surprises the user with a switch. */ private _resolveBackupModel(): Model<any> | undefined { const reference = this.settingsManager.getProviderBackupModel(); if (!reference) return undefined; const backupModel = findExactModelReferenceMatch(reference, this._modelRegistry.getAvailable()); if (!backupModel || !this._modelRegistry.hasConfiguredAuth(backupModel)) { return undefined; } return backupModel; }
/** Route the failed turn to the backup model and retry immediately on it. */ private _handleBackupModelRetry( message: AssistantMessage, options: | { markAuthStaleOnFailure?: boolean; authSourceTokens?: readonly AuthSourceToken[]; } | undefined, backupModel: Model<any>, ): Promise<boolean> { const previousModel = this.agent.state.model; const previousThinkingLevel = this.agent.state.thinkingLevel; const previousServiceTier = this.agent.state.serviceTier; this.agent.state.model = backupModel; // Clamp per-request fields to what the backup supports; all of them are // restored when the turn returns to the primary. this.agent.state.thinkingLevel = clampThinkingLevel(backupModel, previousThinkingLevel) as ThinkingLevel; this._clampServiceTierForModel(); // Session-log the switch so primary->backup->primary transitions stay debuggable. this.sessionManager.appendModelChange(backupModel.provider, backupModel.id); this._backupModel = { backup: backupModel, primary: previousModel, thinkingLevel: previousThinkingLevel, serviceTier: previousServiceTier, }; this._retryAttempt++; this._providerWait = undefined;
return this._retryAfterDelay( message, options, { type: "auto_retry_start", attempt: this._retryAttempt, maxAttempts: this.settingsManager.getRetrySettings().maxRetries, delayMs: 0, errorMessage: message.errorMessage || "Unknown error", reason: "backup", backupModel: `${backupModel.provider}/${backupModel.id}`, }, 0, ); }
/** * One bounded wait-for-recovery ping: exponential backoff with jitter, a * scheduled resume when the provider reports a reset time, and hard abort * bounds so the wait can never hang. */ private async _handleProviderWait( message: AssistantMessage, options: | { markAuthStaleOnFailure?: boolean; authSourceTokens?: readonly AuthSourceToken[]; } | undefined, policy: ProviderWaitPolicy, reason: "usage" | "unavailable", ): Promise<boolean> { const wait = this._providerWait; const startedAtMs = wait?.startedAtMs ?? Date.now(); const pingAttempt = (wait?.attempts ?? 0) + 1; this._providerWait = { attempts: pingAttempt, startedAtMs };
const resetMs = providerStreamFailureRetryAfterMs(message) ?? parseProviderResetMs(message.errorMessage); const decision = providerWaitDecision(pingAttempt, Date.now() - startedAtMs, resetMs, policy); if (decision.kind === "abort") { if (resetMs !== undefined) { const reason = this._isProviderQuotaExhaustedFailure(message) ? "quota_exhausted" : "rate_limit"; this._modelRegistry.tripProviderQuotaBreaker(message.provider, { retryAfterMs: resetMs, reason, }); if (message.model) { this._modelRegistry.tripProviderQuotaBreaker(`${message.provider}/${message.model}`, { retryAfterMs: resetMs, reason, }); } } this._markProviderAuthStaleForRetryFailure(message, options); const restoredModel = this._restorePrimaryModelAfterBackup(); this._emit({ type: "auto_retry_end", success: false, attempt: pingAttempt - 1, finalError: `${decision.message}: ${message.errorMessage || "unknown error"}`, ...(restoredModel ? { restoredModel } : {}), }); this._retryAttempt = 0; this._providerWait = undefined; this._retryAuthFailureSources = []; this._resolveRetry(); return false; }
return this._retryAfterDelay( message, options, { type: "auto_retry_start", attempt: pingAttempt, maxAttempts: policy.maxAttempts, delayMs: decision.delayMs, errorMessage: message.errorMessage || "Unknown error", reason, }, decision.delayMs, ); }
/** * Return to the primary model after a backup-model retry, unless the user * switched models meanwhile. Returns the restored "provider/model-id" * reference for the retry-end event. */ private _restorePrimaryModelAfterBackup(): string | undefined { const backup = this._backupModel; this._backupModel = undefined; if (!backup || !modelsAreEqual(this.model, backup.backup)) return undefined; this.agent.state.model = backup.primary; this.agent.state.thinkingLevel = backup.thinkingLevel; // Restore the saved effective tier: reclamping from the current state // would keep the tier the backup clamped it to. this._clampServiceTierForModel(backup.serviceTier); this.sessionManager.appendModelChange(backup.primary.provider, backup.primary.id); return `${backup.primary.provider}/${backup.primary.id}`; }
abortRetry(): void { if (this._retryAbortController) { this._retryAbortController.abort(); return; } if (this._retryAttempt > 0) { this._autoCompactionAbortController?.abort(); this._cancelPostCompactionContinue(); const restoredModel = this._restorePrimaryModelAfterBackup(); this._emit({ type: "auto_retry_end", success: false, attempt: this._retryAttempt, finalError: "Retry cancelled", ...(restoredModel ? { restoredModel } : {}), }); this._retryAttempt = 0; this._providerWait = undefined; } this._retryAuthFailureSources = []; this._resolveRetry(); }
private async waitForRetry(): Promise<void> { if (!this._retryPromise) { return; }
await this._retryPromise; await this.agent.waitForIdle(); }
get isRetrying(): boolean { return this._retryPromise !== undefined; }
get hasAcceptedPromptInFlight(): boolean { return this._actionStore .unfinishedActions() .some( (action) => action.payload.kind === "turn" && !action.payload.queueVisible && action.payload.acceptedBeforeCompletion, ); }
get autoRetryEnabled(): boolean { return this.settingsManager.getRetryEnabled(); }
setAutoRetryEnabled(enabled: boolean): void { this.settingsManager.setRetryEnabled(enabled); }
/** * Execute a bash command. * Adds result to agent context and session. * @param command The bash command to execute * @param onChunk Optional streaming callback for output * @param options.excludeFromContext If true, command output won't be sent to LLM (!! prefix) * @param options.operations Custom BashOperations for remote execution */ async executeBash( command: string, onChunk?: (chunk: string) => void, options?: { excludeFromContext?: boolean; operations?: BashOperations; transient?: boolean; }, ): Promise<BashResult> { // Each invocation owns its controller so abortBash reaches every in-flight command. const abortController = new AbortController(); this._bashAbortControllers.add(abortController);
const prefix = this.settingsManager.getShellCommandPrefix(); const shellPath = this.settingsManager.getShellPath(); const resolvedCommand = prefix ? `${prefix}\n${command}` : command;
try { const result = await executeBashWithOperations( resolvedCommand, this.sessionManager.getCwd(), options?.operations ?? createLocalBashOperations({ shellPath }), { onChunk, signal: abortController.signal, }, );
if (!options?.transient) { this.recordBashResult(command, result, options); } return result; } finally { this._bashAbortControllers.delete(abortController); this._notifySessionInputCheckpointChange(); } }
/** * Run a user-initiated bash command (! / !! prefix), emitting bash_start, * bash_output, and bash_end session events so any attached client can render * streaming output. Extensions can intercept execution via the user_bash event. * Execution failures are reported through bash_end rather than a rejected promise; * only the already-running guard and extension dispatch errors reject. * @param command The bash command to execute * @param options.excludeFromContext If true, command output won't be sent to LLM (!! prefix) */ async runUserBash( command: string, options?: { excludeFromContext?: boolean; transient?: boolean; runId?: string; }, ): Promise<void> { if (this.isBashRunning) { throw new Error("A bash command is already running"); } // Claim the bash slot synchronously: isBashRunning is otherwise false until // executeBash installs its abort controller, which would let a second command // slip through during the user_bash extension dispatch below. this._userBashRunning = true; this._userBashAbortRequested = false; // Echoed on bash_start/bash_end so the requesting client can tell its own // run apart from other clients' runs broadcast on the same session. const identity = { ...(options?.transient ? { transient: true } : {}), ...(options?.runId !== undefined ? { runId: options.runId } : {}), }; let end: UserBashEndDetails; try { end = await this.runUserBashLocked( command, options?.excludeFromContext ?? false, options?.transient ?? false, identity, ); } finally { this._userBashRunning = false; this._notifySessionInputCheckpointChange(); } // Emitted after the slot is released so clients never observe a bash_end // while the session still rejects new commands as already running. this._emit({ type: "bash_end", ...end, ...identity }); void this._drainQueuedMessagesAfterBash().catch(() => undefined); }
private async _drainQueuedMessagesAfterBash(): Promise<void> { await this.agent.waitForIdle(); this._scheduleSessionInputPump(); }
private async runUserBashLocked( command: string, excludeFromContext: boolean, transient: boolean, identity: { transient?: boolean; runId?: string }, ): Promise<UserBashEndDetails> { const eventResult = await this._extensionRunner.emitUserBash({ type: "user_bash", command, excludeFromContext, cwd: this.sessionManager.getCwd(), });
// Transient runs (side-conversation bash) live only in their pane: they // are never recorded, so reloads and rebuilds cannot resurface them. const record = transient ? () => {} : (result: BashResult) => this.recordBashResult(command, result, { excludeFromContext });
this._emit({ type: "bash_start", command, excludeFromContext, ...identity, }); try { // If an extension returned a full result, surface it without executing if (eventResult?.result) { const result = eventResult.result; if (result.output) { this._emit({ type: "bash_output", chunk: result.output }); } record(result); return { exitCode: result.exitCode, cancelled: result.cancelled, truncated: result.truncated, fullOutputPath: result.fullOutputPath, }; }
// An abort that arrived before the process spawned (during extension // dispatch) has no abort controller to act on; honor it here instead. if (this._userBashAbortRequested) { record({ output: "", exitCode: undefined, cancelled: true, truncated: false, }); return { exitCode: undefined, cancelled: true, truncated: false }; }
const result = await this.executeBash(command, (chunk) => this._emit({ type: "bash_output", chunk }), { excludeFromContext, operations: eventResult?.operations, transient, }); return { exitCode: result.exitCode, cancelled: result.cancelled, truncated: result.truncated, fullOutputPath: result.fullOutputPath, }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); // Persist the failure like every other outcome so replayed transcripts // and the LLM context reflect that the command did not run. record({ output: `bash failed: ${errorMessage}`, exitCode: undefined, cancelled: false, truncated: false, }); return { exitCode: undefined, cancelled: false, truncated: false, errorMessage, }; } }
recordBashResult(command: string, result: BashResult, options?: { excludeFromContext?: boolean }): void { const bashMessage: BashExecutionMessage = { role: "bashExecution", command, output: result.output, exitCode: result.exitCode, cancelled: result.cancelled, truncated: result.truncated, fullOutputPath: result.fullOutputPath, timestamp: Date.now(), excludeFromContext: options?.excludeFromContext, };
// If agent is streaming, defer adding to avoid breaking tool_use/tool_result ordering if (this.isStreaming) { this._pendingBashMessages.push(bashMessage); } else { this.agent.state.messages.push(bashMessage);
this.sessionManager.appendMessage(bashMessage); } }
/** * Cancel running bash command. */ abortBash(): void { // A user bash command may not have spawned yet (extension dispatch in // progress); flag the request so runUserBash cancels before executing. // runUserBash clears the flag at each start, so a stale flag is harmless. if (this._userBashRunning) { this._userBashAbortRequested = true; } for (const controller of this._bashAbortControllers) { controller.abort(); } }
get isBashRunning(): boolean { return this._bashAbortControllers.size > 0 || this._userBashRunning; }
/** Whether there are pending bash messages waiting to be flushed */ get hasPendingBashMessages(): boolean { return this._pendingBashMessages.length > 0; }
/** * Flush pending bash messages to agent state and session. * Called after agent turn completes to maintain proper message ordering. */ private _flushPendingBashMessages(): void { if (this._pendingBashMessages.length === 0) return;
for (const bashMessage of this._pendingBashMessages) { this.agent.state.messages.push(bashMessage);
this.sessionManager.appendMessage(bashMessage); }
this._pendingBashMessages = []; }
getRlmMaxDepthStatus(): RlmMaxDepthStatus { return { maxDepth: this._rlmMaxDepth, source: this._rlmMaxDepthSource }; }
async setRlmMaxDepth(maxDepth: number, options: { global?: boolean } = {}): Promise<SetRlmMaxDepthResult> { if (!isNonNegativeInteger(maxDepth)) { throw new Error("RLM max depth must be a non-negative integer."); }
this.sessionManager.appendCustomEntryWithRollback(RLM_MAX_DEPTH_STATE_CUSTOM_TYPE, { maxDepth }); this._rlmMaxDepth = maxDepth; this._rlmMaxDepthSource = "chat"; const oldBase = this._baseSystemPrompt; this._baseSystemPrompt = this._rebuildSystemPrompt(this.getActiveToolNames()); this.agent.state.systemPrompt = this._refreshExtensionSystemPrompt(this.agent.state.systemPrompt, oldBase);
let globalError: string | undefined; if (options.global) { await this.settingsManager.flush(); const staleErrors = this.settingsManager.drainErrors("global"); for (const { error } of staleErrors) { console.warn(`Warning: Earlier global settings write failed: ${error.message}`); } this.settingsManager.setRlmMaxDepth(maxDepth); await this.settingsManager.flush(); const errors = this.settingsManager.drainErrors("global"); globalError = errors.map(({ error }) => error.message).join("; ") || undefined; }
return { ...this.getRlmMaxDepthStatus(), globalSaved: options.global === true && globalError === undefined, ...(globalError ? { globalError } : {}), }; }
setSessionName(name: string): void { this.sessionManager.appendSessionInfo(name); this._emit({ type: "session_info_changed", name: this.sessionManager.getSessionName(), }); }
/** * Navigate to a different node in the session tree. * Unlike fork() which creates a new session file, this stays in the same file. * * @param targetId The entry ID to navigate to * @param options.summarize Whether user wants to summarize abandoned branch * @param options.customInstructions Custom instructions for summarizer * @param options.replaceInstructions If true, customInstructions replaces the default prompt * @param options.label Label to attach to the branch summary entry * @returns Result with editorText (if user message) and cancelled status */ private _branchNavigationQueue: Promise<void> = Promise.resolve();
async navigateTree( targetId: string, options: { summarize?: boolean; customInstructions?: string; replaceInstructions?: boolean; label?: string; } = {}, ): Promise<{ editorText?: string; cancelled: boolean; aborted?: boolean; summaryEntry?: BranchSummaryEntry; }> { const previous = this._branchNavigationQueue; let release = () => {}; this._branchNavigationQueue = new Promise<void>((resolve) => { release = resolve; }); await previous; try { return await this._navigateTree(targetId, options); } finally { release(); } }
private async _navigateTree( targetId: string, options: { summarize?: boolean; customInstructions?: string; replaceInstructions?: boolean; label?: string; } = {}, ): Promise<{ editorText?: string; cancelled: boolean; aborted?: boolean; summaryEntry?: BranchSummaryEntry; }> { if (options.summarize && !this.model) { throw new Error("No model available for summarization"); }
const targetEntry = this.sessionManager.getEntry(targetId); if (!targetEntry) { throw new Error(`Entry ${targetId} not found`); }
const queuedWorkPause = this.acquireQueuedWorkPause(); let commitFence: { owner: symbol; release(): void } | undefined; try { // Branch navigation and turn dispatch mutate the same transcript leaf. commitFence = await this._acquireSessionActionCommitFence(); return await this._sessionActionCommitContext.run(commitFence.owner, async () => { await this.agent.waitForIdle(); await this._agentEventQueue; return this._navigateTreeUnderPause(targetId, targetEntry, options); }); } finally { queuedWorkPause.release(); commitFence?.release(); } }
private async _navigateTreeUnderPause( targetId: string, targetEntry: NonNullable<ReturnType<SessionManager["getEntry"]>>, options: { summarize?: boolean; customInstructions?: string; replaceInstructions?: boolean; label?: string; }, ): Promise<{ editorText?: string; cancelled: boolean; aborted?: boolean; summaryEntry?: BranchSummaryEntry; }> { const oldLeafId = this.sessionManager.getLeafId();
// No-op if already at target after admitted work has settled. if (targetId === oldLeafId) { return { cancelled: false }; }
// Do not switch branches while /refine has detached event handling and is // about to persist harness/session entries for the current branch. await this._invalidatePendingAutoRefineForBranchChange();
const { entries: entriesToSummarize, commonAncestorId } = collectEntriesForBranchSummary( this.sessionManager, oldLeafId, targetId, );
let customInstructions = options.customInstructions; let replaceInstructions = options.replaceInstructions; let label = options.label;
const preparation: TreePreparation = { targetId, oldLeafId, commonAncestorId, entriesToSummarize, userWantsSummary: options.summarize ?? false, customInstructions, replaceInstructions, label, };
this._branchSummaryAbortController = new AbortController(); let resolveBranchSummaryOperation: () => void = () => {}; const branchSummaryOperation = new Promise<void>((resolve) => { resolveBranchSummaryOperation = resolve; }); this._branchSummaryOperation = branchSummaryOperation;
try { let extensionSummary: { summary: string; details?: unknown } | undefined; let fromExtension = false;
if (this._extensionRunner.hasHandlers("session_before_tree")) { const result = (await this._extensionRunner.emit({ type: "session_before_tree", preparation, signal: this._branchSummaryAbortController.signal, })) as SessionBeforeTreeResult | undefined;
if (result?.cancel) { return { cancelled: true }; }
if (result?.summary && options.summarize) { extensionSummary = result.summary; fromExtension = true; }
if (result?.customInstructions !== undefined) { customInstructions = result.customInstructions; } if (result?.replaceInstructions !== undefined) { replaceInstructions = result.replaceInstructions; } if (result?.label !== undefined) { label = result.label; } }
let summaryText: string | undefined; let summaryDetails: unknown; let summaryUsage: Usage | undefined; if (options.summarize && entriesToSummarize.length > 0 && !extensionSummary) { const { apiKey, headers, requestModel: model } = await this._getRequiredRequestAuth(this.model!); const branchSummarySettings = this.settingsManager.getBranchSummarySettings(); const result = await generateBranchSummary(entriesToSummarize, { model, apiKey, headers, signal: this._branchSummaryAbortController.signal, sessionId: this.sessionId, customInstructions, replaceInstructions, reserveTokens: branchSummarySettings.reserveTokens, retry: providerRetryPolicy(this.settingsManager), }); if (result.aborted) { return { cancelled: true, aborted: true }; } if (result.error) { throw new Error(result.error); } summaryText = result.summary; summaryUsage = result.usage; summaryDetails = { readFiles: result.readFiles || [], modifiedFiles: result.modifiedFiles || [], }; } else if (extensionSummary) { summaryText = extensionSummary.summary; summaryDetails = extensionSummary.details; }
let newLeafId: string | null; let editorText: string | undefined;
if (targetEntry.type === "message" && targetEntry.message.role === "user") { newLeafId = targetEntry.parentId; editorText = this._extractUserMessageText(targetEntry.message.content); } else if (targetEntry.type === "custom_message") { newLeafId = targetEntry.parentId; editorText = typeof targetEntry.content === "string" ? targetEntry.content : targetEntry.content .filter((c): c is { type: "text"; text: string } => c.type === "text") .map((c) => c.text) .join(""); } else { newLeafId = targetId; }
let summaryEntry: BranchSummaryEntry | undefined; if (summaryText) { const summaryId = this.sessionManager.branchWithSummary( newLeafId, summaryText, summaryDetails, fromExtension, summaryUsage, ); summaryEntry = this.sessionManager.getEntry(summaryId) as BranchSummaryEntry;
if (label) { this.sessionManager.appendLabelChange(summaryId, label); } } else if (newLeafId === null) { this.sessionManager.resetLeaf(); } else { this.sessionManager.branch(newLeafId); }
if (label && !summaryText) { this.sessionManager.appendLabelChange(targetId, label); }
const sessionContext = this.sessionManager.buildSessionContext(); this.agent.state.messages = sessionContext.messages; this._mergeUnpersistedOutcomes(this.agent.state.messages); this._restoreLateIpythonSentAgentMessages(); // Context rebuild = cold boundary: refresh the digest like resume. // Summary navigation continues the same timeline (compaction), so the // same goal's accounting must never regress; a plain branch move is // time travel and keeps faithful branch semantics. this._ensureHarnessDigestContext(); this._reloadGoalStateFromBranch({ monotonicTokens: Boolean(summaryText) }); this._reloadRlmMaxDepthFromBranch(); this._reloadCompactionThresholdFromBranch(); this._invalidateQueuedPromptPreparation();
await this._extensionRunner.emit({ type: "session_tree", newLeafId: this.sessionManager.getLeafId(), oldLeafId, summaryEntry, fromExtension: summaryText ? fromExtension : undefined, });
return { editorText, cancelled: false, summaryEntry }; } finally { this._branchSummaryAbortController = undefined; if (this._branchSummaryOperation === branchSummaryOperation) { this._branchSummaryOperation = undefined; } resolveBranchSummaryOperation(); this._notifySessionInputCheckpointChange(); } }
getUserMessagesForForking(): Array<{ entryId: string; text: string }> { const entries = this.sessionManager.getEntries(); const result: Array<{ entryId: string; text: string }> = [];
for (const entry of entries) { if (entry.type !== "message") continue; if (entry.message.role !== "user") continue;
const text = this._extractUserMessageText(entry.message.content); if (text) { result.push({ entryId: entry.id, text }); } }
return result; }
private _extractUserMessageText(content: string | Array<{ type: string; text?: string }>): string { if (typeof content === "string") return content; if (Array.isArray(content)) { return content .filter((c): c is { type: "text"; text: string } => c.type === "text") .map((c) => c.text) .join(""); } return ""; }
getSessionStats(): SessionStats { const state = this.state; const userMessages = state.messages.filter((m) => m.role === "user").length; const assistantMessages = state.messages.filter((m) => m.role === "assistant").length; const toolResults = state.messages.filter((m) => m.role === "toolResult").length;
let toolCalls = 0; let totalInput = 0; let totalOutput = 0; let totalCacheRead = 0; let totalCacheWrite = 0; let totalCost = 0;
for (const message of state.messages) { if (message.role === "assistant") { const assistantMsg = message as AssistantMessage; toolCalls += assistantMsg.content.filter((c) => c.type === "toolCall").length; totalInput += assistantMsg.usage.input; totalOutput += assistantMsg.usage.output; totalCacheRead += assistantMsg.usage.cacheRead; totalCacheWrite += assistantMsg.usage.cacheWrite; totalCost += assistantMsg.usage.cost.total; } }
return { sessionFile: this.sessionFile, sessionId: this.sessionId, userMessages, assistantMessages, toolCalls, toolResults, totalMessages: state.messages.length, tokens: { input: totalInput, output: totalOutput, cacheRead: totalCacheRead, cacheWrite: totalCacheWrite, total: totalInput + totalOutput + totalCacheRead + totalCacheWrite, }, cost: totalCost, contextUsage: this.getContextUsage(), }; }
getContextUsage(): ContextUsage | undefined { const model = this.model; if (!model) return undefined;
const contextWindow = model.contextWindow ?? 0; if (contextWindow <= 0) return undefined;
// After compaction, the last assistant usage reflects pre-compaction context size. // We can only trust usage from an assistant that responded after the latest compaction. // If no such assistant exists, context token count is unknown until the next LLM response. // Window-scoped: the latest compaction and any usage after it live in the // open context window, so a lazily opened transcript answers without // materializing every earlier entry. const branchEntries = this.sessionManager.getContextBranch(); const latestCompaction = getLatestCompactionEntry(branchEntries);
if (latestCompaction) { // Check if there's a valid assistant usage after the compaction boundary const compactionIndex = branchEntries.lastIndexOf(latestCompaction); let hasPostCompactionUsage = false; for (let i = branchEntries.length - 1; i > compactionIndex; i--) { const entry = branchEntries[i]; if (entry.type === "message" && entry.message.role === "assistant") { const assistant = entry.message; if (assistant.stopReason !== "aborted" && assistant.stopReason !== "error") { const contextTokens = calculateContextTokens(assistant.usage); if (contextTokens > 0) { hasPostCompactionUsage = true; } break; } } }
if (!hasPostCompactionUsage) { return { tokens: null, contextWindow, percent: null, compactionCount: this.sessionManager.countEntriesOfType("compaction"), threshold: this._compactionThresholdTokens ?? null, }; } }
const estimate = estimateContextTokens(this.messages); const percent = (estimate.tokens / contextWindow) * 100;
const latestCompactionTimestamp = latestCompaction ? new Date(latestCompaction.timestamp).getTime() : undefined; const supportsLong = model.compat?.supportsLongCacheRetention !== false; const cacheState = extractSessionCacheState(this.messages, latestCompactionTimestamp, undefined, supportsLong); const evaluated = evaluateActiveCacheTtl({ provider: model.provider, id: model.id }, cacheState); const promptCache: PromptCacheSnapshot | undefined = cacheState && evaluated.status !== "none" ? { cachedTokens: cacheState.cachedTokens, ttlSeconds: cacheState.ttlSeconds, expiresAt: cacheState.ttlSeconds ? cacheState.timestamp + cacheState.ttlSeconds * 1000 : undefined, deterministic: cacheState.deterministic, status: evaluated.status, canRestore: evaluated.status === "mismatch" ? evaluated.canRestore : undefined, } : undefined;
return { tokens: estimate.tokens, contextWindow, percent, compactionCount: this.sessionManager.countEntriesOfType("compaction"), promptCache, threshold: this._compactionThresholdTokens ?? null, }; }
private _rlmSessionDirForReading(): string | undefined { return this._rlmSessionDir ?? this.sessionManager.getSessionArtifactDir(); }
private _contextWindowResolver(): ContextWindowResolver { return (provider, modelId) => this._modelRegistry.find(provider, modelId)?.contextWindow; }
private _subtractUnindexedChildUsage(ownUsage: Usage, entries: SessionEntry[]): void { for (const entry of entries) { if (entry.type !== "message" || entry.message.role !== "assistant") continue; const unindexedUsage = this._rlmUnindexedChildUsage.get(entry.message); if (unindexedUsage) subtractAssistantUsage(ownUsage, unindexedUsage); } }
private _ownUsageMemo?: { count: number; tailId: string | undefined; usage: SessionUsageSummary | undefined };
// Whole-file own spend, identical to the catalog scan so rows never shift at passivation. getOwnUsageSummary(): SessionUsageSummary | undefined { // A lazily opened transcript answers from the usage the catalog already // summed; materializing a 100MB session for one summary line is not worth it. const cached = this.sessionManager.getCachedUsageSummary?.(); if (cached) return cached; const entries = this.sessionManager.getEntries(); const tailId = entries.at(-1)?.id; const memo = this._ownUsageMemo; if (memo && memo.count === entries.length && memo.tailId === tailId) { return memo.usage; } const { ownUsage } = computeOwnAndTotalUsage(entries, entries); this._subtractUnindexedChildUsage(ownUsage, entries); const usage = sessionUsageSummaryFrom(ownUsage); this._ownUsageMemo = { count: entries.length, tailId, usage }; return usage; }
/** * Build the agent context overview for /context: this session as the root * plus one node per RLM sub-agent, recursively. Running children are read * from their live sessions; completed children from their persisted session * dirs, so the tree survives child disposal and session resume. */ getContextTree(): ContextTreeNode { const resolveContextWindow = this._contextWindowResolver(); const branch = this.sessionManager.getBranch(); const { ownUsage, totalUsage } = computeOwnAndTotalUsage(branch, this.sessionManager.getEntries()); this._subtractUnindexedChildUsage(ownUsage, branch);
const children: ContextTreeNode[] = []; const liveIds = new Set<string>(); for (const run of this._activeRlmChildRuns.values()) { liveIds.add(run.id); const node = run.session?.getContextTree() ?? loadContextTreeChildFromDisk(run.sessionDir, resolveContextWindow); children.push({ ...(node ?? { ownUsage: emptyUsage(), totalUsage: emptyUsage(), children: [], }), id: run.id, label: rlmChildLabel(run.prompt), status: run.status, }); } children.push(...loadContextTreeChildrenFromDisk(this._rlmSessionDirForReading(), resolveContextWindow, liveIds));
const model = this.model; return { id: "root", label: this.sessionName ?? "main agent", status: "active", model: model ? { provider: model.provider, id: model.id } : undefined, ownUsage, totalUsage, contextUsage: this.getContextUsage(), children, }; }
/** * Export session to HTML. * @param outputPath Optional output path (defaults to session directory) * @returns Path to exported file */ async exportToHtml(outputPath?: string): Promise<string> { const themeName = this.settingsManager.getTheme();
const toolRenderer: ToolHtmlRenderer = createToolHtmlRenderer({ getToolDefinition: (name) => this.getToolDefinition(name), theme, cwd: this.sessionManager.getCwd(), });
return await exportSessionToHtml(this.sessionManager, this.state, { outputPath, themeName, toolRenderer, }); }
/** * Export the current session branch to a JSONL file. * Writes the session header followed by all entries on the current branch path. * @param outputPath Target file path. If omitted, generates a timestamped file in cwd. * @returns The resolved output file path. */ exportToJsonl(outputPath?: string): string { const filePath = resolve(outputPath ?? `session-${new Date().toISOString().replace(/[:.]/g, "-")}.jsonl`); const dir = dirname(filePath); if (!existsSync(dir)) { mkdirSync(dir, { recursive: true }); }
const header: SessionHeader = { type: "session", version: CURRENT_SESSION_VERSION, id: this.sessionManager.getSessionId(), timestamp: new Date().toISOString(), cwd: this.sessionManager.getCwd(), };
const branchEntries = this.sessionManager.getBranch(); const lines = [JSON.stringify(header)];
// Re-chain parentIds to form a linear sequence let prevId: string | null = null; for (const entry of branchEntries) { const linear = { ...entry, parentId: prevId }; lines.push(JSON.stringify(linear)); prevId = entry.id; }
writeFileSync(filePath, `${lines.join("\n")}\n`); return filePath; }
/** * Get text content of last assistant message. * Useful for /copy command. * @returns Text content, or undefined if no assistant message exists */ getLastAssistantText(): string | undefined { const lastAssistant = this.messages .slice() .reverse() .find((m) => { if (m.role !== "assistant") return false; const msg = m as AssistantMessage; // Skip aborted messages with no content if (msg.stopReason === "aborted" && msg.content.length === 0) return false; return true; });
if (!lastAssistant) return undefined;
let text = ""; for (const content of (lastAssistant as AssistantMessage).content) { if (content.type === "text") { text += content.text; } }
return text.trim() || undefined; }
// ========================================================================= // Extension System // =========================================================================
createReplacedSessionContext(): ReplacedSessionContext { // The initiating extension is not in scope here; label timers with the context kind instead. const context = Object.defineProperties( {}, Object.getOwnPropertyDescriptors(this._extensionRunner.createCommandContext("<session-replacement>")), ) as ReplacedSessionContext; context.sendMessage = (message, options) => this.sendCustomMessage(message, options); context.sendUserMessage = (content, options) => this.sendUserMessage(content, options); return context; }
hasExtensionHandlers(eventType: string): boolean { return this._extensionRunner.hasHandlers(eventType); }
get extensionRunner(): ExtensionRunner { return this._extensionRunner; }}
function isRlmHeartbeatStatusUpdate(value: unknown): value is AgentRlmHeartbeatStatusUpdate { return value === "pause" || value === "resume";}
function rlmHeartbeatHostResponse(job: AgentCronJob): Record<string, unknown> { return { id: job.id, status: job.status, label: job.label ?? null, delivery_mode: job.deliveryMode ?? "steer", instruction: job.prompt, schedule: job.schedule, created_at: job.createdAt, updated_at: job.updatedAt, next_run_at: job.nextRunAt ?? null, last_run_at: job.lastRunAt ?? null, last_error: job.lastError ?? null, run_count: job.runCount, };}