opencode-ralph-rlm 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -221,7 +221,8 @@ Create `.opencode/ralph.json`. All fields are optional — the plugin runs with
221
221
  | `statusVerbosity` | `"normal"` | Supervisor status emission level: `minimal` (warnings/errors), `normal`, or `verbose`. |
222
222
  | `maxAttempts` | `20` | Hard stop after this many failed verify attempts. |
223
223
  | `heartbeatMinutes` | `15` | Warn if active strategist/worker has no progress for this many minutes. |
224
- | `verify.command` | | Shell command to run as an array, e.g. `["bun", "run", "verify"]`. If omitted, verify always returns `unknown`. |
224
+ | `verifyTimeoutMinutes` | `0` | Timeout for verify command in minutes. `0` disables timeouts. |
225
+ | `verify.command` | - | Shell command to run as an array, e.g. `["bun", "run", "verify"]`. If omitted, verify always returns `unknown`. |
225
226
  | `verify.cwd` | `"."` | Working directory for the verify command, relative to the repo root. |
226
227
  | `gateDestructiveToolsUntilContextLoaded` | `true` | Block `write`, `edit`, `bash`, etc. until `ralph_load_context()` has been called in the current attempt. |
227
228
  | `maxRlmSliceLines` | `200` | Maximum lines a single `rlm_slice` call may return. |
@@ -438,6 +439,9 @@ Bind the current session as the supervisor and optionally start attempt 1 immedi
438
439
  Stop supervision for the current process. This prevents further auto-loop orchestration until restarted.
439
440
 
440
441
  - Use this when you want to pause/stop Ralph from spawning more sessions.
442
+ - Use this after verification passes and the user confirms they are done, or when the user asks to stop the loop.
443
+ - Active child sessions are aborted and any running verify command is stopped.
444
+ - Pass `delete_sessions: true` to delete child sessions after aborting them.
441
445
  - Resume later with `ralph_create_supervisor_session(restart_if_done=true)`.
442
446
 
443
447
  #### `ralph_supervision_status()`
@@ -572,6 +576,16 @@ args:
572
576
  post_to_conversation boolean optional Post to main conversation (default: true)
573
577
  ```
574
578
 
579
+ #### `ralph_peek_worker(maxLines?, post_to_conversation?)`
580
+
581
+ Snapshot the active worker's `CURRENT_STATE.md` and optionally post it into the main conversation for quick "peek" access in the TUI.
582
+
583
+ ```
584
+ args:
585
+ maxLines number optional Max lines to include (default: 120)
586
+ post_to_conversation boolean optional Post to main conversation (default: true)
587
+ ```
588
+
575
589
  #### `ralph_ask(question, context?, timeout_minutes?)`
576
590
 
577
591
  Ask a question and **block** until you respond via `ralph_respond()`. The question is written to `.opencode/pending_input.json`, a toast appears in the main session, and the main conversation is prompted with the question ID and response instruction. The calling session polls every 5 seconds.
package/dist/ralph-rlm.js CHANGED
@@ -23417,6 +23417,7 @@ var RalphConfigSchema = exports_Schema.Struct({
23417
23417
  statusVerbosity: exports_Schema.optional(exports_Schema.Union(exports_Schema.Literal("minimal"), exports_Schema.Literal("normal"), exports_Schema.Literal("verbose"))),
23418
23418
  maxAttempts: exports_Schema.optional(exports_Schema.Number),
23419
23419
  heartbeatMinutes: exports_Schema.optional(exports_Schema.Number),
23420
+ verifyTimeoutMinutes: exports_Schema.optional(exports_Schema.Number),
23420
23421
  verify: exports_Schema.optional(VerifyConfigSchema),
23421
23422
  gateDestructiveToolsUntilContextLoaded: exports_Schema.optional(exports_Schema.Boolean),
23422
23423
  maxRlmSliceLines: exports_Schema.optional(exports_Schema.Number),
@@ -23455,6 +23456,7 @@ var CONFIG_DEFAULTS = {
23455
23456
  statusVerbosity: "normal",
23456
23457
  maxAttempts: 20,
23457
23458
  heartbeatMinutes: 15,
23459
+ verifyTimeoutMinutes: 0,
23458
23460
  gateDestructiveToolsUntilContextLoaded: true,
23459
23461
  maxRlmSliceLines: 200,
23460
23462
  requireGrepBeforeLargeSlice: true,
@@ -23480,6 +23482,7 @@ function resolveConfig(raw) {
23480
23482
  statusVerbosity: raw.statusVerbosity ?? CONFIG_DEFAULTS.statusVerbosity,
23481
23483
  maxAttempts: toBoundedInt(raw.maxAttempts, CONFIG_DEFAULTS.maxAttempts, 1, 500),
23482
23484
  heartbeatMinutes: toBoundedInt(raw.heartbeatMinutes, CONFIG_DEFAULTS.heartbeatMinutes, 1, 240),
23485
+ verifyTimeoutMinutes: toBoundedInt(raw.verifyTimeoutMinutes, CONFIG_DEFAULTS.verifyTimeoutMinutes, 0, 240),
23483
23486
  ...verify !== undefined ? { verify } : {},
23484
23487
  gateDestructiveToolsUntilContextLoaded: raw.gateDestructiveToolsUntilContextLoaded ?? CONFIG_DEFAULTS.gateDestructiveToolsUntilContextLoaded,
23485
23488
  maxRlmSliceLines,
@@ -23509,6 +23512,7 @@ var DEFAULT_TEMPLATES = {
23509
23512
  " When you receive one, call ralph_respond(id, answer) to unblock the session.",
23510
23513
  "- Use ralph_doctor() to check setup, ralph_bootstrap_plan() to generate PLAN/TODOS,",
23511
23514
  " ralph_create_supervisor_session() to bind/start explicitly, ralph_pause_supervision()/ralph_resume_supervision() to control execution, and ralph_end_supervision() to stop.",
23515
+ "- End supervision when verification has passed and the user confirms they are done, or when the user explicitly asks to stop the loop.",
23512
23516
  "- Optional reviewer flow: worker marks readiness with ralph_request_review(); supervisor runs ralph_run_reviewer().",
23513
23517
  "- Monitor progress in SUPERVISOR_LOG.md, CONVERSATION.md, or via toast notifications."
23514
23518
  ].join(`
@@ -23761,7 +23765,26 @@ var writePendingInput = async (root, data) => {
23761
23765
  await NodeFs.writeFile(p, JSON.stringify(data, null, 2), "utf8");
23762
23766
  };
23763
23767
  var sleep5 = (ms) => new Promise((r) => setTimeout(r, ms));
23764
- async function runCommand(command, cwd) {
23768
+ var activeCommands = new Set;
23769
+ var stopCommand = (cmd, reason) => {
23770
+ if (cmd.child.killed)
23771
+ return;
23772
+ cmd.timedOut = cmd.timedOut || reason === "timeout";
23773
+ try {
23774
+ cmd.child.kill();
23775
+ } catch {}
23776
+ cmd.killTimer = setTimeout(() => {
23777
+ try {
23778
+ cmd.child.kill("SIGKILL");
23779
+ } catch {}
23780
+ }, 2000);
23781
+ };
23782
+ var stopAllCommands = (reason) => {
23783
+ for (const cmd of activeCommands) {
23784
+ stopCommand(cmd, reason);
23785
+ }
23786
+ };
23787
+ async function runCommand(command, cwd, options) {
23765
23788
  return await new Promise((resolve) => {
23766
23789
  const child = spawn(command[0] ?? "", command.slice(1), {
23767
23790
  cwd,
@@ -23771,6 +23794,18 @@ async function runCommand(command, cwd) {
23771
23794
  });
23772
23795
  let stdout = "";
23773
23796
  let stderr = "";
23797
+ const entry = {
23798
+ child,
23799
+ label: options?.label ?? command.join(" "),
23800
+ startedAt: Date.now(),
23801
+ timedOut: false
23802
+ };
23803
+ activeCommands.add(entry);
23804
+ if (options?.timeoutMs && options.timeoutMs > 0) {
23805
+ entry.timeoutTimer = setTimeout(() => {
23806
+ stopCommand(entry, "timeout");
23807
+ }, options.timeoutMs);
23808
+ }
23774
23809
  child.stdout?.on("data", (chunk3) => {
23775
23810
  stdout += String(chunk3);
23776
23811
  });
@@ -23778,11 +23813,23 @@ async function runCommand(command, cwd) {
23778
23813
  stderr += String(chunk3);
23779
23814
  });
23780
23815
  child.on("error", (err) => {
23816
+ if (entry.timeoutTimer)
23817
+ clearTimeout(entry.timeoutTimer);
23818
+ if (entry.killTimer)
23819
+ clearTimeout(entry.killTimer);
23820
+ activeCommands.delete(entry);
23781
23821
  resolve({ ok: false, code: null, stdout, stderr: `${stderr}
23782
23822
  ${String(err)}`.trim() });
23783
23823
  });
23784
23824
  child.on("close", (code) => {
23785
- resolve({ ok: code === 0, code, stdout, stderr });
23825
+ if (entry.timeoutTimer)
23826
+ clearTimeout(entry.timeoutTimer);
23827
+ if (entry.killTimer)
23828
+ clearTimeout(entry.killTimer);
23829
+ activeCommands.delete(entry);
23830
+ const timeoutNote = entry.timedOut ? `
23831
+ [ralph] command timed out` : "";
23832
+ resolve({ ok: code === 0 && !entry.timedOut, code, stdout, stderr: `${stderr}${timeoutNote}`.trim() });
23786
23833
  });
23787
23834
  });
23788
23835
  }
@@ -24102,7 +24149,21 @@ var RalphRLM = async ({ client, $, worktree }) => {
24102
24149
  - ${nowISO()} rotated
24103
24150
  `)).catch(() => {});
24104
24151
  };
24152
+ const DEDUPE_WINDOW_MS = 5000;
24153
+ const recentNotices = new Map;
24154
+ const shouldDedupe = (key) => {
24155
+ const now2 = Date.now();
24156
+ const last3 = recentNotices.get(key);
24157
+ if (last3 && now2 - last3 < DEDUPE_WINDOW_MS)
24158
+ return true;
24159
+ if (recentNotices.size > 200)
24160
+ recentNotices.clear();
24161
+ recentNotices.set(key, now2);
24162
+ return false;
24163
+ };
24105
24164
  const appendConversationEntry = async (source, message) => {
24165
+ if (shouldDedupe(`conv|${source}|${message}`))
24166
+ return;
24106
24167
  const cfg = await run(getConfig());
24107
24168
  await rotateConversationLogIfNeeded(cfg);
24108
24169
  const ts = nowISO();
@@ -24113,6 +24174,8 @@ var RalphRLM = async ({ client, $, worktree }) => {
24113
24174
  });
24114
24175
  };
24115
24176
  const notifySupervisor = async (source, message, level = "info", postToConversation = true, originSessionId) => {
24177
+ if (shouldDedupe(`sup|${source}|${level}|${message}`))
24178
+ return;
24116
24179
  const cfg = await run(getConfig());
24117
24180
  if (!messagePassesVerbosity(cfg.statusVerbosity, level))
24118
24181
  return;
@@ -24137,6 +24200,34 @@ var RalphRLM = async ({ client, $, worktree }) => {
24137
24200
  }).catch(() => {});
24138
24201
  }
24139
24202
  };
24203
+ const detectProjectDefaults = (root) => exports_Effect.gen(function* () {
24204
+ const j = (f) => NodePath.join(root, f);
24205
+ const hasBunLock = (yield* fileExists(j("bun.lockb"))) || (yield* fileExists(j("bun.lock")));
24206
+ if (hasBunLock)
24207
+ return { verify: ["bun", "run", "verify"], install: "bun install" };
24208
+ const hasYarnLock = yield* fileExists(j("yarn.lock"));
24209
+ if (hasYarnLock)
24210
+ return { verify: ["yarn", "test"], install: "yarn install" };
24211
+ const hasPnpmLock = yield* fileExists(j("pnpm-lock.yaml"));
24212
+ if (hasPnpmLock)
24213
+ return { verify: ["pnpm", "test"], install: "pnpm install" };
24214
+ const hasPkg = yield* fileExists(j("package.json"));
24215
+ if (hasPkg)
24216
+ return { verify: ["npm", "test"], install: "npm install" };
24217
+ const hasCargo = yield* fileExists(j("Cargo.toml"));
24218
+ if (hasCargo)
24219
+ return { verify: ["cargo", "test"], install: "cargo build" };
24220
+ const hasPy = yield* fileExists(j("pyproject.toml"));
24221
+ const hasReq = yield* fileExists(j("requirements.txt"));
24222
+ if (hasReq)
24223
+ return { verify: ["python", "-m", "pytest"], install: "pip install -r requirements.txt" };
24224
+ if (hasPy)
24225
+ return { verify: ["python", "-m", "pytest"], install: "pip install ." };
24226
+ const hasMake = yield* fileExists(j("Makefile"));
24227
+ if (hasMake)
24228
+ return { verify: ["make", "test"], install: "make" };
24229
+ return { verify: ["bun", "run", "verify"], install: "bun install" };
24230
+ });
24140
24231
  const checkSetup = async (root, cfg) => {
24141
24232
  const diagnostics = {
24142
24233
  ready: true,
@@ -24148,7 +24239,8 @@ var RalphRLM = async ({ client, $, worktree }) => {
24148
24239
  if (!cfg.verify || cfg.verify.command.length === 0) {
24149
24240
  diagnostics.ready = false;
24150
24241
  diagnostics.issues.push("Missing verify.command in .opencode/ralph.json.");
24151
- diagnostics.suggestions.push('Set verify.command, e.g. ["bun", "run", "verify"].');
24242
+ const defaults = await run(detectProjectDefaults(root));
24243
+ diagnostics.suggestions.push(`Set verify.command, e.g. ${JSON.stringify(defaults.verify)}.`);
24152
24244
  }
24153
24245
  const planExists = await run(fileExists(j(FILES.PLAN)));
24154
24246
  if (!planExists) {
@@ -24249,9 +24341,11 @@ var RalphRLM = async ({ client, $, worktree }) => {
24249
24341
  }
24250
24342
  const verifyCmd = cfg.verify.command;
24251
24343
  const cwd = NodePath.join(root, cfg.verify.cwd ?? ".");
24344
+ const timeoutMs = cfg.verifyTimeoutMinutes > 0 ? cfg.verifyTimeoutMinutes * 60000 : null;
24252
24345
  return yield* exports_Effect.tryPromise({
24253
24346
  try: async () => {
24254
- const result = await runCommand(verifyCmd, cwd);
24347
+ const options = timeoutMs ? { timeoutMs, label: "verify" } : { label: "verify" };
24348
+ const result = await runCommand(verifyCmd, cwd, options);
24255
24349
  if (result.ok) {
24256
24350
  return JSON.stringify({ verdict: "pass", output: result.stdout }, null, 2);
24257
24351
  }
@@ -24502,6 +24596,41 @@ ${args2.nextStep}
24502
24596
  return run(runVerify(root));
24503
24597
  }
24504
24598
  });
24599
+ const tool_ralph_peek_worker = tool({
24600
+ description: "Snapshot the active RLM worker's CURRENT_STATE.md and optionally post it into the main conversation.",
24601
+ args: {
24602
+ maxLines: tool.schema.number().int().min(20).max(400).optional(),
24603
+ post_to_conversation: tool.schema.boolean().optional().describe("Whether to post the peek into the main conversation (default: true).")
24604
+ },
24605
+ async execute(args2, ctx) {
24606
+ const root = ctx.worktree ?? worktree;
24607
+ const currPath = NodePath.join(root, FILES.CURR);
24608
+ const ok = await run(fileExists(currPath));
24609
+ if (!ok)
24610
+ return JSON.stringify({ ok: false, missing: FILES.CURR }, null, 2);
24611
+ const raw = await run(readFile(currPath).pipe(exports_Effect.orElseSucceed(() => "")));
24612
+ const text = clampLines(raw, args2.maxLines ?? 120);
24613
+ const attempt = supervisor.attempt;
24614
+ const workerId = supervisor.currentWorkerSessionId;
24615
+ const header = `Worker peek${attempt ? ` (attempt ${attempt})` : ""}${workerId ? ` \u2014 ${workerId}` : ""}`;
24616
+ await notifySupervisor("peek", header, "info", false, ctx.sessionID);
24617
+ const postToConv = args2.post_to_conversation !== false;
24618
+ if (postToConv && supervisor.sessionId && supervisor.sessionId !== ctx.sessionID) {
24619
+ await client.session.promptAsync({
24620
+ path: { id: supervisor.sessionId },
24621
+ body: { parts: [{ type: "text", text: `[peek] ${header}
24622
+
24623
+ ${text}` }] }
24624
+ }).catch(() => {});
24625
+ }
24626
+ return JSON.stringify({
24627
+ ok: true,
24628
+ attempt: attempt || null,
24629
+ workerSessionId: workerId ?? null,
24630
+ text
24631
+ }, null, 2);
24632
+ }
24633
+ });
24505
24634
  const tool_subagent_peek = tool({
24506
24635
  description: "Read file-first state from a sub-agent directory (.opencode/agents/<name>/).",
24507
24636
  args: {
@@ -24777,6 +24906,7 @@ No pending questions found.`));
24777
24906
  const diagnosticsBefore = await checkSetup(root, cfg);
24778
24907
  const actions = [];
24779
24908
  if (args2.autofix) {
24909
+ const defaults = await run(detectProjectDefaults(root));
24780
24910
  const configPath = NodePath.join(root, ".opencode", "ralph.json");
24781
24911
  const configExists = await run(fileExists(configPath));
24782
24912
  if (!configExists) {
@@ -24786,7 +24916,7 @@ No pending questions found.`));
24786
24916
  statusVerbosity: "normal",
24787
24917
  maxAttempts: 25,
24788
24918
  heartbeatMinutes: 15,
24789
- verify: { command: ["bun", "run", "verify"], cwd: "." },
24919
+ verify: { command: defaults.verify, cwd: "." },
24790
24920
  gateDestructiveToolsUntilContextLoaded: true,
24791
24921
  maxRlmSliceLines: 200,
24792
24922
  requireGrepBeforeLargeSlice: true,
@@ -24814,8 +24944,8 @@ No pending questions found.`));
24814
24944
  "# Project Agent Rules",
24815
24945
  "",
24816
24946
  "## Build and verify",
24817
- "- Install: bun install",
24818
- "- Verify: bun run verify",
24947
+ `- Install: ${defaults.install}`,
24948
+ `- Verify: ${defaults.verify.join(" ")}`,
24819
24949
  "",
24820
24950
  "## Loop note",
24821
24951
  "- This project uses ralph-rlm.",
@@ -24942,6 +25072,7 @@ No pending questions found.`));
24942
25072
  description: "Stop Ralph supervision for this process. Prevents further auto-loop orchestration until restarted.",
24943
25073
  args: {
24944
25074
  reason: tool.schema.string().optional().describe("Optional reason for ending supervision."),
25075
+ delete_sessions: tool.schema.boolean().optional().describe("Also delete child sessions after aborting them (default false)."),
24945
25076
  clear_binding: tool.schema.boolean().optional().describe("Clear supervisor session binding after stop (default false).")
24946
25077
  },
24947
25078
  async execute(args2, ctx) {
@@ -24949,6 +25080,15 @@ No pending questions found.`));
24949
25080
  const reason = args2.reason?.trim();
24950
25081
  supervisor.done = true;
24951
25082
  supervisor.paused = true;
25083
+ const sessionsToAbort = Array.from(sessionMap.keys());
25084
+ for (const id of sessionsToAbort) {
25085
+ await client.session.abort({ path: { id } }).catch(() => {});
25086
+ if (args2.delete_sessions) {
25087
+ await client.session.delete({ path: { id } }).catch(() => {});
25088
+ }
25089
+ }
25090
+ stopAllCommands("supervision-ended");
25091
+ sessionMap.clear();
24952
25092
  supervisor.currentRalphSessionId = undefined;
24953
25093
  supervisor.currentWorkerSessionId = undefined;
24954
25094
  supervisor.activeReviewerName = undefined;
@@ -25149,13 +25289,14 @@ Set a new goal and run again.
25149
25289
  const configPath = NodePath.join(root, ".opencode", "ralph.json");
25150
25290
  const configExists = await run(fileExists(configPath));
25151
25291
  if (!configExists) {
25292
+ const defaults = await run(detectProjectDefaults(root));
25152
25293
  const defaultCfg = {
25153
25294
  enabled: true,
25154
25295
  autoStartOnMainIdle: false,
25155
25296
  statusVerbosity: "normal",
25156
25297
  maxAttempts: 25,
25157
25298
  heartbeatMinutes: 15,
25158
- verify: { command: ["bun", "run", "verify"], cwd: "." },
25299
+ verify: { command: defaults.verify, cwd: "." },
25159
25300
  gateDestructiveToolsUntilContextLoaded: true,
25160
25301
  maxRlmSliceLines: 200,
25161
25302
  requireGrepBeforeLargeSlice: true,
@@ -25368,6 +25509,8 @@ Set a new goal and run again.
25368
25509
  }
25369
25510
  });
25370
25511
  const emitHeartbeatWarnings = async () => {
25512
+ if (supervisor.done || supervisor.paused)
25513
+ return;
25371
25514
  const cfg = await run(getConfig());
25372
25515
  const thresholdMs = cfg.heartbeatMinutes * 60000;
25373
25516
  const now2 = Date.now();
@@ -25390,6 +25533,30 @@ Set a new goal and run again.
25390
25533
  await maybeWarn(supervisor.currentRalphSessionId, "Strategist");
25391
25534
  await maybeWarn(supervisor.currentWorkerSessionId, "Worker");
25392
25535
  };
25536
+ const clearSessionTracking = async (sessionId, reason) => {
25537
+ const st = sessionMap.get(sessionId);
25538
+ sessionMap.delete(sessionId);
25539
+ let didUpdate = false;
25540
+ if (supervisor.currentRalphSessionId === sessionId) {
25541
+ supervisor.currentRalphSessionId = undefined;
25542
+ didUpdate = true;
25543
+ }
25544
+ if (supervisor.currentWorkerSessionId === sessionId) {
25545
+ supervisor.currentWorkerSessionId = undefined;
25546
+ didUpdate = true;
25547
+ }
25548
+ if (supervisor.activeReviewerSessionId === sessionId) {
25549
+ supervisor.activeReviewerSessionId = undefined;
25550
+ supervisor.activeReviewerName = undefined;
25551
+ supervisor.activeReviewerAttempt = undefined;
25552
+ supervisor.activeReviewerOutputPath = undefined;
25553
+ didUpdate = true;
25554
+ await persistReviewerState();
25555
+ }
25556
+ if (didUpdate && st) {
25557
+ await notifySupervisor(`${st.role}/attempt-${st.attempt}`, `${st.role} session ended (${reason}).`, "info", true, sessionId);
25558
+ }
25559
+ };
25393
25560
  const runAndParseVerify = async () => {
25394
25561
  const raw = await run(runVerify(worktree));
25395
25562
  try {
@@ -25592,6 +25759,7 @@ ${interpolate(templates.continuePrompt, { attempt: String(attemptN), verdict })}
25592
25759
  ralph_update_rlm_instructions: tool_ralph_update_rlm_instructions,
25593
25760
  ralph_rollover: tool_ralph_rollover,
25594
25761
  ralph_verify: tool_ralph_verify,
25762
+ ralph_peek_worker: tool_ralph_peek_worker,
25595
25763
  ralph_spawn_worker: tool_ralph_spawn_worker,
25596
25764
  subagent_peek: tool_subagent_peek,
25597
25765
  subagent_spawn: tool_subagent_spawn,
@@ -25656,7 +25824,13 @@ ${templates.systemPromptAppend}` : base;
25656
25824
  getSession(sessionID);
25657
25825
  }
25658
25826
  if (event?.type === "session.status" && sessionID) {
25827
+ if (supervisor.done || supervisor.paused)
25828
+ return;
25659
25829
  const state = sessionMap.get(sessionID);
25830
+ if (state?.role === "worker" && supervisor.currentWorkerSessionId !== sessionID)
25831
+ return;
25832
+ if (state?.role === "ralph" && supervisor.currentRalphSessionId !== sessionID)
25833
+ return;
25660
25834
  if (state) {
25661
25835
  mutateSession(sessionID, (s) => {
25662
25836
  s.lastProgressAt = Date.now();
@@ -25668,6 +25842,8 @@ ${templates.systemPromptAppend}` : base;
25668
25842
  }
25669
25843
  }
25670
25844
  if (event?.type === "session.idle" && sessionID) {
25845
+ if (supervisor.done || supervisor.paused)
25846
+ return;
25671
25847
  await emitHeartbeatWarnings().catch((err) => {
25672
25848
  appLog("error", "heartbeat warning error", { error: String(err) });
25673
25849
  });
@@ -25685,6 +25861,9 @@ ${templates.systemPromptAppend}` : base;
25685
25861
  });
25686
25862
  }
25687
25863
  }
25864
+ if (sessionID && (event?.type === "session.closed" || event?.type === "session.ended" || event?.type === "session.deleted")) {
25865
+ await clearSessionTracking(sessionID, event.type);
25866
+ }
25688
25867
  }
25689
25868
  };
25690
25869
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-ralph-rlm",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "OpenCode plugin: Ralph outer loop + RLM inner loop. Iterative AI development with file-first discipline and sub-agent support.",
5
5
  "type": "module",
6
6
  "main": "./dist/ralph-rlm.js",