# G-038 Piped-to-interactive restart continues the same session (context preserved) Status: proposed Scope: src/lib/app-punkshell/punkshell.tcl (eof-restart handover), src/modules/punk/repl-999999.0a1.0.tm (eof-restart done-mode that skips codethread teardown), src/modules/punk/repl/codethread-999999.0a1.0.tm (as touched) Acceptance: after `'set ::jjj blah;error xxxx' | shell` reaches the interactive prompt: `set ::jjj` returns blah; the xxxx message, errorInfo traceback and errorCode are inspectable (via preserved ::errorInfo if nothing in the handover overwrites it, else a documented record - the chosen mechanism noted in the detail file); a proc defined and a package required during the piped phase remain available; a cwd change from the piped phase persists; a one-line notice at the restart says the session continued from piped input (mentioning the error when the last piped command errored, quiet otherwise); interactive exit/quit teardown afterwards is clean (the G-036 regression harness still passes); PUNK_PIPE_EOF policy semantics and the script subcommand are unchanged; verified on both Tcl generations. ## Context Reworked 2026-07-08 from a narrower draft ("piped-phase errors inspectable in the restarted interactive shell") after the user identified the general least-surprise violation: - `'set ::jjj blah' | punk902z` -> interactive prompt -> `set ::jjj` fails: no such variable. - `'puts test;error xxxx' | punk902z shell` -> `set errorInfo` at the prompt shows unrelated boot noise (`can't unset "timer"` from tcllib virtchannel_core's `catch { unset timer }`, stamped during the fresh repl::init's fifo2 log-tee construction) instead of xxxx. Cause: app-punkshell's piped-EOF handling (the path that actually runs for pipes, since a pipe is not tcl_interactive) rebuilds the shell from scratch - `do_shell` runs a fresh `repl::init` (new codethread + code interp) and `repl::start`. Everything the piped phase did in the code interp - variables, procs, namespaces, package requires, cwd, errorInfo - is silently discarded. The user's mental model is "my session continues interactively", i.e. the same expectation a tclsh user has typing those commands then continuing. Precedent in the codebase: the repl-internal restart path (`repl::reopen_stdin`) is context-preserving by design - it reuses the live codethread and restarts the repl on the reopened console channel. Its problem is structural, not conceptual, and this goal should own that path's fate (retire it in favour of the caller-driven restart, or fix it), because it DOES run in a real scenario: console EOF at an interactive prompt (ctrl-Z/ctrl-D style) with tcl_interactive true. Latent races mapped during the G-036 investigation (conversation-level analysis, recorded here since this goal's implementer needs it): - The repl EOF branch schedules TWO independent continuations: `after 1 repl::reopen_stdin` (timer) and `after idle {set ::repl::done {eof }}` (idle). Which fires first is nondeterministic (depends whether ~1ms elapses before the event loop drains to idle). - reopen_stdin calls repl::start directly from inside the timer callback, i.e. NESTED inside the still-active outer repl::start's vwait. Both frames vwait the same shared `::repl::done` namespace variable - a single write (e.g. from exit/quit, or the stale idle eof-write landing late) satisfies both traces and unwinds both frames. - Both frames share the `codethread`/`codethread_cond` namespace variables: whichever frame's teardown runs first cancels the codethread and empties the variables; the second frame's teardown then runs `tsv::unset` on a missing array (caught, warning) and `thread::cancel ""` (uncaught error). - The reopened CONIN$ channel is adopted into the stdin std-channel slot, so the outer frame's teardown `chan event stdin readable {}` strips the readable handler off the nested repl's live input channel. - The stale idle done-write can also be absorbed harmlessly during nested startup (sync thread::send services events), making the whole ensemble timing-dependent. The caller-driven design eliminates the nesting (single repl::start frame at a time, done state owned per invocation), and should cover the console-EOF restart as well as the piped-EOF restart so reopen_stdin can be retired rather than left as a second, racy path. ## Approach The caller-driven restart already identified during G-036 as the clean fix direction: 1. `repl::start` gains an eof-restart outcome that returns WITHOUT tearing down the codethread (today's teardown at the end of repl::start unconditionally cancels it - the exit/quit/eof done-values must branch). 2. app-punkshell's eof branch, on deciding to go interactive (existing PUNK_PIPE_EOF policy unchanged), opens CONIN$/SetStdHandle as today but then calls `repl::start` again on the new channel WITHOUT `repl::init` - same codethread, same code interp, session continues. (do_shell currently bakes `repl::init` into the script it runs - the restart invocation needs a variant.) 3. What renews vs persists: - renewed: input channel + repl reader, tcl_interactive, prompt state; the anchored default console object must be rebound to the new channels (known stale-after-reopen comment at repl-999999 ~line 3168; adjacent to G-001/G-011 console-object work). - persists: everything in the codethread/code interp, including `after` timers and fileevents registered by the piped script (a feature, but note it as a behaviour change - a piped script's scheduled work now keeps running into the interactive session; the notice line mitigates surprise). - decide: repl-side editbuf/history fresh or carried (fresh is acceptable; record the choice). 4. Error inspectability likely falls out free: with no re-init there is no boot-noise re-stamping, so the piped phase's ::errorInfo is simply still present. If some handover step does overwrite it, fall back to the superseded draft's mechanism: capture at the repl's error-reporting site (the lowercase p% prompt path) into a punk-owned record (e.g. ::punk::last_pipeline_error) surfaced with the notice line. Note regardless: bare ::errorInfo in a long-lived shell is always subject to later caught-error stamping by internals - the notice line should show the error message itself so the user need not race to inspect. Subshell continuity (user note 2026-07-08, support if practical - not in acceptance): once per-subshell configurations exist (G-008/G-009), a user might reasonably run `'subshell ' | ` and expect the interactive prompt to be IN that subshell, with `quit` unwinding to the initial shell. This does NOT fall out of this goal's changes automatically: continuity preserves code-interp state, but the subshell stack is repl control flow - under the current synchronous nested model the piped `subshell` command nests a repl on the same stdin, and pipe-EOF unwinds the whole subshell stack before the restart decision is made, so the prompt would land in the top shell (with the subshell's interp-side effects intact but the subshell exited). Options: (a) within this goal - capture the active subshell stack at EOF and re-enter it on restart (fights the nesting model); (b) the natural home - G-002's non-nested subshell architecture with console targeting, where pipe-EOF becomes "reattach the reopened console to the innermost active subshell" and quit unwinds normally. Record the outcome here; if deferred to G-002, this note is the cross-reference. Related idea (separate concern, launcher surface): a `-subshell ` flag on the `shell` subcommand as the primary way to start inside a named subshell (candidate for the G-032 punk::args launcher declarations when subshell configs exist). Risks / interactions: - The teardown-mode branching touches exactly the machinery mapped during G-036 (shellthread worker lifecycle, shellfilter::run tail, thread teardown ordering) - the G-036 regression harness is the safety net and is in this goal's acceptance. - shellfilter::run/do_shell wrap the repl in tee stacks per invocation; the restart variant must not double-stack or leak them across the handover. ## Alternatives considered - Standalone piped-error record without session continuity (this goal's original draft) - superseded by the rework: it fixed one symptom (error inspection) while leaving the general context loss (::jjj case) in place, and continuity likely obviates the record. - Keeping fresh-session semantics but documenting them + printing a "new session" notice - rejected as the primary outcome (documentation doesn't restore the lost work), though the notice-line idea is retained within the continuity design. ## Notes - Related: G-036 detail (restart machinery map, teardown races, regression harness); G-002 (non-nested subshell architecture - same family of repl lifecycle decoupling); G-031 (componentized boot); G-001/G-011 (console object rebinding on reopen); G-015 (script subcommand unaffected - it never drops to interactive).