fix(resume): allow fresh spawn fallback and refresh epoch on 0-turn Class A resume
- Branch on Class A agents (agy, hermes, opencode) in resume_session.sh to fall back to clean spawn (CMD_FULL) when stopped before first turn (empty UUID) - Update update_yaml_resumed.sh to support empty UUID with --cmd-full and refresh herdr_session_epoch on fresh spawn resume to prevent stale transcript capture - Strictly preserve Class B (claude, grok) assigned UUID escape hatch and test_t8 contract - Add regression tests test_t14, test_t15, test_t16 (Closes #3 Item 2)
This commit is contained in:
@@ -11,7 +11,11 @@ source "$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/lib.sh"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $0 --session <name> --uuid <id> [--agent claude|agy|hermes|grok|opencode] [--herdr-session <name>]
|
||||
Usage: $0 --session <name> --uuid <id> [--agent claude|agy|hermes|grok|opencode] [--herdr-session <name>] [--cmd-full <cmd>]
|
||||
|
||||
--uuid may be empty for an Issue-3 Class A fresh-spawn resume (agy/hermes/
|
||||
opencode 0-turn stopped session, no conversation id ever assigned) - pass
|
||||
--cmd-full so the display command can be recorded without a uuid to embed.
|
||||
EOF
|
||||
}
|
||||
|
||||
@@ -22,6 +26,7 @@ WORKSPACE=""
|
||||
ROLE=""
|
||||
HERDR_SERVER_OPT=""
|
||||
HERDR_WORKSPACE_OPT=""
|
||||
CMD_FULL_OPT=""
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
@@ -32,13 +37,13 @@ while [ $# -gt 0 ]; do
|
||||
--role) ROLE="$2"; shift 2 ;;
|
||||
--herdr-session|--herdr-server) HERDR_SERVER_OPT="$2"; shift 2 ;;
|
||||
--herdr-workspace) HERDR_WORKSPACE_OPT="$2"; shift 2 ;;
|
||||
--cmd-full) CMD_FULL_OPT="$2"; shift 2 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "ERROR: unknown arg: $1" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[ -n "$SESSION_NAME" ] || { echo "ERROR: --session required" >&2; exit 2; }
|
||||
[ -n "$UUID" ] || { echo "ERROR: --uuid required" >&2; exit 2; }
|
||||
[ -f "$AGENT_SESSIONS_YAML" ] || { echo "ERROR: $AGENT_SESSIONS_YAML not found" >&2; exit 1; }
|
||||
|
||||
if [ -n "$HERDR_SERVER_OPT" ]; then
|
||||
@@ -104,7 +109,8 @@ atomic_dump_yaml "$AGENT_SESSIONS_YAML" \
|
||||
SESSION_NAME="$SESSION_NAME" UUID="$UUID" AGENT="$AGENT" NOW_ISO="$NOW_ISO" \
|
||||
NOW_EPOCH="$NOW_EPOCH" TARGET_WORKSPACE="${WORKSPACE:-$WORKSPACE_ROOT}" ROLE="$ROLE" \
|
||||
PANE_PID="$PANE_PID" CHILD_PID="$CHILD_PID" HERDR_SERVER_OPT_EXPLICIT="${HERDR_SERVER_OPT_EXPLICIT:-0}" \
|
||||
MAM_WS_LABEL="$MAM_WS_LABEL" MAM_WS_LABEL_EXPLICIT="${MAM_WS_LABEL_EXPLICIT:-0}" <<'PYEOF'
|
||||
MAM_WS_LABEL="$MAM_WS_LABEL" MAM_WS_LABEL_EXPLICIT="${MAM_WS_LABEL_EXPLICIT:-0}" \
|
||||
CMD_FULL_OPT="$CMD_FULL_OPT" <<'PYEOF'
|
||||
name = os.environ['SESSION_NAME']
|
||||
uuid = os.environ['UUID']
|
||||
agent = os.environ['AGENT']
|
||||
@@ -157,6 +163,18 @@ else:
|
||||
target['herdr_workspace'] = wsl
|
||||
|
||||
target['status'] = 'running'
|
||||
if not uuid:
|
||||
# ISSUE-3 (challenge c4b0a075): a Class A fresh-spawn resume re-arms
|
||||
# discovery from scratch - the resumed agent has not spoken yet, so any
|
||||
# on-disk conversation older than THIS resume must not be auto-pinned to
|
||||
# it by reconcile.sh's mode="discover" mtime >= epoch check. Without this,
|
||||
# the row's watermark stays at the original create_session.sh timestamp,
|
||||
# letting reconcile.sh capture a stale/unrelated transcript before the
|
||||
# resumed agent's first real turn.
|
||||
target['herdr_session_epoch'] = epoch
|
||||
target['herdr_session_created_at'] = now
|
||||
target['session_id_source'] = 'pending-discovery'
|
||||
target['session_id_verified'] = False
|
||||
target.pop('terminated_at', None)
|
||||
target.pop('terminated_at_epoch', None)
|
||||
target.pop('termination_mode', None)
|
||||
@@ -166,7 +184,11 @@ target.pop('stopped_at', None)
|
||||
target.pop('stopped_at_epoch', None)
|
||||
target.pop('stop_reason', None)
|
||||
target.pop('resumable', None)
|
||||
target['last_visible_status'] = f'resumed conversation {uuid} at {now}'
|
||||
if uuid:
|
||||
target['last_visible_status'] = f'resumed conversation {uuid} at {now}'
|
||||
else:
|
||||
# ISSUE-3 Class A fresh-spawn resume: no conversation id existed yet.
|
||||
target['last_visible_status'] = f'resumed (fresh spawn, no prior turn) at {now}'
|
||||
|
||||
target.setdefault('pane', {})
|
||||
if pane_pid.isdigit():
|
||||
@@ -178,15 +200,21 @@ if agent == 'claude':
|
||||
target['claude_session_id_own'] = uuid
|
||||
elif agent == 'agy':
|
||||
target['pane']['cmd'] = 'agy'
|
||||
target['pane']['cmd_full'] = f'agy --dangerously-skip-permissions --conversation {uuid}'
|
||||
target['agy_conversation_id_own'] = uuid
|
||||
if uuid:
|
||||
target['pane']['cmd_full'] = f'agy --dangerously-skip-permissions --conversation {uuid}'
|
||||
target['agy_conversation_id_own'] = uuid
|
||||
else:
|
||||
target['pane']['cmd_full'] = os.environ.get('CMD_FULL_OPT', '') or 'agy --dangerously-skip-permissions'
|
||||
cp = os.environ.get('CHILD_PID', '0')
|
||||
if cp.isdigit() and int(cp) > 0:
|
||||
target['child_pid'] = int(cp)
|
||||
elif agent == 'hermes':
|
||||
target['pane']['cmd'] = 'hermes'
|
||||
target['pane']['cmd_full'] = f'hermes --resume {uuid}'
|
||||
target['hermes_conversation_id_own'] = uuid
|
||||
if uuid:
|
||||
target['pane']['cmd_full'] = f'hermes --resume {uuid}'
|
||||
target['hermes_conversation_id_own'] = uuid
|
||||
else:
|
||||
target['pane']['cmd_full'] = os.environ.get('CMD_FULL_OPT', '') or 'hermes --yolo --accept-hooks'
|
||||
cp = os.environ.get('CHILD_PID', '0')
|
||||
if cp.isdigit() and int(cp) > 0:
|
||||
target['child_pid'] = int(cp)
|
||||
@@ -199,8 +227,11 @@ elif agent == 'grok':
|
||||
target['child_pid'] = int(cp)
|
||||
elif agent == 'opencode':
|
||||
target['pane']['cmd'] = 'opencode'
|
||||
target['pane']['cmd_full'] = f'opencode --session {uuid} --auto --agent build'
|
||||
target['opencode_session_id_own'] = uuid
|
||||
if uuid:
|
||||
target['pane']['cmd_full'] = f'opencode --session {uuid} --auto --agent build'
|
||||
target['opencode_session_id_own'] = uuid
|
||||
else:
|
||||
target['pane']['cmd_full'] = os.environ.get('CMD_FULL_OPT', '') or 'opencode --auto --agent build'
|
||||
cp = os.environ.get('CHILD_PID', '0')
|
||||
if cp.isdigit() and int(cp) > 0:
|
||||
target['child_pid'] = int(cp)
|
||||
|
||||
Reference in New Issue
Block a user