From 6d96819ccba45bd5b55b803a2f4c9483ed72b49e Mon Sep 17 00:00:00 2001 From: Julian Noble Date: Wed, 8 Jul 2026 04:34:19 +1000 Subject: [PATCH] goal detail housekeeping: embed trap script, record reopen_stdin races, note twapi drift G-036: the CloseHandle trap script is embedded in the detail file (was a dangling reference to session-ephemeral scratchpad). G-038: the repl::reopen_stdin race analysis from the G-036 investigation is written in (the prior cross-reference pointed at content that existed only in conversation), and the goal now explicitly owns that path's fate - it runs for real on console-EOF at an interactive prompt, and the caller-driven restart should cover that scenario so reopen_stdin can be retired. G-037: twapi vendorlib(5.0b1)/kit-vfs(5.2.0) drift recorded - divergence exists in both directions, so the sync step needs explicit authority semantics. Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com --- goals/G-036-tcl9-udp-console-worker-wedge.md | 14 ++++++++-- goals/G-037-vendorlib-vfs-propagation.md | 4 +++ goals/G-038-piped-session-continuity.md | 29 ++++++++++++++++++-- 3 files changed, 42 insertions(+), 5 deletions(-) diff --git a/goals/G-036-tcl9-udp-console-worker-wedge.md b/goals/G-036-tcl9-udp-console-worker-wedge.md index 223c6798..00f08644 100644 --- a/goals/G-036-tcl9-udp-console-worker-wedge.md +++ b/goals/G-036-tcl9-udp-console-worker-wedge.md @@ -218,8 +218,18 @@ Round-2 artifacts: %TEMP%/punk_wedge2.dmp + _stacks/_disasm/_h2 logs. ## Findings round 3: ROOT CAUSE - tcludp 1.0.12 UDP_ExitProc closes process-global events at thread exit (2026-07-08) Live-attach WinDbgX with a conditional breakpoint on KERNELBASE!CloseHandle for the -sockListLock handle value (read from the tcludp statics at attach; trap script listing in -scratchpad/notes) caught the closer red-handed: the CloseHandle is invoked from Tcl's +sockListLock handle value (read from the tcludp statics at attach) caught the closer +red-handed. Trap script (run via `WinDbgX -p -c "$$>`, +attached during a long-lived piped phase so the bp arms before the transition; statics at +tcl9udp1012+0x8800/+0x8818 for the 1.0.12 DLL; note `||` is not valid in .if conditions): + + r $t0 = poi(tcl9udp1012+0x8800) + r $t1 = poi(tcl9udp1012+0x8818) + .printf "tcludp sockListLock handle: %p waitForSock handle: %p\n", @$t0, @$t1 + bp KERNELBASE!CloseHandle ".if (@rcx == @$t0) {.printf \"*** CloseHandle on sockListLock %p ***\n\", @rcx; ~.; k 30; .logclose; qd} .else {gc}" + g + +It caught the closer: the CloseHandle is invoked from Tcl's thread-exit-handler walker in a dying Thread-extension worker, with no udp frame visible (tail-call). The fossil diff `tcludp-1_0_12 -> current` names it exactly - **tcludp 1.0.12's Windows per-thread exit handler**: diff --git a/goals/G-037-vendorlib-vfs-propagation.md b/goals/G-037-vendorlib-vfs-propagation.md index a23bf1ae..72ad6cb3 100644 --- a/goals/G-037-vendorlib-vfs-propagation.md +++ b/goals/G-037-vendorlib-vfs-propagation.md @@ -64,3 +64,7 @@ What the existing targets actually cover (verified in make.tcl at the time of wr motivating tcludp upgrade). - Interim state: tcl9 kits carry tcludp1.0.13 (hand-copied, commit a1b232dc); punk8win.vfs still carries udp1.0.12. +- Further drift evidence (2026-07-08): src/vendorlib_tcl9/win32-x86_64 carries twapi-5.0b1 + while the punk9win kit vfs bundles twapi 5.2.0 - vendorlib and kit vfs trees have already + diverged in BOTH directions (vendorlib newer for udp, older for twapi), so the sync step + needs explicit direction/authority semantics, not just "copy newer over older". diff --git a/goals/G-038-piped-session-continuity.md b/goals/G-038-piped-session-continuity.md index ef05065e..f97d2fab 100644 --- a/goals/G-038-piped-session-continuity.md +++ b/goals/G-038-piped-session-continuity.md @@ -23,9 +23,32 @@ 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. +reopened console channel. Its problem is structural, not conceptual, and this goal should +own that path's fate (retire it in favour of the caller-driven restart, or fix it), because +it DOES run in a real scenario: console EOF at an interactive prompt (ctrl-Z/ctrl-D style) +with tcl_interactive true. Latent races mapped during the G-036 investigation +(conversation-level analysis, recorded here since this goal's implementer needs it): + +- The repl EOF branch schedules TWO independent continuations: `after 1 repl::reopen_stdin` + (timer) and `after idle {set ::repl::done {eof }}` (idle). Which fires first is + nondeterministic (depends whether ~1ms elapses before the event loop drains to idle). +- reopen_stdin calls repl::start directly from inside the timer callback, i.e. NESTED + inside the still-active outer repl::start's vwait. Both frames vwait the same shared + `::repl::done` namespace variable - a single write (e.g. from exit/quit, or the stale + idle eof-write landing late) satisfies both traces and unwinds both frames. +- Both frames share the `codethread`/`codethread_cond` namespace variables: whichever + frame's teardown runs first cancels the codethread and empties the variables; the second + frame's teardown then runs `tsv::unset` on a missing array (caught, warning) and + `thread::cancel ""` (uncaught error). +- The reopened CONIN$ channel is adopted into the stdin std-channel slot, so the outer + frame's teardown `chan event stdin readable {}` strips the readable handler off the + nested repl's live input channel. +- The stale idle done-write can also be absorbed harmlessly during nested startup (sync + thread::send services events), making the whole ensemble timing-dependent. + +The caller-driven design eliminates the nesting (single repl::start frame at a time, done +state owned per invocation), and should cover the console-EOF restart as well as the +piped-EOF restart so reopen_stdin can be retired rather than left as a second, racy path. ## Approach