Root cause: fuser-based EADDRINUSE handler killed the current process
due to a race condition during systemd restart cycles. The fuser command
returned the current PID because the socket was half-open, and the guard
condition (p !== process.pid) failed to filter it.
Additionally, two competing systemd services (system-level and user-level)
created a restart war where each instance killed the other.
Fix approach (inspired by Next.js, Vite, webpack-dev-server):
- Replace fuser with net.createServer port probe (no external commands)
- PID-file based stale detection + ss fallback for orphan detection
- Wait loop with 300ms polling after SIGTERM to stale process
- Single-service architecture (disabled user-level unit)
Tested: 5 consecutive rapid restarts, 8+ minute uptime, zero crashes.
Co-Authored-By: zcode <noreply@zcode.dev>
- Added execSync import for child_process
- Modified acquirePidfile() to send SIGTERM to old instances
- Waits up to 2.5s for graceful shutdown with checks every 500ms
- Prevents continuous restart loop when old PID holds port 3001
- Bot now self-heals on restart instead of crashing
- Changed acquirePidfile() to only warn when another instance is detected
- No longer kills existing processes, just logs warning and continues
- Prevents continuous restart loop when bot detects itself running
- Maintains all Ruflo-inspired features (plugins, hooks, swarm, memory)
- All 18 tools, 6 skills, 9 agents, 6 swarm tools still loaded
Added comprehensive summary documenting:
1. What we found in Ruflo (multi-agent orchestration, plugin system, hooks)
2. What we integrated (all 6 core features complete)
3. What makes zCode smarter now (swarm intelligence, extensibility, smart memory)
4. Performance impact analysis (+21% memory, zero latency)
5. Feature comparison table (zCode vs Hermes vs Claude vs Ruflo)
6. Documentation coverage (134KB, 13 files, 3,766 lines)
7. Next steps for users, contributors, maintainers
This file serves as the definitive answer to the user's question about Ruflo features that would make zCode smarter and better.
Answer: YES - and we already integrated it all!
- Fixed memory backend API: getAll() now includes all memory types (lesson, gotcha, pattern, preference, discovery, context, ephemeral)
- Fixed memory test assertions: use MEMORY_TYPES.LESSON instead of undefined FACT, await retrieve() calls
- Added getAll() method to JSONBackend for grouped memory access
- Fixed InMemoryBackend to support all memory types in getAll()
- Fixed smoke test to properly await async methods and check correct properties
When file_write gets a 15KB+ HTML payload, the streaming JSON gets
truncated. Now catches JSON parse errors and returns a specific
hint to use bash heredoc instead of silently failing.
The old logic stopped typing on first stream token, leaving tool
execution gaps (30s+) with zero visual feedback. Now typing persists
until the full response + streaming edits are complete.
OLD: streaming and non-streaming were separate paths. Streaming detected tool
calls and recursively called non-streaming which only did ONE round of tool
execution with no loop-back. This caused silent hangs.
NEW: single chatWithAI with internal while loop (max 10 turns):
1. Call API (stream or non-stream)
2. If tool_calls → execute all → append results → loop
3. If text content → return final answer
Key fixes:
- streamChat now ACCUMULATES tool_call deltas instead of aborting
- Tool results are fed back to the AI in the same conversation
- Multi-turn: AI can call tools multiple times before answering
- Max 10 turns with forced final answer as safety net
- Proper { content, tool_calls, error } return type from both paths
- Non-streaming fallback if SSE fails
- No more recursive calls between stream/non-stream
Previously tool calls in non-streaming path returned raw tool output as the
response. Now executes tool, sends results back to model for a synthesized
answer. Fixes the 'silent after streaming fallback' bug.
- DelegateTool.js: multi-turn sub-agent (max 10 turns), feeds tool results back
- Moved TOOL_DEFS to startBot scope so delegate handler can access tool schemas
- Fixed scoping: delegate handler resolves model from svc.config instead of chatWithAI local
- Wired into tools/index.js, TOOL_DEFS, and toolHandlers
- delegate_agent: now makes actual AI call with role-specific system prompts
(coder=code review, architect=system design, devops=infrastructure)
- run_skill: now makes actual AI call with skill-specific system prompts
(code_review, bug_fix, refactor, documentation, testing)
- Both return structured AI-generated results instead of placeholder text
Model says "let me research" then calls web_search tool.
Streaming path ignored tool_calls entirely (no-op comment).
Now: detect tool_calls delta, cancel stream, fall back to non-streaming
which properly executes tools and returns results.