github / github/copilot-cli

`--resume` replays orphaned permission.requested events (no matching permission.completed) after mid-prompt process death

Đang mở
#4,259 0 bình luận 0 reaction 0 người được giao Xem trên GitHub

Chưa có ai nhận issue này.

area:permissions area:sessions
Ngôn ngữ chính
Shell
Star
11.2k
Fork
1.9k
Merge trung bình
14 giờ 16 phút
Pull request đã merge (30 ngày)
6

Mô tả

Describe the bug

On --resume, the CLI re-presents permission prompts that were never resolved in a prior run. These originate from permission.requested events in the session's events.jsonl that have no matching permission.completed event. The prompts replay on every resume, repeatedly asking to approve/cancel shell commands (and file writes) that are long dead, with no in-CLI way to clear them. /tasks shows no active tasks or subagents and no OS process is running.

This is effectively the read-side sibling of #3366 (orphan tool_use), but for the permission subsystem — it manifests as a nagging-prompt loop rather than a hard API 400 wedge.

Affected version

1.0.73

Steps to reproduce the behavior
  1. Start a session and run a command that triggers a permission prompt (e.g. a shell command, or a file write).
  2. While the prompt is pending, kill the CLI abruptly — reboot the host, or otherwise terminate the process before answering. (Also reproducible via backgrounded/detached shells — e.g. nohup … &, a while pgrep poll loop, or timeout … & — whose lifecycle doesn't get a terminal event before the session is interrupted.)
  3. Run copilot --resume and select the session.
  4. The unresolved prompt(s) replay. /tasks is empty, so there's nothing to cancel; they return on every subsequent resume.

Evidence from the affected session (pairing permission.requested.data.requestId with permission.completed.data.requestId):

total permission.requested: 3234 | completed: 3230 | DANGLING: 4
  - write: Create file
  - shell: df -h ...; ls -la .../*BF16* ...
  - shell: dpkg upgrade history loop ...
  - shell: unattended-upgrades log loop ...

Exactly 4 unmatched permission.requested events → exactly the 4 prompts replayed on resume. command-history-state.json was clean, confirming the source is the event log, not a live task registry.

Expected behavior

On resume, a permission.requested with no matching permission.completed should be treated as stale and auto-resolved (cancelled/denied) rather than re-prompting — mirroring the write/read reconciliation asked for in #3366. Ideally the CLI would also write a terminal permission.completed{status:"cancelled"} during graceful shutdown and reconcile orphans on startup.

Additional context
  • OS: Ubuntu 24.04, x86_64, headless server over SSH, bash.
  • No in-CLI recovery: /tasks empty, prompts persist across resumes; manual events.jsonl surgery is currently the only fix.
  • Related: #3366 (orphan tool_use, same root family), #3559 (fc_call_* replay from session-state), #1696 (resume & previously granted permissions).

Workaround we used

Run this only after /exit (so the CLI isn't appending to the log). It backs up events.jsonl, then removes only the permission.requested lines whose requestId has no matching permission.completed; all other events are preserved byte-for-byte.

SESSION_DIR="$HOME/.copilot/session-state/<your-session-uuid>"
cd "$SESSION_DIR"
cp -a events.jsonl "events.jsonl.bak.$(date +%s)"

python3 - <<'PY'
import json, os

f = "events.jsonl"

# Pass 1: collect every requestId that has a permission.completed
completed = set()
with open(f, encoding="utf-8", errors="replace") as fh:
    for line in fh:
        try:
            o = json.loads(line)
        except Exception:
            continue
        if o.get("type") == "permission.completed":
            rid = (o.get("data") or {}).get("requestId")
            if rid:
                completed.add(rid)

# Pass 2: rewrite, dropping only permission.requested with no completion
kept = dropped = 0
tmp = f + ".tmp"
with open(f, encoding="utf-8", errors="replace") as fh, \
     open(tmp, "w", encoding="utf-8") as out:
    for line in fh:
        drop = False
        try:
            o = json.loads(line)
        except Exception:
            o = None
        if o and o.get("type") == "permission.requested":
            rid = (o.get("data") or {}).get("requestId")
            if rid not in completed:
                drop = True
        if drop:
            dropped += 1
        else:
            out.write(line)
            kept += 1

os.replace(tmp, f)
print(f"kept={kept} dropped={dropped}")
PY

Also delete the session's stale lock if its owning PID is dead:

# inuse.<pid>.lock left behind when the CLI is killed (e.g. by reboot)
for lk in inuse.*.lock; do
  pid=$(printf '%s\n' "$lk" | sed -E 's/inuse\.([0-9]+)\.lock/\1/')
  if ! kill -0 "$pid" 2>/dev/null; then
    echo "removing stale $lk (pid $pid dead)"; rm -f "$lk"
  fi
done

After this, copilot --resume on the session comes up clean with no replayed prompts.

Hướng dẫn đóng góp

Mở hướng dẫn đóng góp

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Hướng nghiên cứu

Bắt đầu bằng cách lần theo đường dẫn --resume đọc các sự kiện của phiên từ events.jsonl, tập trung vào việc ghép cặp permission.requested và permission.completed theo requestId; command-history-state.json được báo cáo là sạch và không phải nguồn gây ra vấn đề. Tái hiện một prompt bị gián đoạn, sau đó xác minh rằng các request mồ côi được hủy hoặc từ chối khi tiếp tục và không còn được phát lại trong những lần tiếp tục sau.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Đánh giá

Công nghệ
shell
Lĩnh vực
cli
Loại issue
Lỗi
Độ khó
4/5
Thời gian dự kiến
3-5 ngày
Mức độ hoạt động
Ít trao đổi
Độ rõ ràng
Khá rõ ràng
Mức phù hợp với người mới
48/100

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.