You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
fix: retain admission ownership while cancelled handler workers remain active #728
safe_handler owns admission around asyncio.to_thread. Cancelling the awaiting task exits admission while the dispatched worker continues. An event-controlled probe with the real safe_handler and synthetic handlers returned:
No store, model or elapsed-time verdict was used. This establishes the internal boundary behavior; native MCP cancellation delivery must be verified separately.
Acceptance evidence
Keep work counted until completion or a documented cooperative cancellation boundary.
A cancelled caller cannot allow active workers to exceed the declared budget.
Define and report in-flight mutation status and work completing after client abandonment.
Exercise real MCP cancellation/disconnect with isolated stores and verify side effects and teardown.
Synchronize correctness cases on events rather than machine-load-dependent delays.
Scope and related work
#722 concerns recall latency. This concerns active-work ownership under cancellation.
Raised by the global Hindsight/Cortex audit. The target is a measurable local-first experience with the same supported semantics in Claude and Codex. Acceptance criteria describe a target, not current conformance. Static findings refer to the pinned source; probes identify their boundary.
Tranche 1 : #885 (Refs #728, ne ferme pas l'issue).
Cause racine : safe_handler rendait le créneau d'admission à la sortie de async with admit(...), donc à l'annulation de l'appelant, alors que le thread de asyncio.to_thread continuait. run_admitted libère désormais le créneau depuis le callback de fin du worker (l'appelant attend via asyncio.shield) et journalise l'issue d'un worker abandonné.
Preuve : la sonde de l'issue (budget 4, quatre appelants annulés, cinquième requête) est un test via le vrai safe_handler ; rouge sans le correctif (sémaphore [unlocked, value:4]), vert avec (pic 4). Gate de mutation ADR-1122 local : 0 survivant dans les fonctions modifiées.
Tranches restantes :
Statut des mutations en vol et du travail terminé après abandon du client (au-delà du log).
Annulation/déconnexion MCP réelle avec stores isolés : effets de bord et teardown.
Frontière d'annulation coopérative documentée pour les handlers qui peuvent s'arrêter tôt.
Changement : worker_ledger tient par outil les workers actifs, ceux qui tournent encore sans appelant et l'issue des abandonnés (completed / raised / cancelled). memory_stats les renvoie sous tool_workers. Un seul callback de fin met le ledger à jour sous un seul verrou.
Preuve : tests sur événements via le vrai run_admitted, rouges sans le câblage. Gate de mutation ADR-1122 local à 0 survivant, y compris les 58 survivants préexistants de memory_stats.handler, tués par un test de correspondance exact.
Tranches restantes :
Annulation/déconnexion MCP réelle avec stores isolés : effets de bord et teardown.
Frontière d'annulation coopérative documentée pour les handlers qui peuvent s'arrêter tôt.
Tranche 2 : #903 (Refs #728, ne ferme pas l'issue).
Ce qu'elle fait : mcp_server/handlers/abandoned_work.py compte, par outil, les workers encore en vol après l'annulation de leur appelant, et l'issue de chacun de ceux qui ont fini (completed / raised / cancelled). Le log de _log_abandoned indique combien de workers abandonnés de l'outil restent en cours.
Preuve : 4 tests pilotés par threading.Event, rouges sans le changement de admission.py. Gate de mutation ADR-1122 local : 0 survivant dans les fonctions modifiées. 1848 tests passés sur tests_py/handlers, tests_py/server et I10.
Pas fait dans cette tranche : exposer ce compte à un client. L'ajouter à memory_stats met tout handler dans le gate de mutation, où 58 mutants préexistants survivent.
Tranches restantes :
Exposer abandoned_work.snapshot() dans memory_stats, après avoir renforcé test_memory_stats.py contre ces 58 survivants.
Annulation/déconnexion MCP réelle avec stores isolés : effets de bord et teardown.
Frontière d'annulation coopérative documentée pour les handlers qui peuvent s'arrêter tôt.
Tranche 3 : #936 (Refs #728, ne ferme pas l'issue). Tests seuls, aucun changement de code de production.
Ce qu'elle prouve : un tools/call passe par le client et le serveur du SDK MCP (transport en mémoire) et par le vrai safe_handler. Il est annulé une fois par notifications/cancelled, une fois par la déconnexion du client. Dans les deux cas le worker garde son créneau d'admission jusqu'à sa fin, abandoned_work le compte puis l'enregistre completed, et son écriture SQLite arrive après que le client a cessé d'écouter. La session survit à l'appel annulé, et le teardown n'attend pas le thread et ne le tue pas.
Preuve : un mutant qui rend le créneau à l'annulation fait échouer les deux tests ; ils sont verts sur main, 5 exécutions de suite.
Pas vérifié : le store est un fichier SQLite simple, pas un store Cortex (le modèle d'embedding n'a pas pu être installé ici), et le transport n'est pas stdio avec un vrai client.
Tranches restantes :
Le même scénario avec un outil mutant réel (remember) sur un store SQLite Cortex isolé.
Une frontière d'annulation coopérative documentée pour les handlers qui peuvent s'arrêter tôt.
Tranche 4 : #943 (Refs #728, ne ferme pas l'issue). Ce sont des tests seuls, sans changement du code de production.
Ce qu'elle prouve : c'est le scénario de test(admission): MCP client cancel and disconnect keep the worker's slot and land its write (#728, slice 3) #936 (annulation par notifications/cancelled, puis déconnexion du client), mais avec le vrai outil remember enregistré par tool_registry_memory.register, sur le store SQLite Cortex isolé de la suite. Le seul double est une barrière sur insert_memory_exact : le handler a déjà franchi la write gate et la curation, puis il bloque juste avant l'écriture. Dans les deux cas, le worker garde le créneau remember. abandoned_work l'enregistre completed, rien n'est écrit avant la libération, et la ligne arrive ensuite avec le contenu envoyé.
Preuve : un mutant qui rend le créneau à l'annulation fait échouer les deux tests (assert 4 == (4 - 1)). Ils sont verts sur main, 5 exécutions de suite. 72 tests voisins passent.
Pas vérifié : la suite complète (installée sans torch, dont l'hôte de téléchargement est bloqué ici), un store PostgreSQL, et le transport stdio avec un vrai client.
Tranche restante :
Une frontière d'annulation coopérative documentée pour les handlers qui peuvent s'arrêter tôt.
Les tranches 1 à 3 sont fusionnées (#885, #902/#903, #936), donc pas de hold.
Preuve : l'ADR cite les tests existants qui tiennent chaque point ; 51 + 52 tests voisins passés, miroir des décisions vérifié (check_mirror vide), gate craftsmanship OK.
Pas vérifié : la suite complète (sans torch, hôte de téléchargement bloqué ici) ; le lint est laissé à la CI.
Activity
cdeust commentedon Nov 5, 2026
Tranche 1 : #885 (
Refs #728, ne ferme pas l'issue).safe_handlerrendait le créneau d'admission à la sortie deasync with admit(...), donc à l'annulation de l'appelant, alors que le thread deasyncio.to_threadcontinuait.run_admittedlibère désormais le créneau depuis le callback de fin du worker (l'appelant attend viaasyncio.shield) et journalise l'issue d'un worker abandonné.safe_handler; rouge sans le correctif (sémaphore[unlocked, value:4]), vert avec (pic 4). Gate de mutation ADR-1122 local : 0 survivant dans les fonctions modifiées.Tranches restantes :
Generated by Claude Code
Merge pull request #885 from cdeust/fix/728-admission-worker-ownership
feat(admission): memory_stats reports abandoned workers and how they …
cdeust commentedon Nov 6, 2026
Tranche 2 : #902 (
Refs #728, ne ferme pas l'issue).worker_ledgertient par outil les workers actifs, ceux qui tournent encore sans appelant et l'issue des abandonnés (completed / raised / cancelled).memory_statsles renvoie soustool_workers. Un seul callback de fin met le ledger à jour sous un seul verrou.run_admitted, rouges sans le câblage. Gate de mutation ADR-1122 local à 0 survivant, y compris les 58 survivants préexistants dememory_stats.handler, tués par un test de correspondance exact.Tranches restantes :
Generated by Claude Code
fixup! feat(admission): memory_stats reports abandoned workers and ho…
cdeust commentedon Nov 6, 2026
Tranche 2 : #903 (
Refs #728, ne ferme pas l'issue).mcp_server/handlers/abandoned_work.pycompte, par outil, les workers encore en vol après l'annulation de leur appelant, et l'issue de chacun de ceux qui ont fini (completed / raised / cancelled). Le log de_log_abandonedindique combien de workers abandonnés de l'outil restent en cours.threading.Event, rouges sans le changement deadmission.py. Gate de mutation ADR-1122 local : 0 survivant dans les fonctions modifiées. 1848 tests passés surtests_py/handlers,tests_py/serveretI10.memory_statsmet touthandlerdans le gate de mutation, où 58 mutants préexistants survivent.Tranches restantes :
abandoned_work.snapshot()dansmemory_stats, après avoir renforcétest_memory_stats.pycontre ces 58 survivants.Generated by Claude Code
feat(admission): count workers still running after their caller was c…
feat(stats): memory_stats reports abandoned workers from abandoned_wo…
cdeust commentedon Nov 7, 2026
Tranche 3 : #936 (
Refs #728, ne ferme pas l'issue). Tests seuls, aucun changement de code de production.tools/callpasse par le client et le serveur du SDK MCP (transport en mémoire) et par le vraisafe_handler. Il est annulé une fois parnotifications/cancelled, une fois par la déconnexion du client. Dans les deux cas le worker garde son créneau d'admission jusqu'à sa fin,abandoned_workle compte puis l'enregistrecompleted, et son écriture SQLite arrive après que le client a cessé d'écouter. La session survit à l'appel annulé, et le teardown n'attend pas le thread et ne le tue pas.Tranches restantes :
remember) sur un store SQLite Cortex isolé.Generated by Claude Code
fixup! test(admission): MCP client cancel and disconnect keep the wor…
fixup! test(admission): MCP client cancel and disconnect keep the wor…
test(admission): MCP client cancel and disconnect keep the worker's s…
test(admission): a cancelled real remember keeps its slot and lands i…
cdeust commentedon Nov 7, 2026
Tranche 4 : #943 (
Refs #728, ne ferme pas l'issue). Ce sont des tests seuls, sans changement du code de production.notifications/cancelled, puis déconnexion du client), mais avec le vrai outilrememberenregistré partool_registry_memory.register, sur le store SQLite Cortex isolé de la suite. Le seul double est une barrière surinsert_memory_exact: le handler a déjà franchi la write gate et la curation, puis il bloque juste avant l'écriture. Dans les deux cas, le worker garde le créneauremember.abandoned_workl'enregistrecompleted, rien n'est écrit avant la libération, et la ligne arrive ensuite avec le contenu envoyé.assert 4 == (4 - 1)). Ils sont verts sur main, 5 exécutions de suite. 72 tests voisins passent.torch, dont l'hôte de téléchargement est bloqué ici), un store PostgreSQL, et le transport stdio avec un vrai client.Tranche restante :
Les tranches 1 à 3 sont fusionnées (#885, #902/#903, #936), donc pas de hold.
Generated by Claude Code
fixup! test(admission): a cancelled real remember keeps its slot and …
fixup! test(admission): a cancelled real remember keeps its slot and …
test(admission): a cancelled real remember keeps its slot and lands i…
cdeust commentedon Nov 7, 2026
Tranche 5 : #946 (
Closes #728). Documentation seule : ADR-1125.check_mirrorvide), gate craftsmanship OK.torch, hôte de téléchargement bloqué ici) ; le lint est laissé à la CI.Generated by Claude Code
fixup! docs(admission): ADR-1125, a cancelled tool call's worker runs…
docs(admission): ADR-1125, a cancelled tool call's worker runs to com…
github-actions commentedon Nov 7, 2026
Closed by #946, merged by the resolver (its GITHUB_TOKEN merge does not trigger GitHub's closing keywords).