fix(agents): harden shell adapter bridge and address double-check review feedback

- create_session.sh: add explicit case fallback for delegate_agent (R1)
- lib_py.agents: add spawn-spec, resume-spec, exit-key argv CLI subcommands (R2)
- resume_session.sh, stop_session.sh: replace python string interpolation with safe argv subcommands (R2)
- stop_session.sh: dynamically iterate adapter.identity_cache_fields (R3)
- verify_session.py, workspace_uuid.py: remove dead imports (R9/N4)
- tests/test_a4_adapter_contract.py: add CLI bridge subcommand, clean-env PYTHONPATH safety, and fallback contract tests (R12/N1)
- SKILL.md, docs, logs: synchronize IMPROVEMENTS.md, LOG.md, and resolve_session_id wording (R8/R10/R11)
- promote verified peer review reports for cline (e7b9812b) and claude (31730364)
This commit is contained in:
2026-08-17 08:54:57 +09:00
parent 7708d3ade3
commit 5ed39f899b
14 changed files with 615 additions and 31 deletions
+1 -2
View File
@@ -1326,8 +1326,7 @@ PYEOF
# if none). find_workspace_uuid is already a workspace-scoped, 3-tier, race-free
# resolver (per-row own id -> workspace-scoped disk scan -> cwd-matched cache),
# so recording its result into the row before kill guarantees tier-1 on the next
# resume. Pass session_name to scope resolution to that row (required for
# isolated sessions — see isolation block / T5). Always exits 0.
# resume. Pass session_name to prefer that specific row's recorded id. Always exits 0.
# ---------------------------------------------------------------------------
capture_conversation_id() {
local agent="$1" workdir="$2" session_name="${3:-}"
+34
View File
@@ -46,6 +46,40 @@ def main():
print(f"ERROR: {ex}", file=sys.stderr)
sys.exit(5)
elif cmd == 'spawn-spec':
agent_name = sys.argv[2] if len(sys.argv) > 2 else ''
binary = sys.argv[3] if len(sys.argv) > 3 else agent_name
uuid = sys.argv[4] if len(sys.argv) > 4 else ''
use_wrapper = (sys.argv[5].lower() in ('1', 'true', 'yes')) if len(sys.argv) > 5 else False
adapter = get_adapter(agent_name)
if not adapter:
sys.exit(1)
print(adapter.spawn_spec(binary, uuid, use_wrapper=use_wrapper))
sys.exit(0)
elif cmd == 'resume-spec':
agent_name = sys.argv[2] if len(sys.argv) > 2 else ''
binary = sys.argv[3] if len(sys.argv) > 3 else agent_name
uuid = sys.argv[4] if len(sys.argv) > 4 else ''
workspace = sys.argv[5] if len(sys.argv) > 5 else ''
adapter = get_adapter(agent_name)
if not adapter:
sys.exit(1)
from lib_py.agents.base import DiscoveryContext
ctx = DiscoveryContext(workspace, agent_name) if workspace else None
mat = adapter.verify_artifact(uuid, ctx) if (ctx and uuid) else False
print(adapter.resume_spec(binary, uuid, materialized=mat))
sys.exit(0)
elif cmd == 'exit-key':
agent_name = sys.argv[2] if len(sys.argv) > 2 else ''
adapter = get_adapter(agent_name)
if adapter:
print(adapter.exit_key)
sys.exit(0)
print('/exit')
sys.exit(0)
else:
print(f"ERROR: Unknown CLI command {cmd!r}", file=sys.stderr)
sys.exit(1)
-3
View File
@@ -79,11 +79,8 @@ def workspace_key(path):
p = path
return p.replace("/", "-").replace("_", "-")
from lib_py.paths import resolve_home
def verify_session_uuid(ws, agent, uuid, row=None, home_dir=None, claude_dir=None, mode="discover"):
import os, json, sqlite3
from lib_py.paths import resolve_home
from lib_py.agents.registry import get_adapter
from lib_py.agents.base import DiscoveryContext
+2 -2
View File
@@ -1,8 +1,8 @@
# workspace_uuid.py — workspace UUID discovery logic
# Extracted from lib.sh find_workspace_uuid PYEOF block
import os, sys, json, glob, sqlite3
from lib_py.verify_session import verify_session_uuid, workspace_key, mam_orchestrator_uuids, mam_row_own_uuid
import os, sys, json, sqlite3
from lib_py.verify_session import verify_session_uuid, mam_orchestrator_uuids, mam_row_own_uuid
OWN_KEY = {
'claude': 'claude_session_id_own',
@@ -159,7 +159,7 @@ fi
# Retrieve agent facts once
eval "$("$(_delegate_py_bin)" -m lib_py.agents facts "$AGENT" 2>/dev/null || true)"
CMD_FULL="$("$(_delegate_py_bin)" -c "from lib_py.agents.registry import get_adapter; a = get_adapter('$AGENT'); print(a.spawn_spec('$RESOLVED_BIN', '$SESSION_UUID', False) if a else '')" 2>/dev/null || true)"
CMD_FULL="$("$(_delegate_py_bin)" -m lib_py.agents spawn-spec "$AGENT" "$RESOLVED_BIN" "$SESSION_UUID" "${USE_WRAPPER:-0}" 2>/dev/null || true)"
if [ -z "$CMD_FULL" ]; then
case "$AGENT" in
claude) CMD_FULL="${RESOLVED_BIN} --dangerously-skip-permissions --session-id ${SESSION_UUID}" ;;
@@ -177,7 +177,7 @@ spawn() {
claude)
if { [ -x "$WRAPPER" ] && [ "$(basename "$WRAPPER")" != "claude" ]; } || [ "$USE_WRAPPER" = "1" ]; then
SESSION_UUID=""
CMD_FULL="${RESOLVED_BIN} --dangerously-skip-permissions"
CMD_FULL="$("$(_delegate_py_bin)" -m lib_py.agents spawn-spec "$AGENT" "$RESOLVED_BIN" "" "true" 2>/dev/null || echo "${RESOLVED_BIN} --dangerously-skip-permissions")"
nohup "$WRAPPER" >/dev/null 2>&1 &
disown
else
@@ -244,7 +244,16 @@ fi
# agent-sessions.yaml 에 append
DELEGATE_JOB_ID=""
if [ -n "$SUBMIT_JOB_PROMPT" ]; then
delegate_agent="${MAM_DELEGATE_AGENT_KEY:-antigravity-cli}"
delegate_agent="${MAM_DELEGATE_AGENT_KEY:-}"
if [ -z "$delegate_agent" ]; then
case "$AGENT" in
claude) delegate_agent="claude-code" ;;
hermes) delegate_agent="hermes-agent" ;;
cline) delegate_agent="cline-agent" ;;
agy) delegate_agent="antigravity-cli" ;;
*) echo "ERROR: cannot resolve delegate agent key for '$AGENT'" >&2; exit 2 ;;
esac
fi
agent_session="herdr:$SESSION_NAME"
DELEGATE_JOB_ID=$(delegate_submit_job "$SUBMIT_JOB_PROMPT" "$delegate_agent" "$agent_session")
echo "Submitted delegated job: $DELEGATE_JOB_ID"
@@ -67,7 +67,7 @@ SESSION_NAME=<workspace>-creator-<agent> # same convention as multi-agent-mux-c
# Resolve the isolated herdr server name & load common utils
source .agents/skills/lib.sh
# 1. Resolve the session id (T5: pass session name for target-row isolation check)
# 1. Resolve the session id (pass session name to prefer target-row recorded id)
UUID=$(bash .agents/skills/multi-agent-mux-resume/scripts/resolve_session_id.sh \
--workspace "$WORKSPACE" --agent "$AGENT" --session "$SESSION_NAME")
@@ -15,8 +15,7 @@ usage() {
cat <<EOF
Usage: $0 --workspace <path> --agent <claude|agy|hermes|cline> [--session <name>]
Outputs the resolved UUID on stdout (empty if not found).
--session scopes resolution to that registry row required for sessions
created with --isolate (their conversation lives only in the row's isolation root).
--session prefers that registry row's recorded id; falls back to workspace-wide discovery if it does not verify.
EOF
}
@@ -36,6 +36,10 @@ done
[ -n "$WORKSPACE" ] || { echo "ERROR: --workspace required" >&2; exit 2; }
[ -n "$AGENT" ] || { echo "ERROR: --agent required" >&2; exit 2; }
case "$AGENT" in
claude|agy|hermes|cline) ;;
*) echo "ERROR: unsupported agent: $AGENT" >&2; exit 2 ;;
esac
[ -n "$SESSION_NAME" ] || { echo "ERROR: --session required" >&2; exit 2; }
# 1. Resolve the session id
@@ -81,7 +85,7 @@ if [ "$(uname)" = "Darwin" ] && [ -f "$RESOLVED_BIN" ]; then
fi
# Determine CMD_FULL via adapter
CMD_FULL="$("$(_delegate_py_bin)" -c "from lib_py.agents.registry import get_adapter; from lib_py.agents.base import DiscoveryContext; a = get_adapter('$AGENT'); ctx = DiscoveryContext('$WORKSPACE', '$AGENT'); mat = a.verify_artifact('$UUID', ctx) if a else False; print(a.resume_spec('$RESOLVED_BIN', '$UUID', mat) if a else '')" 2>/dev/null || true)"
CMD_FULL="$("$(_delegate_py_bin)" -m lib_py.agents resume-spec "$AGENT" "$RESOLVED_BIN" "$UUID" "$WORKSPACE" 2>/dev/null || true)"
if [ -z "$CMD_FULL" ]; then
case "$AGENT" in
claude) CMD_FULL="${RESOLVED_BIN} --dangerously-skip-permissions -r $UUID" ;;
@@ -173,7 +173,7 @@ delegate_publish_event "$DELEGATE_JOB_ID" progress "terminating"
graceful_stop() {
local pane_pid exitkey
pane_pid=$(herdr list-panes -t "$SESSION_NAME" -F '#{pane_pid}' 2>/dev/null | head -1 || true)
exitkey="$("$(_delegate_py_bin)" -c "from lib_py.agents.registry import get_adapter; a = get_adapter('$AGENT'); print(a.exit_key if a else '/exit')" 2>/dev/null || echo "/exit")"
exitkey="$("$(_delegate_py_bin)" -m lib_py.agents exit-key "$AGENT" 2>/dev/null || echo "/exit")"
echo "graceful: send-keys '$exitkey' to $SESSION_NAME"
send_keys_safe "$SESSION_NAME" "$exitkey" "stop$$" || echo "graceful: safe delivery failed (rc=$?) — falling back to kill chain"
_wait_session_gone "$SESSION_NAME" 5 || true
@@ -280,14 +280,7 @@ if purge and purge_uuid:
if ai.get('project_cwd') == ws:
if adapter and (ai.get('session_id') == purge_uuid or ai.get('conversation_id') == purge_uuid):
for field in adapter.identity_cache_fields:
ai.pop(field, None)
if 'session_id' in adapter.identity_cache_fields or 'session_jsonl' in adapter.identity_cache_fields:
ai['session_id'] = None
ai['session_jsonl'] = None
if 'conversation_id' in adapter.identity_cache_fields:
ai['conversation_id'] = None
ai['conversation_db'] = None
ai['conversation_brain_dir'] = None
ai[field] = None
elif purge and not purge_uuid:
print("WARN: --purge-conversation requested but no workspace-scoped UUID resolved; nothing purged", flush=True)