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 3 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) ## 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 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 sockListLock handle value (read from the tcludp statics at attach) caught the closer
scratchpad/notes) caught the closer red-handed: the CloseHandle is invoked from Tcl's 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 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 (tail-call). The fossil diff `tcludp-1_0_12 -> current` names it exactly - **tcludp 1.0.12's
Windows per-thread exit handler**: 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). motivating tcludp upgrade).
- Interim state: tcl9 kits carry tcludp1.0.13 (hand-copied, commit a1b232dc); punk8win.vfs - Interim state: tcl9 kits carry tcludp1.0.13 (hand-copied, commit a1b232dc); punk8win.vfs
still carries udp1.0.12. 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 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 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 reopened console channel. Its problem is structural, not conceptual, and this goal should
the dying repl's event stack, sharing ::repl::done/codethread state - see the G-036 detail own that path's fate (retire it in favour of the caller-driven restart, or fix it), because
file's early analysis of that path's races), not conceptual. 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 ## Approach

Loading…
Cancel
Save