`--resume` replays orphaned permission.requested events (no matching permission.completed) after mid-prompt process death
Nadie ha tomado este issue todavía.
- 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
- Start a session and run a command that triggers a permission prompt (e.g. a shell command, or a file write).
- 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 … &, awhile pgreppoll loop, ortimeout … &— whose lifecycle doesn't get a terminal event before the session is interrupted.) - Run
copilot --resumeand select the session. - The unresolved prompt(s) replay.
/tasksis 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:
/tasksempty, prompts persist across resumes; manualevents.jsonlsurgery 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
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- 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