- Track failed tool calls in call history (parse errors, execution errors)
- Increment turns counter for failed tool calls too
- Stuck detection now works even when tools fail repeatedly
- Inspired by Ruflo and Hermes Agent best practices
Fixes the bug where zCode would get stuck in infinite loops when tool calls fail.
Test results: ✅ All stuck detection tests passing
Replace 158 lines of fragile inline port logic (probePort, bindPort,
killStaleProcess, waitForPort, readStalePid) with a proper module:
- State machine: idle → probing → claiming → owned → releasing
- Triple holder detection: pidfile → ss → lsof fallback
- Age-based kill strategy (young siblings get waited on, not killed)
- Exponential backoff retry (5 attempts) instead of instant process.exit
- EventEmitter for stateChange/claimed/retry/failed events
- getStatus() for diagnostics
- Exposed in bot return object for external health checks
All previous features preserved, zero downgrades.
1. Reply context: When user replies/tags a message in Telegram, inject the
original message text as [Replying to previous message:] prefix so the AI
has full context. Previously ignored reply_to_message entirely, causing
'make hero more exciting' to have zero context about which page.
2. System prompt: Added CONTEXT AWARENESS section instructing the AI to
use reply context and never ask 'which page?' when context is provided.
3. Crash-loop guard: killStaleProcess now checks /proc/pid/stat to get
process age. Skips killing processes younger than 15 seconds, preventing
the mutual-kill cycle where systemd restarts before old instance dies.
- Add missing clearInterval(typingInterval) in intent bypass early return path
- Fix intent-detector category detection: pattern.test(regex) → regex.test(trimmed)
- Fix short-answer patterns: same reversed .test() bug
- Prevent 'now' being matched as 'no' by adding \b word boundary to greeting regex
- Also tighten other greeting patterns with $ anchor where appropriate
Root causes:
1. uncaughtException/unhandledRejection called gracefulShutdown() -> process.exit(0)
Any minor error killed the entire bot. Changed to LOG ONLY (Hermes/OpenCode pattern).
2. User-level systemd service was running alongside system-level, fighting for port 3001.
Masked user service permanently.
3. Fragile new Promise(() => {}) keepalive replaced with setInterval-based keepalive.
4. Syntax error in uncaughtException handler (literal newline in single-quoted string).
Tested: 5 rapid consecutive restarts all pass. Uptime stable.
Co-Authored-By: zcode <noreply@zcode.dev>
- Fix mangled system prompt rule 3 — now explicitly instructs batching
- Add parallel_tool_calls: true to API body (required by many providers)
- Strengthen batching language: #1 speed optimization, NEVER serialize
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
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.
- Pidfile lock prevents duplicate instances (auto-kills stale PIDs)
- EADDRINUSE retry: kills port hog, retries up to 3x with 1.5s delay
- releasePidfile() on graceful shutdown
- Added fs/path imports needed by pidfile utilities
- ConversationStore: per-chat JSON files in data/, survives restarts
- 6000 token budget per chat context (fits ~20-30 exchanges)
- Auto-trims old messages, always includes most recent
- Wired into message handler: loads history before AI call, saves after
- /reset command to clear chat history per chat
- Cross-session, cross-model, cross-chat isolation
- New memory.js: JSON-backed MemoryStore with 5 categories (lesson, pattern, preference, discovery, gotcha)
- Memory injected into system prompt — bot sees past learnings every session
- Curiosity engine: auto-detects errors/fixes, corrections, successful patterns, new tool discoveries
- New commands: /memory (stats), /remember (save), /recall (search), /forget (delete)
- Runs AFTER response delivery — zero latency impact
- 500 memory cap with smart eviction (keeps gotchas/lessons, evicts old discoveries)
- data/ directory gitignored (memory is local to each deployment)
- Add markdownToHtml() converter: **bold**, *italic*, code blocks, links, headings, quotes, lists
- StreamConsumer: intermediate edits stay plain text, FINAL message gets full HTML formatting
- sendFormatted() now uses HTML parse_mode with fallback to stripped plain text
- stripMarkdown() for plain-text fallback (no raw syntax chars)
- All Telegram sends now use HTML instead of legacy Markdown mode
- Removed SSE streaming from chatWithAI()
- Keep sendStreamingMessage() for chunked delivery
- Self-correction loops still active
- Messages will be delivered in chunks with typing indicator
- Add sendStreamingMessage() to message-sender.js with typing indicators
- Enable stream: true in chatWithAI() with SSE parsing
- Replace all ctx.reply() calls with sendStreamingMessage()
- Real-time text streaming with 50ms delay between chunks
- Add RTK utility module (src/utils/rtk.js)
- Integrate RTK into BashTool for all bash commands
- Integrate RTK into GitTool for git operations
- Initialize RTK on bot startup
- Support 60+ command types (git, npm, cargo, pytest, docker, etc.)
- Track and report token savings per command
- Graceful fallback when RTK is not available
Expected savings: 60-90% token reduction for supported commands