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
[MCP host] Manage local ports and stop idle tool runtimes automatically #57
Casys owns the ports and running processes needed by its engineering tools. A user enables Build123d, works in one or several chats, and leaves the app without having to inspect Docker, free a port, or stop forgotten containers. Opening the catalogue or reading saved work must not start the engineering fleet.
Parent: #48. Builds on the runtime backend #56, catalogue #54, chat #49 and saved work #51. Include this lifecycle in the packaged first-use proof #53. Start with Build123d on the supported Desktop target.
Verified current foundation and gap
Observed at 12d5559de173a6d5a18e36b1e1de2dcc41b6607a:
The selected provider starts on demand. Its Compose document already binds to loopback and limits CPU, memory and process count.
The plan uses a fixed fleet port and a shared per-tool Compose project. There is no app-managed free-port allocation. Merely overriding hostPort is insufficient: mcpUrl and healthUrl still come from the fixed fleet entry.
Desktop shutdown stops the chat host and application helpers, without draining the catalogue's Docker runtime. Detached provider containers can therefore remain running after quitting Casys.
The existing project JIT supervisor has lease/release mechanisms, but those are not evidence that standalone chat lifecycle is wired. Reuse suitable mechanisms without imposing briefs, registered engineering runs or project authorization on ordinary MCP use.
Acceptance
Start only what is used. Plain chat, catalogue browsing and saved-work inspection do not launch engineering providers. Start required provider components and dedicated relays when needed; do not create a container per chat or one listener per unused catalogue entry.
Own endpoint allocation. For app-owned local providers, allocate an available host port automatically and retain the actual binding as host-owned runtime state. Bind only to loopback; never require the user to edit a port or displace an unrelated process. Handle concurrent allocation safely. An already occupied historical port (e.g. 3014) must not prevent a new owned instance from becoming usable.
Use the actual endpoint everywhere. Readiness probes, catalogue state, Node chat relays and provider viewer/resource reads use the current runtime binding. Reconnect correctly when the binding changes after restart. Keep provider identity/image pins separate from runtime addresses; update packaged permissions coherently without granting endpoint selection to the agent or renderer.
Share and release safely. Track demand across conversations and in-flight tool/viewer operations. Disconnecting one chat must not stop a provider still used by another. After the last demand ends, stop owned containers and close unused relays after a bounded, documented idle delay. Define inactivity explicitly; merely switching tabs must not interrupt work. Retain saved source/results and required persistent volumes, and restart transparently on the next use.
Drain on quit; reconcile after interruption. Normal application shutdown settles/cancels active work according to an explicit policy, then stops app-owned provider resources and releases their listeners. Report unresolved shutdown honestly. On the next launch after a crash, reconcile exact owned resources before reusing or replacing them; do not blindly repeat an uncertain tool execution.
Preserve external ownership. Reused external providers are disconnected, not stopped or adopted implicitly. Other Casys project consumers must count as demand when sharing is permitted. Do not stop the shared Docker engine/VM, alter its global settings, prune unrelated resources, or promise zero Docker background cost.
Expose simple controls. Show running/idle/stopped/error states and an explicit stop/restart action in the app, respecting active/shared work. Ports and container IDs belong in diagnostics rather than the ordinary workflow. Preserve existing resource limits; no monitoring dashboard is required.
Prove the lifecycle through the packaged host. Cover: an occupied fixed port; two chats sharing Build123d; detach of only one; last release and idle expiry; an active operation protected from idle cleanup; clean app quit; crash/relaunch reconciliation; restart on demand; and reopening/exporting saved work without re-executing geometry. Record before/after owned containers and listeners. Unrelated containers and listeners must remain untouched.
Scope
This completes ordinary app-managed runtime lifecycle beyond the backend primitives in #56. It does not require a new OCI engine, a universal fleet orchestrator, a cloud service, or a redesign of the engineering authority model. A private network/shared gateway may be considered if useful, but a single-port architecture is not a prerequisite: the requirement is bounded, local, automatically managed resources with no unnecessary idle providers.
Docker reference: port publishing and loopback bindings. Validate the supported engine's actual exposure in the packaged proof rather than assuming a Compose declaration alone proves network isolation.
Allocation éphémère loopback possédée par le host (tool-runtime/ports.ts) : sélecteur, jamais de port fixe.
Demande/idle (chat-demand.ts, lifecycle.ts) : l'activité chat retient les providers, arrêt après 180 s d'inactivité (TOOL_RUNTIME_IDLE_DELAY_MS=180_000, MANAGED_TOOL_IDS=build123d).
Drain gracieux au quit + réconciliation au démarrage (adopte les conteneurs managés vivants, arrête les périmés, ne touche jamais aux autres conteneurs).
Relais MCP paresseux + resolvers fail-closed ; le renderer ne voit ni ports ni ids conteneurs (scrubRuntimeIdentity).
Catalogue : stop/restart avec reporting honnête ; décoration demande côté chat ; relais pièces jointes sans fuite d'identité.
Gates
246 tests ciblés verts (deno test --allow-all sur le périmètre), deno lint, deno fmt, deno check main.ts, npm run check (UI), catalogue:profile-test verts.
Hors périmètre, pré-existant sur arbre propre : 1 échec startup_test, deno task check/test avec erreurs @std/cli/graphology.
Preuve E2E packagée (Docker réel + Build123d réel) — ALL GREEN
Run final avec idle production 180 s (phases rapides via E2E57_IDLE_MS, run final sans override) :
partage : 2 conversations partagent 1 conteneur, arrêt après départ des 2 demandeurs ;
idle : conteneur arrêté après 180 s réelles sans demande ;
quit : drain gracieux, binding conservée ;
crash : conteneurs tiers (erpnext-*, 3 decoys) intacts, pas de docker stop aveugle ;
restart : binding conservée, squatter sur l'ancien port contourné par réallocation.
Review
Relecture Codex indépendante : 5 findings VRAI/NUANCE (arrêts reconcile, try/finally demande, scrubs viewer/catalogue) corrigés puis re-vérifiés par gates ciblés verts.
Clôture : comportement #57 vérifié de bout en bout contre Docker réel.
Réouverture après revue indépendante Sol et deux reproductions relancées par le coordinateur, le 30 septembre 2026, sur 0be84e7edba3c2f71f87d01867104039c048b3be. NEEDS-WORK. L'allocation des ports, le partage, les relais paresseux et le drain sont implémentés ; deux scénarios échappent au comptage de demande/idle.
1. P2 — les chats fermés relancent et retiennent le provider au démarrage. synchronizeStartupDemand (desktop/src/tool-runtime/chat-demand.ts:171) compte tous les attachements connected, y compris les conversations status:"closed". Fermer une conversation (desktop/src/chat/coordinator.ts:1342) conserve son attachement dans le store.
Reproduction avec snapshot fermé+connected et lifecycle simulé : ensure | relay | holders=chat:closed-1. Un chat fermé provoque donc le lancement du fournisseur et constitue une demande permanente, empêchant l'arrêt après 180 s. Ignorer les conversations fermées lors de la réconciliation de demande ; couvrir la fermeture puis la relance à partir du snapshot persisté.
2. P2 — Prepare depuis le catalogue ne déclenche pas son propre arrêt idle. prepare() (desktop/src/tool-runtime/lifecycle.ts:155) passe par ensure, et le démarrage réussi (desktop/src/tool-runtime/lifecycle.ts:559) ajoute le provider aux ressources vivantes sans armer un délai quand la demande vaut zéro. Seuls release/syncDemand arment le délai.
Reproduction backend et horloge simulés : {out:"ready",state:"idle",timers:0,stops:0} après prepare("build123d") sans chat attaché. Si le délai initial de lancement de l'app a déjà expiré, ce nouveau provider ne s'arrête jamais ; s'il existe encore, le délai part du lancement de l'app au lieu de la préparation. Armer/réinitialiser l'arrêt après une préparation réussie sans demande, en conservant la protection des opérations et du partage.
Validation : 136 tests ciblés existants et contrôle de types passent. Deux reproductions sur les sources actuelles ont été relancées indépendamment dans /tmp/casys-sep30-review-repros.mjs. Aucun Docker réel, fournisseur ou LLM lancé ; aucun fichier dépôt modifié. Ceci ne conteste pas les scénarios E2E déjà mesurés : il ajoute deux scénarios non couverts qui empêchent la clôture du cycle de vie promis.
Implémentation reprise et validée le 2026-09-30. Changements dans le commit local, non poussé070f66c8 sur codex/desktop-mcp-host-reliability ; ces preuves ne sont pas une livraison sur main.
Le démarrage ignore maintenant les conversations fermées pour calculer la demande. Toute préparation réussie sans demande arme le délai d'arrêt ; une préparation récente invalide les anciennes callbacks de délai déjà mises en file.
Régressions ciblées : conversations fermées, préparation sans chat, expiry et ancien timer pendant une nouvelle préparation passent. La preuve réelle démarre un seul Build123d sur un port loopback éphémère puis l'arrête proprement : aucun container Casys en cours après quit, neuf containers étrangers préservés. Le relais reste partagé ; aucune ouverture d'un port par conversation n'est ajoutée.
Activity
feat(tool-runtime): app-managed provider lifecycle with demand/idle/d…
superWorldSavior commentedon Sep 28, 2026
Record — lifecycle app-managé livré (
94aaa037)Commit :
94aaa037— 34 fichiers (22 modifiés, 12 nouveaux), poussé surmain.Implémentation
tool-runtime/ports.ts) : sélecteur, jamais de port fixe.chat-demand.ts,lifecycle.ts) : l'activité chat retient les providers, arrêt après 180 s d'inactivité (TOOL_RUNTIME_IDLE_DELAY_MS=180_000,MANAGED_TOOL_IDS=build123d).scrubRuntimeIdentity).Gates
deno test --allow-allsur le périmètre),deno lint,deno fmt,deno check main.ts,npm run check(UI),catalogue:profile-testverts.startup_test,deno task check/testavec erreurs@std/cli/graphology.Preuve E2E packagée (Docker réel + Build123d réel) — ALL GREEN
Run final avec idle production 180 s (phases rapides via
E2E57_IDLE_MS, run final sans override) :erpnext-*, 3 decoys) intacts, pas dedocker stopaveugle ;Review
Relecture Codex indépendante : 5 findings VRAI/NUANCE (arrêts reconcile,
try/finallydemande, scrubs viewer/catalogue) corrigés puis re-vérifiés par gates ciblés verts.Clôture : comportement #57 vérifié de bout en bout contre Docker réel.
superWorldSavior commentedon Sep 30, 2026
Réouverture après revue indépendante Sol et deux reproductions relancées par le coordinateur, le 30 septembre 2026, sur
0be84e7edba3c2f71f87d01867104039c048b3be. NEEDS-WORK. L'allocation des ports, le partage, les relais paresseux et le drain sont implémentés ; deux scénarios échappent au comptage de demande/idle.1. P2 — les chats fermés relancent et retiennent le provider au démarrage.
synchronizeStartupDemand(desktop/src/tool-runtime/chat-demand.ts:171) compte tous les attachementsconnected, y compris les conversationsstatus:"closed". Fermer une conversation (desktop/src/chat/coordinator.ts:1342) conserve son attachement dans le store.Reproduction avec snapshot fermé+connected et lifecycle simulé :
ensure | relay | holders=chat:closed-1. Un chat fermé provoque donc le lancement du fournisseur et constitue une demande permanente, empêchant l'arrêt après 180 s. Ignorer les conversations fermées lors de la réconciliation de demande ; couvrir la fermeture puis la relance à partir du snapshot persisté.2. P2 — Prepare depuis le catalogue ne déclenche pas son propre arrêt idle.
prepare()(desktop/src/tool-runtime/lifecycle.ts:155) passe parensure, et le démarrage réussi (desktop/src/tool-runtime/lifecycle.ts:559) ajoute le provider aux ressources vivantes sans armer un délai quand la demande vaut zéro. Seuls release/syncDemand arment le délai.Reproduction backend et horloge simulés :
{out:"ready",state:"idle",timers:0,stops:0}aprèsprepare("build123d")sans chat attaché. Si le délai initial de lancement de l'app a déjà expiré, ce nouveau provider ne s'arrête jamais ; s'il existe encore, le délai part du lancement de l'app au lieu de la préparation. Armer/réinitialiser l'arrêt après une préparation réussie sans demande, en conservant la protection des opérations et du partage.Validation : 136 tests ciblés existants et contrôle de types passent. Deux reproductions sur les sources actuelles ont été relancées indépendamment dans
/tmp/casys-sep30-review-repros.mjs. Aucun Docker réel, fournisseur ou LLM lancé ; aucun fichier dépôt modifié. Ceci ne conteste pas les scénarios E2E déjà mesurés : il ajoute deux scénarios non couverts qui empêchent la clôture du cycle de vie promis.superWorldSavior commentedon Sep 30, 2026
Implémentation reprise et validée le 2026-09-30. Changements dans le commit local, non poussé
070f66c8surcodex/desktop-mcp-host-reliability; ces preuves ne sont pas une livraison surmain.Le démarrage ignore maintenant les conversations fermées pour calculer la demande. Toute préparation réussie sans demande arme le délai d'arrêt ; une préparation récente invalide les anciennes callbacks de délai déjà mises en file.
Régressions ciblées : conversations fermées, préparation sans chat, expiry et ancien timer pendant une nouvelle préparation passent. La preuve réelle démarre un seul Build123d sur un port loopback éphémère puis l'arrête proprement : aucun container Casys en cours après quit, neuf containers étrangers préservés. Le relais reste partagé ; aucune ouverture d'un port par conversation n'est ajoutée.