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