From f6efab7ecba9bd2d1eb6d5632618d528588dcfbe Mon Sep 17 00:00:00 2001 From: JF Date: Thu, 5 Nov 2026 19:15:32 -0500 Subject: [PATCH 1/6] feat(session-tree): a js-debug fork reported after the program is a child session (#1082) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit M2 of the session tree, first slice: with `autoAttachChildProcesses: true` every process js-debug reports after the program's own target — a fork, a spawn of process.execPath, a cluster worker, a grandchild — is adopted through the M1 mechanism as a child debug session instead of being released to run undebugged (#501). The rule: in a js-debug worker the root connection's first report is the program, adopted in-session by ChildSessionManager as before (the #704 launch-ready signal); every other report, on any connection, is a child report held by ChildConnections. Every non-root connection runs the child-safe policy, so no connection but the root ever builds a manager. - The adoption contract gains `readChildReport` (report recognition) and a root config for `buildChildAttachArguments`; js-debug pins `throwaway-attach` / `launch-session` / `release` with the measured attach body (`{ type, request: 'attach', __pendingTargetId, continueOnAttach }`, an attach root's forwardable extras riding along, #466). The js policy's `handleReverseRequest` adopts the first pending target in-session and hands every later one up as a child report, unanswered. - `createChildSafePolicy` moves to its own module: for a contract policy it reads every report through the contract and never adopts in-session; the manager's forward-to-release path goes with it. The program target's initialize advertises `supportsStartDebuggingRequest` too. - A fork is reported on the program target's connection: the manager relays the held request as `childReport`, naming that connection, and MinimalDapClient hands it to the root's holder (`child-report` now carries the origin client, which is where the answer goes). - The throwaway lever in ChildConnections: a js-debug report is acked the moment it is held (measured: the answer decides nothing; refusing parks the child forever) and never answered again; release opens a fresh child-safe connection and runs the #501 sequence (`src/proxy/throwaway-release.ts`: initialize → configurationDone → attach → ready signal → disconnect with the target kept, capped 20 s). A process the released child reports in that window is held on the session the released child was reported on. - `childOutput: 'launch-session'` gets its consumer: a js child's `get_output` and output resource answer `outputOn: ` with a note, since the tree's output arrives on the launch session's connection only. - `closeChild: 'release'` now shapes the close itself: a js child's connection closes with `terminateDebuggee: false` (measured no-op either way; the flag matches the answer `released: true`), in the root's cascade as well. - The contract test pins js-debug's contract and allows the one policy whose program is itself a pending target to combine it with in-session adoption, with per-engine report fixtures. `ChildSessionManager.releaseUndebugged` is unreachable for js-debug after this and stays as the manager's own invariant; its removal is a follow-up. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_015BDrn6AsHQ4rmTXFHzQkn8 --- .../src/interfaces/adapter-policy-js.ts | 154 ++++++++++---- .../src/interfaces/adapter-policy-python.ts | 3 +- .../shared/src/interfaces/adapter-policy.ts | 7 +- .../shared/src/interfaces/child-adoption.ts | 27 ++- src/proxy/child-connections.ts | 116 +++++++++-- src/proxy/child-safe-policy.ts | 68 ++++++ src/proxy/child-session-manager.ts | 78 ++----- src/proxy/minimal-dap.ts | 43 +++- src/proxy/throwaway-release.ts | 118 +++++++++++ src/server/handlers/output-tools.ts | 12 +- src/server/output-resources.ts | 8 +- src/session/session-manager-core.ts | 31 ++- .../server/server-inspection-tools.test.ts | 29 +++ tests/core/unit/server/server-test-helpers.ts | 2 + .../session-manager-child-sessions.test.ts | 58 +++++- tests/proxy/child-connections.test.ts | 196 +++++++++++++++++- tests/proxy/child-session-manager.test.ts | 89 ++++---- .../dap-proxy-worker-child-sessions.test.ts | 24 ++- .../dap-proxy-connection-manager.test.ts | 14 +- tests/unit/proxy/minimal-dap.test.ts | 28 ++- tests/unit/proxy/throwaway-release.test.ts | 93 +++++++++ .../shared/adapter-policy-contract.test.ts | 97 +++++++-- tests/unit/shared/adapter-policy-js.test.ts | 80 +++++++ 23 files changed, 1161 insertions(+), 214 deletions(-) create mode 100644 src/proxy/child-safe-policy.ts create mode 100644 src/proxy/throwaway-release.ts create mode 100644 tests/unit/proxy/throwaway-release.test.ts diff --git a/packages/shared/src/interfaces/adapter-policy-js.ts b/packages/shared/src/interfaces/adapter-policy-js.ts index 4b91a986e..501e56a8b 100644 --- a/packages/shared/src/interfaces/adapter-policy-js.ts +++ b/packages/shared/src/interfaces/adapter-policy-js.ts @@ -20,6 +20,7 @@ import type { StackFrame, Variable } from '../models/index.js'; import { toSourceBreakpoint, type BreakpointFields } from '../utils/to-source-breakpoint.js'; import { compileJsLogpoint, validateJsLogpoint } from '../utils/js-logpoint.js'; import type { DapClientBehavior, DapClientContext, ReverseRequestResult } from './dap-client-behavior.js'; +import type { ChildAdoptionContract, ChildReport } from './child-adoption.js'; /** * Every js-debug scope name this adapter knows, grouped by kind. @@ -119,6 +120,100 @@ function hasNoSource(filePath: string): boolean { return filePath === '' || filePath === ''; } +/** + * The attach a connection sends to adopt one of js-debug's pending targets — + * the program's own (the launch session's single in-session child) and, with + * the session tree, every fork after it (issue #1082). + */ +function buildJsChildStartArgs( + pendingId: string, + parentConfig: Record, + attachArguments?: Readonly> +): { command: 'launch' | 'attach'; args: Record } { + // js-debug's reverse-request values take precedence over parent extras. + // Our request/stopOnEntry intent never enters either adapter argument bag. + const config = { ...attachArguments, ...parentConfig }; + const type = typeof config.type === 'string' ? config.type : 'pwa-node'; + // Carry the parent's forwardable attach extras (localRoot/remoteRoot, + // sourceMaps, skipFiles, …) into the child — source resolution happens + // here, so this is where they take effect (issue #466). The orchestration + // keys stay pinned/excluded: address/port/attachSimplePort would make the + // child a second direct attach to the same inspector (the #124 + // fight-over-the-process failure). These are adapter connection fields, + // not a deny-list of our start-intent fields (issue #730). + const { + name: _name, + __pendingTargetId: _pendingTargetId, + host: _host, + address: _address, + port: _port, + attachSimplePort: _attachSimplePort, + attachExistingChildren: _attachExistingChildren, + continueOnAttach: _continueOnAttach, + type: _type, + ...parentExtras + } = config; + void _name; void _pendingTargetId; void _host; void _address; + void _port; void _attachSimplePort; void _attachExistingChildren; + void _continueOnAttach; void _type; + return { + command: 'attach', + args: { + ...parentExtras, + type, + request: 'attach', + __pendingTargetId: pendingId, + continueOnAttach: true // js-debug requires true to work properly + } + }; +} + +/** + * A js-debug process report (issue #1082): a `startDebugging` whose + * configuration carries a `__pendingTargetId` — the program itself on the + * root connection, a fork or a cluster worker on the program's connection, a + * grandchild on the fork's (docs/javascript/spike-notes-forks.md). The + * request says `request: "launch"` whatever the adoption is; `name` is + * `