diff --git a/src/modules/punk/libunknown-0.2.1.tm b/src/modules/punk/libunknown-0.2.1.tm index 6319daf0..c0d92a16 100644 --- a/src/modules/punk/libunknown-0.2.1.tm +++ b/src/modules/punk/libunknown-0.2.1.tm @@ -7,7 +7,7 @@ # (C) 2025 # # @@ Meta Begin -# Application punk::libunknown 0.2.0 +# Application punk::libunknown 0.2.1 # Meta platform tcl # Meta license MIT # @@ Meta End @@ -18,6 +18,14 @@ # from src/modules/AGENTS.md "Versioning And Releases" - bumping means # renaming the file AND updating the Meta line above, the manpage_begin line # below and the provide-block version, then appending a line here): +#0.2.1 - zipfs_tm_UnknownHandler: the zipfs static-path can_skip_update state is +# written keyed by the SPECIFIC searchpath actually scanned (tm path + +# package namespace subdir) to match its reader and libunknown::init's +# indexbase keying. It was written keyed by the tm-list root, so resolving +# any subdir tm (e.g punk::args) poisoned the ROOT's skip map and later +# root-level requires (e.g commandstack) were skipped as known-absent - +# breaking 'shell' on zipfs kits (single //zipfs:/app/modules tm path; +# filesystem tm paths never take the skip branch). Diagnosed 2026-07-20. #0.2.0 - register_all_tm: deep discovery proc registering ifneeded scripts for # .tm modules at every namespace depth (once per tm epoch, interp-local # tm_fullscan guard); used by 'dev lib.search' by default @@ -34,7 +42,7 @@ # doctools header # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ #*** !doctools -#[manpage_begin punkshell_module_punk::libunknown 0 0.2.0] +#[manpage_begin punkshell_module_punk::libunknown 0 0.2.1] #[copyright "2025"] #[titledesc {Module API}] [comment {-- Name section and table of contents description --}] #[moddesc {-}] [comment {-- Description at end of page heading --}] @@ -355,8 +363,10 @@ tcl::namespace::eval ::punk::libunknown { # the token-to-directory mapping may have changed. #JMN - review. - #dict set epoch tm epochs $pkg_epoch added $currentsearchpath $pkgname [dict create e $pkg_epoch v $pkgversion] - dict set epoch tm epochs $pkg_epoch added $currentsearchpath $pkgname $pkgversion e$pkg_epoch + #0.2.1: keyed by specificsearchpath (the subset actually scanned) - the + #can_skip_update reader and libunknown::init both key that way; keying by + #the tm-list root here let subdir scans poison root-level skip decisions. + dict set epoch tm epochs $pkg_epoch added $specificsearchpath $pkgname $pkgversion e$pkg_epoch if {$must_scan} { #however - if we know we're forced to scan all tm paths we can remove discovered sibling tms from tm untracked dict unset epoch tm untracked $pkgname @@ -389,8 +399,9 @@ tcl::namespace::eval ::punk::libunknown { #JMN #store only once for each name, although there may be multiple versions of same package within this searchpath - #dict set epoch tm epochs $pkg_epoch added $currentsearchpath $pkgname [dict create e $pkg_epoch v $pkgversion] - dict set epoch tm epochs $pkg_epoch added $currentsearchpath $pkgname $pkgversion e$pkg_epoch + #0.2.1: keyed by specificsearchpath - see the matching note at the + #already-ifneeded branch above. + dict set epoch tm epochs $pkg_epoch added $specificsearchpath $pkgname $pkgversion e$pkg_epoch #pkgname here could be the 'name' passed at the beggning - or other .tms at the same location. #we can't always remove other .tms from 'tm untracked' because the search for name might skip some locations. if {$must_scan} { @@ -2119,7 +2130,7 @@ namespace eval ::punk::args::register { package provide punk::libunknown [tcl::namespace::eval ::punk::libunknown { variable pkg punk::libunknown variable version - set version 0.2.0 + set version 0.2.1 }] return diff --git a/src/vfs/_vfscommon.vfs/modules/punk/config-0.2.0.tm b/src/vfs/_vfscommon.vfs/modules/punk/config-0.2.0.tm index 03d42b82..57ad3a41 100644 --- a/src/vfs/_vfscommon.vfs/modules/punk/config-0.2.0.tm +++ b/src/vfs/_vfscommon.vfs/modules/punk/config-0.2.0.tm @@ -275,6 +275,7 @@ tcl::namespace::eval punk::config { PUNK_APPS {type pathlist} PUNK_CONFIG {type string} PUNK_CONFIGSET {type string} + PUNK_FOSSIL_STORE {type string help "location of the punkshell-owned fossil clone store\n(source mirrors for buildsuites - e.g src/buildsuites/suite_tcl90/suite.tcl).\nDefault: ~/.punkshell/fossils (machine-level cache, outside deletable _build areas).\nMissing clones are seeded read-only from ~/.fossils when present, else network-cloned."} PUNK_SCRIPTLIB {type string} PUNK_AUTO_EXEC_MECHANISM {type string} PUNK_AUTO_NOEXEC {type string default 0 help "set 1 to set Tcl's ::auto_noexec true.\nStops 'unknown' from running external programs"} diff --git a/src/vfs/_vfscommon.vfs/modules/punk/lib-0.5.0.tm b/src/vfs/_vfscommon.vfs/modules/punk/lib-0.5.0.tm index 432617c0..24576228 100644 --- a/src/vfs/_vfscommon.vfs/modules/punk/lib-0.5.0.tm +++ b/src/vfs/_vfscommon.vfs/modules/punk/lib-0.5.0.tm @@ -252,11 +252,14 @@ tcl::namespace::eval punk::lib::check { #G-076: version gate for the tcl9 dead-console defect (upstream ticket f10d91c2d3, root-caused #in G-039). Shared by the 'help tcl' warning and the repl dead-console watchdog arming (both - #consult has_tclbug_console_deadspin). Empty = no released Tcl is known to contain the upstream - #fix, so every Tcl 9 windows runtime classifies as affected. Set this only after the G-039 kill - #procedure, re-run on the fixed released runtime with the watchdog disabled, shows a clean - #script-visible eof exit (see goals/G-076-tcl9-deadconsole-fix-adoption.md). - variable tclbug_console_deadspin_fixed_in "" + #consult has_tclbug_console_deadspin). Empty would mean no Tcl is known to contain the upstream + #fix (every Tcl 9 windows runtime classifies as affected). Set to 9.0.5 2026-07-20: the upstream + #fix (ticket closed 2026-07-14; merged to core-9-0-branch, whose head reports 9.0.5) passed the + #G-039 kill procedure on a zig-built core-9-0-branch runtime with the watchdog disarmed - the + #dead console arrived as a readable fileevent and the repl exited via the normal eof path + #(requires the punk::repl 0.5.2 repl_handler guard; an official release is not required per the + #amended acceptance - see goals/archive/G-076-tcl9-deadconsole-fix-adoption.md 2026-07-20 entries). + variable tclbug_console_deadspin_fixed_in "9.0.5" #pure classifier, separated for testability - facts in, verdict out proc tclbug_console_deadspin_applies {platform tclversion fixed_in} { @@ -8412,7 +8415,9 @@ namespace eval punk::lib { if {$do_libunknown} { if {[info exists ::punk::libunknown::epoch]} { append script "namespace eval ::punk::libunknown {}\n" - append script "set ::punk::libunknown::epoch [list $::punk::libunknown::epoch]\n" + #reduced copy - a verbatim copy carries 'added' short-circuit state that is + #only sound in the donor interp (see libunknown_epoch_export) + append script "set ::punk::libunknown::epoch [list [libunknown_epoch_export]]\n" } #find and source libunknown, then call init append script { @@ -8538,7 +8543,9 @@ namespace eval punk::lib { if {$do_libunknown} { if {[info exists ::punk::libunknown::epoch]} { interp eval $interp {namespace eval ::punk::libunknown {}} - interp eval $interp [list set ::punk::libunknown::epoch $::punk::libunknown::epoch] + #reduced copy - a verbatim copy carries 'added' short-circuit state that is + #only sound in the donor interp (see libunknown_epoch_export) + interp eval $interp [list set ::punk::libunknown::epoch [libunknown_epoch_export]] } #use snapshot's libunknown sourcing logic via interp eval set libunknown_script [punk::lib::snapshot_package_paths -libunknown 1] @@ -8578,6 +8585,49 @@ namespace eval punk::lib { } } + namespace eval argdoc { + variable PUNKARGS + lappend PUNKARGS [list { + @id -id ::punk::lib::libunknown_epoch_export + @cmd -name punk::lib::libunknown_epoch_export\ + -summary\ + "Reduced copy of the punk::libunknown epoch record, safe to seed into a fresh interp or thread."\ + -help\ + "Returns ::punk::libunknown::epoch with every per-epoch 'added' map emptied. + + The 'added' maps record which package names an interp's own scans registered from + each searchpath during each epoch - zipfs_tclPkgUnknown / zipfs_tm_UnknownHandler + consult the CURRENT epoch's map to skip re-sourcing a searchpath's indexes for a + name the path didn't provide before. That short-circuit is only sound in the interp + that performed the scans: packages the donor obtained at an EARLIER epoch, or with + no package unknown call at all (e.g Thread baked into a thread::create'd thread), + are absent from the donor's current-epoch map, so a fresh interp inheriting the + record verbatim skips their pkgIndex/tm sourcing and fails requires that stock + Tcl's tclPkgUnknown would satisfy (G-098 tkconsole diagnosis 2026-07-20). + + Epoch counters, untracked markers, ifneeded-script records and index-glob caches + are preserved; the recipient performs its own first sourcing pass per searchpath + and accumulates its own 'added' state. Errors if ::punk::libunknown::epoch is not + set - callers that tolerate an uninitialised libunknown should guard with + info exists as they would for the raw variable." + @values -min 0 -max 0 + }] + } + proc libunknown_epoch_export {} { + if {![info exists ::punk::libunknown::epoch]} { + error "punk::lib::libunknown_epoch_export - ::punk::libunknown::epoch not set (punk::libunknown not initialised in this interp?)" + } + set e $::punk::libunknown::epoch + foreach domain {tm pkg} { + if {![dict exists $e $domain epochs]} {continue} + dict for {ep state} [dict get $e $domain epochs] { + dict set state added [dict create] + dict set e $domain epochs $ep $state + } + } + return $e + } + proc valcopy {obj} { append obj2 $obj {} } diff --git a/src/vfs/_vfscommon.vfs/modules/punk/libunknown-0.2.0.tm b/src/vfs/_vfscommon.vfs/modules/punk/libunknown-0.2.1.tm similarity index 98% rename from src/vfs/_vfscommon.vfs/modules/punk/libunknown-0.2.0.tm rename to src/vfs/_vfscommon.vfs/modules/punk/libunknown-0.2.1.tm index 6319daf0..c0d92a16 100644 --- a/src/vfs/_vfscommon.vfs/modules/punk/libunknown-0.2.0.tm +++ b/src/vfs/_vfscommon.vfs/modules/punk/libunknown-0.2.1.tm @@ -7,7 +7,7 @@ # (C) 2025 # # @@ Meta Begin -# Application punk::libunknown 0.2.0 +# Application punk::libunknown 0.2.1 # Meta platform tcl # Meta license MIT # @@ Meta End @@ -18,6 +18,14 @@ # from src/modules/AGENTS.md "Versioning And Releases" - bumping means # renaming the file AND updating the Meta line above, the manpage_begin line # below and the provide-block version, then appending a line here): +#0.2.1 - zipfs_tm_UnknownHandler: the zipfs static-path can_skip_update state is +# written keyed by the SPECIFIC searchpath actually scanned (tm path + +# package namespace subdir) to match its reader and libunknown::init's +# indexbase keying. It was written keyed by the tm-list root, so resolving +# any subdir tm (e.g punk::args) poisoned the ROOT's skip map and later +# root-level requires (e.g commandstack) were skipped as known-absent - +# breaking 'shell' on zipfs kits (single //zipfs:/app/modules tm path; +# filesystem tm paths never take the skip branch). Diagnosed 2026-07-20. #0.2.0 - register_all_tm: deep discovery proc registering ifneeded scripts for # .tm modules at every namespace depth (once per tm epoch, interp-local # tm_fullscan guard); used by 'dev lib.search' by default @@ -34,7 +42,7 @@ # doctools header # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ #*** !doctools -#[manpage_begin punkshell_module_punk::libunknown 0 0.2.0] +#[manpage_begin punkshell_module_punk::libunknown 0 0.2.1] #[copyright "2025"] #[titledesc {Module API}] [comment {-- Name section and table of contents description --}] #[moddesc {-}] [comment {-- Description at end of page heading --}] @@ -355,8 +363,10 @@ tcl::namespace::eval ::punk::libunknown { # the token-to-directory mapping may have changed. #JMN - review. - #dict set epoch tm epochs $pkg_epoch added $currentsearchpath $pkgname [dict create e $pkg_epoch v $pkgversion] - dict set epoch tm epochs $pkg_epoch added $currentsearchpath $pkgname $pkgversion e$pkg_epoch + #0.2.1: keyed by specificsearchpath (the subset actually scanned) - the + #can_skip_update reader and libunknown::init both key that way; keying by + #the tm-list root here let subdir scans poison root-level skip decisions. + dict set epoch tm epochs $pkg_epoch added $specificsearchpath $pkgname $pkgversion e$pkg_epoch if {$must_scan} { #however - if we know we're forced to scan all tm paths we can remove discovered sibling tms from tm untracked dict unset epoch tm untracked $pkgname @@ -389,8 +399,9 @@ tcl::namespace::eval ::punk::libunknown { #JMN #store only once for each name, although there may be multiple versions of same package within this searchpath - #dict set epoch tm epochs $pkg_epoch added $currentsearchpath $pkgname [dict create e $pkg_epoch v $pkgversion] - dict set epoch tm epochs $pkg_epoch added $currentsearchpath $pkgname $pkgversion e$pkg_epoch + #0.2.1: keyed by specificsearchpath - see the matching note at the + #already-ifneeded branch above. + dict set epoch tm epochs $pkg_epoch added $specificsearchpath $pkgname $pkgversion e$pkg_epoch #pkgname here could be the 'name' passed at the beggning - or other .tms at the same location. #we can't always remove other .tms from 'tm untracked' because the search for name might skip some locations. if {$must_scan} { @@ -2119,7 +2130,7 @@ namespace eval ::punk::args::register { package provide punk::libunknown [tcl::namespace::eval ::punk::libunknown { variable pkg punk::libunknown variable version - set version 0.2.0 + set version 0.2.1 }] return diff --git a/src/vfs/_vfscommon.vfs/modules/punk/repl-0.5.2.tm b/src/vfs/_vfscommon.vfs/modules/punk/repl-0.5.2.tm index f63942fa..8968a81c 100644 --- a/src/vfs/_vfscommon.vfs/modules/punk/repl-0.5.2.tm +++ b/src/vfs/_vfscommon.vfs/modules/punk/repl-0.5.2.tm @@ -1012,20 +1012,32 @@ proc repl::console_watchdog {inputchan} { } if {[catch {chan configure $inputchan -inputmode}]} { #GetConsoleMode failed - the hosting console is gone. - #Close the channel (stops the tclWinConsole.c reader thread's error busy-loop) - #and finish the repl via the normal eof path. Do not attempt a console reopen: - #with the console dead, CONIN$ cannot be opened either (app-punkshell's eof - #handling makes the same discovery and exits cleanly). - array unset console_watchdog_afterids $inputchan - catch {chan event $inputchan readable {}} - catch {chan close $inputchan} - catch {puts stderr "|repl> console_watchdog: console unavailable for '$inputchan' - closing input and finishing repl (eof)"} - set ::repl::done [list eof $inputchan] + console_input_lost $inputchan "console_watchdog: console unavailable for '$inputchan' - closing input and finishing repl (eof)" return } set console_watchdog_afterids($inputchan) [after $console_watchdog_ms [list [namespace current]::console_watchdog $inputchan]] } +proc repl::console_input_lost {inputchan msg} { + #Shared terminal path for a dead/unusable console input channel (G-039/G-076), + #used by console_watchdog (polling rescue on defect-carrying tcl9 runtimes) and by + #repl_handler (runtimes carrying the upstream f10d91c2d3 fix deliver the dead + #console as a readable event instead). + #Close the channel (stops the tclWinConsole.c reader thread's error busy-loop) + #and finish the repl via the normal eof done-path. Do not attempt a console reopen: + #with the console dead, CONIN$ cannot be opened either (app-punkshell's eof + #handling makes the same discovery and exits cleanly). + variable console_watchdog_afterids + if {[info exists console_watchdog_afterids($inputchan)]} { + catch {after cancel $console_watchdog_afterids($inputchan)} + array unset console_watchdog_afterids $inputchan + } + catch {chan event $inputchan readable {}} + catch {chan close $inputchan} + catch {puts stderr "|repl> $msg"} + set ::repl::done [list eof $inputchan] +} + namespace eval repl::argdoc { lappend PUNKARGS [list { @id -id ::repl::console_get_size @@ -1852,7 +1864,18 @@ proc repl::repl_handler {inputchan readmore prompt_config} { #According to DKF - -buffering option doesn't affect input channels set rawmode 0 set waiting_needs_reader 0 ;#set when the 8.6-console waiting-chunks path skips its read on a drained channel - the readable handler must be armed (not the after-idle reinvoke) or the pending partial line could never complete - set original_input_conf [chan configure $inputchan] ;#whether repl is in line or raw mode - we restore the inputchan (stdin) state + if {[catch {chan configure $inputchan} original_input_conf]} { + #A runtime carrying the upstream tcl9 dead-console fix (f10d91c2d3) DELIVERS a + #dead console as a readable event, and this full option probe then errors + #(e.g 'couldn't read console mode: broken pipe'). Pre-fix runtimes never notify + #script level of a dead console (the console_watchdog poll covers those), so + #reaching here with a dead console requires the fixed driver. Terminal input + #loss - same teardown as the watchdog, no reopen attempt. + set in_repl_handler [list] + console_input_lost $inputchan "repl_handler: input channel unusable ($original_input_conf) - closing input and finishing repl (eof)" + return + } + #original_input_conf: whether repl is in line or raw mode - we restore the inputchan (stdin) state if {[dict exists $original_input_conf -inputmode]} { #review - when terminal is mintty and we switch to raw mode, the change isn't relected in chan configures '-inputmode' why? @@ -2923,11 +2946,19 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config #to the selected console's channels (rputs maps stdout/stderr to conout/conerr). #Interleaving between the two streams within a run is not preserved (single #terminal stream; per-console err semantics are G-011). + #The vars are guarded: if the code-interp init aborted before installing the + #junction (e.g a package resolution failure) they don't exist - treat as no + #pending output rather than erroring here, which would leave the repl reader + #disabled and wedge the session (G-098 tkconsole diagnosis 2026-07-20). set pending [thread::send $codethread { interp eval code { - set _pendinglist [list $::codeinterp::console_pending_out $::codeinterp::console_pending_err] - set ::codeinterp::console_pending_out "" - set ::codeinterp::console_pending_err "" + if {[info exists ::codeinterp::console_pending_out]} { + set _pendinglist [list $::codeinterp::console_pending_out $::codeinterp::console_pending_err] + set ::codeinterp::console_pending_out "" + set ::codeinterp::console_pending_err "" + } else { + set _pendinglist [list "" ""] + } set _pendinglist } }] @@ -3517,7 +3548,7 @@ namespace eval repl { } %replthread_interp% [list $opt_callback_interp] {*}{ } %tmlist% [list [tcl::tm::list]] {*}{ } %autopath% [list $::auto_path] {*}{ - } %lib_epoch% [list $::punk::libunknown::epoch] {*}{ + } %lib_epoch% [list [punk::lib::libunknown_epoch_export]] {*}{ } %packageprefer% [list [package prefer]] {*}{ } %staticprefixes% [list [expr {[info exists ::punkboot::static_prefixes] ? $::punkboot::static_prefixes : ""}]] {*}{ } %staticpackages% [list [expr {[info exists ::punkboot::static_packages] ? $::punkboot::static_packages : ""}]] {*}{ @@ -3923,7 +3954,9 @@ namespace eval repl { catch {punk::lib::interp_sync_package_paths code} code eval [list namespace eval ::punk::libunknown {}] catch { - code eval [list set ::punk::libunknown::epoch $::punk::libunknown::epoch] + #reduced copy - 'added' short-circuit state is only sound in the donor + #interp (see punk::lib::libunknown_epoch_export) + code eval [list set ::punk::libunknown::epoch [punk::lib::libunknown_epoch_export]] } } safebase {