`--resume` replays orphaned permission.requested events (no matching permission.completed) after mid-prompt process death
Personne n'a encore pris cette issue.
- Langage dominant
- Shell
- Étoiles
- 11.2k
- Forks
- 1.9k
- Merge moyen
- 14 h 16 min
- PR mergées (30 j)
- 6
Description
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.
Guide de contribution
Ouvrir le guide de contribution
Par où commencer
- Lisez l'issue en entier, puis le guide de contribution du projet.
- Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
- Forkez le dépôt et travaillez sur une branche.
- Ouvrez une pull request qui référence le numéro de l'issue.
Piste de recherche
Commencez par suivre le chemin --resume qui lit les événements de la session dans events.jsonl, en vous concentrant sur l’association entre permission.requested et permission.completed via requestId ; il est indiqué que command-history-state.json est propre et n’est pas à l’origine du problème. Reproduisez un prompt interrompu, puis vérifiez que les requêtes orphelines sont annulées ou refusées lors de la reprise et ne sont plus rejouées lors des reprises ultérieures.
Rédigé par le modèle d'indexation à partir du texte de l'issue.
Évaluation
- Stack technique
- shell
- Domaine
- cli
- Type d'issue
- Bug
- Difficulté
- 4/5
- Temps estimé
- 3-5 jours
- Activité
- Calme
- Clarté
- Plutôt claire
- Accessibilité débutants
- 48/100