--- d2/history/fp-driver-d2-2bd12fc8.sh 2026-09-12 19:30:49.596288700 -0700 +++ d2/fp-driver-d2.sh 2026-09-12 19:20:44.157393000 -0700 @@ -177,6 +177,9 @@ SETUP_END_UTC=UNRECORDED PRE_SETUP_ABSENCE=UNRECORDED RESIDUE_COUNT=UNMEASURED +RESIDUE_LIVE=0 # tracked identities re-queried and still running +RESIDUE_GONE=0 # tracked identities measured absent (or pid-reused) +RESIDUE_UNREADABLE=0 # attribution missing or unreadable: NEVER read as clean DAEMON_STOP_REFUSED=0 # IR-122's expected refusal, recorded with its native exit DAEMON_STOP_RC=NOT_RUN # the refused command's OWN exit, preserved in every case LISTENER_STATE=UNMEASURED # unread is read as LIVE: an unread port is not an empty one @@ -644,6 +647,59 @@ fi } +# ---- run-bound process identity, captured WHILE THE PROCESSES ARE LIVE ---------- +# Cleanup can only re-query an identity somebody wrote down. This is where it is written +# down, immediately after the elevated leg that starts the daemon, and it is the ONLY basis +# on which cleanup later says a process of this run is gone. +# +# WHY NOT A COMMAND-LINE MATCH (doyle, 2026-09-13): r10's actual survivors ran +# `daemon run --detached` and `daemon brain --generation 0 --start-reason cold` -- neither +# carries the isolated home, so a home-string predicate reports a confident CLEAR over exactly +# the processes that were left behind. The pid FILE names the supervisor, the process TABLE +# gives it a creation time, an executable path and a command line, and the parent relationship +# gives the brain. Those are what get re-queried. +# +# It records, and records nothing it could not read. A missing pid file, a non-numeric pid, an +# unreadable table: each is written as IDENT_PROBE=UNREADABLE with its reason, and cleanup reads +# that as UNREADABLE rather than as an empty list. +capture_run_identities() { # WHEN + local when="$1" + SPT_D2_HOME="$H" pwsh -NoProfile -Command ' + $h = $env:SPT_D2_HOME + $pidFile = Join-Path $h "daemon.pid" + if (-not (Test-Path $pidFile)) { "IDENT_PROBE=UNREADABLE reason=no-pid-file"; exit 0 } + $sp = (Get-Content -Raw $pidFile).Trim() + if ($sp -notmatch "^[0-9]+$") { "IDENT_PROBE=UNREADABLE reason=pid-file-not-numeric"; exit 0 } + try { $all = @(Get-CimInstance Win32_Process -ErrorAction Stop) } + catch { "IDENT_PROBE=UNREADABLE reason=enumeration-failed"; exit 0 } + if ($all.Count -eq 0) { "IDENT_PROBE=UNREADABLE reason=empty-enumeration-is-not-an-empty-host"; exit 0 } + "IDENT_ENUM total=$($all.Count) supervisor_pid_from_file=$sp" + $target = [int]$sp + $sup = @($all | Where-Object { $_.ProcessId -eq $target }) + if ($sup.Count -eq 0) { "IDENT_PROBE=UNREADABLE reason=supervisor-pid-$sp-not-in-process-table"; exit 0 } + $rows = @($sup[0]) + @($all | Where-Object { $_.ParentProcessId -eq $target }) + $i = 0 + foreach ($p in $rows) { + $role = if ($i -eq 0) { "supervisor" } else { "brain" } + $c = try { $p.CreationDate.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ") } catch { "UNREADABLE" } + "IDENT role=$role pid=$($p.ProcessId) created=$c exe=[$($p.ExecutablePath)] cmd=[$($p.CommandLine)]" + $i++ + } + "IDENT_PROBE=OK tracked=$($rows.Count)" + ' > "$R/run-identities.txt" 2>&1 + local rc=$? + exits "identity_capture_${when}_EXIT=$rc" + while IFS= read -r line; do record " IDENTITY($when) $line"; done < "$R/run-identities.txt" + local n + n=$(grep -c '^IDENT role=' "$R/run-identities.txt" 2>/dev/null) + case "$n" in (''|*[!0-9]*) n=0 ;; esac + if [ "$n" -eq 0 ]; then + record " IDENTITY($when) NO PROCESS TRACKED. Cleanup will read this as UNREADABLE, never as clean: an identity nobody captured cannot later be shown to be gone." + else + record " IDENTITY($when) $n process identities recorded (supervisor from daemon.pid plus its children). These, re-queried fresh, are what decide the cleanup verdict." + fi + return 0 +} # ---- state-aware cleanup, entered EXACTLY ONCE (D1/F1/F2/C) ---------------- # EVERY STEP RUNS. NO STEP IS SKIPPED BECAUSE AN EARLIER ONE FAILED, and no check in # here may terminate the function (doyle's v5 blocker 4). The rule is structural: inside @@ -784,47 +840,123 @@ record "CLEANUP_SKIP daemon — no daemon_may_run state was ever marked" fi - # 4. RESIDUAL IDENTITY (plan §6). Read FRESH, and reported as an identity the next - # consumer can act on: pid + creation time + executable path + command line. A bare - # pid is unsound — pid reuse on this box is measured — so the creation time and the - # full command line ride with it. - # THE ENUMERATION CARRIES ITS OWN POSITIVE CONTROL. A dead process enumeration - # returns the same clean nothing a tidy host does, so a zero residue count is only - # readable beside a nonzero total; without that total this step reports UNREADABLE - # and the verdict stays PENDING. - # NOTHING IS KILLED HERE. The r10 authorization was pid-specific and is SPENT; a - # fresh pid-naming authorization is a STOP, not a step. + # 4. RESIDUAL IDENTITY -- RUN-BOUND, NOT STRING-MATCHED (doyle's admission blocker, + # 2026-09-13). The previous predicate matched a process whose COMMAND LINE contained the + # isolated home. THE RESIDUALS WE ACTUALLY MET DO NOT: r10's survivors ran + # `daemon run --detached` and `daemon brain --generation 0 --start-reason cold`, neither of + # which carries the home string, so that probe could have printed a confident CLEAR over + # exactly the two processes this cleanup exists to find. A NONEMPTY ENUMERATION VALIDATES + # THAT THE TABLE WAS READ; IT SAYS NOTHING ABOUT WHETHER THE PREDICATE CAN MATCH. + # + # What decides now is IDENTITY CAPTURED WHILE THE PROCESSES WERE LIVE (S9a): the supervisor + # named by /daemon.pid with its creation time, executable path and command + # line, plus every child whose parent is that supervisor. Cleanup RE-QUERIES those exact + # identities, fresh, and compares all three fields -- a bare pid match is unsound because + # pid reuse on this box is measured, so a pid that answers with a different creation time + # is a DIFFERENT process and ours is GONE. + # + # MISSING OR UNREADABLE ATTRIBUTION IS 'UNREADABLE', NEVER 'CLEAR'. If the identities were + # never captured, or the table cannot be read now, this run cannot say the box is tidy. + # NOTHING IS KILLED HERE. The r10 authorization was pid-specific and is SPENT; a fresh + # pid-naming authorization is a STOP, not a step. + RESIDUE_LIVE=0; RESIDUE_UNREADABLE=0; RESIDUE_GONE=0 if has_state daemon_may_run || has_state listener_may_exist; then - SPT_D2_HOME="$H" pwsh -NoProfile -Command ' - $h = $env:SPT_D2_HOME - try { $all = @(Get-CimInstance Win32_Process -ErrorAction Stop) } - catch { "RESIDUE_PROBE=UNREADABLE error=$_"; exit 0 } - "RESIDUE_ENUM total=$($all.Count)" - if ($all.Count -eq 0) { "RESIDUE_PROBE=UNREADABLE reason=empty-enumeration-is-not-an-empty-host"; exit 0 } - $hit = @($all | Where-Object { $_.CommandLine -and $_.CommandLine -like "*$h*" }) - foreach ($p in $hit) { - $c = try { $p.CreationDate.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ") } catch { "UNREADABLE" } - "RESIDUE pid=$($p.ProcessId) created=$c exe=[$($p.ExecutablePath)] cmd=[$($p.CommandLine)]" - } - "RESIDUE_PROBE=OK matched=$($hit.Count)" - ' > "$R/residue.txt" 2>&1 - local rrrc=$? - exits "residue_probe_EXIT=$rrrc" - if [ "$rrrc" -ne 0 ]; then - cfail "the residual-identity probe exited $rrrc; residue is UNMEASURED, which is not the same as none" - fi - while IFS= read -r line; do record " $line"; done < "$R/residue.txt" - RESIDUE_COUNT=$(sed -n 's/^RESIDUE_PROBE=OK matched=\([0-9]*\).*/\1/p' "$R/residue.txt" | tail -1) - case "$RESIDUE_COUNT" in (''|*[!0-9]*) RESIDUE_COUNT=UNMEASURED ;; esac - if [ "$RESIDUE_COUNT" = UNMEASURED ]; then - cfail "residual identity UNMEASURED — the probe did not report a matched count beside a nonzero enumeration" - elif [ "$RESIDUE_COUNT" -gt 0 ]; then - cfail "$RESIDUE_COUNT residual process(es) associated with $H remain, identified above. A fresh pid-naming authorization is REQUIRED and is a STOP, not a step: this driver reports them and acts on none." + if [ ! -f "$R/run-identities.txt" ]; then + RESIDUE_UNREADABLE=1 + cfail "no run-bound identities were ever captured, so nothing can be re-queried: residual state is UNREADABLE, which is NOT clean. Whatever this run started is unattributed." + elif ! grep -q '^IDENT role=' "$R/run-identities.txt"; then + RESIDUE_UNREADABLE=1 + record " $(grep '^IDENT_PROBE=' "$R/run-identities.txt" | tail -1)" + cfail "identity capture produced no tracked process (see run-identities.txt): residual state is UNREADABLE, never CLEAR." + else + : > "$R/residue.txt" + while IFS= read -r ident; do + local ipid irole icreated iexe icmd out + irole=$(printf '%s' "$ident" | sed -n 's/^IDENT role=\([^ ]*\) .*/\1/p') + ipid=$(printf '%s' "$ident" | sed -n 's/^IDENT .* pid=\([0-9][0-9]*\) .*/\1/p') + icreated=$(printf '%s' "$ident" | sed -n 's/^IDENT .* created=\([^ ]*\) .*/\1/p') + iexe=$(printf '%s' "$ident" | sed -n 's/^IDENT .* exe=\[\(.*\)\] cmd=\[.*\]$/\1/p') + icmd=$(printf '%s' "$ident" | sed -n 's/^IDENT .* cmd=\[\(.*\)\]$/\1/p') + if [ -z "$ipid" ]; then + RESIDUE_UNREADABLE=$((RESIDUE_UNREADABLE + 1)) + echo "RECHECK role=$irole state=UNREADABLE reason=unparseable-identity-line" >> "$R/residue.txt" + continue + fi + out=$(SPT_D2_PID="$ipid" pwsh -NoProfile -Command ' + $target = [int]$env:SPT_D2_PID + try { $all = @(Get-CimInstance Win32_Process -ErrorAction Stop) } + catch { "PROC=UNREADABLE reason=enumeration-failed"; exit 0 } + if ($all.Count -eq 0) { "PROC=UNREADABLE reason=empty-enumeration-is-not-an-empty-host"; exit 0 } + $p = @($all | Where-Object { $_.ProcessId -eq $target }) + if ($p.Count -eq 0) { "PROC=ABSENT pid=$target enum_total=$($all.Count)"; exit 0 } + $q = $p[0] + $c = try { $q.CreationDate.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ") } catch { "UNREADABLE" } + "PROC=PRESENT pid=$($q.ProcessId) created=$c exe=[$($q.ExecutablePath)] cmd=[$($q.CommandLine)]" + ' 2>&1 | tr -d '\r' | grep '^PROC=' | tail -1) + case "$out" in + PROC=ABSENT*) + RESIDUE_GONE=$((RESIDUE_GONE + 1)) + echo "RECHECK role=$irole pid=$ipid state=GONE (the tracked identity is no longer in the process table)" >> "$R/residue.txt" ;; + PROC=PRESENT*) + local ncreated nexe ncmd + ncreated=$(printf '%s' "$out" | sed -n 's/^PROC=PRESENT .* created=\([^ ]*\) .*/\1/p') + nexe=$(printf '%s' "$out" | sed -n 's/^PROC=PRESENT .* exe=\[\(.*\)\] cmd=\[.*\]$/\1/p') + ncmd=$(printf '%s' "$out" | sed -n 's/^PROC=PRESENT .* cmd=\[\(.*\)\]$/\1/p') + # WHAT ESTABLISHES WHAT (doyle, 2026-09-13). Only a READABLE creation time that + # DIFFERS establishes pid reuse -- i.e. that our process is gone and this pid now + # belongs to a stranger. An unreadable creation time on either side establishes + # NOTHING and stays UNREADABLE. A path or command-line mismatch ALONE does not make + # our process gone: pid plus creation instant is the identity, so that case is + # reported as LIVE with the mismatch named, never as GONE. + if [ "$ncreated" = UNREADABLE ] || [ "$icreated" = UNREADABLE ]; then + RESIDUE_UNREADABLE=$((RESIDUE_UNREADABLE + 1)) + echo "RECHECK role=$irole pid=$ipid state=UNREADABLE reason=creation-time-unreadable (recorded=[$icreated] now=[$ncreated]); neither reuse nor survival is established" >> "$R/residue.txt" + elif [ "$ncreated" != "$icreated" ]; then + RESIDUE_GONE=$((RESIDUE_GONE + 1)) + echo "RECHECK role=$irole pid=$ipid state=GONE-PID-REUSED (creation time differs: recorded=[$icreated] now=[$ncreated]; the pid answers, OURS is gone, and a bare pid match would have called this stranger residue: exe=[$nexe] cmd=[$ncmd])" >> "$R/residue.txt" + elif [ "$nexe" = "$iexe" ] && [ "$ncmd" = "$icmd" ]; then + RESIDUE_LIVE=$((RESIDUE_LIVE + 1)) + echo "RESIDUE role=$irole pid=$ipid created=$icreated exe=[$iexe] cmd=[$icmd] state=LIVE" >> "$R/residue.txt" + else + RESIDUE_LIVE=$((RESIDUE_LIVE + 1)) + echo "RESIDUE role=$irole pid=$ipid created=$icreated state=LIVE-FIELD-MISMATCH (same pid and same creation instant, so this IS our process and it is still running; the differing read is reported rather than resolved: exe recorded=[$iexe] now=[$nexe]; cmd recorded=[$icmd] now=[$ncmd])" >> "$R/residue.txt" + fi ;; + *) + RESIDUE_UNREADABLE=$((RESIDUE_UNREADABLE + 1)) + echo "RECHECK role=$irole pid=$ipid state=UNREADABLE [$out]" >> "$R/residue.txt" ;; + esac + done < <(grep '^IDENT role=' "$R/run-identities.txt") + while IFS= read -r line; do record " $line"; done < "$R/residue.txt" + record " RESIDUE_SUMMARY live=$RESIDUE_LIVE gone=$RESIDUE_GONE unreadable=$RESIDUE_UNREADABLE (tracked identities re-queried fresh; LIVE and UNREADABLE are different facts and both refuse a clean verdict)" + if [ "$RESIDUE_LIVE" -gt 0 ]; then + cfail "$RESIDUE_LIVE tracked process(es) of THIS run are still LIVE, identified above by pid + creation time + executable + command line. A fresh pid-naming authorization is REQUIRED and is a STOP, not a step: this driver reports them and acts on none." + fi + if [ "$RESIDUE_UNREADABLE" -gt 0 ]; then + cfail "$RESIDUE_UNREADABLE tracked identity/identities could not be re-queried. UNREADABLE is not CLEAR: this run cannot state that its processes are gone." + fi fi + # SUPPLEMENTARY, AND KNOWN-INCOMPLETE ON ITS OWN. Evidence beside the identity verdict, never + # the verdict: a process running the pinned executable may belong to another lane using the + # same worktree, and attributing it here would manufacture residue. It is kept because if + # identity capture came back UNREADABLE, this list is the actionable one. + SPT_D2_EXE="$EXE" pwsh -NoProfile -Command ' + $exe = $env:SPT_D2_EXE + try { $all = @(Get-CimInstance Win32_Process -ErrorAction Stop) } + catch { "SUBJECT_EXE_PROBE=UNREADABLE reason=enumeration-failed"; exit 0 } + if ($all.Count -eq 0) { "SUBJECT_EXE_PROBE=UNREADABLE reason=empty-enumeration"; exit 0 } + $hit = @($all | Where-Object { $_.ExecutablePath -and $_.ExecutablePath -eq $exe }) + foreach ($p in $hit) { "SUBJECT_EXE pid=$($p.ProcessId) cmd=[$($p.CommandLine)]" } + "SUBJECT_EXE_PROBE=OK running_the_pinned_executable=$($hit.Count) enum_total=$($all.Count)" + ' > "$R/subject-exe-processes.txt" 2>&1 + while IFS= read -r line; do record " $line"; done < "$R/subject-exe-processes.txt" + record " NOTE: that line is EVIDENCE, not attribution -- another lane may run the same executable. The verdict is decided by the tracked identities above." else - RESIDUE_COUNT=0 - record "CLEANUP_SKIP residue — neither a daemon nor a listener state was ever marked, so no process of this run's was ever dispatched" + record "CLEANUP_SKIP residue — neither a daemon nor a listener state was ever marked, so no process of this run was ever dispatched" fi + # RESIDUE_COUNT keeps its ledger meaning: how many of OUR processes are still alive. An + # unreadable attribution makes the count itself unmeasured rather than zero. + RESIDUE_COUNT="$RESIDUE_LIVE" + [ "$RESIDUE_UNREADABLE" -gt 0 ] && RESIDUE_COUNT=UNMEASURED # 4b. LISTENER ABSENCE, MEASURED FRESH (doyle's cleanup ruling, 2026-09-13). Rule removal is # not the whole of cleanup: a structural stop refusal may be reported separately ONLY IF @@ -954,6 +1086,7 @@ elif [ "${RESIDUE_COUNT:-UNMEASURED}" = UNMEASURED ] || [ "${LISTENER_STATE:-UNMEASURED}" != ABSENT ]; then residual=UNREADABLE fi echo " residual_state=$residual (CLEAR = measured gone; LIVE = seen; UNREADABLE = not measurable, and NOT the same claim)" + echo " tracked_process_identities: live=${RESIDUE_LIVE:-0} gone=${RESIDUE_GONE:-0} unreadable=${RESIDUE_UNREADABLE:-0} (re-queried by pid + creation time + executable + command line)" echo " rig_daemon_stop_exit=${DAEMON_STOP_RC:-NOT_RUN} listener_after_cleanup=${LISTENER_STATE:-UNMEASURED}" echo " CLEANUP=$CLEANUP_VERDICT residue=${RESIDUE_COUNT:-UNMEASURED} daemon_stop_refused=${DAEMON_STOP_REFUSED:-0} (IR-122: expected from an spt-hosted session; the residue count is what decides whether anything remains)" echo " experimental_ended=[$EXPERIMENTAL_ENDED]" @@ -1543,6 +1676,14 @@ fi # =========================================================================== +# S9a RUN-BOUND PROCESS IDENTITY — captured NOW, while the leg's processes are live +# =========================================================================== +# The elevated setup dispatches through the daemon (ensure_running, serveverb.rs:191), so if a +# supervisor and a brain exist at all, they exist now. Cleanup cannot re-query what was never +# recorded, and it must never fall back to guessing by command-line text: the r10 survivors' +# command lines contain nothing that identifies this run. +capture_run_identities post-setup +# =========================================================================== # S10 t2 — at the outcome, BESIDE D1, BEFORE ANY TEARDOWN IS REQUESTED # =========================================================================== abort_check