11import { childRuntime } from "./runtime.ts" ;
2- import { ensure , Err , Ok , scoped , useScope , withResolvers } from "effection" ;
2+ import { ensure , Err , Ok , scoped , suspend , useScope , withResolvers } from "effection" ;
33import type { Operation , Result , Task } from "effection" ;
44import { ensureDir } from "@effectionx/fs" ;
55import { Agent , content , hasContent } from "@executablemd/core" ;
@@ -49,7 +49,21 @@ interface Child {
4949 close ( ) : Operation < void > ;
5050 readonly path : string ;
5151 readonly descriptor : Descriptor ;
52+ endpoint : ChildEndpoint ;
5253}
54+ interface ChildEndpoint {
55+ readonly task : Task < void > ;
56+ readonly closed : Operation < Result < void > > ;
57+ reported ?: Error ;
58+ }
59+ function * stopEndpoint ( endpoint : ChildEndpoint ) : Operation < void > {
60+ yield * endpoint . task . halt ( ) ;
61+ const closed = yield * endpoint . closed ;
62+ if ( ! closed . ok && closed . error !== endpoint . reported ) {
63+ throw closed . error ;
64+ }
65+ }
66+
5367interface Allocation {
5468 readonly sessionId : string ;
5569 readonly fresh : boolean ;
@@ -66,11 +80,12 @@ export function* useHeadlessChildren(
6680) : Operation < HeadlessChildren > {
6781 const parent = yield * useScope ( ) ;
6882 const live = new Map < string , Child > ( ) ;
83+ const associations = new Map < string , { child : Child ; conversation : string ; path : string } > ( ) ;
6984 const issued = new WeakMap < object , string > ( ) ;
7085 let ended = false ;
7186
7287 function owned ( sessionId : string ) : Child {
73- const child = live . get ( sessionId ) ;
88+ const child = live . get ( sessionId ) ?? associations . get ( sessionId ) ?. child ;
7489 if ( ended || child === undefined ) {
7590 throw new Error (
7691 `XMD child ${ sessionId } is unavailable or ended; no replacement was started.` ,
@@ -85,7 +100,8 @@ export function* useHeadlessChildren(
85100 if ( ! reading . ok ) {
86101 throw reading . error ;
87102 }
88- if ( reading . value . active !== child . descriptor . conversation ) {
103+ const conversation = associations . get ( sessionId ) ?. conversation ?? child . descriptor . conversation ;
104+ if ( reading . value . active !== conversation ) {
89105 throw new Error (
90106 `XMD child ${ sessionId } no longer selects its original Sidekick conversation.` ,
91107 ) ;
@@ -94,7 +110,7 @@ export function* useHeadlessChildren(
94110 }
95111
96112 function * acquire ( allocation : Allocation ) : Operation < Child > {
97- const known = live . get ( allocation . sessionId ) ;
113+ const known = live . get ( allocation . sessionId ) ?? associations . get ( allocation . sessionId ) ?. child ;
98114 if ( known !== undefined ) {
99115 return lookup ( allocation . sessionId ) ;
100116 }
@@ -121,28 +137,173 @@ export function* useHeadlessChildren(
121137 try {
122138 yield * scoped ( function * ( ) {
123139 const view = yield * useReplLiveScreen ( ) ;
140+ const childScope = yield * useScope ( ) ;
141+ const endpoints : ChildEndpoint [ ] = [ ] ;
142+ yield * ensure ( function * ( ) {
143+ const failures : Error [ ] = [ ] ;
144+ for ( const endpoint of endpoints ) {
145+ try {
146+ yield * stopEndpoint ( endpoint ) ;
147+ } catch ( error ) {
148+ failures . push ( error instanceof Error ? error : new Error ( String ( error ) ) ) ;
149+ }
150+ }
151+ if ( failures . length > 0 ) {
152+ throw new AggregateError ( failures , "Child endpoint cleanup failed." ) ;
153+ }
154+ } ) ;
155+ function publishEndpoint (
156+ root : Root ,
157+ path : string ,
158+ sessionId : string ,
159+ conversation ?: string ,
160+ available ?: ( ) => boolean ,
161+ ) {
162+ const ready = withResolvers < Result < Descriptor > > ( ) ;
163+ const closed = withResolvers < Result < void > > ( ) ;
164+ const task = childScope . run ( function * ( ) {
165+ let failure : Error | undefined ;
166+ try {
167+ yield * scoped ( function * ( ) {
168+ ready . resolve (
169+ Ok (
170+ yield * useSidekickAttachment (
171+ path ,
172+ {
173+ owner : root . owner ,
174+ dispatch : root . dispatch ,
175+ reads : root . reads ,
176+ execution : root . execution ,
177+ cwd : root . cwd ,
178+ ...( available === undefined ? { } : { available } ) ,
179+ child : { sessionId, session : root . session , view } ,
180+ } ,
181+ conversation ,
182+ ) ,
183+ ) ,
184+ ) ;
185+ yield * suspend ( ) ;
186+ } ) ;
187+ } catch ( error ) {
188+ failure = error instanceof Error ? error : new Error ( String ( error ) ) ;
189+ ready . resolve ( Err ( failure ) ) ;
190+ } finally {
191+ closed . resolve ( failure === undefined ? Ok ( undefined ) : Err ( failure ) ) ;
192+ }
193+ } ) ;
194+ const service : ChildEndpoint = { task, closed : closed . operation } ;
195+ endpoints . push ( service ) ;
196+ return { service, ready : ready . operation } ;
197+ }
198+ let registered : Child | undefined ;
199+ let pendingReference = false ;
124200 yield * start ( {
125201 view,
202+ handoffReference ( change , acknowledgeAndActivate ) {
203+ return scoped ( function * ( ) {
204+ const child = registered ;
205+ if ( child === undefined ) {
206+ return Err ( new Error ( "The child is not registered." ) ) ;
207+ }
208+ if ( pendingReference ) {
209+ return Err ( new Error ( "This child already has a reference handoff in progress." ) ) ;
210+ }
211+ yield * ensure ( ( ) => {
212+ pendingReference = false ;
213+ } ) ;
214+ pendingReference = true ;
215+ const reading = child . root . owner . reading ;
216+ if ( ! reading . ok ) {
217+ return reading ;
218+ }
219+ const decision = reading . value . recoveries . find (
220+ ( one ) => one . decision . id === change . decision ,
221+ ) ?. decision ;
222+ const next = reading . value . conversations . find ( ( one ) => one . id === change . next ) ;
223+ if (
224+ reading . value . active !== change . previous ||
225+ decision ?. action !== "replace" ||
226+ decision . conversation !== change . previous ||
227+ decision . target !== change . next ||
228+ next ?. phase !== "prepared" ||
229+ next . previous !== change . previous
230+ ) {
231+ return Err (
232+ new Error (
233+ "The child reference change no longer matches its acknowledged decision and preparation." ,
234+ ) ,
235+ ) ;
236+ }
237+ const sessionId = ( yield * SidekickTransport . expect ( ) ) . identify ( ) ;
238+ const reference = { sessionId } ;
239+ const path = yield * childAddressPath ( sessionId ) ;
240+ let endpoint : ChildEndpoint | undefined ;
241+ let available = false ;
242+ yield * ensure ( function * ( ) {
243+ const selected = child . root . owner . reading ;
244+ if ( ! selected . ok ) {
245+ throw selected . error ;
246+ }
247+ if ( selected . value . active === change . next ) {
248+ if ( endpoint !== undefined ) {
249+ associations . set ( sessionId , { child, conversation : change . next , path } ) ;
250+ const previous = child . endpoint ;
251+ child . endpoint = endpoint ;
252+ yield * stopEndpoint ( previous ) ;
253+ available = true ;
254+ }
255+ } else if ( endpoint !== undefined ) {
256+ yield * stopEndpoint ( endpoint ) ;
257+ }
258+ } ) ;
259+ const publication = publishEndpoint (
260+ child . root ,
261+ path ,
262+ sessionId ,
263+ change . next ,
264+ ( ) => available ,
265+ ) ;
266+ endpoint = publication . service ;
267+ const published = yield * publication . ready ;
268+ if ( ! published . ok ) {
269+ endpoint . reported = published . error ;
270+ return published ;
271+ }
272+ const activated = yield * acknowledgeAndActivate ( reference ) ;
273+ if ( ! activated . ok ) {
274+ return activated ;
275+ }
276+ const selected = child . root . owner . reading ;
277+ if ( ! selected . ok ) {
278+ return selected ;
279+ }
280+ if ( selected . value . active !== change . next ) {
281+ return Err ( new Error ( "The fresh reference was not canonically selected." ) ) ;
282+ }
283+ return Ok ( reference ) ;
284+ } ) ;
285+ } ,
126286 * ready ( root ) {
127287 const selected = yield * root . owner . configure ( { } ) ;
128288 if ( ! selected . ok ) {
129289 throw selected . error ;
130290 }
131291 const path = yield * childAddressPath ( allocation . sessionId ) ;
132- const descriptor = yield * useSidekickAttachment ( path , {
133- owner : root . owner ,
134- dispatch : root . dispatch ,
135- reads : root . reads ,
136- execution : root . execution ,
137- cwd : root . cwd ,
138- child : { sessionId : allocation . sessionId , session : root . session , view } ,
139- } ) ;
292+ const publication = publishEndpoint ( root , path , allocation . sessionId ) ;
293+ const published = yield * publication . ready ;
294+ if ( ! published . ok ) {
295+ publication . service . reported = published . error ;
296+ throw published . error ;
297+ }
298+ const descriptor = published . value ;
299+ const endpoint = publication . service ;
140300 const child : Child = {
141301 sessionId : allocation . sessionId ,
142302 root,
143303 view,
144304 path,
145305 descriptor,
306+ endpoint,
146307 task : childTask ,
147308 * close ( ) {
148309 yield * childTask . halt ( ) ;
@@ -152,6 +313,7 @@ export function* useHeadlessChildren(
152313 }
153314 } ,
154315 } ;
316+ registered = child ;
155317 live . set ( child . sessionId , child ) ;
156318 established = true ;
157319 ready . resolve ( Ok ( child ) ) ;
@@ -172,6 +334,11 @@ export function* useHeadlessChildren(
172334 return Err ( error ) ;
173335 } finally {
174336 live . delete ( allocation . sessionId ) ;
337+ for ( const [ id , association ] of associations ) {
338+ if ( association . child . sessionId === allocation . sessionId ) {
339+ associations . delete ( id ) ;
340+ }
341+ }
175342 closed . resolve ( failure === undefined ? Ok ( undefined ) : Err ( failure ) ) ;
176343 }
177344 } ) ;
@@ -218,12 +385,10 @@ export function* useHeadlessChildren(
218385 }
219386 } ,
220387 createRuntime ( runtimeOptions ) {
221- return childRuntime (
222- runtimeOptions ,
223- createRuntime ,
224- providerScope ,
225- ( id ) => lookup ( id ) . path ,
226- ) ;
388+ return childRuntime ( runtimeOptions , createRuntime , providerScope , ( id ) => {
389+ const child = lookup ( id ) ;
390+ return associations . get ( id ) ?. path ?? child . path ;
391+ } ) ;
227392 } ,
228393 sessions : {
229394 * requestSession ( request ) {
@@ -245,9 +410,10 @@ export function* useHeadlessChildren(
245410 }
246411 const identity = request . sessionIdentity ;
247412 if ( identity === undefined ) {
248- const selected = lookup ( reference . parse ( { sessionId : request . session } ) . sessionId ) ;
413+ const sessionId = reference . parse ( { sessionId : request . session } ) . sessionId ;
414+ const selected = lookup ( sessionId ) ;
249415 return {
250- sessionKey : selected . sessionId ,
416+ sessionKey : sessionId ,
251417 cwd : selected . root . cwd ,
252418 state : "existing" ,
253419 } ;
@@ -272,7 +438,7 @@ export function* useHeadlessChildren(
272438 } ;
273439 } ,
274440 * recordSessionIdentity ( details , identity ) {
275- if ( live . has ( details . sessionKey ) ) {
441+ if ( live . has ( details . sessionKey ) || associations . has ( details . sessionKey ) ) {
276442 lookup ( details . sessionKey ) ;
277443 } else if ( dependencies . sessions !== undefined ) {
278444 yield * dependencies . sessions . recordSessionIdentity ( details , identity ) ;
0 commit comments