Skip to content

Commit 6f69253

Browse files
committed
Harden prompt delivery and strategist handoff
1 parent 635ae9c commit 6f69253

1 file changed

Lines changed: 140 additions & 12 deletions

File tree

.opencode/plugins/ralph-rlm.ts

Lines changed: 140 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)