feat(cli,registry): introduce --herdr-workspace option and decouple socket fallback chains

This commit is contained in:
2026-08-24 12:50:09 +09:00
parent 320f036575
commit e6e70dbb21
17 changed files with 1670 additions and 47 deletions
@@ -132,7 +132,8 @@ _changed = False
for s in d.get('herdr_sessions', []):
if s.get('delegate_job_id') == _jid and s.get('status') == 'running':
_name = s.get('name')
_srv = s.get('herdr_session') or s.get('herdr_server') or s.get('herdr_workspace') or 'default'
# herdr_workspace 는 워크스페이스 *라벨* 이지 소켓 이름이 아니다.
_srv = s.get('herdr_session') or s.get('herdr_server') or 'default'
if _event in ('completed', 'cancelled'):
s['delegate_job_id'] = None
print('MQTT Monitor: job ' + _event + ' on ' + str(_name) + ' — session kept alive', flush=True)
@@ -316,12 +317,23 @@ fi
# atomic_dump_yaml(flock + temp+rename) 로 같은 소스를 돌린다. atomic 래퍼에서는
# 'actions' 가 없으면 SystemExit(0) 으로 쓰기를 건너뛴다 (불필요한 재포맷 방지).
read -r -d '' RECON_SRC <<'PYEOF' || true
import os, json, glob, subprocess, time, sqlite3
import os, json, glob, subprocess, time, sqlite3, re
from datetime import datetime, timezone
import yaml
from lib_py.verify_session import verify_session_uuid, workspace_key
def _slug(path):
if not path:
return ''
a = os.path.abspath(path)
parent = os.path.basename(os.path.dirname(a)) or 'workspace'
work = os.path.basename(a) or 'root'
if parent in ('/', '.'): parent = 'workspace'
if work in ('/', '.'): work = 'root'
s = f'{parent}-{work}'.lower().replace('_', '-')
return re.sub(r'[^a-zA-Z0-9-]', '', s).lstrip('-')
yaml_path = os.environ['YAML_PATH']
home = os.environ['HOME_DIR']
skills_dir = os.environ.get('SKILLS_DIR', '')
@@ -386,7 +398,8 @@ if 'HERDR_SESSION_NAME' in os.environ:
elif 'HERDR_SERVER_NAME' in os.environ:
unique_servers.add(os.environ['HERDR_SERVER_NAME'])
for s in d.get('herdr_sessions', []):
srv = s.get('herdr_session') or s.get('herdr_server') or s.get('herdr_workspace') or 'default'
# herdr_workspace 는 워크스페이스 *라벨* 이지 소켓 이름이 아니다.
srv = s.get('herdr_session') or s.get('herdr_server') or 'default'
unique_servers.add(srv)
try:
@@ -396,8 +409,6 @@ try:
cmd += ['-L', srv]
cmd += ['ls', '-F', '#{session_name}|#{session_created}']
r = subprocess.run(cmd, capture_output=True, text=True)
import sys
sys.stderr.write(f"LS CMD: {cmd} | RC: {r.returncode} | STDOUT: {r.stdout} | STDERR: {r.stderr}\n")
if r.returncode == 0:
for line in r.stdout.strip().split('\n'):
if not line or '|' not in line:
@@ -423,9 +434,7 @@ try:
is_empty = ('no server running' in err) or ('no sessions' in err) or ('failed to connect' in err)
if not is_empty:
herdr_confirmed = False
except Exception as ex:
import sys
sys.stderr.write(f"EX IN RECONCILE LS: {ex}\n")
except Exception:
herdr_confirmed = False
@@ -472,8 +481,7 @@ yaml_sessions = d.get('herdr_sessions', [])
yaml_session_names = {s['name'] for s in yaml_sessions if s.get('name')}
alive_set = {(t['name'], t.get('server', 'default')) for t in herdr_sessions}
# === drift A: herdr dead + YAML running → auto-terminate ===
# herdr 응답을 확정했을 때만. transient 실패 시 모두 terminated 로 마크하지 않음 (P1-E)
# === drift A: YAML running + herdr dead → mark terminated ===
if herdr_confirmed:
for s in yaml_sessions:
name = s.get('name')
@@ -483,7 +491,8 @@ if herdr_confirmed:
# (없으면 herdr-dead stopped 세션을 'terminated' 로 덮어써 resumable 플래그가 소실됨)
if s.get('status') in ('terminated', 'archived', 'stopped'):
continue
srv = s.get('herdr_session') or s.get('herdr_server') or s.get('herdr_workspace') or 'default'
# herdr_workspace 는 워크스페이스 *라벨* 이지 소켓 이름이 아니다.
srv = s.get('herdr_session') or s.get('herdr_server') or 'default'
if (name, srv) not in alive_set and (_sanitize(name), srv) not in alive_set:
s['status'] = 'terminated'
s['terminated_at'] = now_iso
@@ -498,7 +507,8 @@ if herdr_confirmed:
if herdr_confirmed:
for t in herdr_sessions:
name = t['name']
if name in yaml_session_names or any(_sanitize(y) == name for y in yaml_session_names):
srv = t.get('server', 'default')
if (name, srv) in yaml_session_names or any(_sanitize(y) == name for y in yaml_session_names):
continue
workspace_root = os.environ.get('WORKSPACE_ROOT')
if not workspace_root:
@@ -518,8 +528,7 @@ if herdr_confirmed:
if not agent:
# Check MAM_MANAGED env marker from pane process environment if available
srv_opt = t.get('server', 'default')
pm_check = pane_meta(name, srv_opt)
pm_check = pane_meta(name, srv)
if pm_check and pm_check.get('pid'):
try:
pid_val = pm_check['pid']
@@ -539,7 +548,6 @@ if herdr_confirmed:
if not agent:
continue
srv = t.get('server', 'default')
pm = pane_meta(name, srv)
if not pm:
continue
@@ -564,6 +572,8 @@ if herdr_confirmed:
'herdr_session_created_at': datetime.fromtimestamp(created_epoch, tz=timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ'),
'herdr_session_epoch': created_epoch,
'herdr_session': srv,
'herdr_server': srv,
'herdr_workspace': _slug(pm['cwd']),
'pane': {'index': 0, 'pid': pm['pid'], 'cmd': agent, 'cmd_full': cmd_full, 'cwd': pm['cwd']},
'start_command': f'HERDR_SESSION_NAME={srv} herdr new-session -d -s "{name}" -x 140 -y 40 -c "{pm["cwd"]}" "{cmd_full}"',
'attach_command': f'HERDR_SESSION_NAME={srv} herdr agent attach {name}',