Browse Source
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.commaster
3 changed files with 87 additions and 61 deletions
@ -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). |
||||
@ -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…
Reference in new issue