You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

5.8 KiB

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' | <punkexe> 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 (it nests a second repl::start inside the dying repl's event stack, sharing ::repl::done/codethread state - see the G-036 detail file's early analysis of that path's races), not conceptual.

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.

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).