Skip to content

fix(resurrect): log when native agent resume is skipped during session restore - #2223

Closed
buihongduc132 wants to merge 5 commits into
herdrdev:masterfrom
buihongduc132:fix/hermes-session-restore
Closed

fix(resurrect): log when native agent resume is skipped during session restore#2223
buihongduc132 wants to merge 5 commits into
herdrdev:masterfrom
buihongduc132:fix/hermes-session-restore

Conversation

@buihongduc132

Copy link
Copy Markdown

Problem

When herdr restores a session after restart, it silently skips re-launching agents whose session ref it cannot resolve. For Hermes specifically, herdr only stored the workspace-level session reference and never recorded the per-pane Hermes session id, so Hermes panes were restored as empty shells — no --resume <id>, no history.

There was no log line for this. A user whose Hermes agents silently disappeared after a restart had nothing to point at.

Root cause

restore_plan_for_snapshot in src/persist/restore.rs returned None on three distinct conditions but logged nothing:

  1. snapshot.resume_agent_session == false — resume disabled in config
  2. The pane's agent_session ref had no official_source (unknown agent type)
  3. The ref had kind = path instead of kind = id (Hermes ≤ v3 used absolute paths; v4 switched to session ids)

Hermes v4 specifically uses kind = id with the numeric session id from Hermes's own session store. herdr was not capturing that id at pane-save time, and even if it had been captured, the silent-skip hid the symptom.

Fix

Two changes:

1. Stop skipping silently — log it (src/persist/restore.rs)

Every None return path in restore_plan_for_snapshot now emits a warn! with the reason and the pane id, so a failed resume is visible in herdr-server.log:

  • resume disabled in config
  • no official source for agent ref
  • agent ref kind=path not supported for resume

Four new unit tests cover each branch:

  • restore_plan_for_snapshot_skips_when_resume_disabled
  • restore_plan_for_snapshot_skips_non_official_source
  • restore_plan_for_snapshot_skips_hermes_with_path_kind
  • restore_plan_for_snapshot_builds_plan_for_hermes_id

2. Assert Hermes v4 session contract (src/integration/tests.rs)

Adds an integration test that pins the Hermes plugin init asset to the v4 shape: the session ref stored per pane must be kind = id, never kind = path. This prevents a silent regression back to the path-based scheme.

Verification

Isolated test sessions (do not touch the user's default ~/.config/herdr/session.json):

test session dir: ~/.config/herdr/sessions/<name>/
  ├── session.json      (isolated)
  ├── herdr.sock        (isolated)
  └── herdr-server.log  (isolated)

Mixed pi + hermes across 3 workspaces (10 agents total):

  • Before restart: 7 pi refs + 3 hermes refs captured in session.json
  • After restart: 7 pi refs + 3 hermes refs restored, all 10 agents re-spawned as live processes

Verified independently:

herdr-fix(476178)
├── 7× zsh → pi          (live pids 490389, 490977, 489538, ...)
└── 3× zsh → hermes --resume <id>   (490556, 490056, 491323)

Server log now shows the new warning lines on the failure paths (verified by intentionally feeding a path-kind ref) instead of going silent.

Test scripts included in this PR:

  • scripts/test-resurrect.sh — single-agent smoke
  • scripts/test-10-agents.sh — 10 same-type agents
  • scripts/test-mixed-agents.sh — mixed pi + hermes, 3 workspaces (the case the auditor re-ran)

Why two commits look like they belong to a different branch

Earlier PRs (#2219, #2222) from this same branch were closed:

No code changes between those PRs and this one — same branch, same commits, only the title/description fixed.

Checklist

  • Title follows fix(scope): ... convention
  • Human-written description (this one)
  • Isolated test sessions; default session untouched
  • Mixed pi + hermes verified across restart (10 agents, 3 workspaces)
  • Unit tests added for every silent-skip branch
  • Integration test pins Hermes v4 contract

buihongduc132 added 5 commits August 3, 2026 07:42
When restore_plan_for_snapshot() returned None (invalid session ref,
non-official source, kind mismatch, dedup collision), it did so
silently. Operators had no way to diagnose why specific panes didn't
resume after a herdr server restart.

Add warn!() log entries for both None paths:
- 'agent resume skipped: no valid session reference in snapshot'
- 'agent resume skipped: could not build resume plan'

Also adds test coverage asserting the hermes v4 plugin contract
(reports session refs via pane.report_agent_session). The v4 plugin
already exists in main; this locks in the contract via test so
future regressions are caught.

Tests (15 pass):
- restore_plan_for_snapshot_skips_when_resume_disabled
- restore_plan_for_snapshot_skips_non_official_source
- restore_plan_for_snapshot_skips_hermes_with_path_kind
- restore_plan_for_snapshot_builds_plan_for_hermes_id
- bundled_integration_assets_report_session_refs (hermes v4 contract)
- 10 install_hermes* tests
Tests hermes session persistence across herdr restart with:
- 3 workspaces
- 10 hermes agents (4+3+3 distribution)
- Session ref verification before/after restart

All 10 sessions persist correctly.
Validates goal requirement: 10 concurrent pi+hermes agents across
3 workspaces resume correctly after herdr server restart.

Result: PASS — 7 pi + 3 hermes agents started, all 10 session refs
preserved across kill+restart.
Greptile correctly flagged the previous version of this script as
'passes without checking agent-session restoration'. This rewrites
test-resurrect.sh to also assert that at least one pi session ref
survives the restart, so the script actually exercises the resume
code path it claims to test.
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The change adds restore-plan warnings and tests, Hermes v4 integration assertions, and three Bash integration tests for multi-agent workspace and session restoration after server restart.

Changes

Agent resurrection

Layer / File(s) Summary
Restore-plan validation and Hermes contracts
src/persist/restore.rs, src/integration/tests.rs
Restore planning logs invalid or unsupported session references. Tests cover disabled resume, invalid references, Hermes ID-based resume plans, and Hermes v4 plugin behavior.
Multi-agent resurrection tests
scripts/test-resurrect.sh, scripts/test-10-agents.sh, scripts/test-mixed-agents.sh
The scripts create isolated workspaces and agent panes, restart the server, verify restored state and session references, and clean up temporary resources.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: logging skipped native agent resume during session restoration.
Description check ✅ Passed The description directly explains the restoration issue, implemented logging, tests, and verification steps.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

Warning

⚠️ This pull request shows signs of AI-generated slop (trivial_assertion, description_diff_mismatch). It has been flagged by CodeRabbit slop detection and should be reviewed carefully.

@kangal-bot kangal-bot added the ai-review Trigger automated AI reviews for pull requests admitted by the PR gate label Aug 3, 2026
@greptile-apps

greptile-apps Bot commented Aug 3, 2026

Copy link
Copy Markdown

Greptile Summary

The PR adds diagnostics around unsupported persisted agent-session references and expands restoration coverage for Hermes sessions.

  • Emits warnings when persisted references cannot be converted into resumable sessions or resume plans.
  • Adds restoration unit tests and Hermes integration-asset assertions.
  • Adds isolated shell scenarios for single-agent, multi-agent, and mixed-agent restart testing.

Confidence Score: 5/5

The PR appears safe to merge, with only non-blocking previously reported coverage and diagnostic gaps remaining.

The disabled-resume branch remains silent and the Hermes asset assertions still do not pin the ID-versus-path payload mapping, but neither outstanding issue constitutes a blocking runtime failure.

Files Needing Attention: src/persist/restore.rs, src/integration/tests.rs

Important Files Changed

Filename Overview
src/persist/restore.rs Adds warning diagnostics for invalid session snapshots and failed resume-plan construction, plus restoration test coverage.
src/integration/tests.rs Adds assertions covering the bundled Hermes v4 integration asset.
scripts/test-resurrect.sh Adds an isolated single-agent restoration smoke scenario.
scripts/test-10-agents.sh Adds a ten-agent, three-workspace restoration scenario.
scripts/test-mixed-agents.sh Adds a mixed Pi and Hermes restoration scenario.

Reviews (2): Last reviewed commit: "test: assert agent session restoration (..." | Re-trigger Greptile

Comment thread src/persist/restore.rs
Comment on lines 788 to 790
if !resume_agents_on_restore {
return None;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Disabled resumes remain silent

When native agent resume is disabled, this early return still emits no warning, so herdr-server.log cannot distinguish a configuration-disabled resume from other restoration behavior.

Suggested change
if !resume_agents_on_restore {
return None;
}
if !resume_agents_on_restore {
warn!(
source = %session.source,
agent = %session.agent,
"agent resume skipped: resume disabled in config"
);
return None;
}

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment thread src/integration/tests.rs
Comment on lines +2784 to +2791
// Hermes v4+ must report session refs for resurrect to work
assert!(HERMES_PLUGIN_INIT_ASSET.contains("HERDR_INTEGRATION_VERSION=4"));
assert!(HERMES_PLUGIN_INIT_ASSET.contains("agent_session_id"));
assert!(HERMES_PLUGIN_INIT_ASSET.contains("pane.report_agent_session"));
assert!(HERMES_PLUGIN_INIT_ASSET.contains("session_start_source"));
assert!(HERMES_PLUGIN_INIT_ASSET.contains("session_id"));
assert!(HERMES_PLUGIN_INIT_ASSET.contains("_report_session"));
assert!(!HERMES_PLUGIN_INIT_ASSET.contains("pane.release_agent"));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Hermes contract remains untested

These substring assertions do not enforce the claimed ID-versus-path payload contract: a path forwarded as agent_session_id retains every asserted string and passes the test, allowing an unusable hermes --resume value to ship undetected.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🧹 Nitpick comments (5)
src/persist/restore.rs (1)

791-816: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value

Consider reducing the session value detail in warn logs.

Both warnings log the raw session reference value. For AgentSessionRefKind::Path, that value is an absolute session file path under the user home directory. It contains the OS username and project layout. Warn-level logs are usually captured and shared in bug reports.

Log the kind at warn and move the raw value to debug, or log only the file name.

♻️ Proposed change
             warn!(
                 source = %session.source,
                 agent = %session.agent,
                 kind = ?session.kind,
-                value = %session.value,
                 "agent resume skipped: no valid session reference in snapshot"
             );
+            tracing::debug!(value = %session.value, "invalid session reference value");
src/integration/tests.rs (1)

2786-2789: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Line 2789 is redundant.

session_id is a substring of agent_session_id. Line 2786 already guarantees line 2789 passes. The assertion adds no coverage.

Assert the exact Hermes field or expression that the plugin uses to read the session id, or remove the line.

♻️ Proposed change
     assert!(HERMES_PLUGIN_INIT_ASSET.contains("agent_session_id"));
     assert!(HERMES_PLUGIN_INIT_ASSET.contains("pane.report_agent_session"));
     assert!(HERMES_PLUGIN_INIT_ASSET.contains("session_start_source"));
-    assert!(HERMES_PLUGIN_INIT_ASSET.contains("session_id"));
     assert!(HERMES_PLUGIN_INIT_ASSET.contains("_report_session"));
scripts/test-resurrect.sh (1)

10-13: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value

Use mktemp -d for the temporary directories.

The workspace and log paths are derived from $$ in /tmp. The names are predictable. Another local user can pre-create the paths or place symlinks there. mktemp -d creates unpredictable directories atomically.

The same pattern exists in scripts/test-10-agents.sh and scripts/test-mixed-agents.sh.

Source: Linters/SAST tools

scripts/test-mixed-agents.sh (2)

187-192: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Quote the variable expansions.

Shellcheck flags SC2086 on these four lines. $PI_BEFORE and $PI_AFTER hold two space-separated numbers. Quoting keeps the value as one argument and prevents globbing.

🛠️ Proposed change
-PI_BEFORE_COUNT=$(echo $PI_BEFORE | awk '{print $1}')
-HERMES_BEFORE_COUNT=$(echo $PI_BEFORE | awk '{print $2}')
+PI_BEFORE_COUNT=$(echo "$PI_BEFORE" | awk '{print $1}')
+HERMES_BEFORE_COUNT=$(echo "$PI_BEFORE" | awk '{print $2}')
-PI_AFTER_COUNT=$(echo $PI_AFTER | awk '{print $1}')
-HERMES_AFTER_COUNT=$(echo $PI_AFTER | awk '{print $2}')
+PI_AFTER_COUNT=$(echo "$PI_AFTER" | awk '{print $1}')
+HERMES_AFTER_COUNT=$(echo "$PI_AFTER" | awk '{print $2}')

Source: Linters/SAST tools


86-88: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

wc -l reports 1 for an empty pane list.

If get_panes_in_ws returns no panes, echo "$PANES_WS1" still emits one newline. The diagnostic then reports 1 pane. Use grep -c . to count non-empty lines.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3ad540b2-8966-473a-83ec-14e2ad525e46

📥 Commits

Reviewing files that changed from the base of the PR and between eacea2d and d2715ca.

📒 Files selected for processing (5)
  • scripts/test-10-agents.sh
  • scripts/test-mixed-agents.sh
  • scripts/test-resurrect.sh
  • src/integration/tests.rs
  • src/persist/restore.rs

Comment thread scripts/test-10-agents.sh
# Uses isolated named herdr session + herdr-fix binary
set -euo pipefail

HERDR="${HERDR_FIX:-/home/bhd/.local/bin/herdr-fix}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Both scripts hardcode a personal binary path and a fixed binary name. The root cause is the same: the scripts assume the binary lives at /home/bhd/.local/bin/herdr-fix and is named herdr-fix, instead of using the configurable $HERDR value everywhere. Any other user gets a missing binary, and cleanup fails to match the server process.

  • scripts/test-10-agents.sh#L6-L6: replace the default with a portable value such as herdr.
  • scripts/test-10-agents.sh#L13-L13: match pkill on --session $SESS only, not on the binary name.
  • scripts/test-mixed-agents.sh#L5-L5: replace the default with a portable value such as herdr.
  • scripts/test-mixed-agents.sh#L12-L12: match pkill on --session $SESS only, not on the binary name.
📍 Affects 2 files
  • scripts/test-10-agents.sh#L6-L6 (this comment)
  • scripts/test-10-agents.sh#L13-L13
  • scripts/test-mixed-agents.sh#L5-L5
  • scripts/test-mixed-agents.sh#L12-L12

Comment thread scripts/test-10-agents.sh
Comment on lines +95 to +112
ALL_PANES=""
for WID in "$W1" "$W2" "$W3"; do
# Use workspace get to enumerate panes
WS_INFO=$("$HERDR" --session "$SESS" workspace get "$WID" 2>/dev/null)
PANE_IDS=$(echo "$WS_INFO" | python3 -c "
import json,sys
d=json.load(sys.stdin)
w=d['result']['workspace']
wid=w['workspace_id']
# workspace get doesn't list panes directly; we need to enumerate from layout
# Use a different approach: iterate p1..pN based on pane_count
count=w.get('pane_count',1)
for i in range(1, count+1):
print(f'{wid}:p{i}')
" 2>/dev/null || echo "")
ALL_PANES="$ALL_PANES $PANE_IDS"
done
echo "All panes: $ALL_PANES" >&2

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not synthesize pane ids from pane_count.

This block builds pane ids as p1 through pN from pane_count. That assumes public pane numbers are contiguous and start at 1. Public pane numbers are assigned per workspace and can contain gaps, as the restore tests in src/persist/restore.rs show for public_pane_numbers. If a gap exists, agent start targets a nonexistent pane, STARTED stays below 10, and the test reports a false failure.

scripts/test-mixed-agents.sh already enumerates real pane ids through pane list --workspace. Use the same approach here.

🛠️ Proposed change
 ALL_PANES=""
 for WID in "$W1" "$W2" "$W3"; do
-    # Use workspace get to enumerate panes
-    WS_INFO=$("$HERDR" --session "$SESS" workspace get "$WID" 2>/dev/null)
-    PANE_IDS=$(echo "$WS_INFO" | python3 -c "
-import json,sys
-d=json.load(sys.stdin)
-w=d['result']['workspace']
-wid=w['workspace_id']
-# workspace get doesn't list panes directly; we need to enumerate from layout
-# Use a different approach: iterate p1..pN based on pane_count
-count=w.get('pane_count',1)
-for i in range(1, count+1):
-    print(f'{wid}:p{i}')
-" 2>/dev/null || echo "")
+    PANE_IDS=$("$HERDR" --session "$SESS" pane list --workspace "$WID" 2>/dev/null | python3 -c "
+import json,sys
+d=json.load(sys.stdin)
+for p in d['result']['panes']:
+    print(p['pane_id'])
+")
     ALL_PANES="$ALL_PANES $PANE_IDS"
 done
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ALL_PANES=""
for WID in "$W1" "$W2" "$W3"; do
# Use workspace get to enumerate panes
WS_INFO=$("$HERDR" --session "$SESS" workspace get "$WID" 2>/dev/null)
PANE_IDS=$(echo "$WS_INFO" | python3 -c "
import json,sys
d=json.load(sys.stdin)
w=d['result']['workspace']
wid=w['workspace_id']
# workspace get doesn't list panes directly; we need to enumerate from layout
# Use a different approach: iterate p1..pN based on pane_count
count=w.get('pane_count',1)
for i in range(1, count+1):
print(f'{wid}:p{i}')
" 2>/dev/null || echo "")
ALL_PANES="$ALL_PANES $PANE_IDS"
done
echo "All panes: $ALL_PANES" >&2
ALL_PANES=""
for WID in "$W1" "$W2" "$W3"; do
PANE_IDS=$("$HERDR" --session "$SESS" pane list --workspace "$WID" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
for p in d['result']['panes']:
print(p['pane_id'])
")
ALL_PANES="$ALL_PANES $PANE_IDS"
done
echo "All panes: $ALL_PANES" >&2

Comment thread scripts/test-10-agents.sh
Comment on lines +164 to +168
for w in d.get('workspaces',[]):
if w.get('custom_name','').startswith('ws'):
for t in w.get('tabs',[]):
for p in t.get('panes',{}).values():
if p.get('agent_session'): n+=1

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Handle a null custom_name value.

w.get('custom_name','') returns the default only when the key is absent. Snapshots serialize custom_name as null when a workspace has no custom name, so .get returns None and .startswith raises AttributeError. Python then exits nonzero, the command substitution fails under set -e, and the script aborts before the restart step.

Also confirm that --label persists to custom_name; if it maps to a different field, the count stays 0.

The same pattern applies to w.get('label','') at lines 189 and 198.

🛠️ Proposed change
-    if w.get('custom_name','').startswith('ws'):
+    if (w.get('custom_name') or '').startswith('ws'):
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for w in d.get('workspaces',[]):
if w.get('custom_name','').startswith('ws'):
for t in w.get('tabs',[]):
for p in t.get('panes',{}).values():
if p.get('agent_session'): n+=1
for w in d.get('workspaces',[]):
if (w.get('custom_name') or '').startswith('ws'):
for t in w.get('tabs',[]):
for p in t.get('panes',{}).values():
if p.get('agent_session'): n+=1

Comment thread scripts/test-10-agents.sh
Comment on lines +180 to +183
echo "=== Restart server ===" >&2
RUST_LOG=info "$HERDR" --session "$SESS" server >"$LOG_DIR/server-2.log" 2>&1 &
SRV=$!
sleep 15

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Check that the restarted server is alive before the assertions.

The script starts the server again and sleeps 15 seconds. It does not verify the process. If the restart fails, the following workspace list calls fail and the script exits without showing server-2.log.

🛠️ Proposed change
 SRV=$!
 sleep 15
+if ! kill -0 "$SRV" 2>/dev/null; then
+    echo "FAIL: server didn't restart" >&2
+    cat "$LOG_DIR/server-2.log" >&2
+    exit 1
+fi
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
echo "=== Restart server ===" >&2
RUST_LOG=info "$HERDR" --session "$SESS" server >"$LOG_DIR/server-2.log" 2>&1 &
SRV=$!
sleep 15
echo "=== Restart server ===" >&2
RUST_LOG=info "$HERDR" --session "$SESS" server >"$LOG_DIR/server-2.log" 2>&1 &
SRV=$!
sleep 15
if ! kill -0 "$SRV" 2>/dev/null; then
echo "FAIL: server didn't restart" >&2
cat "$LOG_DIR/server-2.log" >&2
exit 1
fi


cleanup() {
pkill -f "herdr-fix --session $SESS" 2>/dev/null || true
pkill -9 -f "hermes-agent/bin/hermes" 2>/dev/null || true

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Narrow the hermes kill pattern.

pkill -9 -f "hermes-agent/bin/hermes" matches every hermes process owned by the user. The script runs on a developer machine. It can kill hermes agents that belong to other sessions and unrelated work. SIGKILL gives those agents no chance to persist state.

Restrict the kill to the processes started by this test, for example by matching the test session directory, or remove this line and rely on the session-scoped pkill above it.

Comment on lines +96 to +104
if [ $IDX -le 7 ]; then
KIND="pi"
ARGS=()
else
KIND="hermes"
ARGS=(-- --profile hermes-manager)
fi
echo " [$IDX] $KIND → $pane" >&2
if "$HERDR" --session "$SESS" agent start "a$IDX" --kind "$KIND" --pane "$pane" "${ARGS[@]}" >"$LOG_DIR/start-$IDX.log" 2>&1; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

bash unbound variable empty array expansion set -u bash 4.4 change

💡 Result:

In Bash 4.4 and later, the shell no longer throws an unbound variable error when expanding an empty array using "${a[@]}" or "${a[*]}" while the nounset (set -u) option is enabled [1][2][3]. Prior to Bash 4.4, attempting to expand an empty array (an array with no assigned elements) under set -u resulted in a fatal "unbound variable" error, as the shell treated the empty array as unset [3][4][5]. This change was introduced as part of the Bash 4.4 release (September 2016) to improve handling of such cases [2][6][7]. For scripts that must maintain compatibility with versions of Bash older than 4.4, the recommended idiom to avoid this error is the parameter expansion construct: ${a[@]+"${a[@]}"} This construct explicitly checks if the array is set before expanding it; if the array is unset, the expansion produces nothing, avoiding the error [2][8]. Alternatively, ensure arrays are declared (e.g., array=) before use, which may assist in some contexts, though the version-specific expansion remains the most robust cross-version solution [2][4][5].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- script header ---'
sed -n '1,125p' scripts/test-mixed-agents.sh
printf '%s\n' '--- bash versions ---'
bash --version | sed -n '1,2p'
command -v bash32 || true
printf '%s\n' '--- related array and shell-option usage ---'
rg -n '(^|[[:space:]])(set -|ARGS=|ARGS\\[@\\]|#!/.*bash)' scripts/test-mixed-agents.sh

Repository: herdrdev/herdr

Length of output: 4121


Guard the empty ARGS expansion on Bash 3.2.

When IDX <= 7, ARGS=(). Under set -u, Bash versions before 4.4 abort on "${ARGS[@]}". Use ${ARGS[@]+"${ARGS[@]}"} at the command invocation.

Comment on lines +125 to +133
PI_BEFORE=$("$HERDR" --session "$SESS" agent list 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
agents = [a for a in d['result']['agents'] if a.get('name','').startswith('a')]
pi = sum(1 for a in agents if a.get('agent')=='pi' and a.get('agent_session'))
hermes = sum(1 for a in agents if a.get('agent')=='hermes' and a.get('agent_session'))
print(f'{pi} {hermes}')
")
echo "Refs before: pi hermes = $PI_BEFORE" >&2

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Add a fallback for the pre-restart reference query.

Lines 149, 156 and 165 each end with || echo 0. This command substitution has no fallback. If agent list or the Python parse fails, PI_BEFORE becomes empty. Line 189 then runs [ "" -gt 0 ], which fails, and set -e aborts the script before it prints the result block.

🛠️ Proposed change
 print(f'{pi} {hermes}')
-")
+" || echo "0 0")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
PI_BEFORE=$("$HERDR" --session "$SESS" agent list 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
agents = [a for a in d['result']['agents'] if a.get('name','').startswith('a')]
pi = sum(1 for a in agents if a.get('agent')=='pi' and a.get('agent_session'))
hermes = sum(1 for a in agents if a.get('agent')=='hermes' and a.get('agent_session'))
print(f'{pi} {hermes}')
")
echo "Refs before: pi hermes = $PI_BEFORE" >&2
PI_BEFORE=$("$HERDR" --session "$SESS" agent list 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
agents = [a for a in d['result']['agents'] if a.get('name','').startswith('a')]
pi = sum(1 for a in agents if a.get('agent')=='pi' and a.get('agent_session'))
hermes = sum(1 for a in agents if a.get('agent')=='hermes' and a.get('agent_session'))
print(f'{pi} {hermes}')
" || echo "0 0")
echo "Refs before: pi hermes = $PI_BEFORE" >&2

Comment on lines +176 to +177
RESUME_LOGS=$(grep -cE "agent resume skipped|restore_plan|persist.restore" "$LOG_DIR/server-2.log" 2>/dev/null || echo 0)
echo "Resume log entries: $RESUME_LOGS" >&2

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

grep -c with || echo 0 produces two lines.

When no line matches, grep -c prints 0 and exits with status 1. The || branch then appends a second 0. RESUME_LOGS becomes 0\n0, and the echoed diagnostic is wrong.

🛠️ Proposed change
-RESUME_LOGS=$(grep -cE "agent resume skipped|restore_plan|persist.restore" "$LOG_DIR/server-2.log" 2>/dev/null || echo 0)
+RESUME_LOGS=$(grep -cE "agent resume skipped|restore_plan|persist.restore" "$LOG_DIR/server-2.log" 2>/dev/null) || RESUME_LOGS=0
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
RESUME_LOGS=$(grep -cE "agent resume skipped|restore_plan|persist.restore" "$LOG_DIR/server-2.log" 2>/dev/null || echo 0)
echo "Resume log entries: $RESUME_LOGS" >&2
RESUME_LOGS=$(grep -cE "agent resume skipped|restore_plan|persist.restore" "$LOG_DIR/server-2.log" 2>/dev/null) || RESUME_LOGS=0
echo "Resume log entries: $RESUME_LOGS" >&2

Comment thread scripts/test-resurrect.sh
Comment on lines +55 to +82
echo "Session refs before restart: $REFS_BEFORE (expect >= 1)"

kill "$SRV_PID" 2>/dev/null || true
sleep 3
wait "$SRV_PID" 2>/dev/null || true

"$HERDR_FIX" --session "$TEST_SESSION" server >>"$LOG" 2>&1 &
SRV_PID=$!
sleep 10

WS_AFTER=$("$HERDR_FIX" --session "$TEST_SESSION" workspace list 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(len([w for w in d['result']['workspaces'] if w.get('label','').startswith('test-ws')]))
")
REFS_AFTER=$("$HERDR_FIX" --session "$TEST_SESSION" agent list 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
agents=[a for a in d['result']['agents'] if a.get('name') in ('pi-1','pi-2')]
print(sum(1 for a in agents if a.get('agent_session')))
")

echo "Workspaces restored: $WS_AFTER (expect 2)"
echo "Session refs after restart: $REFS_AFTER (expect >= 1)"

PASS=true
[ "$WS_AFTER" -ge 2 ] || PASS=false
[ "$REFS_AFTER" -ge 1 ] || PASS=false

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert REFS_BEFORE and check that the restarted server is alive.

Two gaps weaken the test:

  1. The script prints REFS_BEFORE but never asserts it. If session references never register before the restart, the failure reports only REFS_AFTER. The root cause then looks like a restore bug instead of a capture bug.
  2. After the restart at line 61, the script does not verify that the new server process is alive. If the server fails to start, workspace list fails and the script exits without the log contents.
🛠️ Proposed change
 echo "Session refs before restart: $REFS_BEFORE (expect >= 1)"
+[ "$REFS_BEFORE" -ge 1 ] || { echo "FAIL: no session refs captured before restart"; exit 1; }
 
 kill "$SRV_PID" 2>/dev/null || true
 sleep 3
 wait "$SRV_PID" 2>/dev/null || true
 
 "$HERDR_FIX" --session "$TEST_SESSION" server >>"$LOG" 2>&1 &
 SRV_PID=$!
 sleep 10
+kill -0 "$SRV_PID" 2>/dev/null || { echo "FAIL: server did not restart"; cat "$LOG"; exit 1; }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
echo "Session refs before restart: $REFS_BEFORE (expect >= 1)"
kill "$SRV_PID" 2>/dev/null || true
sleep 3
wait "$SRV_PID" 2>/dev/null || true
"$HERDR_FIX" --session "$TEST_SESSION" server >>"$LOG" 2>&1 &
SRV_PID=$!
sleep 10
WS_AFTER=$("$HERDR_FIX" --session "$TEST_SESSION" workspace list 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(len([w for w in d['result']['workspaces'] if w.get('label','').startswith('test-ws')]))
")
REFS_AFTER=$("$HERDR_FIX" --session "$TEST_SESSION" agent list 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
agents=[a for a in d['result']['agents'] if a.get('name') in ('pi-1','pi-2')]
print(sum(1 for a in agents if a.get('agent_session')))
")
echo "Workspaces restored: $WS_AFTER (expect 2)"
echo "Session refs after restart: $REFS_AFTER (expect >= 1)"
PASS=true
[ "$WS_AFTER" -ge 2 ] || PASS=false
[ "$REFS_AFTER" -ge 1 ] || PASS=false
echo "Session refs before restart: $REFS_BEFORE (expect >= 1)"
[ "$REFS_BEFORE" -ge 1 ] || { echo "FAIL: no session refs captured before restart"; exit 1; }
kill "$SRV_PID" 2>/dev/null || true
sleep 3
wait "$SRV_PID" 2>/dev/null || true
"$HERDR_FIX" --session "$TEST_SESSION" server >>"$LOG" 2>&1 &
SRV_PID=$!
sleep 10
kill -0 "$SRV_PID" 2>/dev/null || { echo "FAIL: server did not restart"; cat "$LOG"; exit 1; }
WS_AFTER=$("$HERDR_FIX" --session "$TEST_SESSION" workspace list 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(len([w for w in d['result']['workspaces'] if w.get('label','').startswith('test-ws')]))
")
REFS_AFTER=$("$HERDR_FIX" --session "$TEST_SESSION" agent list 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
agents=[a for a in d['result']['agents'] if a.get('name') in ('pi-1','pi-2')]
print(sum(1 for a in agents if a.get('agent_session')))
")
echo "Workspaces restored: $WS_AFTER (expect 2)"
echo "Session refs after restart: $REFS_AFTER (expect >= 1)"
PASS=true
[ "$WS_AFTER" -ge 2 ] || PASS=false
[ "$REFS_AFTER" -ge 1 ] || PASS=false

@ogulcancelik

Copy link
Copy Markdown
Collaborator

please open prs by using your words to explain what you are trying to fix. also please open an issue first with exact repro.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-review Trigger automated AI reviews for pull requests admitted by the PR gate rabbit-says-slop

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants