Files
tutti/tools/scripts/rescue-edit-retry-poison-pill.sh
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

106 lines
4.7 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# rescue-edit-retry-poison-pill.sh
#
# Unbricks a Tutti install that crashes on launch with:
# "build tuttid server: recover agent host:
# process runtime operation <id>: agent session not found"
# "failed to start managed tuttid: tuttid exited before it published its listener info."
#
# Cause: a durable `edit_retry` runtime operation is stuck (rolled back but never
# resent). It is harmless while the daemon runs, but on the next daemon restart
# the cold-recovery pass treats it as fatal, so tuttid exits(1) on every launch.
#
# This script quarantines those stuck operations (marks them `failed`, which
# removes them from the recovery claimable set) so the daemon can boot again.
# It does NOT delete history and it backs the database up first.
#
# Usage:
# 1. Fully quit Tutti (Cmd-Q). The daemon must be stopped.
# 2. bash tools/scripts/rescue-edit-retry-poison-pill.sh
# 3. Reopen Tutti.
#
# Options (env vars):
# TUTTI_DB=/path/to/tuttid.db Override DB path (default: ~/.tutti/tuttid.db)
# MODE=delete Delete the rows instead of marking them failed
# YES=1 Skip the confirmation prompt
#
set -euo pipefail
DB="${TUTTI_DB:-$HOME/.tutti/tuttid.db}"
MODE="${MODE:-quarantine}"
die() { echo "❌ $*" >&2; exit 1; }
command -v sqlite3 >/dev/null 2>&1 || die "sqlite3 not found (macOS ships it at /usr/bin/sqlite3)."
[[ -f "$DB" ]] || die "database not found: $DB (set TUTTI_DB=... to override)"
# 1. Refuse to run while the daemon/app is alive (avoids a locked DB and racing recovery).
if pgrep -x tuttid >/dev/null 2>&1 || pgrep -f "Tutti.app/Contents/" >/dev/null 2>&1; then
die "Tutti / tuttid is still running. Fully quit Tutti (Cmd-Q) and re-run this script."
fi
WHERE="kind='edit_retry' AND status IN ('prepared','leased')"
# 2. Show what will change.
CNT="$(sqlite3 "$DB" "SELECT count(*) FROM workspace_agent_runtime_operations WHERE $WHERE;")"
echo "Database: $DB"
echo "Stuck edit_retry operations found: $CNT"
if [[ "$CNT" -eq 0 ]]; then
echo "✅ Nothing to fix — no stuck edit_retry operations. The crash is caused by something else."
exit 0
fi
echo
echo "-------------------------------------------------------------------------------"
sqlite3 -header -column "$DB" \
"SELECT substr(operation_id,1,8) AS op, substr(agent_session_id,1,8) AS session, status, attempt, substr(last_error,1,40) AS last_error
FROM workspace_agent_runtime_operations WHERE $WHERE;"
echo "-------------------------------------------------------------------------------"
echo
if [[ "$MODE" == "delete" ]]; then
echo "Action: DELETE these $CNT operation row(s)."
else
echo "Action: mark these $CNT operation row(s) as 'failed' (quarantine; reversible from backup)."
fi
# 3. Confirm.
if [[ "${YES:-}" != "1" ]]; then
read -r -p "Proceed? [y/N] " reply
[[ "$reply" == "y" || "$reply" == "Y" ]] || die "Aborted; no changes made."
fi
# 4. Consistent backup (includes any WAL) before mutating.
BAK="${DB}.bak.$(date +%Y%m%d%H%M%S)"
sqlite3 "$DB" ".backup '$BAK'"
echo "🗄 Backup written: $BAK"
# 5. Apply. Two parts, both required:
# (a) unfence the affected sessions' effective history back to 'ready' so they
# can send again — a stuck edit_retry leaves recovery_state at
# resend_pending/rollback_pending/recovery_required, which blocks every
# subsequent prompt. This MUST run before deleting the operation rows,
# because it identifies the fenced sessions via those rows.
# (b) fail (or delete) the stuck operation rows so recovery no longer trips.
NOW_MS="CAST((julianday('now')-2440587.5)*86400000 AS INTEGER)"
sqlite3 "$DB" ".timeout 5000" "
UPDATE workspace_agent_session_history
SET recovery_state='ready', operation_id='', updated_at_unix_ms=$NOW_MS
WHERE recovery_state != 'ready'
AND operation_id IN (SELECT operation_id FROM workspace_agent_runtime_operations WHERE kind='edit_retry');"
if [[ "$MODE" == "delete" ]]; then
sqlite3 "$DB" ".timeout 5000" "DELETE FROM workspace_agent_runtime_operations WHERE $WHERE;"
else
sqlite3 "$DB" ".timeout 5000" "
UPDATE workspace_agent_runtime_operations
SET status='failed', result='failed',
lease_owner=NULL, lease_expires_at_unix_ms=NULL, next_attempt_at_unix_ms=NULL,
last_error='quarantined by rescue-edit-retry-poison-pill.sh',
version=version+1, updated_at_unix_ms=$NOW_MS
WHERE $WHERE;"
fi
REMAIN="$(sqlite3 "$DB" "SELECT count(*) FROM workspace_agent_runtime_operations WHERE $WHERE;")"
[[ "$REMAIN" -eq 0 ]] || die "expected 0 stuck rows after fix, still $REMAIN — restore from $BAK and report."
echo "✅ Done. Cleared $CNT stuck operation(s). Reopen Tutti now."
echo " (If anything looks wrong, restore with: cp '$BAK' '$DB')"