# G-106 powershell console-mode fallback: maintained raw-mode path for twapi-less runtimes Status: achieved 2026-07-22 Scope: src/modules/punk/console-999999.0a1.0.tm (enableRaw_powershell/disableRaw_powershell + the persistent server lifecycle), scriptlib/utils/pwsh/ (consolemode_server_async.ps1 canonical + experiment-variant reconciliation), ps-script resolution/packaging across launch contexts (argv0-derived pstooldir; kits via the G-089 scriptlib-in-kits interplay), src/make.tcl shell (the verified launch context) Goal: the powershell console-mode fallback - a persistent pwsh/powershell named-pipe server driven by punk::console when twapi is absent - is a MAINTAINED, dependable raw-mode path for twapi-less runtimes (the suite-built shells and kits, until/unless zig-built twapi lands): quiet in normal operation, reliable across the session lifecycle, and resolvable from every supported launch shape rather than only argv0-relative luck. Acceptance: on a twapi-less suite runtime, raw enable/disable work via the fallback from (a) the make.tcl shell launch and (b) a kit / plain-tclsh repl launch, with no stderr noise in normal operation; the server lifecycle is verified - starts once per session, stays up for the session's raw transitions (the 2026-07-20 observed early-shutdown mode diagnosed and fixed), and shuts down with the session leaving no orphan pwsh processes; ps-script resolution no longer depends solely on argv0 parent-dir derivation (works from kits and unusual cwds, with the fallback-to-pwd branch replaced by something principled); scriptlib/utils/pwsh is reconciled to one canonical server script with the experiment variants retired or explicitly labelled; the fallback's role as the no-twapi contingency is documented where G-103's twapi investigation will find it, and a repeatable verification recipe is recorded in this file. ## Context User verification 2026-07-20 on the freshly built tclsh90spr (piperepl variant from G-096, achieved 2026-07-20): with `tcl::tm::add .../modules` + `package require repl; repl::init; repl::start stdin`, basic punk features are functional on the bare suite runtime - except raw mode, which is twapi-reliant. Launched instead via `./tclsh90spr .../src/make.tcl shell`, the powershell fallback engaged and raw mode WORKED - modulo consolemode_server_async.ps1 noise and an early server shutdown. That working-but-rough state is what this goal hardens. User direction: "we would like to look at maintaining this powershell fallback - especially if we can't get zig-based twapi builds going." Mechanism today (punk::console): when twapi is absent, enableRaw/disableRaw are aliased to *_powershell variants that talk over a named pipe to a persistent `consolemode_server_async.ps1` process (templated per-console id; pwsh.exe probed first, then powershell.exe). Known weak points at goal creation: - Resolution: pstooldir = argv0's grandparent + /scriptlib/utils/pwsh, else [pwd] - works for `make.tcl shell` (argv0 = src/make.tcl -> repo root) and little else by construction. Kits need the script carried/resolved properly (G-089 scriptlib in kits). - Lifecycle: observed early shutdown of the server mid-session; startup noise on stderr ("twapi not present, using persistent powershell process: ..." plus ps1 output). - Hygiene: scriptlib/utils/pwsh holds several sibling experiments (consolemode_server.ps1, *_async1.ps1, *_async.2ps1, consolemode.ps1, consolemode_enableraw.ps1) with no marking of which is canonical. Relationships: G-103 (bi-kit twapi under the zig-only policy is needs-investigation - THIS fallback is the mitigation; if zig-built twapi proves impractical the fallback becomes the primary raw path on suite runtimes and this goal's priority rises), G-013 (raw mode as the repl default ultimately requires raw mode to be dependable on twapi-less runtimes), G-089 (scriptlib in kits - the delivery vehicle for the ps script), G-061-era console-context findings (agent console-context traps around attached consoles apply to verifying this). ## Approach Design decisions of the 2026-07-22 overhaul (punk::console 0.8.0): - Root cause of the 2026-07-20 early shutdown - two compounding defects in the old server script: (1) a 20s keepalive killed the server when no ping arrived, but only `enableraw` messages refreshed the ping and NO tcl-side pinger ever existed, so the server always died ~20s after the last raw enable ("ping stale for pipe ... - exiting" was this kill's console noise); (2) the per-connection `StreamReader.Close()` disposed the underlying pipe stream, so the following `Disconnect()/Dispose()` threw and killed the listener runspace after the FIRST message - later toggles could not reach the server even while its process lived. - Keepalive replaced by a parent-process watch: the tcl side templates its pid into the script; the server holds a `System.Diagnostics.Process` object obtained once by that pid (handle-based, immune to pid reuse) and exits when `HasExited` reports the owner gone (<=5s poll cadence). Orphan prevention needs no cooperation from the tcl side - hard kills included. - Lazy singleton server: spawn moved from module load to first enableRaw/disableRaw use, so loading punk::console in piped/non-console or worker-thread contexts spawns nothing and prints nothing. Server identity lives in tsv `punk_console` (`ps_server_pipename`/`ps_server_pid`/`ps_server_spawntime`) under `tsv::lock`, so all interps/threads of a process share ONE server (the codethread and the repl thread previously would each have spawned their own). - Quiet by default: the server child is spawned `-nop -nol -noni` with stdout/stderr redirected to NUL (stdin MUST stay inherited - the console input handle is how the server reaches the console); all ps1 output is debug-gated. Env `PUNK_PS_CONSOLEMODE_DEBUG=1` enables diagnostics on both sides (tcl spawn note on stderr + unredirected ps1 write-host trail). Failure paths (server unreachable, spawn failure) still emit one actionable stderr line - that is not normal operation. - Resolution chain (`punk::console::system::ps_consolemode_script_get`): env `PUNK_PS_CONSOLEMODE_SCRIPT` override (used only when the file exists) -> argv0-derived project root -> module-location-derived roots (2-up for built `modules/`, 3-up for `src/modules/`; a zipfs kit module path just fails the probes) -> embedded copy of the script carried in the module (`ps_consolemode_script_embedded`). The `[pwd]` fallback is gone. The embedded copy makes kits and arbitrary cwds resolution-proof with no scriptlib delivery dependency (the G-089 interplay is thereby optional for THIS script: a resolvable scriptlib file wins - dev-editable - and the embedded text is the floor). - Delivery stays `-c