Files
roboco/scripts/reset_runtime_state.sh
T
306de1e656 Merge 'master' into 'dogfood feature branch' (smoke test run) (#82)
* Fix: human surface lifecycle hardening (#81)

* fix: harden agent-idle, redis loop, git errors, escalation audit

- i_am_idle no longer 500s when auto-pausing a task whose commits are
  stored as dicts: tolerate dict-or-object commit refs and run the
  synthetic-checkpoint computation inside the swallowing try block.
- The stream event loop no longer logs an idle redis read-timeout as an
  ERROR every cycle; the blocking-read timeout is treated as a normal idle.
- Git command failures surface git's own (secret-scrubbed) stderr in the
  error message instead of a bare 'Command failed', so push/fetch
  rejections are diagnosable; the injected PAT is redacted.
- The escalate-to-pool redirect emits the task.pending audit event,
  closing a status mutation that previously skipped the audit log.

* fix: let privileged operators set task status via an audited override

The task update route silently dropped a 'status' field in the request body,
so a CEO/admin could not transition a task wedged in a state with no valid
in-band move (e.g. a blocked task whose work merged out-of-band) — the panel
returned 200 while nothing changed. Add 'status' to the update schema and
apply it through a new audited 'admin_set_status' that bypasses the strict
transition validator but always records the audit event. The override
requires elevated permissions; ordinary field updates are unchanged.

* fix: stop human chat sessions from expiring between messages

Messaging sessions fell back to a hardcoded 300s idle timeout, shorter than a
normal pause in a human conversation: the sweeper closed the session and the
next message opened a new one, so a person could not hold a continuous chat.
Make the idle timeout configurable (session_idle_timeout_seconds, default
3600) and resolve an unset timeout to it at every session-creation path
instead of the 300s column fallback.

* fix: resolve doubled doc paths and stop the indexer warning flood

The doc-path resolver returned absolute paths verbatim, so a documenter path
that doubled the base segment (/app/docs/docs/...) never resolved on disk and
the docs never indexed into RAG. Reduce an absolute path under the docs base
to a relative one before normalizing, leaving truly-external absolute paths
for the indexer to skip. The indexer now skips non-markdown source files and
logs a missing/non-doc source at debug instead of warning on every pass.

* fix: reject project repo URLs that point at a protected repository

Add a configurable denylist (protected_git_urls) enforced in the project
create and update paths, so a project cannot be registered against a
repository that must not receive agent commits or merges (e.g. the roboco
source repo during a smoke run). Empty by default (no behavior change);
operators set it to sandbox smoke-test projects.

* fix: let an agent release a blocked task back to the pool

A developer (or QA/doc) trapped on a 'blocked' task had no legal forward
move — every verb rejected from that state — so the dispatcher kept
respawning it with nothing to do. Allow 'unclaim' to release a blocked task
the agent owns back to pending (assignment cleared, work session abandoned,
audited), so the cell PM can re-delegate it instead of the agent churning.

* fix: keep blocked-dev churn out and cell tasks out of board hands

- The dispatcher no longer respawns the owner of a blocked task: from blocked
  the owner has no legal move, so respawning only churns; it is revived on
  unblock or released via unclaim.
- Escalation no longer hands a cell (backend/frontend/ux_ui) coordination task
  to a board/advisory role — such an escalation is diverted to the cell pool,
  matching the existing executable-task guard. main_pm targets are unaffected.

* chore: add an opt-in full clean-slate to the reset script

FULL_RESET=1 wipes everything under the roboco data root except the
persistent service stores (ollama/postgres/redis) and clears the persisted
agent Claude session dirs (ROBOCO_CLAUDE_STATE_DIRS), which otherwise replay
across runs. Default off — the existing DB/Redis wipe + workspace git-reset
is unchanged.

* ++

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>

* fix: read the real commit key (hash) and audit the restore-unblock path

- The auto-pause checkpoint and _extract_first_commit_sha read commit dicts by
  key 'sha', but persisted commits are keyed 'hash' (CommitRef.hash) — the prior
  change stopped the crash but silently dropped every ref. Read 'hash' (sha
  fallback) at both sites; the test now uses the production dict shape so the
  regression can't hide.
- unblock_with_restore set status directly and skipped the audit log; emit the
  status-transition audit there too, like the other direct-set paths.

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-06-08 03:27:45 +02:00

248 lines
9.9 KiB
Bash
Executable File

#!/usr/bin/env bash
# Reset the smoke-test runtime state (tasks, sessions, messages, journals,
# journal entries, notifications, audit, waiting, work_sessions, groups)
# while preserving the project scaffolding (agents, projects, channels).
#
# Also resets agent git workspaces to a clean state on the default
# branch — uncommitted edits or stale feature branches from prior runs
# trap agents that try to claim a fresh task (checkout fails on dirty
# tree, branch names collide).
#
# Works from the host or from inside the orchestrator container — the
# script auto-detects.
#
# Usage:
# ./scripts/reset_runtime_state.sh # local docker
# ssh renzof-nas.local "sudo bash -s" < scripts/reset_runtime_state.sh
# # remote over ssh
#
# Env:
# WORKSPACES_ROOT — override workspace root (default: tries
# /volume1/roboco/data/workspaces then /data/workspaces; skip reset
# if none exist).
# SKIP_WORKSPACE_RESET=1 — skip the workspace cleanup step entirely
# (DB + Redis only).
# FULL_RESET=1 — opt-in aggressive clean-slate: after the DB + Redis wipe,
# delete everything under the roboco data root EXCEPT ollama/postgres/redis
# (overridable via ROBOCO_DATA_ROOT) and clear the persisted agent Claude
# session dirs listed in ROBOCO_CLAUDE_STATE_DIRS (space-separated). Skips
# the per-workspace git reset (the clones are removed and re-cloned).
set -euo pipefail
SQL_FILE="$(dirname "$0")/reset_runtime_state.sql"
if [ ! -f "$SQL_FILE" ]; then
echo "reset_runtime_state.sql not found next to this script" >&2
exit 1
fi
# Prefer sudo when available (NAS env); fall back to plain docker.
DOCKER="docker"
if command -v sudo >/dev/null 2>&1 && [ "$(id -u)" -ne 0 ]; then
DOCKER="sudo docker"
fi
if ! $DOCKER ps --format '{{.Names}}' | grep -q '^roboco-postgres$'; then
echo "roboco-postgres container not running; start the stack first." >&2
exit 1
fi
# Stop all running agent containers first so they don't keep writing to
# the DB while we're wiping it (journal FK violations, task-not-found, etc.).
# `grep` returns 1 when no matches; combined with `set -o pipefail` + `set -e`
# that would abort the entire script before the SQL wipe ever runs. Capture
# the list with `|| true` so a no-match exit is benign.
echo ">>> Stopping all agent containers..."
agents_running=$($DOCKER ps --format '{{.Names}}' | grep '^roboco-agent-' || true)
if [ -n "$agents_running" ]; then
while IFS= read -r container; do
echo " Stopping $container"
$DOCKER stop -t 5 "$container" >/dev/null 2>&1 || true
$DOCKER rm -f "$container" >/dev/null 2>&1 || true
done <<< "$agents_running"
fi
echo ">>> Wiping runtime DB state (preserving agents/projects/channels)..."
$DOCKER exec -i roboco-postgres psql -U roboco -d roboco < "$SQL_FILE"
# Flush Redis — it caches permission checks, session lookups, dispatcher
# heartbeats, and Redis Streams for events. Stale entries after a wipe
# mask the clean state (e.g. cached agent metrics). `FLUSHDB` drops
# everything in the default DB (which is all we use), preserving Redis
# auth/config.
if $DOCKER ps --format '{{.Names}}' | grep -q '^roboco-redis$'; then
echo ">>> Flushing Redis cache..."
$DOCKER exec roboco-redis redis-cli FLUSHDB | sed 's/^/ /'
fi
# Optional full clean-slate (opt-in via FULL_RESET=1): wipe everything under the
# roboco data root EXCEPT the persistent service stores (ollama / postgres /
# redis), and clear any persisted agent Claude session state that would
# otherwise replay across runs. Default OFF — the workspace git-reset below is
# the usual path. The data root is resolved from the workspaces root's parent
# (or ROBOCO_DATA_ROOT); the Claude-state paths come from ROBOCO_CLAUDE_STATE_DIRS
# (space-separated) so nothing is guessed.
if [ "${FULL_RESET:-0}" = "1" ]; then
DATA_ROOT="${ROBOCO_DATA_ROOT:-}"
if [ -z "$DATA_ROOT" ]; then
for candidate in /volume1/roboco/data /data; do
if [ -d "$candidate/workspaces" ]; then
DATA_ROOT="$candidate"
break
fi
done
fi
if [ -n "$DATA_ROOT" ] && [ -d "$DATA_ROOT" ]; then
echo ">>> FULL_RESET: wiping $DATA_ROOT/* except ollama/postgres/redis ..."
for entry in "$DATA_ROOT"/*; do
[ -e "$entry" ] || continue
case "$(basename "$entry")" in
ollama | postgres | redis)
echo " keep $(basename "$entry")"
;;
*)
echo " wipe $(basename "$entry")"
rm -rf "$entry"
;;
esac
done
else
echo ">>> FULL_RESET: no data root resolved — skipping data wipe."
fi
for cdir in ${ROBOCO_CLAUDE_STATE_DIRS:-}; do
if [ -e "$cdir" ]; then
echo " clearing Claude session state $cdir"
rm -rf "$cdir"
fi
done
echo ">>> FULL_RESET done."
exit 0
fi
# Workspace reset — each agent has a private git clone at
# {root}/{project}/{team}/{agent}. Leftover staged/untracked edits and
# feature branches from a previous run will fail the claim→start
# workflow (the `roboco_task_start` handler aborts on UNCOMMITTED_CHANGES
# and branch create collides with an already-existing feature branch).
# We bring each clone back to origin/<default> with a hard reset +
# `git clean -fdx` + local feature-branch prune.
if [ "${SKIP_WORKSPACE_RESET:-0}" = "1" ]; then
echo ">>> Skipping workspace reset (SKIP_WORKSPACE_RESET=1)."
echo ">>> Done."
exit 0
fi
# Resolve workspace root. Explicit env wins; then the NAS path; then the
# in-container path. If none exist, skip the reset (fresh deploy —
# there's nothing to clean yet).
_resolve_workspaces_root() {
if [ -n "${WORKSPACES_ROOT:-}" ]; then
echo "$WORKSPACES_ROOT"
return
fi
for candidate in /volume1/roboco/data/workspaces /data/workspaces; do
if [ -d "$candidate" ]; then
echo "$candidate"
return
fi
done
echo ""
}
WORKSPACES_ROOT=$(_resolve_workspaces_root)
if [ -z "$WORKSPACES_ROOT" ]; then
echo ">>> No workspaces directory found — skipping workspace reset."
echo ">>> Done."
exit 0
fi
echo ">>> Resetting agent workspaces under $WORKSPACES_ROOT ..."
_reset_workspace() {
local workspace="$1"
if [ ! -d "$workspace/.git" ]; then
return
fi
# Each clone is owned by the agent uid (typically the invoking
# user on the NAS, uid 1000 inside containers). Running git as
# that user keeps .git/logs from being rewritten root-owned.
local owner
owner=$(stat -c '%U' "$workspace" 2>/dev/null || stat -f '%Su' "$workspace")
local as_owner=""
if command -v sudo >/dev/null 2>&1 && [ "$owner" != "$(id -un)" ]; then
as_owner="sudo -u $owner"
fi
# Find default branch once — `git remote show origin` can fail if
# the repo has no origin or auth is missing (private clones keep no
# persisted PAT), so fall back to main, then master. We swallow both
# stderr AND the exit code: under `set -e` + `pipefail` a 128 from a
# credential-less fetch would otherwise abort the whole reset before
# the local-branch fallback below.
local default
default=$(
{ $as_owner git -C "$workspace" remote show origin 2>/dev/null \
| awk '/HEAD branch/{print $3}'; } || true
)
if [ -z "$default" ] || [ "$default" = "(unknown)" ]; then
if $as_owner git -C "$workspace" show-ref --verify --quiet refs/heads/main; then
default="main"
elif $as_owner git -C "$workspace" show-ref --verify --quiet refs/heads/master; then
default="master"
else
echo " SKIP $workspace (no default branch resolved)"
return
fi
fi
# Fetch is best-effort — if origin is unreachable we still want to
# reset locally so the next claim isn't blocked by dirty state.
$as_owner git -C "$workspace" fetch --prune --quiet origin 2>/dev/null || true
$as_owner git -C "$workspace" reset --hard --quiet HEAD 2>/dev/null || true
$as_owner git -C "$workspace" checkout --quiet "$default" 2>/dev/null \
|| $as_owner git -C "$workspace" checkout --quiet -B "$default" "origin/$default" 2>/dev/null \
|| true
# Align the default branch with the remote; swallow failures when
# origin isn't reachable (we're still better off than a dirty tree).
$as_owner git -C "$workspace" reset --hard --quiet "origin/$default" 2>/dev/null || true
$as_owner git -C "$workspace" clean -fdx --quiet 2>/dev/null || true
# Delete every local branch except the default — these are
# smoke-test feature branches that would otherwise collide with
# next-run branch creation (`fatal: branch already exists`).
# `grep -v` returning 1 when only the default exists is normal;
# `|| true` on the pipe prevents `pipefail` + `set -e` from
# aborting on a clean workspace.
local all_branches stray_branches
all_branches=$(
$as_owner git -C "$workspace" for-each-ref \
--format='%(refname:short)' refs/heads 2>/dev/null || true
)
stray_branches=$(echo "$all_branches" | grep -vxF "$default" || true)
if [ -n "$stray_branches" ]; then
while IFS= read -r branch; do
[ -z "$branch" ] && continue
$as_owner git -C "$workspace" branch -D --quiet "$branch" \
2>/dev/null || true
done <<< "$stray_branches"
fi
echo " OK $workspace (→ $default)"
}
# Workspace layout is {root}/{project}/{team}/{agent} — depth 3 under
# WORKSPACES_ROOT. Null-separated output handles paths with spaces.
count=0
while IFS= read -r -d '' workspace; do
_reset_workspace "$workspace"
count=$((count + 1))
done < <(find "$WORKSPACES_ROOT" -mindepth 3 -maxdepth 3 -type d -print0)
if [ "$count" -eq 0 ]; then
echo " (no workspaces to reset)"
fi
echo ">>> Done."