fix(issue-34): a pane caller reaches an agent of another workspace on its own server; an unseen target is not recorded as busy
- mam_herdr: for name lookups (agent / has-session / kill-session) a HERDR_WORKSPACE_ID that is the caller pane's own workspace (it matches the HERDR_PANE_ID prefix) is dropped. Live agent names are unique per server (herdr --skill), so this cannot address a same-named agent of another workspace; an id the caller chose, or one set outside a pane, still scopes (H-18/H-21/H-23). The generated shim is unchanged (S11). - wrapper: a preflight that cannot see the target closes the job cancelled with detail target_not_found instead of session_busy; the loop's sub-job table counts it as NOT-DISPATCHED. - tests: T10-T12 with the measured pane env (same-server socket, caller w6, target w2); T8 follows the sub-job-armed guard. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1d4f338b4c
commit
ed293dcef4
4 files changed
+131
-12
No files matched your search
@@ -1446,6 +1446,31 @@ _herdr_call_is_cross_server() {
|
||||
return 1
|
||||
}
|
||||
|
||||
# _herdr_scope_is_pane_inherited <herdr argv...>
|
||||
#
|
||||
# ISSUE-34: true for a call that names an agent (agent / has-session /
|
||||
# kill-session) from inside a pane whose own workspace is the exported
|
||||
# HERDR_WORKSPACE_ID. A pane exports it together with HERDR_PANE_ID
|
||||
# "<workspace>:<pane>"; when the two agree the id says where the caller lives,
|
||||
# not where to look - a worktree creator in its own workspace must reach a
|
||||
# reviewer in mam-agent on the same server. Live agent names are unique per
|
||||
# server (herdr --skill: "Names must ... be unique among live agents", "IDs and
|
||||
# live agent names are scoped to one server"), so a name lookup without that
|
||||
# filter cannot pick a same-named agent of another workspace: the ISSUE-3 /
|
||||
# F-2b hazard exists only across servers (_herdr_call_is_cross_server). An id
|
||||
# that disagrees with the pane, or one set outside any pane, is still a
|
||||
# deliberate scope (H-18/H-21/H-23); new-session and pane calls keep it, so a
|
||||
# pane caller still creates panes in its own workspace.
|
||||
_herdr_scope_is_pane_inherited() {
|
||||
while [ "${1:-}" = "-L" ]; do shift 2 || return 1; done
|
||||
case "${1:-}" in
|
||||
agent|has-session|kill-session) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
local pane="${HERDR_PANE_ID:-}"
|
||||
[ -n "${HERDR_WORKSPACE_ID:-}" ] && [ -n "$pane" ] && [ "${pane%%:*}" = "$HERDR_WORKSPACE_ID" ]
|
||||
}
|
||||
|
||||
mam_herdr() {
|
||||
_init_herdr_isolation
|
||||
# Built as an argv array and decided inside `if`: this runs under
|
||||
@@ -1457,6 +1482,11 @@ mam_herdr() {
|
||||
if [ "${MAM_SHIM_DEBUG:-0}" = "1" ]; then
|
||||
echo "mam_herdr: dropping caller pane scope ids (caller server '$(_herdr_caller_server)' != target '${HERDR_SESSION_NAME:-}')" >&2
|
||||
fi
|
||||
elif _herdr_scope_is_pane_inherited "$@"; then
|
||||
_scope_strip=(env -u HERDR_WORKSPACE_ID)
|
||||
if [ "${MAM_SHIM_DEBUG:-0}" = "1" ]; then
|
||||
echo "mam_herdr: '$1' looks up by name; the caller pane's own workspace ${HERDR_WORKSPACE_ID} is not a scope" >&2
|
||||
fi
|
||||
fi
|
||||
${_scope_strip[@]+"${_scope_strip[@]}"} "$WORKSPACE_ROOT/.mam/shim/herdr" "$@"
|
||||
}
|
||||
|
||||
@@ -170,7 +170,7 @@ Job lifecycle execution events are persistently mirrored to an append-only log u
|
||||
## Best Practices and Pitfalls
|
||||
|
||||
- **Subscribe-Before-Publish**: The subscriber must be running before the agent starts publishing. The `submit` command handles this automatically by launching the subscriber in the background first.
|
||||
- **Sub-delegation carries `--parent-job` (Issue #34)**: an agent that hands part of its job to another agent must pass `--parent-job <its own job_id>` (the injected instructions and `brief.md` say so). Without it the loop does not wait for the sub-job; reviewers see it only as an unattributed open job. From a git worktree the worktree's copy of this wrapper runs; the pane's `MAM_JOBS_DIR` keeps the registry the same one.
|
||||
- **Sub-delegation carries `--parent-job` (Issue #34)**: an agent that hands part of its job to another agent must pass `--parent-job <its own job_id>` (the injected instructions and `brief.md` say so). Without it the loop does not wait for the sub-job; reviewers see it only as an unattributed open job. From a git worktree the worktree's copy of this wrapper runs; the pane's `MAM_JOBS_DIR` keeps the registry the same one. The target may live in another herdr workspace of the same server: inside a pane, the pane's own `HERDR_WORKSPACE_ID` (the one matching `HERDR_PANE_ID`) is not used as a scope for name lookups, since live agent names are unique per server; a `HERDR_WORKSPACE_ID` you set yourself still scopes. A target the preflight cannot see is cancelled with detail `target_not_found` (exit 5), not `session_busy`.
|
||||
- **Fresh job_id Propagation**: Make sure the worker agent receives the correct `JOB_ID` generated for the current run, rather than reusing stale IDs from previous sessions.
|
||||
- **Brief delivery via file path**: For long or complex prompts, write the instructions to a file (e.g. `/tmp/task-brief.md`) and pass a short prompt pointing to the file path to prevent terminal buffer overflows.
|
||||
- **Prompts injected into a live agent session MUST be English, ASCII-only, and short** — this is exactly what `--prompt`/the `instructions` string sent to `run_agent()` end up as. Any Korean (or other non-ASCII) content the task needs to convey must go in a markdown brief file (e.g. `.mam/jobs/<id>/brief.md`, written in Korean is fine) that the injected prompt merely tells the agent to read. Two independent bugs in `send_keys_safe`'s paste-verification (in `lib.sh`) made this matter in practice: (a) its marker was taken with a byte-based `tail -c 24`, which can slice a multi-byte UTF-8 (e.g. Korean) character in half; (b) the rendered pane soft-wraps long lines at the terminal width, which can split the marker across two visual lines. Both are now fixed at the source (character-safe truncation + newline-stripped matching before comparison), but keeping injected prompts short/English/file-referencing is still the cheapest way to avoid ever exercising this edge case at all — it's also simply what `submit`'s own default instruction template already does (see `Core Commands` above).
|
||||
|
||||
@@ -892,6 +892,8 @@ finalize_unfinished() {
|
||||
if [ "$phase" = "preflight" ] || [ "$phase" = "idle_wait" ]; then
|
||||
close_req="cancelled"
|
||||
detail="session_busy"
|
||||
# Issue #34: a target the preflight could not see was never busy
|
||||
[ "$phase" = "preflight" ] && [ -n "${PREFLIGHT_REASON:-}" ] && detail="$PREFLIGHT_REASON"
|
||||
fi
|
||||
|
||||
local close_args=(--job "$job_id" --request "$close_req")
|
||||
@@ -921,7 +923,7 @@ finalize_unfinished() {
|
||||
local disp_arg=()
|
||||
[ -n "${_disp_tok:-}" ] && disp_arg=(--expect-dispatch-token "$_disp_tok")
|
||||
if [ "$close_req" = "cancelled" ]; then
|
||||
"$PY" "$SCRIPT_DIR/scripts/publish_event.py" --registry-dir "$REGISTRY_DIR" --job "$job_id" --event cancelled --detail "session_busy" ${disp_arg[@]+"${disp_arg[@]}"} 2>/dev/null || true
|
||||
"$PY" "$SCRIPT_DIR/scripts/publish_event.py" --registry-dir "$REGISTRY_DIR" --job "$job_id" --event cancelled --detail "$detail" ${disp_arg[@]+"${disp_arg[@]}"} 2>/dev/null || true
|
||||
elif [ "$sub_rc" -ne 1 ] || [ "$phase" = "inject" ]; then
|
||||
local err_detail="deadline_timeout"
|
||||
[ "$phase" = "inject" ] && err_detail="agent_blocked"
|
||||
@@ -947,6 +949,7 @@ preflight_agent() {
|
||||
export HERDR_SESSION_NAME="$(resolve_herdr_session "$sess" "$WORKDIR")"
|
||||
|
||||
if ! herdr has-session -t "$sess" 2>/dev/null; then
|
||||
PREFLIGHT_REASON="target_not_found" # Issue #34: not a busy session
|
||||
echo "ERROR: 에이전트 세션 '$sess'이 존재하지 않습니다. 작업을 위임하기 전에 먼저 에이전트 세션을 기동해 주세요." >&2
|
||||
echo " 팁: 'multi-agent-mux-resume' 또는 'multi-agent-mux-create'를 통해 에이전트를 먼저 생성할 수 있습니다." >&2
|
||||
return 1
|
||||
|
||||
Reference in new issue
Block a user