clawmoney 0.17.19 → 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.
- package/dist/hub/executor.js +149 -3
- package/package.json +1 -1
package/dist/hub/executor.js
CHANGED
|
@@ -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,
|
|
@@ -173,6 +253,56 @@ function runCli(command, prompt, timeoutMs, orderId) {
|
|
|
173
253
|
});
|
|
174
254
|
let stdout = "";
|
|
175
255
|
let stderr = "";
|
|
256
|
+
let earlyResolved = false;
|
|
257
|
+
let pollHandle = null;
|
|
258
|
+
const finishWithEarlyImage = async (imagePath) => {
|
|
259
|
+
if (earlyResolved)
|
|
260
|
+
return;
|
|
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)
|
|
266
|
+
return;
|
|
267
|
+
earlyResolved = true;
|
|
268
|
+
if (pollHandle) {
|
|
269
|
+
clearInterval(pollHandle);
|
|
270
|
+
pollHandle = null;
|
|
271
|
+
}
|
|
272
|
+
try {
|
|
273
|
+
child.kill("SIGTERM");
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
// pid may already be gone
|
|
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";
|
|
290
|
+
resolve({ stdout, stderr, exitCode: 0 });
|
|
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
|
+
}
|
|
176
306
|
child.stdout.on("data", (chunk) => {
|
|
177
307
|
stdout += chunk.toString();
|
|
178
308
|
});
|
|
@@ -180,9 +310,21 @@ function runCli(command, prompt, timeoutMs, orderId) {
|
|
|
180
310
|
stderr += chunk.toString();
|
|
181
311
|
});
|
|
182
312
|
child.on("close", (code) => {
|
|
313
|
+
if (earlyResolved)
|
|
314
|
+
return;
|
|
315
|
+
if (pollHandle) {
|
|
316
|
+
clearInterval(pollHandle);
|
|
317
|
+
pollHandle = null;
|
|
318
|
+
}
|
|
183
319
|
resolve({ stdout, stderr, exitCode: code });
|
|
184
320
|
});
|
|
185
321
|
child.on("error", (err) => {
|
|
322
|
+
if (earlyResolved)
|
|
323
|
+
return;
|
|
324
|
+
if (pollHandle) {
|
|
325
|
+
clearInterval(pollHandle);
|
|
326
|
+
pollHandle = null;
|
|
327
|
+
}
|
|
186
328
|
stderr += err.message;
|
|
187
329
|
resolve({ stdout, stderr, exitCode: null });
|
|
188
330
|
});
|
|
@@ -508,7 +650,11 @@ export class Executor {
|
|
|
508
650
|
const timeoutS = Math.max(call.timeout - TIMEOUT_BUFFER_S, 30);
|
|
509
651
|
const command = this.config.provider.cli_command;
|
|
510
652
|
logger.info(`Executing: ${command} for skill="${call.skill}" order=${call.order_id} (timeout=${timeoutS}s)`);
|
|
511
|
-
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"));
|
|
512
658
|
if (exitCode !== 0) {
|
|
513
659
|
const errMsg = stderr.trim() || `CLI exited with code ${exitCode}`;
|
|
514
660
|
logger.error(`CLI failed (code=${exitCode}):`, errMsg);
|