github / github/copilot-cli

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

Aperta
#4,259 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub
area:permissions area:sessions
Lingua principale
Shell
Stelle
11.2k
Fork
1.9k
Merge medio
14h 16m
PR unite (30g)
6

Descrizione

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

Guida per i contributori

Apri la guida per i contributori

Direzione di ricerca

Inizia tracciando il percorso --resume che legge gli eventi della sessione da events.jsonl, concentrandoti sull’associazione tra permission.requested e permission.completed tramite requestId; command-history-state.json risulta pulito e non è la fonte. Riproduci un prompt interrotto, quindi verifica che le richieste orfane vengano annullate o rifiutate al momento della ripresa e non vengano più riprodotte nelle riprese successive.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Valutazione

Stack tecnologico
shell
Ambito
cli
Tipo di issue
Bug
Difficoltà
4/5
Tempo stimato
3-5 giorni
Stato di attività
Tranquilla
Chiarezza
Abbastanza chiara
Idoneità per principianti
48/100

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.