@jiggai/recipes 0.4.35 → 0.4.37
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
|
@@ -4,6 +4,19 @@
|
|
|
4
4
|
<img src="https://github.com/JIGGAI/ClawRecipes/blob/main/clawcipes_cook.jpg" alt="ClawRecipes logo" width="240" />
|
|
5
5
|
</p>
|
|
6
6
|
|
|
7
|
+
<p align="center">
|
|
8
|
+
<img src="https://img.shields.io/github/v/release/JIGGAI/ClawRecipes?color=green&label=Latest%20Release" alt="Latest Release">
|
|
9
|
+
<img src="https://img.shields.io/github/license/JIGGAI/ClawRecipes?color=orange" alt="License Apache 2.0">
|
|
10
|
+
<img src="https://img.shields.io/github/actions/workflow/status/JIGGAI/ClawRecipes/release.yml?label=Build" alt="Build Status">
|
|
11
|
+
<img src="https://img.shields.io/badge/TypeScript-007ACC?logo=typescript&logoColor=white" alt="TypeScript">
|
|
12
|
+
<img src="https://img.shields.io/badge/Node.js-18+-339933?logo=node.js&logoColor=white" alt="Node.js 18+">
|
|
13
|
+
<br>
|
|
14
|
+
<img src="https://img.shields.io/badge/OpenClaw-Plugin-6366f1" alt="OpenClaw Plugin">
|
|
15
|
+
<img src="https://img.shields.io/badge/CLI-Tool-blue" alt="CLI Tool">
|
|
16
|
+
<img src="https://img.shields.io/badge/Team-Automation-8b5cf6" alt="Team Automation">
|
|
17
|
+
<img src="https://img.shields.io/badge/Workflow-Engine-0891b2" alt="Workflow Engine">
|
|
18
|
+
</p>
|
|
19
|
+
|
|
7
20
|
ClawRecipes is an **OpenClaw plugin** for scaffolding agents, teams, and file-first workflows from Markdown recipes.
|
|
8
21
|
|
|
9
22
|
If you want the short version:
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@ import { MediaDriver, MediaDriverInvokeOpts, MediaDriverResult, DurationConstrai
|
|
|
3
3
|
import { findSkillDir, findVenvPython, runScript, parseMediaOutput } from './utils';
|
|
4
4
|
|
|
5
5
|
export class LumaVideo implements MediaDriver {
|
|
6
|
-
slug = '
|
|
6
|
+
slug = 'luma-video';
|
|
7
7
|
mediaType = 'video' as const;
|
|
8
8
|
displayName = 'Luma Video Generation';
|
|
9
9
|
requiredEnvVars = ['LUMAAI_API_KEY'];
|
|
@@ -3,7 +3,7 @@ import { MediaDriver, MediaDriverInvokeOpts, MediaDriverResult, DurationConstrai
|
|
|
3
3
|
import { findSkillDir, findVenvPython, runScript, parseMediaOutput } from './utils';
|
|
4
4
|
|
|
5
5
|
export class RunwayVideo implements MediaDriver {
|
|
6
|
-
slug = '
|
|
6
|
+
slug = 'runway-video';
|
|
7
7
|
mediaType = 'video' as const;
|
|
8
8
|
displayName = 'Runway Video Generation';
|
|
9
9
|
requiredEnvVars = ['RUNWAYML_API_SECRET'];
|
|
@@ -193,6 +193,19 @@ export async function runWorkflowWorkerTick(api: OpenClawPluginApi, opts: {
|
|
|
193
193
|
|
|
194
194
|
const results: Array<{ taskId: string; runId: string; nodeId: string; status: string }> = [];
|
|
195
195
|
|
|
196
|
+
// Default lock TTL (used when we don't know the node config yet).
|
|
197
|
+
// This must be comfortably larger than typical media generation durations.
|
|
198
|
+
const DEFAULT_LOCK_TTL_MS = 30 * 60 * 1000;
|
|
199
|
+
|
|
200
|
+
// Once we know the node config, we can set a tighter (but still safe) TTL.
|
|
201
|
+
const MIN_NODE_LOCK_TTL_MS = 10 * 60 * 1000;
|
|
202
|
+
const LOCK_TTL_BUFFER_MS = 2 * 60 * 1000;
|
|
203
|
+
const getNodeLockTtlMs = (node: WorkflowNode): number => {
|
|
204
|
+
const timeoutMsRaw = asRecord(node?.config ?? {})['timeoutMs'];
|
|
205
|
+
const timeoutMs = typeof timeoutMsRaw === 'number' && Number.isFinite(timeoutMsRaw) ? timeoutMsRaw : 0;
|
|
206
|
+
return Math.max(MIN_NODE_LOCK_TTL_MS, timeoutMs + LOCK_TTL_BUFFER_MS);
|
|
207
|
+
};
|
|
208
|
+
|
|
196
209
|
for (let i = 0; i < limit; i++) {
|
|
197
210
|
const dq = await dequeueNextTask(teamDir, agentId, { workerId, leaseSeconds: 120 });
|
|
198
211
|
if (!dq.ok || !dq.task) break;
|
|
@@ -209,9 +222,19 @@ export async function runWorkflowWorkerTick(api: OpenClawPluginApi, opts: {
|
|
|
209
222
|
|
|
210
223
|
await ensureDir(lockDir);
|
|
211
224
|
|
|
225
|
+
const claimedAtIso = new Date().toISOString();
|
|
226
|
+
const lockInfo = {
|
|
227
|
+
workerId,
|
|
228
|
+
pid: process.pid,
|
|
229
|
+
taskId: task.id,
|
|
230
|
+
claimedAt: claimedAtIso,
|
|
231
|
+
ttlMs: DEFAULT_LOCK_TTL_MS,
|
|
232
|
+
expiresAt: new Date(Date.now() + DEFAULT_LOCK_TTL_MS).toISOString(),
|
|
233
|
+
};
|
|
234
|
+
|
|
212
235
|
// Node-level lock to prevent double execution.
|
|
213
236
|
try {
|
|
214
|
-
await fs.writeFile(lockPath, JSON.stringify(
|
|
237
|
+
await fs.writeFile(lockPath, JSON.stringify(lockInfo, null, 2), { encoding: 'utf8', flag: 'wx' });
|
|
215
238
|
lockHeld = true;
|
|
216
239
|
} catch {
|
|
217
240
|
// Lock exists. Treat it as contention unless it looks stale.
|
|
@@ -219,10 +242,27 @@ export async function runWorkflowWorkerTick(api: OpenClawPluginApi, opts: {
|
|
|
219
242
|
let unlocked = false;
|
|
220
243
|
try {
|
|
221
244
|
const raw = await readTextFile(lockPath);
|
|
222
|
-
const parsed = JSON.parse(raw) as { claimedAt?: string };
|
|
245
|
+
const parsed = JSON.parse(raw) as { claimedAt?: string; ttlMs?: number; expiresAt?: string };
|
|
246
|
+
|
|
247
|
+
const expiresAtMs = parsed?.expiresAt ? Date.parse(String(parsed.expiresAt)) : NaN;
|
|
223
248
|
const claimedAtMs = parsed?.claimedAt ? Date.parse(String(parsed.claimedAt)) : NaN;
|
|
224
|
-
const
|
|
225
|
-
|
|
249
|
+
const parsedTtlMs = typeof parsed?.ttlMs === 'number' && Number.isFinite(parsed.ttlMs) ? parsed.ttlMs : NaN;
|
|
250
|
+
|
|
251
|
+
const computedExpiryMs = Number.isFinite(claimedAtMs) && Number.isFinite(parsedTtlMs)
|
|
252
|
+
? claimedAtMs + parsedTtlMs
|
|
253
|
+
: NaN;
|
|
254
|
+
|
|
255
|
+
// Prefer explicit expiresAt from the lock file; otherwise fall back to (claimedAt + ttlMs).
|
|
256
|
+
// If neither exists (older locks), fall back to DEFAULT_LOCK_TTL_MS.
|
|
257
|
+
const effectiveExpiryMs = Number.isFinite(expiresAtMs)
|
|
258
|
+
? expiresAtMs
|
|
259
|
+
: Number.isFinite(computedExpiryMs)
|
|
260
|
+
? computedExpiryMs
|
|
261
|
+
: Number.isFinite(claimedAtMs)
|
|
262
|
+
? claimedAtMs + DEFAULT_LOCK_TTL_MS
|
|
263
|
+
: NaN;
|
|
264
|
+
|
|
265
|
+
const stale = Number.isFinite(effectiveExpiryMs) && Date.now() > effectiveExpiryMs;
|
|
226
266
|
if (stale) {
|
|
227
267
|
await fs.unlink(lockPath);
|
|
228
268
|
unlocked = true;
|
|
@@ -233,7 +273,7 @@ export async function runWorkflowWorkerTick(api: OpenClawPluginApi, opts: {
|
|
|
233
273
|
|
|
234
274
|
if (unlocked) {
|
|
235
275
|
try {
|
|
236
|
-
await fs.writeFile(lockPath, JSON.stringify(
|
|
276
|
+
await fs.writeFile(lockPath, JSON.stringify(lockInfo, null, 2), { encoding: 'utf8', flag: 'wx' });
|
|
237
277
|
lockHeld = true;
|
|
238
278
|
} catch { // intentional: lock contention, skip task
|
|
239
279
|
results.push({ taskId: task.id, runId: task.runId, nodeId: task.nodeId, status: 'skipped_locked' });
|
|
@@ -255,35 +295,48 @@ export async function runWorkflowWorkerTick(api: OpenClawPluginApi, opts: {
|
|
|
255
295
|
const runId = task.runId;
|
|
256
296
|
|
|
257
297
|
const { run } = await loadRunFile(teamDir, runsDir, runId);
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
298
|
+
const workflowFile = String(run.workflow.file);
|
|
299
|
+
const workflowPath = path.join(workflowsDir, workflowFile);
|
|
300
|
+
const workflowRaw = await readTextFile(workflowPath);
|
|
301
|
+
const workflow = normalizeWorkflow(JSON.parse(workflowRaw));
|
|
302
|
+
|
|
303
|
+
const nodeIdx = workflow.nodes.findIndex((n) => String(n.id) === String(task.nodeId));
|
|
304
|
+
if (nodeIdx < 0) throw new Error(`Node not found in workflow: ${task.nodeId}`);
|
|
305
|
+
const node = workflow.nodes[nodeIdx]!;
|
|
306
|
+
|
|
307
|
+
// Now that we know the node, tighten the lock TTL based on node.config.timeoutMs.
|
|
308
|
+
try {
|
|
309
|
+
const nodeLockTtlMs = getNodeLockTtlMs(node);
|
|
310
|
+
if (nodeLockTtlMs !== lockInfo.ttlMs) {
|
|
311
|
+
await fs.writeFile(
|
|
312
|
+
lockPath,
|
|
313
|
+
JSON.stringify({ ...lockInfo, ttlMs: nodeLockTtlMs, expiresAt: new Date(Date.now() + nodeLockTtlMs).toISOString() }, null, 2),
|
|
314
|
+
{ encoding: 'utf8' },
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
} catch { // intentional: best-effort lock metadata update
|
|
318
|
+
// ignore
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Stale-task guard: expired claim recovery can surface older queue entries from behind the
|
|
322
|
+
// cursor. Before executing a dequeued task, verify that this node is still actually runnable
|
|
323
|
+
// for the current run state. Otherwise we can resurrect pre-approval work and overwrite
|
|
324
|
+
// canonical node outputs for runs that already advanced.
|
|
325
|
+
const currentNodeStates = loadNodeStatesFromRun(run);
|
|
326
|
+
const currentStatus = currentNodeStates[String(node.id)]?.status;
|
|
327
|
+
const currentlyRunnableIdx = pickNextRunnableNodeIndex({ workflow, run });
|
|
328
|
+
if (
|
|
329
|
+
currentStatus === 'success' ||
|
|
330
|
+
currentStatus === 'error' ||
|
|
331
|
+
currentStatus === 'waiting' ||
|
|
332
|
+
currentlyRunnableIdx === null ||
|
|
333
|
+
String(workflow.nodes[currentlyRunnableIdx]?.id ?? '') !== String(node.id)
|
|
334
|
+
) {
|
|
335
|
+
results.push({ taskId: task.id, runId: task.runId, nodeId: task.nodeId, status: 'skipped_stale' });
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
285
338
|
|
|
286
|
-
|
|
339
|
+
// Determine current lane + ticket path.
|
|
287
340
|
const laneRaw = String(run.ticket.lane);
|
|
288
341
|
assertLane(laneRaw);
|
|
289
342
|
let curLane: WorkflowLane = laneRaw as WorkflowLane;
|
|
@@ -1064,7 +1117,7 @@ export async function runWorkflowWorkerTick(api: OpenClawPluginApi, opts: {
|
|
|
1064
1117
|
}
|
|
1065
1118
|
|
|
1066
1119
|
// ── Step 2: Invoke the media driver to generate actual media ─────
|
|
1067
|
-
const providerSlug = provider
|
|
1120
|
+
const providerSlug = provider;
|
|
1068
1121
|
const configEnv = await loadConfigEnv();
|
|
1069
1122
|
const mergedEnv = { ...process.env, ...configEnv } as Record<string, string>;
|
|
1070
1123
|
|