github / github/copilot-cli

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

Abierto
#4,259 0 comentarios 0 reacciones 0 asignados Ver en GitHub

Nadie ha tomado este issue todavía.

area:permissions area:sessions
Lenguaje dominante
Shell
Estrellas
11.2k
Forks
1.9k
Merge medio
14 h 16 min
PR fusionados (30 d)
6

Descripción

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.

Guía de contribución

Abrir la guía de contribución

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Línea de trabajo

Empieza siguiendo la ruta de --resume que lee los eventos de la sesión desde events.jsonl, centrándote en la correspondencia entre permission.requested y permission.completed mediante requestId; se informa de que command-history-state.json está limpio y no es el origen. Reproduce un prompt interrumpido y verifica después que las solicitudes huérfanas se cancelen o rechacen al reanudar y que ya no se reproduzcan en reanudaciones posteriores.

Escrito por el modelo de indexación a partir del texto del issue.

Evaluación

Stack tecnológico
shell
Área
cli
Tipo de issue
Error
Dificultad
4/5
Tiempo estimado
3-5 días
Estado de actividad
Tranquilo
Claridad
Bastante claro
Aptitud para principiantes
48/100

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.