clawmoney 0.17.20 → 0.17.21

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/hub/executor.js +132 -65
  2. package/package.json +1 -1
@@ -1,5 +1,7 @@
1
1
  import { spawn } from "node:child_process";
2
- import { existsSync, statSync } from "node:fs";
2
+ import { existsSync, statSync, readdirSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
3
5
  import { isProcessed, markProcessed } from "./dedup.js";
4
6
  import { replaceLocalPaths, uploadFile } from "./media.js";
5
7
  import { logger } from "./logger.js";
@@ -123,8 +125,83 @@ function validateImageDelivery(output) {
123
125
  return "Image upload to R2 failed.";
124
126
  }
125
127
  }
128
+ // ── Codex generated-image directory watcher ──
129
+ //
130
+ // codex saves all image_gen output under $CODEX_HOME/generated_images/
131
+ // in a per-thread subdir. Once the .png lands on disk and reaches a stable
132
+ // size, the deliverable is ready — everything codex does after (cp,
133
+ // verification, final reasoning) is overhead the buyer doesn't see. We
134
+ // snapshot the directory before spawning codex, then poll for any new
135
+ // thread dir that contains a stable image file. When we find one we
136
+ // fabricate a synthetic agent_message into the stdout stream so the
137
+ // downstream parser picks up the path naturally, then SIGTERM codex.
138
+ const CODEX_HOME = process.env.CODEX_HOME || join(homedir(), ".codex");
139
+ const CODEX_GEN_DIR = join(CODEX_HOME, "generated_images");
140
+ const FILE_STABLE_MS = 1500;
141
+ const POLL_INTERVAL_MS = 400;
142
+ function snapshotCodexThreads() {
143
+ try {
144
+ return new Set(readdirSync(CODEX_GEN_DIR));
145
+ }
146
+ catch {
147
+ // Directory may not exist yet on a fresh install — that's fine,
148
+ // codex will create it the first time it generates anything.
149
+ return new Set();
150
+ }
151
+ }
152
+ function findNewCodexImage(baseline) {
153
+ let entries;
154
+ try {
155
+ entries = readdirSync(CODEX_GEN_DIR);
156
+ }
157
+ catch {
158
+ return null;
159
+ }
160
+ for (const entry of entries) {
161
+ if (baseline.has(entry))
162
+ continue;
163
+ // A new per-thread directory has appeared. Look inside for the
164
+ // first image file the model dropped there.
165
+ const dir = join(CODEX_GEN_DIR, entry);
166
+ let files;
167
+ try {
168
+ files = readdirSync(dir);
169
+ }
170
+ catch {
171
+ continue;
172
+ }
173
+ for (const f of files) {
174
+ if (IMAGE_EXT_RE.test(f))
175
+ return join(dir, f);
176
+ }
177
+ }
178
+ return null;
179
+ }
180
+ async function waitForStableSize(filePath) {
181
+ let lastSize = -1;
182
+ let stableSince = 0;
183
+ // Cap at 20s in case the file truly never settles — let the rest of
184
+ // the flow handle that as a delivery failure rather than hanging.
185
+ const deadline = Date.now() + 20_000;
186
+ while (Date.now() < deadline) {
187
+ try {
188
+ const size = statSync(filePath).size;
189
+ if (size !== lastSize) {
190
+ lastSize = size;
191
+ stableSince = Date.now();
192
+ }
193
+ else if (size > 0 && Date.now() - stableSince >= FILE_STABLE_MS) {
194
+ return;
195
+ }
196
+ }
197
+ catch {
198
+ // file may have moved between stat() calls — try again
199
+ }
200
+ await new Promise((r) => setTimeout(r, 200));
201
+ }
202
+ }
126
203
  // ── CLI execution (openclaw agent / claude -p) ──
127
- function runCli(command, prompt, timeoutMs, orderId) {
204
+ function runCli(command, prompt, timeoutMs, orderId, watchForImage) {
128
205
  return new Promise((resolve) => {
129
206
  // Build args based on command
130
207
  let args;
@@ -166,6 +243,9 @@ function runCli(command, prompt, timeoutMs, orderId) {
166
243
  // claude -p "..." --output-format json --dangerously-skip-permissions
167
244
  args = ["-p", prompt, "--output-format", "json", "--dangerously-skip-permissions"];
168
245
  }
246
+ // For codex generation/image calls, snapshot the generated-images dir
247
+ // BEFORE spawning so we can identify the new thread dir later.
248
+ const dirBaseline = watchForImage && command === "codex" ? snapshotCodexThreads() : null;
169
249
  const child = spawn(command, args, {
170
250
  stdio: ["ignore", "pipe", "pipe"],
171
251
  timeout: timeoutMs,
@@ -174,36 +254,57 @@ function runCli(command, prompt, timeoutMs, orderId) {
174
254
  let stdout = "";
175
255
  let stderr = "";
176
256
  let earlyResolved = false;
177
- const tryEarlyExit = () => {
257
+ let pollHandle = null;
258
+ const finishWithEarlyImage = async (imagePath) => {
178
259
  if (earlyResolved)
179
260
  return;
180
- // Only attempt early exit for codex others either don't stream
181
- // events with this shape, or already finish promptly after their
182
- // last output. Codex on xhigh/high reasoning_effort wastes 60–90s
183
- // doing "final reflection" reasoning even after the agent_message
184
- // is already emitted with the image_path. Once we see that
185
- // message in the JSONL stream the deliverable is fully on disk
186
- // and we can ship the order — kill the child to avoid burning
187
- // buyer wall-time on reasoning we won't read.
188
- if (command !== "codex")
189
- return;
190
- if (!hasCodexDeliverable(stdout))
261
+ // Wait until the file size stops growing before we tell the rest
262
+ // of the flow it's ready otherwise we might upload a partial PNG
263
+ // and confuse R2 / the buyer's image viewer.
264
+ await waitForStableSize(imagePath);
265
+ if (earlyResolved)
191
266
  return;
192
267
  earlyResolved = true;
193
- // SIGTERM is enough; codex closes its writer and we'll get a
194
- // 'close' event shortly. Don't await — let the resolve below
195
- // return the snapshot we already have.
268
+ if (pollHandle) {
269
+ clearInterval(pollHandle);
270
+ pollHandle = null;
271
+ }
196
272
  try {
197
273
  child.kill("SIGTERM");
198
274
  }
199
275
  catch {
200
276
  // pid may already be gone
201
277
  }
278
+ // Inject a synthetic agent_message into the stream so the codex
279
+ // JSONL parser downstream picks up the image_path naturally,
280
+ // without us needing to plumb a separate "early path" through
281
+ // the rest of executeTask.
282
+ const synthetic = JSON.stringify({
283
+ type: "item.completed",
284
+ item: {
285
+ type: "agent_message",
286
+ text: JSON.stringify({ image_path: imagePath }),
287
+ },
288
+ });
289
+ stdout += "\n" + synthetic + "\n";
202
290
  resolve({ stdout, stderr, exitCode: 0 });
203
291
  };
292
+ if (dirBaseline) {
293
+ pollHandle = setInterval(() => {
294
+ if (earlyResolved)
295
+ return;
296
+ const newImage = findNewCodexImage(dirBaseline);
297
+ if (newImage) {
298
+ if (pollHandle) {
299
+ clearInterval(pollHandle);
300
+ pollHandle = null;
301
+ }
302
+ void finishWithEarlyImage(newImage);
303
+ }
304
+ }, POLL_INTERVAL_MS);
305
+ }
204
306
  child.stdout.on("data", (chunk) => {
205
307
  stdout += chunk.toString();
206
- tryEarlyExit();
207
308
  });
208
309
  child.stderr.on("data", (chunk) => {
209
310
  stderr += chunk.toString();
@@ -211,62 +312,24 @@ function runCli(command, prompt, timeoutMs, orderId) {
211
312
  child.on("close", (code) => {
212
313
  if (earlyResolved)
213
314
  return;
315
+ if (pollHandle) {
316
+ clearInterval(pollHandle);
317
+ pollHandle = null;
318
+ }
214
319
  resolve({ stdout, stderr, exitCode: code });
215
320
  });
216
321
  child.on("error", (err) => {
217
322
  if (earlyResolved)
218
323
  return;
324
+ if (pollHandle) {
325
+ clearInterval(pollHandle);
326
+ pollHandle = null;
327
+ }
219
328
  stderr += err.message;
220
329
  resolve({ stdout, stderr, exitCode: null });
221
330
  });
222
331
  });
223
332
  }
224
- /**
225
- * Detect whether codex's JSONL stream already contains a final
226
- * agent_message event whose text parses to JSON with an `image_path`.
227
- * That's the signal that the image is on disk and the agent has
228
- * acknowledged it — everything codex does after this point is its
229
- * own final reasoning loop, which the buyer never sees.
230
- *
231
- * Scans newest-to-oldest so we exit as soon as we find the marker.
232
- */
233
- function hasCodexDeliverable(streamSoFar) {
234
- const lines = streamSoFar.split("\n");
235
- for (let i = lines.length - 1; i >= 0; i--) {
236
- const line = lines[i].trim();
237
- if (!line.startsWith("{"))
238
- continue;
239
- let event;
240
- try {
241
- event = JSON.parse(line);
242
- }
243
- catch {
244
- continue;
245
- }
246
- if (event.type !== "item.completed")
247
- continue;
248
- const item = event.item;
249
- if (item?.type !== "agent_message")
250
- continue;
251
- const text = item.text;
252
- if (typeof text !== "string")
253
- continue;
254
- // Cheap pre-check before JSON.parse: must mention image_path.
255
- if (!text.includes("image_path"))
256
- continue;
257
- try {
258
- const parsed = JSON.parse(text);
259
- const path = parsed.image_path;
260
- if (typeof path === "string" && path.length > 0)
261
- return true;
262
- }
263
- catch {
264
- // text wasn't JSON; agent might be talking about image_path
265
- // in prose. Keep looking — don't early-exit on prose.
266
- }
267
- }
268
- return false;
269
- }
270
333
  // ── JSON parser ──
271
334
  function parseJsonOutput(raw) {
272
335
  try {
@@ -587,7 +650,11 @@ export class Executor {
587
650
  const timeoutS = Math.max(call.timeout - TIMEOUT_BUFFER_S, 30);
588
651
  const command = this.config.provider.cli_command;
589
652
  logger.info(`Executing: ${command} for skill="${call.skill}" order=${call.order_id} (timeout=${timeoutS}s)`);
590
- const { stdout, stderr, exitCode } = await runCli(command, prompt, timeoutS * 1000, call.order_id);
653
+ const { stdout, stderr, exitCode } = await runCli(command, prompt, timeoutS * 1000, call.order_id,
654
+ // Watch the codex image dir for generation/image so we can
655
+ // ship the moment the file lands, skipping codex's final
656
+ // reasoning tail (~60–90s of pure overhead).
657
+ call.category?.startsWith("generation/image"));
591
658
  if (exitCode !== 0) {
592
659
  const errMsg = stderr.trim() || `CLI exited with code ${exitCode}`;
593
660
  logger.error(`CLI failed (code=${exitCode}):`, errMsg);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawmoney",
3
- "version": "0.17.20",
3
+ "version": "0.17.21",
4
4
  "description": "ClawMoney CLI -- Earn rewards with your AI agent",
5
5
  "type": "module",
6
6
  "bin": {