Background
Previously, we published a guide titled NATS Communication Common Issues Troubleshooting, which addressed foundational health issues in the communication chain. However, weixiaoduo raised a deeper efficiency concern: cross-VM collaboration chains are too long, requiring manual orchestration at every step.
A typical scenario:
wenpai identifies spam domains → sends a message to weixiaoduo → weixiaoduo checks against the production database → sends results to kali for review → kali replies → weixiaoduo forwards reply to wenpai → wenpai executes deletion. Four steps across three VMs — all manually forwarded.
This post documents the full implementation of vm-task pipeline automation—including pitfalls encountered along the way.
What Was Implemented
1. vm-task complete --result: Data passing between stages
Previously, vm-task complete only marked a stage as completed and notified the next VM (“your turn”), carrying no data. Now:
# Complete current stage and attach output
vm-task complete <task-id> --result 'Query result: 15 spam domains matched'
- The
resultis stored inside thestageobject in the task’s JSON. - When notifying the next VM, the previous stage’s
resultis automatically included. vm-task show <id>highlights each stage’s output.
2. Pipeline Auto-Execution
The watcher’s check_pipeline_tasks() function polls the NAS pipeline directory every 60 seconds. Upon detecting a ready stage assigned to this VM:
- Reads the task description, action, and previous stage’s
result. - Marks the stage as
in_progress. - Launches an isolated Claude session using
systemd-run --user --scope. - Claude automatically executes the action; upon completion, calls
vm-task complete --result. - The next VM’s watcher detects the updated state → repeats the process.
No human intervention required end-to-end.
3. Inbox Noise Reduction
- Watcher automatically archives messages whose TTL has expired (info: 24h / normal: 48h / urgent: 7 days).
session-bootstrapgroups inbox items by priority (requires immediate action → pending → for reference only).
Pitfalls Encountered
Pitfall 1: vm-nats-doctor connection test false positives
Symptom: All VMs report “Unable to connect to NATS server”, yet actual communication works fine.
Root Cause:
- The
doctorscript didn’t setNATS_CONTEXT=vm-hub, so thenatsCLI couldn’t locate the correct connection config. - Even after fixing that, it still failed —
nats pub vm.healthcheck.xxxtriggered NATS subject permission restrictions (“Permissions Violation”).
Fix: Set export NATS_CONTEXT="vm-hub", and use vm.dm.${VM_NAME} as the test subject instead (each VM has publish permissions on its own DM subject).
Pitfall 2: Chinese characters stripped from action field
Symptom: In pipeline prompts, “your stage:” appears followed by blank space.
Root Cause: tr -cd 'a-zA-Z0-9._- ' filtered out all Chinese characters.
Fix: Keep strict filtering for task_id (to prevent path traversal), but for action, apply only length truncation—no character-set filtering.
Pitfall 3: Claude subprocess core dump (most subtle)
Symptom: Watcher auto-triggers a Claude session → aborts with Aborted (core dumped) after ~9 seconds. Yet running claude -p manually via SSH on the same VM works perfectly.
Debugging Steps:
- Checked Claude CLI version → OK (2.1.59)
- Checked credentials → no
.credentials.json, but API key env var was valid - Manually tested
claude -p 'reply HELLO'→ worked fine - Checked watcher logs →
line 124: Aborted (core dumped) - Key clue: Works over SSH, fails as watcher subprocess → environment difference
Root Cause: The vm-watcher.service systemd unit had MemoryMax=256M. Claude Code is a Node.js app — startup alone consumes >200 MB. Subprocesses launched by watcher via nohup inherited the cgroup memory limit and were killed outright.
# vm-watcher.service
MemoryMax=256M # ← This constrained *all* child processes
MemoryHigh=192M
TasksMax=64
Fix: Replace all nohup-based Claude launches in watcher with systemd-run --user --scope, running in an independent cgroup:
# Before (inherits watcher’s 256M limit)
nohup "$auto_script" >> "$LOG_FILE" 2>&1 &
# After (independent scope, 1 GB memory)
systemd-run --user --scope -p MemoryMax=1G -p CPUQuota=80% \
"$auto_script" >> "$LOG_FILE" 2>&1 &
Lesson Learned: systemd resource limits apply at the cgroup level — all descendant processes inherit them. When the watcher needs to launch heavyweight subprocesses, always use systemd-run --scope to isolate them.
Validation Results
Test pipeline created: devops → wenpai
12:17:57 devops creates task test-auto-v4
12:18:02 devops completes stage 1, result: "hostname: devops"
→ wenpai receives notification
~12:18:30 wenpai watcher detects ready stage → systemd-run launches Claude
12:19:16 wenpai’s Claude auto-executes hostname → vm-task complete --result "hostname: wenpai"
→ all stages completed
End-to-end time from trigger to full pipeline completion: ~1 minute 15 seconds — zero manual intervention.
Current Status
- Deployed across all 9 VMs cluster-wide
vm-nats-doctorpasses on all (12/12, 0 failures)- Pipeline auto-execution verified
- Code pushed to feicode Ansible repository
Next Steps
- Predefined common pipeline templates (e.g., security review flow, product launch flow)
vm-task dashboard: global view of task flow status- Pipeline timeout mechanism: auto-alert if a stage remains incomplete beyond N minutes