@jiggai/recipes 0.4.59 → 0.4.62

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.
@@ -2,7 +2,7 @@
2
2
  "id": "recipes",
3
3
  "name": "Recipes",
4
4
  "description": "Markdown recipes that scaffold agents and teams (workspace-local).",
5
- "version": "0.4.59",
5
+ "version": "0.4.62",
6
6
  "configSchema": {
7
7
  "type": "object",
8
8
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jiggai/recipes",
3
- "version": "0.4.59",
3
+ "version": "0.4.62",
4
4
  "description": "ClawRecipes plugin for OpenClaw (markdown recipes -> scaffold agents/teams)",
5
5
  "main": "index.ts",
6
6
  "type": "commonjs",
@@ -655,9 +655,16 @@ export async function runWorkflowWorkerTick(api: OpenClawPluginApi, opts: {
655
655
  } else {
656
656
  // The lock is still live and held by another worker. The cursor has
657
657
  // already advanced past this task, so under naive logic we'd lose it.
658
- // Re-enqueue — but ONLY if no equivalent task is already pending.
659
- // Otherwise repeated ticks against a stuck lock would pile up dozens
660
- // of duplicates for the same {runId, nodeId}.
658
+ // Re-enqueue — but ONLY if no equivalent task is already pending
659
+ // AND the run still exists. Without the run.json check, a lock-held
660
+ // task for a deleted run gets endlessly re-enqueued, crash-looping
661
+ // every tick that dequeues it.
662
+ const runStillExists = await fileExists(runFilePathFor(runsDir, task.runId));
663
+ if (!runStillExists) {
664
+ countedTowardLimit = false;
665
+ results.push({ taskId: task.id, runId: task.runId, nodeId: task.nodeId, status: 'skipped_stale' });
666
+ continue;
667
+ }
661
668
  const alreadyPending = await hasPendingTaskFor(teamDir, agentId, {
662
669
  runId: task.runId,
663
670
  nodeId: task.nodeId,
@@ -678,14 +685,35 @@ export async function runWorkflowWorkerTick(api: OpenClawPluginApi, opts: {
678
685
 
679
686
  const runId = task.runId;
680
687
 
681
- const { run } = await loadRunFile(teamDir, runsDir, runId);
682
- const workflowFile = String(run.workflow.file);
683
- const workflowPath = path.join(workflowsDir, workflowFile);
684
- const workflowRaw = await readTextFile(workflowPath);
685
- const workflow = normalizeWorkflow(JSON.parse(workflowRaw));
686
-
687
- const nodeIdx = workflow.nodes.findIndex((n) => String(n.id) === String(task.nodeId));
688
- if (nodeIdx < 0) throw new Error(`Node not found in workflow: ${task.nodeId}`);
688
+ // loadRunFile + workflow/node lookup can throw if the run dir was
689
+ // removed out from under us (manual cleanup, workspace reset) or if
690
+ // the workflow JSON renamed/removed this node between enqueue and
691
+ // dequeue. The outer try has only a finally, so a throw here would
692
+ // escape the tick and crash the CLI — which in a 1-minute cron loop
693
+ // means every subsequent tick dies the same way until an operator
694
+ // manually drains the queue. Skip stale instead.
695
+ let run: RunLog;
696
+ let workflow: ReturnType<typeof normalizeWorkflow>;
697
+ let workflowFile: string;
698
+ let nodeIdx: number;
699
+ try {
700
+ ({ run } = await loadRunFile(teamDir, runsDir, runId));
701
+ workflowFile = String(run.workflow.file);
702
+ const workflowPath = path.join(workflowsDir, workflowFile);
703
+ const workflowRaw = await readTextFile(workflowPath);
704
+ workflow = normalizeWorkflow(JSON.parse(workflowRaw));
705
+ nodeIdx = workflow.nodes.findIndex((n) => String(n.id) === String(task.nodeId));
706
+ if (nodeIdx < 0) throw new Error(`Node not found in workflow: ${task.nodeId}`);
707
+ } catch (err) {
708
+ countedTowardLimit = false;
709
+ // Log the reason so operators can diagnose; don't add it to the
710
+ // result shape (kept narrow for downstream consumers).
711
+ try {
712
+ console.warn(`[workflow-worker] skip_stale taskId=${task.id} runId=${task.runId} nodeId=${task.nodeId} reason=${(err as Error)?.message ?? String(err)}`);
713
+ } catch { /* log best-effort */ }
714
+ results.push({ taskId: task.id, runId: task.runId, nodeId: task.nodeId, status: 'skipped_stale' });
715
+ continue;
716
+ }
689
717
  const node = workflow.nodes[nodeIdx]!;
690
718
 
691
719
  // Now that we know the node, tighten the lock TTL based on node.config.timeoutMs.