Skip to content

Commit a439bca

Browse files
authored
CtrlNode Bridge v2026.2.5
This release adds task cancellation, user input delivery, and persistent session resume for follow-ups across all providers.
1 parent dff298d commit a439bca

18 files changed

Lines changed: 721 additions & 6 deletions
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
## CtrlNode Bridge v2026.2.5 Release Notes
2+
3+
This release adds task cancellation, user input delivery, and persistent session resume for follow-ups across all providers.
4+
5+
---
6+
7+
### What's new
8+
9+
#### Task cancellation
10+
11+
Tasks can now be cancelled mid-run from the CtrlNode UI. Bridge receives a `cancel_task` command and signals the active provider to stop immediately:
12+
13+
- **Claude Code / Hermes CLI** — sends `SIGTERM` to the child process, then `SIGKILL` after 3 seconds if still running
14+
- **Claude Agent SDK** — aborts via `AbortController`
15+
- **Codex SDK** — sets a cancel flag that breaks the event stream loop cleanly
16+
- **Copilot / Gemini / Hermes ACP** — sends `SIGTERM` to the ACP subprocess
17+
18+
#### User input delivery
19+
20+
When an ACP agent (Copilot, Gemini, Hermes ACP) encounters a permission prompt it cannot auto-approve, it now notifies the UI with a `task_waiting_for_input` event instead of silently cancelling. The UI can respond with an `input_response` message that Bridge forwards to the running process. Claude Code supports writing directly to `stdin`; ACP providers log a warning for now (full interactive support coming in a future release).
21+
22+
#### Persistent session resume for follow-ups (Claude Agent SDK)
23+
24+
Follow-up intents dispatched to Claude Agent SDK agents now resume the original conversation session instead of starting a new one:
25+
26+
- The session ID is saved to disk (`tasks/<taskId>/session_id`) when a task completes so it survives Bridge restarts
27+
- On follow-up, Bridge reads the session ID from memory or disk and passes it to the SDK for full conversation history resumption
28+
- A fresh dispatch (e.g. RERUN) always clears the cached session so it never accidentally resumes a stale one
29+
30+
#### Follow-up file preparation (all providers)
31+
32+
Follow-up messages now consistently write a numbered input file (`<shortId>-followup-N.md`) to the task's `input/` folder before dispatching to any provider. The agent prompt is automatically augmented with:
33+
34+
- An instruction pointing to the follow-up output file the agent should write (`<shortId>-followup-N-output.md`)
35+
- For stateless providers (Codex, Hermes CLI/ACP, Gemini, Copilot): the prior `agent_log.md` is prepended as context so the agent knows what was done in the original run
36+
37+
Providers with native session history (Claude Code, Claude Agent SDK) receive only the output-file instruction since the conversation history is already available in the session.
38+
39+
#### Follow-up emits `task_complete`
40+
41+
Follow-up runs now emit a `task_complete` event when they finish, causing the backend to properly transition the task status back to `done` or `failed` after the follow-up completes.
42+
43+
---
44+
45+
### Upgrade
46+
47+
Replace the binary and restart. No configuration changes are required.

Releases/v2026.2.5/install.ps1

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# CtrlNode Bridge — Windows installer
2+
# Usage:
3+
# iwr https://github.com/ctrlnode-ai/ctrlnode/releases/latest/download/install.ps1 | iex
4+
#
5+
# With custom install directory:
6+
# & ([scriptblock]::Create((iwr https://github.com/ctrlnode-ai/ctrlnode/releases/latest/download/install.ps1))) -InstallDir "C:\tools\ctrlnode"
7+
8+
param(
9+
[string]$InstallDir = "$env:LOCALAPPDATA\Programs\ctrlnode"
10+
)
11+
12+
$ErrorActionPreference = "Stop"
13+
$REPO = "ctrlnode-ai/ctrlnode"
14+
$BINARY_NAME = "ctrlnode.exe"
15+
$DEFAULT_WORKSPACE = $env:USERPROFILE
16+
17+
Write-Host ""
18+
Write-Host "CtrlNode Bridge Installer" -ForegroundColor Cyan
19+
Write-Host "--------------------------" -ForegroundColor Cyan
20+
Write-Host ""
21+
22+
# --- workspace directory ---
23+
Write-Host "Where is your workspace?"
24+
Write-Host "This is the root folder where ctrlnode will read and write files." -ForegroundColor DarkGray
25+
Write-Host "For security, the bridge cannot access anything outside this folder." -ForegroundColor DarkGray
26+
Write-Host "If you are a developer, point this to your source code root (e.g. C:\Code)." -ForegroundColor DarkGray
27+
$workspaceAnswer = Read-Host "Workspace [$DEFAULT_WORKSPACE]"
28+
$WorkspaceRoot = if ($workspaceAnswer.Trim()) { $workspaceAnswer.Trim() } else { $DEFAULT_WORKSPACE }
29+
Write-Host " Workspace: $WorkspaceRoot" -ForegroundColor Gray
30+
Write-Host " Tip: to change it later, set the BASE_PATH environment variable and restart ctrlnode." -ForegroundColor DarkGray
31+
Write-Host ""
32+
33+
[System.Environment]::SetEnvironmentVariable('BASE_PATH', $WorkspaceRoot, 'User')
34+
35+
# --- fetch latest release from GitHub ---
36+
Write-Host "Fetching latest release..."
37+
$releaseInfo = Invoke-RestMethod "https://api.github.com/repos/$REPO/releases/latest"
38+
$tag = $releaseInfo.tag_name
39+
40+
if (-not $tag) {
41+
Write-Error "Could not determine latest release tag."
42+
exit 1
43+
}
44+
45+
Write-Host " Release: $tag"
46+
Write-Host " Asset: $BINARY_NAME"
47+
Write-Host ""
48+
Write-Host "Downloading..."
49+
50+
$downloadUrl = "https://github.com/$REPO/releases/download/$tag/$BINARY_NAME"
51+
$tmpFile = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), $BINARY_NAME)
52+
Invoke-WebRequest -Uri $downloadUrl -OutFile $tmpFile -UseBasicParsing
53+
54+
# --- install ---
55+
New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null
56+
$dest = Join-Path $InstallDir $BINARY_NAME
57+
if (Test-Path $dest) {
58+
Get-Process | Where-Object { $_.Path -eq $dest } | ForEach-Object {
59+
Write-Host " Stopping running instance (PID $($_.Id))..." -ForegroundColor Yellow
60+
Stop-Process -Id $_.Id -Force
61+
Start-Sleep -Milliseconds 500
62+
}
63+
Remove-Item $dest -Force
64+
}
65+
Copy-Item $tmpFile $dest -Force
66+
67+
Write-Host ""
68+
Write-Host "OK Installed: $dest" -ForegroundColor Green
69+
Write-Host " Version: $tag"
70+
71+
# --- add to PATH if not already there ---
72+
$currentPath = [System.Environment]::GetEnvironmentVariable("PATH", "User")
73+
if ($currentPath -notlike "*$InstallDir*") {
74+
[System.Environment]::SetEnvironmentVariable("PATH", "$currentPath;$InstallDir", "User")
75+
Write-Host "OK Added $InstallDir to your PATH (restart terminal to apply)" -ForegroundColor Green
76+
} else {
77+
Write-Host " $InstallDir is already in PATH"
78+
}
79+
$env:PATH = "$InstallDir;$env:PATH"
80+
81+
Write-Host ""
82+
Write-Host "Next: start the Bridge:" -ForegroundColor Cyan
83+
Write-Host " ctrlnode"
84+
Write-Host ""
85+
Write-Host "Workspace: $WorkspaceRoot"
86+
Write-Host "When you run the Bridge for the first time, it will prompt for your pairing token or read it from a .env file."
87+
Write-Host "Full setup (token + API keys): ctrlnode --setup"
88+
Write-Host "Get your token at: https://app.ctrlnode.ai (Settings -> Bridge)"
89+
Write-Host ""
90+
91+
# --- optional: run the bridge now ---
92+
$runNow = Read-Host "Do you want to run ctrlnode now? (Y/n)"
93+
if ($runNow.Trim().ToLower() -ne 'n') {
94+
Write-Host "Starting ctrlnode..." -ForegroundColor Cyan
95+
$env:BASE_PATH = $WorkspaceRoot
96+
& $dest
97+
}

Releases/v2026.2.5/install.sh

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
#!/usr/bin/env sh
2+
# CtrlNode Bridge — Linux/macOS installer
3+
# Usage:
4+
# curl -fsSL https://github.com/ctrlnode-ai/ctrlnode/releases/latest/download/install.sh | sh
5+
#
6+
# With custom install directory:
7+
# curl -fsSL https://github.com/ctrlnode-ai/ctrlnode/releases/latest/download/install.sh | sh -s -- --dir ~/.local/bin
8+
9+
set -e
10+
11+
REPO="ctrlnode-ai/ctrlnode"
12+
BINARY_NAME="ctrlnode"
13+
# Default to ~/.local/bin when not root (no sudo needed).
14+
# Pass --dir /usr/local/bin to install system-wide.
15+
if [ "$(id -u)" -eq 0 ]; then
16+
DEFAULT_DIR="/usr/local/bin"
17+
else
18+
DEFAULT_DIR="$HOME/.local/bin"
19+
fi
20+
INSTALL_DIR="$DEFAULT_DIR"
21+
22+
# --- parse args ---
23+
while [ $# -gt 0 ]; do
24+
case "$1" in
25+
--dir) INSTALL_DIR="$2"; shift 2 ;;
26+
--dir=*) INSTALL_DIR="${1#*=}"; shift ;;
27+
*) shift ;;
28+
esac
29+
done
30+
31+
DEFAULT_WORKSPACE="$HOME"
32+
WORKSPACE_ROOT=""
33+
34+
echo ""
35+
echo "CtrlNode Bridge Installer"
36+
echo "--------------------------"
37+
echo ""
38+
39+
# --- workspace directory ---
40+
echo "Where is your workspace?"
41+
echo " This is the root folder where ctrlnode will read and write files."
42+
echo " For security, the bridge cannot access anything outside this folder."
43+
echo " If you are a developer, point this to your source code root (e.g. ~/code)."
44+
if [ -t 1 ] && [ -e /dev/tty ]; then
45+
printf "Workspace [%s]: " "$DEFAULT_WORKSPACE" > /dev/tty
46+
read -r ws_answer < /dev/tty
47+
WORKSPACE_ROOT="${ws_answer:-$DEFAULT_WORKSPACE}"
48+
else
49+
WORKSPACE_ROOT="$DEFAULT_WORKSPACE"
50+
fi
51+
echo " Workspace: $WORKSPACE_ROOT"
52+
echo " Tip: to change it later, set BASE_PATH and restart ctrlnode."
53+
echo ""
54+
55+
# Persist workspace
56+
SHELL_RC=""
57+
if [ -f "$HOME/.bashrc" ]; then SHELL_RC="$HOME/.bashrc"
58+
elif [ -f "$HOME/.zshrc" ]; then SHELL_RC="$HOME/.zshrc"
59+
elif [ -f "$HOME/.profile" ]; then SHELL_RC="$HOME/.profile"
60+
fi
61+
62+
if [ -n "$SHELL_RC" ]; then
63+
grep -v 'BASE_PATH' "$SHELL_RC" > "${SHELL_RC}.tmp" && mv "${SHELL_RC}.tmp" "$SHELL_RC"
64+
echo "export BASE_PATH=\"$WORKSPACE_ROOT\"" >> "$SHELL_RC"
65+
fi
66+
export BASE_PATH="$WORKSPACE_ROOT"
67+
68+
# --- detect OS and arch ---
69+
OS="$(uname -s)"
70+
ARCH="$(uname -m)"
71+
72+
case "$OS" in
73+
Linux)
74+
case "$ARCH" in
75+
x86_64)
76+
if grep -q avx2 /proc/cpuinfo 2>/dev/null; then
77+
ASSET="ctrlnode-linux-x64"
78+
else
79+
ASSET="ctrlnode-linux-x64-baseline"
80+
fi
81+
;;
82+
aarch64|arm64)
83+
echo "ERROR: Linux ARM64 binary not yet available." >&2
84+
exit 1
85+
;;
86+
*)
87+
echo "ERROR: Unsupported architecture: $ARCH" >&2
88+
exit 1
89+
;;
90+
esac
91+
;;
92+
Darwin)
93+
case "$ARCH" in
94+
arm64) ASSET="ctrlnode-darwin-arm64" ;;
95+
x86_64)
96+
echo "ERROR: macOS Intel binary not yet available." >&2
97+
exit 1
98+
;;
99+
*)
100+
echo "ERROR: Unsupported architecture: $ARCH" >&2
101+
exit 1
102+
;;
103+
esac
104+
;;
105+
*)
106+
echo "ERROR: Unsupported OS: $OS. On Windows use install.ps1 instead." >&2
107+
exit 1
108+
;;
109+
esac
110+
111+
# --- fetch latest release from GitHub ---
112+
echo "Fetching latest release..."
113+
if command -v curl >/dev/null 2>&1; then
114+
TAG="$(curl -fsSL "https://api.github.com/repos/$REPO/releases/latest" | grep '"tag_name"' | sed 's/.*"tag_name": *"\([^"]*\)".*/\1/')"
115+
elif command -v wget >/dev/null 2>&1; then
116+
TAG="$(wget -qO- "https://api.github.com/repos/$REPO/releases/latest" | grep '"tag_name"' | sed 's/.*"tag_name": *"\([^"]*\)".*/\1/')"
117+
else
118+
echo "ERROR: curl or wget required." >&2
119+
exit 1
120+
fi
121+
122+
if [ -z "$TAG" ]; then
123+
echo "ERROR: Could not determine latest release tag." >&2
124+
exit 1
125+
fi
126+
127+
echo " Release: $TAG"
128+
echo " Asset: $ASSET"
129+
echo ""
130+
echo "Downloading..."
131+
132+
DOWNLOAD_URL="https://github.com/$REPO/releases/download/$TAG/$ASSET"
133+
TMP_FILE="$(mktemp)"
134+
135+
if command -v curl >/dev/null 2>&1; then
136+
curl -fsSL "$DOWNLOAD_URL" -o "$TMP_FILE"
137+
else
138+
wget -qO "$TMP_FILE" "$DOWNLOAD_URL"
139+
fi
140+
141+
# --- install ---
142+
DEST="${INSTALL_DIR}/${BINARY_NAME}"
143+
144+
# Stop any running instance before replacing the binary
145+
if [ -f "$DEST" ]; then
146+
if command -v pkill >/dev/null 2>&1; then
147+
pkill -x "$BINARY_NAME" 2>/dev/null && sleep 0.5 || true
148+
fi
149+
fi
150+
151+
mkdir -p "$INSTALL_DIR"
152+
if [ -w "$INSTALL_DIR" ]; then
153+
cp "$TMP_FILE" "$DEST"
154+
chmod +x "$DEST"
155+
else
156+
echo "Requires sudo to install to $INSTALL_DIR..."
157+
sudo cp "$TMP_FILE" "$DEST"
158+
sudo chmod +x "$DEST"
159+
fi
160+
161+
# macOS: remove quarantine flag
162+
if [ "$OS" = "Darwin" ]; then
163+
xattr -d com.apple.quarantine "$DEST" 2>/dev/null || true
164+
fi
165+
166+
echo ""
167+
echo "OK Installed: $DEST"
168+
echo " Version: $TAG"
169+
echo ""
170+
171+
# Ensure ~/.local/bin is in PATH when using user-local install
172+
PATH_UPDATED=0
173+
if [ "$INSTALL_DIR" = "$HOME/.local/bin" ]; then
174+
if [ -n "$SHELL_RC" ] && ! grep -q '\.local/bin' "$SHELL_RC" 2>/dev/null; then
175+
echo 'export PATH="$HOME/.local/bin:$PATH"' >> "$SHELL_RC"
176+
PATH_UPDATED=1
177+
fi
178+
export PATH="$HOME/.local/bin:$PATH"
179+
hash -r 2>/dev/null || true
180+
fi
181+
182+
# If run as root via sudo, fix ownership of ~/.ctrlnode so the real user can write to it
183+
if [ "$(id -u)" -eq 0 ] && [ -n "$SUDO_USER" ]; then
184+
REAL_HOME="$(getent passwd "$SUDO_USER" | cut -d: -f6 2>/dev/null || echo "")"
185+
if [ -n "$REAL_HOME" ] && [ -d "$REAL_HOME/.ctrlnode" ]; then
186+
chown -R "$SUDO_USER:$SUDO_USER" "$REAL_HOME/.ctrlnode" 2>/dev/null || true
187+
echo " Fixed ownership of $REAL_HOME/.ctrlnode -> $SUDO_USER"
188+
fi
189+
fi
190+
if [ "$PATH_UPDATED" -eq 1 ]; then
191+
echo "┌─────────────────────────────────────────────────────┐"
192+
echo "│ IMPORTANT: reload your shell before running ctrlnode │"
193+
echo "│ │"
194+
echo "│ source $SHELL_RC"
195+
echo "│ │"
196+
echo "│ Then start the Bridge: ctrlnode │"
197+
echo "└─────────────────────────────────────────────────────┘"
198+
else
199+
echo "Next: start the Bridge:"
200+
echo " ctrlnode"
201+
fi
202+
echo ""
203+
echo "Workspace: $WORKSPACE_ROOT"
204+
echo "When you run the Bridge for the first time, it will prompt for your pairing token or read it from a .env file."
205+
echo "Full setup (token + API keys): ctrlnode --setup"
206+
echo "Get your token at: https://app.ctrlnode.ai (Settings -> Bridge)"
207+
echo ""
208+
209+
# --- optional: run the bridge now ---
210+
if [ -t 1 ] && [ -e /dev/tty ]; then
211+
printf "Do you want to run ctrlnode now? (Y/n): " > /dev/tty
212+
read -r run_now < /dev/tty
213+
case "$run_now" in
214+
n|N|no|No)
215+
echo "You can start it later with: ctrlnode"
216+
;;
217+
*)
218+
echo "Starting ctrlnode..."
219+
# Redirect stdin from /dev/tty so ctrlnode's readline reads from the
220+
# terminal, not the curl pipe. Without this, readline captures the
221+
# prompt text itself as the answer and saves a corrupted PAIRING_TOKEN.
222+
BASE_PATH="$WORKSPACE_ROOT" "$DEST" < /dev/tty
223+
;;
224+
esac
225+
fi

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "ctrlnode-ai-bridge",
3-
"version": "2026.2.2",
3+
"version": "2026.2.5",
44
"description": "CtrlNode.ai Agent Bridge",
55
"main": "index.ts",
66
"scripts": {

0 commit comments

Comments
 (0)