@sureshsankaran/ralph-wiggum 0.1.4 → 0.1.5

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.
Files changed (2) hide show
  1. package/dist/index.js +32 -58
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import { existsSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  const STATE_FILE = ".opencode/ralph-loop.local.md";
4
- const LOCK_FILE = ".opencode/ralph-loop.lock";
5
4
  function parseState(content) {
6
5
  const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
7
6
  if (!frontmatterMatch)
@@ -44,35 +43,6 @@ function extractPromiseText(text) {
44
43
  const match = text.match(/<promise>(.*?)<\/promise>/s);
45
44
  return match ? match[1].trim().replace(/\s+/g, " ") : null;
46
45
  }
47
- // Atomically increment iteration using file-based locking
48
- // Returns the next iteration number if we won the lock, null if another handler is processing
49
- function tryClaimIteration(stateFilePath, lockFilePath, currentIteration) {
50
- const nextIteration = currentIteration + 1;
51
- // Check if lock exists for THIS specific iteration transition
52
- if (existsSync(lockFilePath)) {
53
- const lockContent = readFileSync(lockFilePath, "utf-8").trim();
54
- // Lock format: "from:to" e.g., "1:2" means transitioning from iteration 1 to 2
55
- const [fromStr, toStr] = lockContent.split(":");
56
- const from = parseInt(fromStr, 10);
57
- const to = parseInt(toStr, 10);
58
- // If lock exists for this exact transition, someone else is handling it
59
- if (from === currentIteration && to === nextIteration) {
60
- return null;
61
- }
62
- }
63
- // Write our lock with format "from:to"
64
- writeFileSync(lockFilePath, `${currentIteration}:${nextIteration}`);
65
- // Double-check we got the lock
66
- const lockCheck = readFileSync(lockFilePath, "utf-8").trim();
67
- if (lockCheck !== `${currentIteration}:${nextIteration}`) {
68
- return null; // Someone else won the race
69
- }
70
- // Update state file
71
- const content = readFileSync(stateFilePath, "utf-8");
72
- const updated = content.replace(/^iteration: \d+$/m, `iteration: ${nextIteration}`);
73
- writeFileSync(stateFilePath, updated);
74
- return nextIteration;
75
- }
76
46
  /**
77
47
  * Ralph Wiggum Plugin - Iterative AI Development
78
48
  *
@@ -87,9 +57,11 @@ function tryClaimIteration(stateFilePath, lockFilePath, currentIteration) {
87
57
  */
88
58
  export const RalphWiggumPlugin = async ({ client, directory }) => {
89
59
  const stateFilePath = join(directory, STATE_FILE);
90
- const lockFilePath = join(directory, LOCK_FILE);
91
- // Track if completion was detected to prevent race conditions
60
+ // In-memory state to prevent race conditions
61
+ // These are scoped to this plugin instance
92
62
  let completionDetected = false;
63
+ let processingIteration = false;
64
+ let lastProcessedIteration = -1;
93
65
  return {
94
66
  event: async ({ event }) => {
95
67
  // Check message.part.updated events for completion promise
@@ -114,10 +86,6 @@ export const RalphWiggumPlugin = async ({ client, directory }) => {
114
86
  unlinkSync(stateFilePath);
115
87
  }
116
88
  catch { }
117
- try {
118
- unlinkSync(lockFilePath);
119
- }
120
- catch { }
121
89
  return;
122
90
  }
123
91
  }
@@ -125,7 +93,7 @@ export const RalphWiggumPlugin = async ({ client, directory }) => {
125
93
  return;
126
94
  }
127
95
  // Handle session idle to continue the loop
128
- // Only listen to session.status (session.idle is deprecated and fires at the same time)
96
+ // Only listen to session.status (session.idle is deprecated)
129
97
  const isIdle = event.type === "session.status" && event.properties?.status?.type === "idle";
130
98
  if (!isIdle)
131
99
  return;
@@ -133,6 +101,11 @@ export const RalphWiggumPlugin = async ({ client, directory }) => {
133
101
  if (completionDetected) {
134
102
  return;
135
103
  }
104
+ // If we're already processing an iteration, skip
105
+ // This prevents race conditions when multiple idle events fire
106
+ if (processingIteration) {
107
+ return;
108
+ }
136
109
  const sessionID = event.properties?.sessionID;
137
110
  // Check if ralph-loop is active
138
111
  if (!existsSync(stateFilePath))
@@ -149,10 +122,11 @@ export const RalphWiggumPlugin = async ({ client, directory }) => {
149
122
  unlinkSync(stateFilePath);
150
123
  }
151
124
  catch { }
152
- try {
153
- unlinkSync(lockFilePath);
154
- }
155
- catch { }
125
+ return;
126
+ }
127
+ // Check if we've already processed this iteration
128
+ // This handles cases where the state file hasn't been updated yet
129
+ if (state.iteration <= lastProcessedIteration) {
156
130
  return;
157
131
  }
158
132
  // Check if max iterations reached
@@ -162,25 +136,22 @@ export const RalphWiggumPlugin = async ({ client, directory }) => {
162
136
  unlinkSync(stateFilePath);
163
137
  }
164
138
  catch { }
165
- try {
166
- unlinkSync(lockFilePath);
167
- }
168
- catch { }
169
139
  return;
170
140
  }
171
- // Try to atomically claim this iteration - this prevents race conditions
172
- const nextIteration = tryClaimIteration(stateFilePath, lockFilePath, state.iteration);
173
- if (nextIteration === null) {
174
- // Another event handler won the race, skip this one silently
175
- return;
176
- }
177
- // Build system message
178
- const systemMsg = state.completion_promise
179
- ? `Ralph iteration ${nextIteration} | To stop: output <promise>${state.completion_promise}</promise> (ONLY when TRUE)`
180
- : `Ralph iteration ${nextIteration} | No completion promise set`;
181
- console.log(`\n${systemMsg}`);
182
- // Send the same prompt back to continue the session
141
+ // Mark as processing to prevent concurrent handling
142
+ processingIteration = true;
183
143
  try {
144
+ const nextIteration = state.iteration + 1;
145
+ lastProcessedIteration = nextIteration;
146
+ // Update state file with new iteration
147
+ const updated = content.replace(/^iteration: \d+$/m, `iteration: ${nextIteration}`);
148
+ writeFileSync(stateFilePath, updated);
149
+ // Build system message
150
+ const systemMsg = state.completion_promise
151
+ ? `Ralph iteration ${nextIteration} | To stop: output <promise>${state.completion_promise}</promise> (ONLY when TRUE)`
152
+ : `Ralph iteration ${nextIteration} | No completion promise set`;
153
+ console.log(`\n${systemMsg}`);
154
+ // Send the same prompt back to continue the session
184
155
  if (sessionID) {
185
156
  await client.session.promptAsync({
186
157
  path: { id: sessionID },
@@ -196,7 +167,10 @@ export const RalphWiggumPlugin = async ({ client, directory }) => {
196
167
  }
197
168
  }
198
169
  catch (err) {
199
- console.error("\nRalph loop: Failed to send prompt", err);
170
+ console.error("\nRalph loop: Failed to process iteration", err);
171
+ }
172
+ finally {
173
+ processingIteration = false;
200
174
  }
201
175
  },
202
176
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sureshsankaran/ralph-wiggum",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Ralph Wiggum iterative AI development plugin for OpenCode - continuously loops the same prompt until task completion",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",