github / github/copilot-cli

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

オープン
#4,259 コメント 0 件 リアクション 0 件 担当者 0 名 GitHub で見る
area:permissions area:sessions
主要言語
Shell
スター
11.2k
フォーク
1.9k
平均マージ
14時間 16分
マージ済み PR(30日)
6

説明

### 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.

```bash
SESSION_DIR="$HOME/.copilot/session-state/"
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:

```bash
# inuse..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.

コントリビューションガイド

コントリビューションガイドを開く

調査の方向性

まず、セッションのイベントを events.jsonl から読み取る --resume パスを追跡し、permission.requested と permission.completed の requestId による対応付けに注目します。command-history-state.json はクリーンであると報告されており、原因ではありません。中断されたプロンプトを再現し、その後、孤立したリクエストが再開時にキャンセルまたは拒否され、以降の再開時に再度再生されないことを確認します。

索引モデルが issue の本文から書いたものです。

評価

技術スタック
shell
領域
cli
issue の種類
バグ
難易度
4/5
見積もり時間
3〜5日
活発さ
静か
明瞭さ
おおむね明確
初心者へのやさしさ
48/100

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。