diff --git a/CLAUDE.md b/CLAUDE.md index 381177e50..367067f82 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -222,7 +222,7 @@ The codebase follows a **layered architecture with dependency injection** and ** - Entry point for MCP protocol communication - `src/server.ts` is the composition root and the shared core (`DebugMcpServer`: dependencies, lifecycle, session validation, breakpoint gating, the public facade methods, the `NotificationHub`); it implements `ToolContext` and passes itself to the handlers. It owns no SDK `Server`: `src/server/protocol-facade.ts` (`createProtocolServer(core, { era, deliver })`) builds one cheap SDK `Server` per connection or request over the core, and `src/server/notification-hub.ts` fans the coalesced `resources/updated` pings and `resources/list_changed` out to every facade's sink (issue #1025) - `src/server/` holds the tool schemas (`tool-schemas.ts`), argument coercion (`tool-arguments.ts`), the tools/call dispatch wrapper (`tool-dispatch.ts` over `handlers/index.ts`), argument validation (`tool-validation.ts`), result envelopes and the session-error catch helpers (`tool-result.ts`), one handler module per tool family (`handlers/*.ts`) over their shared helpers (`handlers/shared.ts`), and the resource (`output-resources.ts`) and prompt (`prompts.ts`) handlers - - Supports STDIO and Streamable HTTP transport modes, both in both protocol eras (issue #1025): `stdio` is served through the SDK's `serveStdio(factory, { transport })`, which pins a connection to the era its opening message chose (2026-07-28 for a `server/discover` probe, 2025-11-25 and earlier for `initialize`); `http` routes each request with the SDK's `isLegacyRequest` — a POST carrying the 2026 per-request envelope goes to a strict `createMcpHandler(factory, { legacy: 'reject' })` behind `toNodeHandler` (one facade per request, sessionless), everything 2025-shaped to the sessionful app as before. One core per process in both: on `http` every MCP session is a facade over the same debug sessions (the id is the capability; one process per agent is the isolation), a debug session outlives the MCP session that created it and ends by its own close, its program ending, or the server exiting. The per-debug-session idle policy is the operator's opt-in (`MCP_DEBUG_SESSION_IDLE_MS`, off by default on every transport; sweep `MCP_DEBUG_SESSION_IDLE_SWEEP_MS`): when set, an abandoned session tree is closed unless a client is attending the process (a 2025 session's open SSE stream, or a 2026-07-28 client's open `subscriptions/listen` stream), `list_debug_sessions` rows show `idleMs`/`expiresInMs`, and a call naming a session the policy closed answers `closedBy: "idle-policy"`. A session outside the caller's owner scope is hidden from the call's lookups (`src/session/session-scope.ts`), so every tool answers it as an unknown id. The HTTP reapers (`MCP_HTTP_STALE_SESSION_MS`, `MCP_HTTP_STREAM_LOST_SESSION_MS`) reap abandoned MCP sessions only (issue #1025) + - Supports STDIO and Streamable HTTP transport modes, both in both protocol eras (issue #1025): `stdio` is served through the SDK's `serveStdio(factory, { transport })`, which pins a connection to the era its opening message chose (2026-07-28 for a `server/discover` probe, 2025-11-25 and earlier for `initialize`); `http` routes each request with the SDK's `isLegacyRequest` — a POST carrying the 2026 per-request envelope goes to a strict `createMcpHandler(factory, { legacy: 'reject' })` behind `toNodeHandler` (one facade per request, sessionless), everything 2025-shaped to the sessionful app as before. One core per process in both: on `http` every MCP session is a facade over the same debug sessions (the id is the capability; one process per agent is the isolation), a debug session outlives the MCP session that created it and ends by its own close, its program ending, or the server exiting. The per-debug-session idle policy is the operator's opt-in (`MCP_DEBUG_SESSION_IDLE_MS`, off by default on every transport; sweep `MCP_DEBUG_SESSION_IDLE_SWEEP_MS`): when set, an abandoned session tree is closed unless a client is attending the process (a 2025 session's open SSE stream, or a 2026-07-28 client's open `subscriptions/listen` stream), `list_debug_sessions` rows show `idleMs`/`expiresInMs`, and a call naming a session the policy closed answers `closedBy: "idle-policy"`. A call in flight holds its tree (issue #1127): `SessionManagerCore.holdForCall` stamps the tree's clock, holds the tree, and its release stamps again, so the window starts over when the call answers; the dispatch (`src/server/tool-dispatch.ts` `holdForTheCall`) takes it for every tool that names a session and makes a call that arrives during the sweep's close wait for it, and `create_debug_session` takes it around its inline attach. A session outside the caller's owner scope is hidden from the call's lookups (`src/session/session-scope.ts`), so every tool answers it as an unknown id. The HTTP reapers (`MCP_HTTP_STALE_SESSION_MS`, `MCP_HTTP_STREAM_LOST_SESSION_MS`) reap abandoned MCP sessions only (issue #1025) - Dynamically discovers available language adapters 2. **Adapter System** (NEW) @@ -248,7 +248,7 @@ The codebase follows a **layered architecture with dependency injection** and ** - Collaborators see the facade through `OperationsContext` (`operations-context.ts`), whose members are late-bound arrows and getters — never captured references — so reassigning a facade method or writing a tunable on a live instance is visible to them; each takes the narrowest `Pick<>` slice it uses. Collaborator-to-collaborator dependencies are constructor arguments (`DebugLauncher(ctx, proxyLauncher, breakpoints, inFlight)`, `AttachController(ctx, proxyLauncher, breakpoints, pauseCoordinator, inFlight)`) held as captured instance references: their methods resolve at call time, so `vi.spyOn(internals(ops).proxyLauncher, 'start')` intercepts, but reassigning a collaborator field after construction is not observed. The facade owns one shared `InFlightGuard` (`in-flight-guard.ts`, issue #711) that the launcher and the attach controller both take: `startDebugging`, `restartDebugging`, `attachToProcess` and `detachFromProcess` each claim the session before their first await and release it in `finally`, so one launch-shaped call per session at a time; `restartDebugging` replays through the launcher's private `launch()` under its own claim, never through the facade's `startDebugging`. Shared per-request DAP helpers live in `dap-request-helpers.ts` - Coordinates ProxyManager instances (one per session) - Handles breakpoint management and queuing - - The session tree (issue #1081, `docs/architecture/session-tree.md`): a child the adapter reports (debugpy and js-debug; on by default for launches and verified-local JavaScript attaches, `dapLaunchArgs: { subProcess: false }` or `{ autoAttachChildProcesses: false }` opts a launch out, `adapterConfig: { autoAttachChildProcesses: false }` a JavaScript attach) is adopted by `SessionManagerCore.adoptReportedChild` as a session of its own — `parentSessionId`, the parent's language and breakpoints, a child proxy view over the parent's worker wired with the same handlers — under the per-tree cap of `child-session-cap.ts` (`DEBUG_MCP_MAX_CHILD_SESSIONS`, default 32, `0` off; beyond it the child is released); `closeSession` closes a subtree deepest first, a root that ends with live children defers its worker's reap until the last ends (`stopOrDeferWorker`), and the launch-shaped tools and the mirror refuse a child id by name (`handlers/shared.ts` `childSessionRefusal`). The tree-aware wait: `ExecutionController.waitForStop` runs on `waitForSubtreeSettle` (`execution/session-state-wait.ts`, the `waitForSettle` primitive shared with `waitForSessionState`) over the core's subtree listeners (`onSubtreeStateChange`, fired for a session and each ancestor on every transition and removal) and `selectSettledInTree` (`execution/tree-settle.ts`, the four-rule read: the session itself when paused or errored, else the first paused or errored descendant depth first in adoption order, else the session itself when ended, else keep waiting; `describeTreeAnswer` builds `children`/`childrenReleased`). The answer fields: `sessionId` (the selected session; set by the wait handler in `handlers/execution-tools.ts`), `children`/`childrenReleased`/`childrenClosed` (hoisted beside `pending` by `handlers/shared.ts` `hoistTreeFields` on the wait, launch, restart and detach answers) and `processEnded`/`released` (spread by the `close_debug_session` handler from the `closeSessionReporting` outcome — the close outcome it answers from, `childrenClosed` included); `releaseDescendants` is the detach cascade (children detached deepest first with `terminateDebuggee: false`, their rows kept as `stopped` with `programEnd.data.released`, then the parent) + - The session tree (issue #1081, `docs/architecture/session-tree.md`): a child the adapter reports (debugpy and js-debug; on by default for launches and verified-local JavaScript attaches, `dapLaunchArgs: { subProcess: false }` or `{ autoAttachChildProcesses: false }` opts a launch out, `adapterConfig: { autoAttachChildProcesses: false }` a JavaScript attach) is adopted by `SessionManagerCore.adoptReportedChild` as a session of its own — `parentSessionId`, the parent's language and breakpoints, a child proxy view over the parent's worker wired with the same handlers — under the per-tree cap of `child-session-cap.ts` (`DEBUG_MCP_MAX_CHILD_SESSIONS`, default 32, `0` off; beyond it the child is released); `closeSession` closes a subtree deepest first, a root that ends with live children defers its worker's reap until the last ends (`stopOrDeferWorker`), and the launch-shaped tools and the mirror refuse a child id by name (`handlers/shared.ts` `childSessionRefusal`). The tree-aware wait: `ExecutionController.waitForStop` runs on `waitForSubtreeSettle` (`execution/session-state-wait.ts`, the `waitForSettle` primitive shared with `waitForSessionState`) over the core's subtree listeners (`onSubtreeStateChange`, fired for a session and each ancestor on every transition and removal) and `selectSettledInTree` (`execution/tree-settle.ts`, the four-rule read: the session itself when paused or errored, else the first paused or errored descendant depth first in adoption order, else the session itself when ended, else keep waiting; `describeTreeAnswer` builds `children`/`childrenReleased`; `describeReleasedChildren` words what the number means from the causes recorded with it, and every launch, restart and wait message that carries the number ends with that sentence, issue #1137). A descendant's user-visible stop is counted on every ancestor (`ManagedSession.descendantStops`, per run), so the end summary of a session whose tree stopped does not say it hit nothing (`summarizeProgramEnd`, issue #1128). The answer fields: `sessionId` (the selected session; set by the wait handler in `handlers/execution-tools.ts`), `children`/`childrenReleased`/`childrenClosed` (hoisted beside `pending` by `handlers/shared.ts` `hoistTreeFields` on the wait, launch, restart and detach answers) and `processEnded`/`released` (spread by the `close_debug_session` handler from the `closeSessionReporting` outcome — the close outcome it answers from, `childrenClosed` included); `releaseDescendants` is the detach cascade (children detached deepest first with `terminateDebuggee: false`, their rows kept as `stopped` with `programEnd.data.released`, then the parent) 4. **ProxyManager** (`src/proxy/proxy-manager.ts`) - Manages communication with debug proxy process diff --git a/README.md b/README.md index ccf31e543..334da317f 100644 --- a/README.md +++ b/README.md @@ -241,8 +241,9 @@ with `MCP_DEBUG_SESSION_IDLE_MS` set, a session tree that no tool call has named closed (a launched program is ended, an attached one released), unless a client is attending the process — while any client holds a stream open (a 2025 session's SSE stream, a 2026-07-28 client's `subscriptions/listen` stream), nothing expires. A 2026 client that only calls tools is -not attending: with the policy on, its sessions live by tool activity (and an in-flight -`wait_for_stop`) alone, so choose a window longer than such a client's pauses. +not attending: with the policy on, its sessions live by tool activity alone — a call in flight +holds its session, and the window starts over when it answers — so choose a window longer than +such a client's pauses between calls. `list_debug_sessions` shows each session's idle time, so a forgotten one is easy to find and close. Abandoned MCP sessions themselves are reaped separately (`MCP_HTTP_STREAM_LOST_SESSION_MS`, diff --git a/changelog.d/1025-idle-policy.added.md b/changelog.d/1025-idle-policy.added.md index 3a3a4a1f7..2d390a237 100644 --- a/changelog.d/1025-idle-policy.added.md +++ b/changelog.d/1025-idle-policy.added.md @@ -1 +1 @@ -**An opt-in per-debug-session idle policy; nothing expires by default.** A debug session ends by its own close, its program ending, or the server exiting, on every transport — no timer ends one unless the operator asks. With `MCP_DEBUG_SESSION_IDLE_MS` set, a debug-session tree that no tool call has named for that long is closed through the normal close cascade — a launch root closes its subtree and ends its program, an attach root releases its targets — unless a client is attending the process (an open SSE stream), in which case nothing expires; a tool call naming any session in a tree refreshes the whole tree, an in-flight `wait_for_stop` holds it, and children never expire on a clock of their own. The sweep runs every `MCP_DEBUG_SESSION_IDLE_SWEEP_MS` (default 60 s). The agent can see it: `list_debug_sessions` rows carry `idleMs` (and `expiresInMs` when a policy is on), a tool call naming a session the policy closed answers what happened — when, under which window, and whether its program was ended, released, or was not running by then (`closedBy: "idle-policy"`) — instead of "Session not found", and the server instructions mention the policy when it is on. The HTTP reapers (`MCP_HTTP_STALE_SESSION_MS`, `MCP_HTTP_STREAM_LOST_SESSION_MS`) still reap abandoned MCP sessions, but reaping one no longer closes any debug session: **a crashed client's debug sessions stay until someone closes them, the idle window (if set) runs out, or the server exits.** Set the variable on a shared or long-lived `http` server, and wherever an attached process must not stay paused (the Kubernetes recipe sets 5 minutes). Step 2 of #1025. +**An opt-in per-debug-session idle policy; nothing expires by default.** A debug session ends by its own close, its program ending, or the server exiting, on every transport — no timer ends one unless the operator asks. With `MCP_DEBUG_SESSION_IDLE_MS` set, a debug-session tree that no tool call has named for that long is closed through the normal close cascade — a launch root closes its subtree and ends its program, an attach root releases its targets — unless a client is attending the process (an open SSE stream), in which case nothing expires; a tool call naming any session in a tree refreshes the whole tree and holds it for as long as the call runs (the window starts over when it answers), and children never expire on a clock of their own. The sweep runs every `MCP_DEBUG_SESSION_IDLE_SWEEP_MS` (default 60 s). The agent can see it: `list_debug_sessions` rows carry `idleMs` (and `expiresInMs` when a policy is on), a tool call naming a session the policy closed answers what happened — when, under which window, and whether its program was ended, released, or was not running by then (`closedBy: "idle-policy"`) — instead of "Session not found", and the server instructions mention the policy when it is on. The HTTP reapers (`MCP_HTTP_STALE_SESSION_MS`, `MCP_HTTP_STREAM_LOST_SESSION_MS`) still reap abandoned MCP sessions, but reaping one no longer closes any debug session: **a crashed client's debug sessions stay until someone closes them, the idle window (if set) runs out, or the server exits.** Set the variable on a shared or long-lived `http` server, and wherever an attached process must not stay paused (the Kubernetes recipe sets 5 minutes). Step 2 of #1025. diff --git a/changelog.d/1127.fixed.md b/changelog.d/1127.fixed.md new file mode 100644 index 000000000..0378a25be --- /dev/null +++ b/changelog.d/1127.fixed.md @@ -0,0 +1 @@ +**The idle policy no longer closes a session underneath its own call.** With `MCP_DEBUG_SESSION_IDLE_MS` set, a tool call counted as activity only at the moment it was dispatched, so a call that ran longer than the window — a launch that builds first, a slow launch on a busy host, an `attach_to_process` with a long `verifyTimeout` — had its session closed while it was still in flight and answered a generic launch failure. Every tool call that names a session now holds that session's tree for as long as it runs (the hold was `wait_for_stop`'s alone), `create_debug_session` with an attach target holds the session it creates while it attaches, and the idle clock starts over when the call answers, so a slow call no longer uses up the window it was made in. `list_debug_sessions` shows `idleMs: 0` for a session with a call in flight, and a call that arrives while the policy is closing a session waits for that close and answers `closedBy: "idle-policy"`. (#1127) diff --git a/changelog.d/1128.fixed.md b/changelog.d/1128.fixed.md new file mode 100644 index 000000000..6cf7b4382 --- /dev/null +++ b/changelog.d/1128.fixed.md @@ -0,0 +1 @@ +**A parent's end summary no longer says the run hit no breakpoint after a child session stopped on one.** With child sessions (Python and JavaScript), the documented flow — a breakpoint in the child's code, start the parent, wait — ended with the parent answering "The program ran to completion (exit code 0) without hitting any breakpoint. Verified but never hit: child.py:12", although a child session had stopped on that line (JavaScript reported the breakpoint as never bound as well). The summary was read off the launched process alone. A stop on any descendant is now counted for its ancestors, and a session whose tree stopped answers "Its child sessions stopped 2 times during this run." with no breakpoint listed as unhit — also when the child was closed before the parent ended. (#1128) diff --git a/changelog.d/1129.fixed.md b/changelog.d/1129.fixed.md new file mode 100644 index 000000000..079ba1645 --- /dev/null +++ b/changelog.d/1129.fixed.md @@ -0,0 +1 @@ +**The server's own idle-policy settings are no longer displayed as redacted secrets.** The environment sanitizer masked any variable whose name contained `session` followed by `id`, and `SESSION_IDLE` contains `SESSION_ID`: the dev proxy's `dev_server_status` showed `MCP_DEBUG_SESSION_IDLE_MS` and `MCP_DEBUG_SESSION_IDLE_SWEEP_MS` as `[REDACTED]`, so an operator could not read back which idle window was in force. The rule still masks whatever follows `session…id` and now knows one exception, the word "idle" (`SESSION_IDLE_MS`, `sessionIdleTimeout`); every other name it masked before is still masked. `session.id` and `SESSION ID`, with a separator the old pattern did not know, are now masked too. (#1129) diff --git a/changelog.d/1137.fixed.md b/changelog.d/1137.fixed.md new file mode 100644 index 000000000..e54112131 --- /dev/null +++ b/changelog.d/1137.fixed.md @@ -0,0 +1 @@ +**`childrenReleased` is explained in the answer that carries it.** When a child process was released to run undebugged — child sessions turned off with `DEBUG_MCP_MAX_CHILD_SESSIONS=0`, the per-tree cap reached, or an adoption that failed — the launch and `wait_for_stop` answers carried only the number, while a pending answer went on promising a pause at a breakpoint that only the released child could reach. Every launch, restart and wait answer with a `childrenReleased` count now ends its message with one sentence saying how many children were released, that they run undebugged, why (naming the setting when it decided), and that a breakpoint only they reach will not hit. (#1137) diff --git a/docs/architecture/session-tree.md b/docs/architecture/session-tree.md index 5352c01fb..c77400684 100644 --- a/docs/architecture/session-tree.md +++ b/docs/architecture/session-tree.md @@ -117,6 +117,11 @@ exists). The agent then uses `sessionId` for inspection, stepping and continuing 2. otherwise the first live descendant that is paused or in error, in adoption order, depth first; 3. otherwise the session itself, if it has ended; 4. otherwise (only descendants have ended and the session still runs) the wait keeps waiting. + When rule 3 answers a session's end, the end summary is the tree's (issue #1128): a stop the + agent could see on any descendant is counted on every ancestor (`descendantStops`, per run — the + child's own record leaves with its row at its close), and a session that never stopped itself + but whose tree did says "Its child sessions stopped N times during this run" instead of + "without hitting any breakpoint", listing no breakpoint as unhit. A paused parent is reported before its paused children: the agent has to deal with it anyway, and the `children` list below shows what else is paused. A parent that ended while a child is paused reports the child (rule 2 before rule 3), because nothing can be done on the parent. The @@ -276,6 +281,13 @@ Two bounds keep that sane: child-session adoption). It excludes js-debug's mandatory first target, which implements the root session and must still be adopted for launch to complete. Beyond the cap the release lever runs (the child runs undebugged) and the parent's next answer says so (`childrenReleased: n`). + The count is never left unexplained (issue #1137): the cause of each release is recorded with it + (`releasedChildCauses`: child sessions off, the cap, a proxy that cannot host children, a failed + adoption), and every launch, restart and wait answer that carries the number ends its message + with one sentence built from the same record (`describeReleasedChildren`, + `src/session/execution/tree-settle.ts`): how many, that they run undebugged, why — naming + `DEBUG_MCP_MAX_CHILD_SESSIONS` when it decided — and that a breakpoint only such a child reaches + will not hit. - Ended children stay listed until closed, as above, and leave with their parent. ## Every adapter, surveyed @@ -495,8 +507,12 @@ Two guardrails: Only roots have a clock: a tool call naming any session in a tree refreshes the tree's root, and a child never expires on a clock of its own — the tree goes with its root (decided 2026-11-09, amending the per-descendant clocks this section first proposed: a worker paused under a parent - the agent is driving must not be terminated by a timer). An in-flight `wait_for_stop` holds the - root for the duration of the call. The sweep is skipped entirely while a client is attending the + the agent is driving must not be terminated by a timer). A tool call in flight holds the root for + as long as it runs, whatever the tool, and the clock restarts when it answers (issue #1127: + the hold was `wait_for_stop`'s alone at first, and a launch slower than the window had its + session closed underneath it; `create_debug_session` with an attach target names no session + and holds the one it creates around its inline attach); a call that arrives while the sweep is + closing its tree waits for that close and is told what happened. The sweep is skipped entirely while a client is attending the process — any open legacy SSE stream or modern `subscriptions/listen` stream — so a connected client never loses a paused session to the clock; idle time still accrues from the last tool call, so a vanished client's already-idle sessions end at the first unattended sweep. Expiry @@ -578,8 +594,9 @@ Taken by the author of this document, marked so they can be overturned: child-session adoption, excluding js-debug's mandatory first target. - The idle policy knob is named `MCP_DEBUG_SESSION_IDLE_MS` (sweep: `MCP_DEBUG_SESSION_IDLE_SWEEP_MS`); it is off unless set (decided with JF, 2026-11-10). - Only roots have an idle clock: activity on any session in a tree refreshes its root, a child never - expires alone, a tree-aware wait holds the root for the duration of the call, and the sweep is - skipped while a client attends the process (decided with JF, 2026-11-09). + expires alone, a call in flight holds the root for as long as it runs (the tree-aware wait at + first, every tool that names a session since issue #1127), and the sweep is skipped while a + client attends the process (decided with JF, 2026-11-09). - The ordering rule for several settles before one wait (parent first, then adoption order). - Ended children stay listed until closed, and leave with their parent. - Closing any session closes its subtree, including when that session is itself a child. diff --git a/docs/tool-reference.md b/docs/tool-reference.md index 7e8637c20..d703aca58 100644 --- a/docs/tool-reference.md +++ b/docs/tool-reference.md @@ -87,7 +87,7 @@ Creates a new debugging session. **Notes:** - Session IDs are UUIDs in the format `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` - Sessions start in `"created"` state -- **One debugger core per process; the id is the capability (issue #1025).** On Streamable HTTP every MCP client of a process sees the same debug sessions: `list_debug_sessions` is the whole process's list, and any client holding an id can drive or close that session — run one `http` process per agent where agents must not see each other's work. A debug session outlives the MCP session that created it, so a one-shot script that opens a fresh MCP session per call reaches the sessions its previous run created, and a client that reconnects can resume or close what it left behind. A session ends by `close_debug_session`, by its program ending, or with the server: nothing expires on a timer by default. An operator can turn on the **idle policy** (`MCP_DEBUG_SESSION_IDLE_MS`, off by default on every transport): a session tree that no tool call has named for that long is then closed through the normal close cascade (a launched program is ended, an attached one released), unless a client is attending the process — while any client holds a stream open (a 2025 session's SSE stream, a 2026-07-28 client's `subscriptions/listen` stream), nothing expires; an in-flight `wait_for_stop` holds its tree; children never expire on a clock of their own. `list_debug_sessions` shows each session's `idleMs` and, with a policy on, `expiresInMs`; a later call naming a session the policy closed answers what happened, with `closedBy: "idle-policy"`. The server is built to scope sessions per principal once it verifies callers itself (no authenticated `http` mode ships yet; an authenticating reverse proxy does not identify callers to the server), so today every caller is in the one shared scope +- **One debugger core per process; the id is the capability (issue #1025).** On Streamable HTTP every MCP client of a process sees the same debug sessions: `list_debug_sessions` is the whole process's list, and any client holding an id can drive or close that session — run one `http` process per agent where agents must not see each other's work. A debug session outlives the MCP session that created it, so a one-shot script that opens a fresh MCP session per call reaches the sessions its previous run created, and a client that reconnects can resume or close what it left behind. A session ends by `close_debug_session`, by its program ending, or with the server: nothing expires on a timer by default. An operator can turn on the **idle policy** (`MCP_DEBUG_SESSION_IDLE_MS`, off by default on every transport): a session tree that no tool call has named for that long is then closed through the normal close cascade (a launched program is ended, an attached one released), unless a client is attending the process — while any client holds a stream open (a 2025 session's SSE stream, a 2026-07-28 client's `subscriptions/listen` stream), nothing expires; a tool call in flight holds the tree it names, whatever the tool, and the window starts over when it answers (issue #1127); children never expire on a clock of their own. `list_debug_sessions` shows each session's `idleMs` and, with a policy on, `expiresInMs`; a later call naming a session the policy closed answers what happened, with `closedBy: "idle-policy"`. The server is built to scope sessions per principal once it verifies callers itself (no authenticated `http` mode ships yet; an authenticating reverse proxy does not identify callers to the server), so today every caller is in the one shared scope - When `port`, `processId` or an `adapterConfig` URI/info-file key is provided in `create_debug_session`, the server performs an inline attach (creating the session and immediately attaching). The session exists before the addressing gate can refuse the form (a `port` on C/C++, say), so such a refusal answers `success: false` with a usable `sessionId`. The response then mirrors `attach_to_process`: alongside `sessionId` it carries `state`, the attach `data` payload, an optional `warning`, and — when a requested post-attach pause has not landed yet — `pending: true`, with `message` saying so (`…; post-attach pause pending — the target stops when it next executes code; call wait_for_stop to block until it does (pass stopOnEntry: false to attach without pausing)`) --- @@ -132,7 +132,7 @@ Lists all active debugging sessions — the whole process's list, whichever MCP Errored sessions include optional `diagnostics` with the current launch attempt's server-host `proxyLogPath` and remote-safe `proxyLogResource`. The record is retained for proxy initialization failures and for proxy/adapter deaths after initialization, and is cleared when a new launch or attach attempt begins. -**`idleMs`:** milliseconds since a tool call last named this session or any session in its tree (a child reports its root's) — what tells a forgotten session from a live one. **`expiresInMs`:** present only when the operator turned the idle policy on (`MCP_DEBUG_SESSION_IDLE_MS`): how long the tree has before the policy closes it; any tool call naming the session resets it, a `wait_for_stop` in flight holds it, and nothing expires while a client is attending the process. +**`idleMs`:** milliseconds since the last tool call naming this session, or any session in its tree, answered (a child reports its root's); `0` while such a call is still running — what tells a forgotten session from a live one. **`expiresInMs`:** present only when the operator turned the idle policy on (`MCP_DEBUG_SESSION_IDLE_MS`): how long the tree has before the policy closes it; any tool call naming the session resets it and holds it for as long as the call runs (a slow launch or a long `wait_for_stop` is never closed underneath itself, and the window starts over when it answers), and nothing expires while a client is attending the process. **`parentSessionId`:** present on child rows only. A child session is created by the server, never by `create_debug_session`, when the adapter reports a process the debuggee started: a Python launch or attach — debugpy's sub-process debugging is on by default; `dapLaunchArgs: { subProcess: false }` opts a launch out and an attach target decides for itself with `--configure-subProcess` — where debugpy reports each child, or a JavaScript launch or verified-local attach — js-debug's `autoAttachChildProcesses` is on by default for launch or an inspector target that can read both a temporary server-side probe file and the installed js-debug bootloader files; other attaches default it off with a warning; `dapLaunchArgs: { autoAttachChildProcesses: false }` opts a launch out, `adapterConfig: { autoAttachChildProcesses: false }` an attach — where js-debug reports every Node process the program forks or spawns, `cluster` workers and grandchildren included (see [the session tree](./architecture/session-tree.md)). The child inherits the parent's `language`, its `name` is the adapter's own label for the process (debugpy: `Subprocess `; js-debug: `