Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a05d82453 | ||
|
|
417e2f19d5 | ||
|
|
99b86a22c8 |
@@ -2038,3 +2038,34 @@ class AgentLoop:
|
|||||||
finally:
|
finally:
|
||||||
await self._runtime_events().run_status_changed(msg, session_key, "idle")
|
await self._runtime_events().run_status_changed(msg, session_key, "idle")
|
||||||
self._runtime_events().clear_turn(session_key)
|
self._runtime_events().clear_turn(session_key)
|
||||||
|
|
||||||
|
def recover_stale_sessions(self) -> int:
|
||||||
|
"""Scan all sessions on startup and recover any with stale turn state.
|
||||||
|
|
||||||
|
Returns the number of sessions that were recovered.
|
||||||
|
"""
|
||||||
|
recovered = 0
|
||||||
|
for key in self.sessions.list_session_keys():
|
||||||
|
try:
|
||||||
|
session = self.sessions.get_or_create(key)
|
||||||
|
changed = False
|
||||||
|
if self._restore_runtime_checkpoint(session):
|
||||||
|
changed = True
|
||||||
|
if self._restore_pending_user_turn(session):
|
||||||
|
changed = True
|
||||||
|
if changed:
|
||||||
|
self.sessions.save(session)
|
||||||
|
recovered += 1
|
||||||
|
logger.info(
|
||||||
|
"Recovered stale session {} on startup",
|
||||||
|
key,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
logger.debug(
|
||||||
|
"Could not recover stale session {}",
|
||||||
|
key,
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
|
if recovered:
|
||||||
|
logger.info("Recovered {} stale session(s) on startup", recovered)
|
||||||
|
return recovered
|
||||||
|
|||||||
@@ -1744,6 +1744,10 @@ def _run_gateway(
|
|||||||
local_trigger_store=trigger_store,
|
local_trigger_store=trigger_store,
|
||||||
hook_factories=[create_file_edit_activity_hook],
|
hook_factories=[create_file_edit_activity_hook],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Recover any sessions left in a stale state from a previous crash/restart.
|
||||||
|
agent.recover_stale_sessions()
|
||||||
|
|
||||||
webui_turn_coordinator = WebuiTurnCoordinator(
|
webui_turn_coordinator = WebuiTurnCoordinator(
|
||||||
bus=bus,
|
bus=bus,
|
||||||
sessions=session_manager,
|
sessions=session_manager,
|
||||||
|
|||||||
@@ -965,3 +965,23 @@ class SessionManager:
|
|||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
return sorted(sessions, key=lambda x: x.get("updated_at", ""), reverse=True)
|
return sorted(sessions, key=lambda x: x.get("updated_at", ""), reverse=True)
|
||||||
|
|
||||||
|
def list_session_keys(self) -> list[str]:
|
||||||
|
"""List all session keys from disk without loading full sessions."""
|
||||||
|
keys: list[str] = []
|
||||||
|
for path in self.sessions_dir.glob("*.jsonl"):
|
||||||
|
storage_key = self._session_key_from_path(path)
|
||||||
|
if storage_key is None:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
with open(path, encoding="utf-8") as f:
|
||||||
|
first_line = f.readline().strip()
|
||||||
|
if first_line:
|
||||||
|
data = json.loads(first_line)
|
||||||
|
if isinstance(data, dict) and data.get("_type") == "metadata":
|
||||||
|
keys.append(data.get("key") or storage_key)
|
||||||
|
else:
|
||||||
|
keys.append(storage_key)
|
||||||
|
except (OSError, _SESSION_DATA_ERRORS):
|
||||||
|
continue
|
||||||
|
return keys
|
||||||
|
|||||||
Executable
+187
@@ -0,0 +1,187 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# nanobot deploy/rollback supervisor
|
||||||
|
# Manages git-checkout-based deployments with health checks and automatic rollback.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# deploy.sh <commit-ish> Deploy a candidate commit
|
||||||
|
# deploy.sh rollback Rollback to last known-good
|
||||||
|
# deploy.sh status Show current deployment state
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
CHECKOUT="/home/claw/nanobot-src"
|
||||||
|
STATE_FILE="/home/claw/.nanobot/deploy-state.json"
|
||||||
|
HEALTH_URL="http://127.0.0.1:18790/health"
|
||||||
|
STABILIZE_SEC=8
|
||||||
|
HEALTH_TIMEOUT_SEC=30
|
||||||
|
SERVICE="nanobot"
|
||||||
|
|
||||||
|
log() { echo "[deploy] $(date '+%H:%M:%S') $*"; }
|
||||||
|
|
||||||
|
# ---- helpers ----
|
||||||
|
|
||||||
|
current_commit() { git -C "$CHECKOUT" rev-parse HEAD; }
|
||||||
|
short_hash() { echo "${1:0:8}"; }
|
||||||
|
|
||||||
|
read_state() {
|
||||||
|
if [[ -f "$STATE_FILE" ]]; then
|
||||||
|
python3 -c "import json; d=json.load(open('$STATE_FILE')); print(d.get('known_good',''))"
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
write_state() {
|
||||||
|
local known_good="$1"
|
||||||
|
python3 -c "
|
||||||
|
import json, sys
|
||||||
|
d = {}
|
||||||
|
try: d = json.load(open('$STATE_FILE'))
|
||||||
|
except: pass
|
||||||
|
d['known_good'] = '$known_good'
|
||||||
|
d['last_updated'] = '$(date -Iseconds)'
|
||||||
|
json.dump(d, open('$STATE_FILE', 'w'), indent=2)
|
||||||
|
"
|
||||||
|
}
|
||||||
|
|
||||||
|
restart_service() {
|
||||||
|
export XDG_RUNTIME_DIR=/run/user/1000
|
||||||
|
systemctl --user restart "$SERVICE"
|
||||||
|
}
|
||||||
|
|
||||||
|
service_active() {
|
||||||
|
export XDG_RUNTIME_DIR=/run/user/1000
|
||||||
|
systemctl --user is-active --quiet "$SERVICE"
|
||||||
|
}
|
||||||
|
|
||||||
|
health_check() {
|
||||||
|
# Returns 0 if healthy, 1 if not
|
||||||
|
local deadline=$((SECONDS + HEALTH_TIMEOUT_SEC))
|
||||||
|
while (( SECONDS < deadline )); do
|
||||||
|
if curl -sf --noproxy '*' --max-time 3 "$HEALTH_URL" >/dev/null 2>&1; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---- commands ----
|
||||||
|
|
||||||
|
cmd_status() {
|
||||||
|
echo "Checkout: $CHECKOUT"
|
||||||
|
echo "Current commit: $(current_commit) ($(short_hash "$(current_commit)"))"
|
||||||
|
echo "Known-good: $(read_state)"
|
||||||
|
echo "Service: $(service_active && echo 'active' || echo 'inactive')"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_deploy() {
|
||||||
|
local candidate="$1"
|
||||||
|
local known_good
|
||||||
|
known_good=$(read_state)
|
||||||
|
|
||||||
|
cd "$CHECKOUT"
|
||||||
|
|
||||||
|
# Record current as known-good if not set
|
||||||
|
if [[ -z "$known_good" ]]; then
|
||||||
|
known_good=$(current_commit)
|
||||||
|
write_state "$known_good"
|
||||||
|
log "Initial known-good: $(short_hash "$known_good")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check candidate exists
|
||||||
|
if ! git cat-file -e "$candidate^{commit}" 2>/dev/null; then
|
||||||
|
log "ERROR: candidate '$candidate' is not a valid commit"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local candidate_hash
|
||||||
|
candidate_hash=$(git rev-parse "$candidate")
|
||||||
|
|
||||||
|
if [[ "$candidate_hash" == "$(current_commit)" ]]; then
|
||||||
|
log "Already at candidate $(short_hash "$candidate_hash"), just restarting"
|
||||||
|
else
|
||||||
|
log "Deploying candidate: $(short_hash "$candidate_hash")"
|
||||||
|
log "Known-good: $(short_hash "$known_good")"
|
||||||
|
|
||||||
|
git checkout "$candidate_hash"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Restarting service..."
|
||||||
|
restart_service
|
||||||
|
|
||||||
|
log "Waiting ${STABILIZE_SEC}s for stabilization..."
|
||||||
|
sleep "$STABILIZE_SEC"
|
||||||
|
|
||||||
|
if ! service_active; then
|
||||||
|
log "ERROR: service not active after restart"
|
||||||
|
cmd_rollback_internal "$known_good" "$candidate_hash"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Running health check..."
|
||||||
|
if health_check; then
|
||||||
|
log "Health check PASSED"
|
||||||
|
write_state "$candidate_hash"
|
||||||
|
log "Promoted $(short_hash "$candidate_hash") to known-good"
|
||||||
|
else
|
||||||
|
log "ERROR: health check FAILED"
|
||||||
|
cmd_rollback_internal "$known_good" "$candidate_hash"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_rollback_internal() {
|
||||||
|
local target="$1"
|
||||||
|
local failed="${2:-unknown}"
|
||||||
|
|
||||||
|
log "ROLLING BACK to $(short_hash "$target")"
|
||||||
|
|
||||||
|
cd "$CHECKOUT"
|
||||||
|
git checkout "$target"
|
||||||
|
restart_service
|
||||||
|
|
||||||
|
sleep "$STABILIZE_SEC"
|
||||||
|
|
||||||
|
if service_active && health_check; then
|
||||||
|
log "Rollback successful, service healthy"
|
||||||
|
# Write failure report
|
||||||
|
cat > "/home/claw/.nanobot/deploy-failure.json" <<EOF
|
||||||
|
{
|
||||||
|
"failed_candidate": "$failed",
|
||||||
|
"known_good": "$target",
|
||||||
|
"timestamp": "$(date -Iseconds)",
|
||||||
|
"rollback_successful": true
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
else
|
||||||
|
log "CRITICAL: rollback also failed! Manual intervention required."
|
||||||
|
cat > "/home/claw/.nanobot/deploy-failure.json" <<EOF
|
||||||
|
{
|
||||||
|
"failed_candidate": "$failed",
|
||||||
|
"known_good": "$target",
|
||||||
|
"timestamp": "$(date -Iseconds)",
|
||||||
|
"rollback_successful": false
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_rollback() {
|
||||||
|
local known_good
|
||||||
|
known_good=$(read_state)
|
||||||
|
if [[ -z "$known_good" ]]; then
|
||||||
|
log "No known-good recorded, cannot rollback"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
cmd_rollback_internal "$known_good" "$(current_commit)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---- main ----
|
||||||
|
|
||||||
|
case "${1:-status}" in
|
||||||
|
status) cmd_status ;;
|
||||||
|
rollback) cmd_rollback ;;
|
||||||
|
*) cmd_deploy "$1" ;;
|
||||||
|
esac
|
||||||
Reference in New Issue
Block a user