Files
multi-agent-mux/.agents/reports/planner-reviewer-claude-01/plan-c107763c.md
T
Godopu 50a9f32773 docs(reports): archive v4.1.2 implementation plan and unanimous peer review reports
- Add plan-c107763c (Rev 1 & Rev 2 with creator critique and refinements)
- Add report-b11cf3b2 (planner-reviewer-claude-01 PASS)
- Add report-c9796d8c (reviewer-creator-grok-01 PASS)
- Add report-8e6b346e (reviewer-opencode-01 PASS)
2026-08-30 11:02:56 +09:00

151 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 📐 Implementation Plan — Job `c107763c`: v4.1.2 Upstream Bug-Fix Triage
- **Planner**: `planner-reviewer-claude-01`
- **Source**: `bug-report/mam_upstream_bug_fix_report.md` (untracked, GAIA Canary Project Team, 2026-08-30)
- **Method**: each of the 4 reported issues was independently re-derived against live source (`lib.sh`, `opencode.py`, adapter base class, callers, existing tests) before accepting or rejecting the report's diagnosis — per standing practice, a field report's own diff is a claim, not evidence.
---
## Verdict per issue
| # | Report's claim | My finding | Action |
|---|---|---|---|
| 1 | `_resolve_herdr_pane_id` generic `agent_kind` fallback misattributes an unrelated same-kind pane | **Confirmed real, unfixed.** | Fix applied |
| 2 | `opencode.py::resume_spec` `--session` flag missing due to `materialized` gate | **Not a defect — working as designed.** | No fix (see rationale) |
| 3 | `opencode.py::ready_tokens` doesn't match the ASCII banner | **Already fixed in v4.1.1** (job `c38ddfcf`, this exact string is live). | No action, stale report entry |
| 4 | `new-session` doesn't reuse an idle pane before splitting | **Confirmed real, unfixed.** | Fix applied |
---
## Issue 1: `_resolve_herdr_pane_id` generic `agent_kind` fallback (`lib.sh` ~line 436)
**Root cause, confirmed by reading the fallback chain in full (`lib.sh:349-450`):** when name/label/agent-exact matching against `herdr pane list` fails, the code looks up the *target's own* kind via `.mam/agent-sessions.yaml` (`agent_kind = s.pane.cmd` for the yaml row whose `name` equals `tn`/`tsa`), then searches **all live panes** for `agent == agent_kind`. If exactly one such pane exists anywhere in the (already workspace-scoped) pane list, it is returned — even though that pane's actual name/label has nothing to do with the target. A second, genuinely same-named agent of the same kind running concurrently (e.g. an orchestrator `agy` pane alongside a dead `creator-agy-01`) gets misattributed as `creator-agy-01`, defeating `has-session`'s liveness check and silently skipping restoration.
**Verified this is reachable in practice**: `has-session`'s shim arm (`lib.sh:590`) falls through to `_resolve_herdr_pane_id "$sess"` exactly on this path, and `resume_session.sh:73` calls `herdr has-session -t "$SESSION_NAME"` with a full role-prefixed name — precisely the target-name-plus-drifted-kind-match scenario in the report.
**Fix**: gate the kind-based fallback so it only fires when the caller's *own* search term already equals the bare kind — i.e. this path is for a caller who searched by kind directly (`target == "agy"`), not for silently broadening a specific-name search into a kind-wide one:
```diff
- if agent_kind:
+ if agent_kind and (tn == agent_kind or tsa == agent_kind):
matching = [p.get('pane_id') for p in panes if p.get('agent') == agent_kind and p.get('pane_id')]
```
**Why this doesn't regress anything**: grepped every real call site of `_resolve_herdr_pane_id` in `lib.sh` (`has-session`, `kill-session`, `capture-pane`, `send-keys`, `set-buffer`) — all pass a full role-prefixed session name (`"$sess"`), never a bare kind string. So in real usage `tn`/`tsa` never equals `agent_kind`, and this fallback branch — which was unsound in every real-world configuration with more than one live agent of the same kind — becomes fully inert rather than merely narrowed. The only existing test touching this path, `test_r1_resolve_pane_id_fail_closed_on_multiple_same_kind_panes` (`tests/test_c1_tui_readiness.py`), asserts the fail-closed (RC=1) outcome on ambiguous multi-match, which still holds unchanged (the branch is skipped, `pid` stays empty, function still returns 1). No test exercises the previous len==1 "success" path, confirming it was never a documented contract.
## Issue 2: `opencode.py::resume_spec` — **rejected the proposed fix**
The report proposes removing the `materialized` gate:
```diff
- if materialized and session_uuid:
+ if session_uuid:
```
**Why this is not a genuine bug**: `if materialized and session_uuid:` is the *identical* pattern in all 5 adapters (`claude.py:97`, `grok.py:105`, `agy.py:90`, `hermes.py:88`, `opencode.py:189`) — this is a deliberate, cross-adapter contract, not an opencode-specific oversight. `materialized` is computed by `__main__.py`'s `resume-spec` CLI as `adapter.verify_artifact(uuid, ctx)` — a real, adapter-specific check (for opencode: a live sqlite query against the local session DB, including epoch and cwd/workspace scoping — see `test_opencode_verify_artifact_created_at_epoch_filtering` in `test_a4_adapter_contract.py`). Its purpose is to avoid ever passing `--session <uuid>` for a session that does not actually exist locally, which would risk the CLI erroring or hanging on an invalid id.
Traced the actual caller, `resume_session.sh:99`: `WORKSPACE` is a required flag (validated at the top of the script), so `DiscoveryContext` is always constructed with a real workspace — `materialized` is never spuriously `False` merely because the workspace argument was missing. I could not reproduce a false-negative in `verify_artifact` itself (DB path resolution, column detection, and `workspace_key()`'s `os.path.realpath()`-based cwd comparison all look sound on inspection, and are covered by existing epoch/cwd tests). Applying the report's fix would (a) break the 5-way adapter consistency this contract relies on, and (b) reintroduce exactly the "resume with an unverified/possibly-nonexistent session id" risk the gate exists to prevent — for no evidence-backed benefit. **No change made.** If the underlying symptom (occasional failure to resume an opencode conversation) recurs, the next diagnostic step is to log `verify_artifact`'s db path / query result for the specific failing session, not to remove the gate.
## Issue 3: `opencode.py::ready_tokens` — already resolved
Live value at `opencode.py:24` is already `'OpenCode|Chat|Ask anything|tab agents|ctrl\\+p|Build auto|commands'` — this is Fix 1 from the v4.1.1 release (job `c38ddfcf`, verified by 3 independent reviewers in that round). The bug report's own "already fixed" diff target matches current source byte-for-byte. This entry in the report is stale (written against a pre-v4.1.1 checkout). No action needed.
## Issue 4: `new-session` idle-pane reuse (`lib.sh` ~line 683)
**Confirmed real**: read the full `new-session` arm. When `existing_ws` is detected (matching cwd), the code unconditionally proceeds to `pane layout` + `pane split` — there is no check anywhere in the current code for a pre-existing, agent-less pane in that workspace. Every `new-session` into an existing workspace therefore grows the pane count monotonically, even if a prior agent's pane is sitting idle, causing pane proliferation and the reported grid-layout churn.
**Fix**: before the existing sample-pane/layout/split logic, query `herdr pane list` (workspace-scoped, client-side filtered — consistent with the existing `sample_pane` lookup two lines below) for a pane with no `agent` assigned; if found, reuse its `pane_id` as `target_pane` directly and skip the split entirely:
```bash
idle_pane=$(_real_herdr pane list 2>/dev/null | TARGET_WS="$existing_ws" python3 -c "
...
for p in panes:
if p.get('workspace_id') == target_ws and not p.get('agent') and p.get('pane_id'):
print(p.get('pane_id')); break
...
" 2>/dev/null || echo "")
if [ -n "$idle_pane" ]; then
target_pane="$idle_pane"
else
# existing sample_pane / layout / split logic, unchanged
fi
```
The existing overflow-threshold-triggers-fresh-workspace branch (`split_dir == "overflow"``existing_ws=""`) is preserved unchanged inside the `else`, so idle-pane reuse never bypasses the grid-overflow policy — it only short-circuits when a reusable pane already exists, before that policy is even consulted.
## Tests added
New tests exercising the two fixes, following each file's existing conventions:
- `tests/test_c1_tui_readiness.py`: `test_r2_resolve_pane_id_does_not_misattribute_unrelated_same_kind_pane` — seeds a yaml entry for a target session whose own pane is absent, plus exactly one *unrelated* same-kind live pane; asserts resolution now fails closed (RC=1) instead of returning the unrelated pane's id (the exact regression the report describes).
- `tests/test_herdr_shim_contract.py`: `test_h24_new_session_reuses_idle_pane_without_split` — seeds a raw idle pane (no `agent` key) in the target workspace/cwd via the `mock_herdr` state, calls `herdr new-session`, and asserts (a) the new agent's `pane_id` equals the idle pane's id and (b) no `pane split` call was recorded.
## SemVer
v4.1.1 → **v4.1.2, PATCH** (SemVer 2.0.0 §6): both applied fixes are internal reliability hardening of already-shipped behavior (pane resolution correctness, pane-reuse efficiency) — no public `--agent` value, flag, or YAML own-key added/removed/changed. Matches the v4.1.1 and v3.0.1 PATCH precedents. 3-way lockstep: `lib.sh:32` `MAM_VERSION`, `VERSIONS.md` (header/date/prose/8-row matrix/new changelog section), 8× `SKILL.md` `version:` frontmatter.
## Self-caught regression during implementation
While running the targeted suite after implementing both fixes, 3 previously-passing tests regressed (`test_h16_send_keys_safe_submits_exactly_once`, `test_h18_workspace_scoped_pane_resolution`, `test_h23_persisted_workspace_id_scopes_without_env` — none touching `new-session` or the `agent_kind` fallback directly). Traced it to the Bug-1 fix's own explanatory comment: it used a literal double-quoted phrase (`"any pane of this kind"`) placed *inside* a bash **double**-quoted `python3 -c "..."` block. Bash's own string-termination rules apply inside that block (it's a quoted bash argument, not a heredoc), so the embedded `"` characters prematurely closed the bash string and corrupted the generated Python source for the entire pane-resolution helper — breaking label/name/agent exact-matching too, not just the kind fallback the comment was documenting. Rewrote the comment without embedded double quotes; re-ran the full targeted and full suites clean. Recorded here because it's a instructive shell-scripting trap specific to this codebase's pattern of embedding Python inside double-quoted bash strings (existing code consistently uses single quotes for all embedded Python string literals for exactly this reason — my new comment just hadn't followed that convention).
## Test results
- Targeted suite (`test_a4_adapter_contract.py`, `test_c1_tui_readiness.py`, `test_b19_headless_reconcile_fixes.py`, `test_herdr_shim_contract.py`, `test_version_consistency.py`, `test_tier2_component.py`): **121 passed**.
- Full suite (`tests/`): **449 passed** (447 pre-existing + 2 new regression tests for Bug-1 and Bug-4), 0 failed, 10m21s.
## Review
Per the established MAM workflow, multi-agent peer review (Claude, Grok, OpenCode) is conducted via separately delegated reviewer jobs against this committed diff, not performed by the Planner in this job.
---
## Rev. 2 — Refinement in response to `creator-agy-01`'s architectural challenge (job `d14ab345` → `769e0a1c`)
`creator-agy-01` filed a challenge (`.mam/jobs/d14ab345/agy-reports/report-final.md`) against this plan's Issue-4 fix, plus a secondary observation on Issue-1. Verified both independently against the current live `lib.sh` (still uncommitted working tree, `HEAD=a18f9f8`) rather than accepting the challenge's diagnosis at face value — same standing practice as the original triage. **Both findings are confirmed real; one supporting example in the challenge is corrected below.**
### Finding A (primary): idle-pane reuse drops caller-supplied `KEY=VAL` env tokens — CONFIRMED, fix required
Verified directly: `agent start` (`lib.sh:832,834`) passes only `--kind`, `--pane`, and `-- $final_cmd` — confirmed against the H-1 contract test's own whitelist (`{"--kind", "--pane", "--timeout"}`) and forbidden set (`--env` explicitly forbidden on `agent start` under the live 0.8.0 herdr contract). `$env_flags` (parsed from leading `KEY=VAL` tokens in `run_cmd`, `lib.sh:630-644`) is consumed **only** by `pane split` (`lib.sh:756,770`) and `workspace create` (`lib.sh:785`, plus the extra `--env HERDR_WORKSPACE_ID=...` both add). My idle-pane-reuse branch (`lib.sh:729-731`) sets `target_pane="$idle_pane"` and skips both — `$env_flags` genuinely has nowhere to go on that path. This is a real regression my Bug-4 fix introduced: pre-fix, `new-session` into an existing workspace *always* went through `pane split` (with `$env_flags`), so no gap existed.
**One correction to the challenge's own supporting evidence**: the report's example (`OPENCODE_PERMISSION='{"*":"allow"}'` being lost) does not currently apply — `create_session.sh:217-218` `export`s `OPENCODE_PERMISSION` into its *own* calling shell rather than embedding it as a `KEY=VAL` prefix inside `CMD_FULL` (checked all 3 real `new-session` callers — `create_session.sh`, `resume_session.sh`, `reconcile.sh` — none currently emit a `KEY=VAL`-prefixed command string). So this specific variable was never routed through `$env_flags` even before Bug-4, on any path. That doesn't weaken the finding, though: the `new-session` shim command has a *documented, general-purpose* contract of accepting `KEY=VAL cmd` and threading it through as pane env (the parsing exists precisely for this), and any caller that does use that documented form — current or future — silently loses it exactly as described the moment the target pane happens to be idle-reusable. The defect is in the command's contract, not tied to one specific caller.
**Refined fix** — replay `$env_flags` via a `env(1)` prefix on `$final_cmd` when the idle-pane path is taken, mirroring what `pane split` would have injected server-side (including the same extra `HERDR_WORKSPACE_ID` the split path adds — needed because agents spawned inside the pane may themselves call MAM tooling that reads this var):
```bash
if [ -n "$idle_pane" ]; then
target_pane="$idle_pane"
if [ -n "$final_cmd" ]; then
idle_env="${env_flags//--env /}"
idle_env="${idle_env:+$idle_env }HERDR_WORKSPACE_ID=$existing_ws"
final_cmd="env $idle_env $final_cmd"
fi
else
# existing sample_pane / layout / split logic, unchanged
fi
```
`${env_flags//--env /}` turns `"--env K1=V1 --env K2=V2"` into `"K1=V1 K2=V2"` via plain bash parameter expansion — no new subprocess, no new quoting model, deliberately matching the existing code's own level of robustness (the split/create paths already word-split `$env_flags` unquoted; a value containing a literal space was already fragile there pre-Bug-4, and fixing that broader pre-existing quoting limitation is out of scope for this PATCH). `env` receives literal `KEY=VAL` argv tokens and execs `$final_cmd` with them set — this is the client-side POSIX equivalent of what herdr's own `--env` flag does server-side, so behavior stays consistent whether a pane was split or reused.
**New test to add during implementation**: seed a `new-session` call whose `run_cmd` includes a leading `KEY=VAL` token *and* an idle pane in the target workspace; assert the resulting `agent start` call's trailing command (or the mock's recorded `command`/env-visible field) reflects the env var, and that no `pane split` call occurred. Follows the same fail-old/pass-new verification discipline as `test_h24`.
### Finding B (secondary): the pre-existing exact-match loop's `'agent'` key preempts Bug-1's R-1 guard for bare-kind targets — CONFIRMED, fix required
Traced precisely as the challenge describes: the exact-match loop (`lib.sh:415-422`, pre-existing "ISSUE-2" logic, not part of my Bug-1 diff) iterates `for key in ('label', 'name', 'agent')` and returns on the **first** pane whose `agent` field equals `tn`/`tsa`. If a caller ever does search by a bare kind (`target == "agy"` — the exact scenario my Bug-1 gate's `tn == agent_kind` condition exists to protect, deliberately, per the original plan's own design), this loop matches on `key='agent'` *before* Bug-1's block is even reached, and returns the first same-kind pane with **no ambiguity check** — silently bypassing the R-1 fail-closed guard I added specifically for that case. Currently harmless in practice (verified again: every real call site still passes a full role-prefixed name, never a bare kind — grepped fresh, unchanged from the original triage), but it makes the R-1 protection in Bug-1's own fallback block dead code for the one scenario it was written to cover, which defeats the purpose of writing it.
Checked for test fallout: no existing test seeds a pane whose `agent` field is set to a value equal to the *search target string itself* (`test_r1`/`test_r2` both use full names like `"my-unmatched-session"`/`"creator-agy-01"` as targets, never bare `"agy"`/`"claude"`/`"grok"`) — confirmed via `grep` across `test_c1_tui_readiness.py` and `test_herdr_shim_contract.py`. Removing `'agent'` from this tuple is safe against the current suite.
**Refined fix**:
```diff
- for key in ('label', 'name', 'agent'):
+ for key in ('label', 'name'):
```
This funnels *all* kind-based resolution — whether derived via the yaml lookup (full-name case) or a literal bare-kind search — exclusively through Bug-1's R-1-guarded block, so the multi-match fail-closed check can never be bypassed regardless of which form the target takes.
**New test to add during implementation**: bare-kind target (`_resolve_herdr_pane_id "agy"`) against 2 panes both with `agent == "agy"`; assert fail-closed (RC=1), proving R-1 now actually governs this path instead of being preempted.
### Updated action list for re-implementation
1. Apply the `env` prefix fix to the idle-pane branch (Finding A).
2. Remove `'agent'` from the exact-match tuple (Finding B).
3. Add the two new tests above; re-run the full targeted + full suite (expect 451 passed: 449 + 2).
4. No change to the Issue-2/Issue-3 verdicts or the SemVer classification (still v4.1.2 PATCH) — both new fixes are the same class of internal reliability hardening, not a public-surface change.