Browse Source

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
master
Julian Noble 2 weeks ago
parent
commit
6d96819ccb
  1. 14
      goals/G-036-tcl9-udp-console-worker-wedge.md
  2. 4
      goals/G-037-vendorlib-vfs-propagation.md
  3. 29
      goals/G-038-piped-session-continuity.md

14
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 <pid> -c "$$><script.txt" -logo <log>`,
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**:

4
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".

29
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 <chan>}}` (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

Loading…
Cancel
Save