@@ -318,6 +318,21 @@ const DEFAULT_TEMPLATES: PromptTemplates = {
318318 "- All implementation happens in RLM worker sessions spawned per attempt." ,
319319 "- Strategy changes must be written to PLAN.md / RLM_INSTRUCTIONS.md so each new worker sees them." ,
320320 "- Stop the loop explicitly with ralph_end_supervision() when the user is done or verification passed." ,
321+ "" ,
322+ "Onboarding checklist (supervisor):" ,
323+ "- If setup is incomplete, run ralph_doctor(autofix=true)." ,
324+ "- If PLAN.md is missing or weak, run ralph_quickstart_wizard(...) or ralph_bootstrap_plan(...) + ralph_validate_plan()." ,
325+ "- Start supervision with ralph_create_supervisor_session(start_loop=true) unless autoStartOnMainIdle is enabled." ,
326+ "- Use ralph_supervision_status() to confirm current attempt and active sessions." ,
327+ "" ,
328+ "Protocol files (purpose):" ,
329+ "- PLAN.md: goals, milestones, definition of done (authoritative)." ,
330+ "- RLM_INSTRUCTIONS.md: inner loop operating manual (authoritative, updated between attempts)." ,
331+ "- CURRENT_STATE.md: scratch for the current attempt only." ,
332+ "- PREVIOUS_STATE.md: snapshot of last attempt's scratch." ,
333+ "- NOTES_AND_LEARNINGS.md: durable learnings across attempts." ,
334+ "- CONVERSATION.md / SUPERVISOR_LOG.md: status feed and timeline for the supervisor." ,
335+ "- CONTEXT_FOR_RLM.md: large reference; workers must use rlm_grep/rlm_slice." ,
321336 ] . join ( "\n" ) ,
322337
323338 systemPromptAppend : "" ,
@@ -333,6 +348,11 @@ const DEFAULT_TEMPLATES: PromptTemplates = {
333348 "- CONVERSATION.md — append-only supervisor-visible status feed" ,
334349 "- CONTEXT_FOR_RLM.md — large reference; access via rlm_grep + rlm_slice" ,
335350 "- .opencode/agents/<name>/ — sub-agent state directories" ,
351+ "" ,
352+ "## Loop control" ,
353+ "- ralph_create_supervisor_session(start_loop=true) to start" ,
354+ "- ralph_pause_supervision() / ralph_resume_supervision() to pause/resume" ,
355+ "- ralph_end_supervision() to stop and clean up" ,
336356 ] . join ( "\n" ) ,
337357
338358 continuePrompt : [
@@ -799,6 +819,8 @@ type SupervisorState = {
799819 currentRalphSessionId ?: string | undefined ;
800820 /** Session ID of the current RLM worker session. */
801821 currentWorkerSessionId ?: string | undefined ;
822+ /** Timer guarding strategist → worker handoff. */
823+ ralphHandoffTimer ?: ReturnType < typeof setTimeout > | undefined ;
802824 /** True once verification has passed — stops the loop. */
803825 done : boolean ;
804826 /** Paused supervision: no automatic spawning while true. */
@@ -1268,6 +1290,46 @@ export const RalphRLM: Plugin = async ({ client, $, worktree }) => {
12681290 }
12691291 } ;
12701292
1293+ const sendPromptWithFallback = async (
1294+ sessionId : string ,
1295+ text : string ,
1296+ label : string ,
1297+ originSessionId ?: string
1298+ ) : Promise < boolean > => {
1299+ try {
1300+ await client . session . promptAsync ( {
1301+ path : { id : sessionId } ,
1302+ body : { parts : [ { type : "text" , text } ] } ,
1303+ } ) ;
1304+ return true ;
1305+ } catch ( err ) {
1306+ await notifySupervisor (
1307+ "supervisor" ,
1308+ `${ label } promptAsync failed: ${ err instanceof Error ? err . message : String ( err ) } ` ,
1309+ "warning" ,
1310+ true ,
1311+ originSessionId
1312+ ) ;
1313+ }
1314+
1315+ try {
1316+ await client . session . prompt ( {
1317+ path : { id : sessionId } ,
1318+ body : { parts : [ { type : "text" , text } ] } ,
1319+ } ) ;
1320+ return true ;
1321+ } catch ( err ) {
1322+ await notifySupervisor (
1323+ "supervisor" ,
1324+ `${ label } prompt fallback failed: ${ err instanceof Error ? err . message : String ( err ) } ` ,
1325+ "error" ,
1326+ true ,
1327+ originSessionId
1328+ ) ;
1329+ return false ;
1330+ }
1331+ } ;
1332+
12711333 const detectProjectDefaults = ( root : string ) : Effect . Effect < { verify : string [ ] ; install : string } > =>
12721334 Effect . gen ( function * ( ) {
12731335 const j = ( f : string ) => NodePath . join ( root , f ) ;
@@ -2476,6 +2538,11 @@ export const RalphRLM: Plugin = async ({ client, $, worktree }) => {
24762538 supervisor . activeReviewerOutputPath = undefined ;
24772539 await persistReviewerState ( ) ;
24782540
2541+ if ( supervisor . ralphHandoffTimer ) {
2542+ clearTimeout ( supervisor . ralphHandoffTimer ) ;
2543+ supervisor . ralphHandoffTimer = undefined ;
2544+ }
2545+
24792546 if ( args . clear_binding === true ) {
24802547 supervisor . sessionId = undefined ;
24812548 }
@@ -3042,7 +3109,8 @@ export const RalphRLM: Plugin = async ({ client, $, worktree }) => {
30423109 sessionMap . delete ( sessionId ) ;
30433110
30443111 let didUpdate = false ;
3045- if ( supervisor . currentRalphSessionId === sessionId ) {
3112+ const clearedRalph = supervisor . currentRalphSessionId === sessionId ;
3113+ if ( clearedRalph ) {
30463114 supervisor . currentRalphSessionId = undefined ;
30473115 didUpdate = true ;
30483116 }
@@ -3059,7 +3127,12 @@ export const RalphRLM: Plugin = async ({ client, $, worktree }) => {
30593127 await persistReviewerState ( ) ;
30603128 }
30613129
3062- if ( didUpdate && st ) {
3130+ if ( supervisor . ralphHandoffTimer && clearedRalph ) {
3131+ clearTimeout ( supervisor . ralphHandoffTimer ) ;
3132+ supervisor . ralphHandoffTimer = undefined ;
3133+ }
3134+
3135+ if ( didUpdate && st ) {
30633136 await notifySupervisor (
30643137 `${ st . role } /attempt-${ st . attempt } ` ,
30653138 `${ st . role } session ended (${ reason } ).` ,
@@ -3126,10 +3199,28 @@ export const RalphRLM: Plugin = async ({ client, $, worktree }) => {
31263199 nextAttempt : String ( attempt + 1 ) ,
31273200 } ) ;
31283201
3129- await client . session . promptAsync ( {
3130- path : { id : workerId } ,
3131- body : { parts : [ { type : "text" , text : promptText } ] } ,
3132- } ) . catch ( ( ) => { } ) ;
3202+ const promptOk = await sendPromptWithFallback (
3203+ workerId ,
3204+ promptText ,
3205+ `Worker prompt (attempt ${ attempt } )` ,
3206+ workerId
3207+ ) ;
3208+ if ( ! promptOk ) {
3209+ mutateSession ( workerId , ( s ) => {
3210+ s . reportedStatus = "error" ;
3211+ s . reportedStatusNote = "Worker prompt failed" ;
3212+ } ) ;
3213+ supervisor . currentWorkerSessionId = undefined ;
3214+ await client . session . abort ( { path : { id : workerId } } ) . catch ( ( ) => { } ) ;
3215+ await notifySupervisor (
3216+ `worker/attempt-${ attempt } ` ,
3217+ "Worker prompt failed; supervision paused. Retry with ralph_create_supervisor_session(restart_if_done=true)." ,
3218+ "error" ,
3219+ true
3220+ ) ;
3221+ supervisor . paused = true ;
3222+ throw new Error ( "Worker prompt failed" ) ;
3223+ }
31333224
31343225 await notifySupervisor (
31353226 `supervisor/attempt-${ attempt } ` ,
@@ -3154,8 +3245,12 @@ export const RalphRLM: Plugin = async ({ client, $, worktree }) => {
31543245 throw new Error ( "ralph_spawn_worker() has already been called for this attempt." ) ;
31553246 }
31563247
3157- mutateSession ( sessionID , ( s ) => { s . workerSpawned = true ; } ) ;
3248+ if ( supervisor . ralphHandoffTimer ) {
3249+ clearTimeout ( supervisor . ralphHandoffTimer ) ;
3250+ supervisor . ralphHandoffTimer = undefined ;
3251+ }
31583252 const workerId = await spawnRlmWorker ( st . attempt ) ;
3253+ mutateSession ( sessionID , ( s ) => { s . workerSpawned = true ; } ) ;
31593254 await notifySupervisor (
31603255 `ralph/attempt-${ st . attempt } ` ,
31613256 `Delegated coding to worker session ${ workerId } .` ,
@@ -3185,10 +3280,43 @@ export const RalphRLM: Plugin = async ({ client, $, worktree }) => {
31853280 nextAttempt : String ( attempt + 1 ) ,
31863281 } ) ;
31873282
3188- await client . session . promptAsync ( {
3189- path : { id : ralphId } ,
3190- body : { parts : [ { type : "text" , text : promptText } ] } ,
3191- } ) . catch ( ( ) => { } ) ;
3283+ const promptOk = await sendPromptWithFallback (
3284+ ralphId ,
3285+ promptText ,
3286+ `Strategist prompt (attempt ${ attempt } )` ,
3287+ ralphId
3288+ ) ;
3289+ if ( ! promptOk ) {
3290+ supervisor . currentRalphSessionId = undefined ;
3291+ await client . session . abort ( { path : { id : ralphId } } ) . catch ( ( ) => { } ) ;
3292+ await notifySupervisor (
3293+ `supervisor/attempt-${ attempt } ` ,
3294+ "Strategist prompt failed; supervision paused. Retry with ralph_create_supervisor_session(restart_if_done=true)." ,
3295+ "error" ,
3296+ true
3297+ ) ;
3298+ supervisor . paused = true ;
3299+ return ;
3300+ }
3301+
3302+ if ( supervisor . ralphHandoffTimer ) {
3303+ clearTimeout ( supervisor . ralphHandoffTimer ) ;
3304+ }
3305+ const cfg = await run ( getConfig ( ) ) ;
3306+ const timeoutMs = Math . max ( cfg . heartbeatMinutes , 1 ) * 60_000 ;
3307+ supervisor . ralphHandoffTimer = setTimeout ( async ( ) => {
3308+ if ( supervisor . done || supervisor . paused ) return ;
3309+ if ( supervisor . currentRalphSessionId !== ralphId ) return ;
3310+ const st = sessionMap . get ( ralphId ) ;
3311+ if ( st ?. workerSpawned ) return ;
3312+ await notifySupervisor (
3313+ `ralph/attempt-${ attempt } ` ,
3314+ "Strategist did not hand off to a worker within the heartbeat window. Re-check prompt delivery or restart supervision." ,
3315+ "warning" ,
3316+ true ,
3317+ ralphId
3318+ ) ;
3319+ } , timeoutMs ) ;
31923320
31933321 await notifySupervisor (
31943322 `supervisor/attempt-${ attempt } ` ,
@@ -3416,7 +3544,7 @@ export const RalphRLM: Plugin = async ({ client, $, worktree }) => {
34163544 // main / other → supervisor prompt (shown to the user's session)
34173545 "experimental.chat.system.transform" : async ( input : any , output : any ) => {
34183546 output . system = output . system ?? [ ] ;
3419- const sessionID : string | undefined = input . sessionID ?? input . session_id ;
3547+ const sessionID : string | undefined = input . sessionID ?? input . session_id ?? input . session ?. id ;
34203548 const role = sessionMap . get ( sessionID ?? "" ) ?. role ;
34213549 const base =
34223550 role === "worker" || role === "subagent" ? templates . workerSystemPrompt :
0 commit comments