Browse Source

G-038 reworked (proposed): piped-to-interactive restart continues the same session

User identified the general least-surprise violation beyond the original
error-retention draft: `'set ::jjj blah' | punkexe` reaches an interactive
prompt where ::jjj does not exist - the piped-EOF restart silently swaps in
a fresh codethread/code interp, discarding variables, procs, packages, cwd
and errorInfo. Goal reworked to session continuity via the caller-driven
restart identified during G-036 (repl::start eof-restart done-mode without
codethread teardown; app-punkshell restarts on the reopened console without
repl::init). Error inspectability becomes an acceptance criterion that
likely falls out free (no re-init means no boot-noise re-stamping of
errorInfo); the standalone error-record mechanism is demoted to a recorded
fallback. Detail file renamed to match (slug change, ID preserved).

Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
master
Julian Noble 1 week ago
parent
commit
b134b514d1
  1. 10
      GOALS.md
  2. 56
      goals/G-038-piped-error-retention.md
  3. 82
      goals/G-038-piped-session-continuity.md

10
GOALS.md

@ -270,8 +270,8 @@ Detail: goals/G-037-vendorlib-vfs-propagation.md
Goal: platform-specific vendored binary packages under src/vendorlib_tcl<N>/<platform> reach the kit vfs lib_tcl<N> trees through a make.tcl step instead of hand-copying - updating a vendored package becomes a vendorlib drop plus standard build invocations (motivating case 2026-07-08: tcludp 1.0.12 -> 1.0.13 for the G-036 wedge fix - `libs`, `vfscommonupdate` and `project` all completed while every kit vfs still bundled udp 1.0.12, requiring a manual copy into each vfs lib_tcl9 folder).
Acceptance: with a newer package version placed under src/vendorlib_tcl9/<platform>, one documented make.tcl invocation updates the participating src/vfs/*/lib_tcl9 trees - installing the new package and removing or explicitly retiring the superseded version (no silent mixed-version provision, per the G-035 concerns) - with punkcheck-tracked provenance; which vfs folders participate is explicitly declared per kit rather than blanket-copied (kit vfs package sets may intentionally differ), with the declaration mechanism recorded (candidate home: the G-024 mapvfs toml); a subsequent `make.tcl project` yields kits loading the new version (provable via the tcludp case: built punk902z reports `package require udp` == 1.0.13 with no udp1.0.12 folder remaining in its vfs); the lib_tcl8 tree gets the same treatment or an explicit exclusion rationale in the goal record.
### G-038 [proposed] Piped-phase errors inspectable in the restarted interactive shell
Scope: src/lib/app-punkshell/punkshell.tcl (eof-restart handover), src/modules/punk/repl-999999.0a1.0.tm (error capture at the reporting site and/or teardown), src/modules/punk/repl/codethread-999999.0a1.0.tm (as needed for code-interp access)
Detail: goals/G-038-piped-error-retention.md
Goal: when piped stdin ends and app-punkshell restarts an interactive shell on the console, the last error from the piped phase is retained across the codethread/interp boundary and inspectable at the interactive prompt via a documented punk-owned record - not bare ::errorInfo, which boot-time internal caught errors (e.g. tcllib virtchannel's `catch {unset timer}`) clobber - with a one-line notice at the first prompt when such an error exists.
Acceptance: `'puts test;error xxxx' | <punkexe> shell` then, at the first interactive prompt, a documented command or variable yields the xxxx message, its errorInfo traceback and errorCode from the piped phase, and the first prompt is preceded by a one-line notice that a piped-phase error was retained; the retained record survives boot-time caught-error noise (proven against the motivating transcript, where plain `set errorInfo` showed virtchannel's `can't unset "timer"` instead); an error-free piped phase produces no notice and an explicitly-empty record; plain ::errorInfo semantics are otherwise unchanged; verified on both Tcl generations.
### G-038 [proposed] Piped-to-interactive restart continues the same session (context preserved)
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)
Detail: goals/G-038-piped-session-continuity.md
Goal: when piped stdin ends and app-punkshell opens the interactive console shell, the session continues rather than restarts - the piped phase's codethread/code interp survives (variables, procs, namespaces, loaded packages, cwd, ::errorInfo/::errorCode) and only the input channel and repl reader are renewed - so `'set ::jjj blah' | <punkexe> shell` leaves ::jjj inspectable at the prompt, replacing today's silent fresh-session swap (least-surprise violation; motivating transcripts in the detail file) and obsoleting the standalone piped-error-record mechanism this goal described before its 2026-07-08 rework.
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.

56
goals/G-038-piped-error-retention.md

@ -1,56 +0,0 @@
# G-038 Piped-phase errors inspectable in the restarted interactive shell
Status: proposed
Scope: src/lib/app-punkshell/punkshell.tcl (eof-restart handover), src/modules/punk/repl-999999.0a1.0.tm (error capture at the reporting site and/or teardown), src/modules/punk/repl/codethread-999999.0a1.0.tm (as needed for code-interp access)
Acceptance: `'puts test;error xxxx' | <punkexe> shell` then, at the first interactive prompt, a documented command or variable yields the xxxx message, its errorInfo traceback and errorCode from the piped phase, and the first prompt is preceded by a one-line notice that a piped-phase error was retained; the retained record survives boot-time caught-error noise (proven against the motivating transcript, where plain `set errorInfo` showed virtchannel's `can't unset "timer"` instead); an error-free piped phase produces no notice and an explicitly-empty record; plain ::errorInfo semantics are otherwise unchanged; verified on both Tcl generations.
## Context
Motivating transcript (user, 2026-07-08): `'puts test;error xxxx' | punk902z shell` - the
piped phase prints `test` then the xxxx error, EOF triggers app-punkshell's CONIN$ restart,
and at the interactive prompt `set errorInfo` shows an unrelated
`can't unset "timer": no such variable ... "unset timer"` instead of the xxxx traceback.
The user had hoped the piped error would be retained for interactive inspection.
Two mechanisms defeat the natural expectation:
1. **Restart boundary**: the restarted shell is a fresh `repl::init`/`repl::start` with a new
codethread and code interp (app-punkshell do_shell). The piped phase's ::errorInfo lives in
run-1's code interp, which is torn down with its codethread. Nothing carries it across.
2. **::errorInfo is boot-noise-prone anyway**: a *caught* error still stamps ::errorInfo.
tcllib's `virtchannel_core/events.tcl` (`tcl::chan::events`) runs `catch { unset timer }`
as ordinary control flow, and the repl-exec log tee construction (fifo2 stacks during
repl::init) triggers it in the fresh code interp - so a brand-new shell's resident
::errorInfo is whatever internal caught error happened last during boot. Restoring the
retained error into plain ::errorInfo would be clobbered by the same noise, hence the
punk-owned record in the goal statement (or injection strictly after boot completes -
but a dedicated record is robust and self-describing).
## Approach (design points)
- **Capture at the reporting site, not by scraping ::errorInfo at teardown**: the repl already
distinguishes command errors when it renders the tcl-err (lowercase `p%`) prompt path - that
is the natural place to record {message errorInfo errorCode command-string timestamp} for
the most recent errored command. Teardown-time capture of the code interp's ::errorInfo is
the fallback but inherits the noise problem above.
- **Handover**: the record must cross thread/interp teardown - candidates: tsv (already used
by the repl for runchunks), or app-punkshell capturing it from the run-1 exitinfo/donevalue
plumbing and seeding the run-2 code interp after init.
- **Surface**: a documented variable in the code interp (e.g. ::punk::last_pipeline_error) or
a small command; the first-prompt notice should be one line and only appear when a retained
error exists (piped runs that end cleanly stay quiet - agents/CI pipe scripts constantly).
- **Scope question to settle**: retain only across the piped->interactive restart, or also
expose "last error of the previous run" generally (subshell exits, `repl::start` restarts)?
The goal commits only to the piped->interactive case; wider retention can be recorded here
if it falls out naturally.
- Note the errorInfo-noise observation stands alone too: even without retention,
`set errorInfo` in a fresh punk shell reports internals' caught errors. Out of scope to
"fix" tcllib, but the dedicated-record design sidesteps it and the doc/help for the new
record can mention the caveat.
## Notes
- Related: G-036 detail (same eof-restart machinery, where the codethread/interp lifecycle is
mapped); G-015 (script subcommand is the agent-facing piped path and already reports errors
on stderr with honest exit codes - this goal is about the *shell* subcommand's interactive
restart, a human-facing convenience).

82
goals/G-038-piped-session-continuity.md

@ -0,0 +1,82 @@
# 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).
Loading…
Cancel
Save