From d33cf85b468047e95ce1c7976123466c5bbe73e8 Mon Sep 17 00:00:00 2001 From: Ardy Dedase Date: Fri, 23 Oct 2026 07:42:08 +0700 Subject: [PATCH 1/6] fix(bin): parse each Herdr presentation journal once per session cleanup pass Candidate workspace discovery in fm-herdr-session-cleanup.sh rescanned every state/*.herdr-presentation journal for every candidate workspace, re-reading each journal field through separate grep/cut processes. On a home with tens of candidate workspaces and journals that multiplied into thousands of journal parses per pass and blew past the locked session-start time bound. Every journal is now snapshotted once per cleanup pass into a title-keyed in-memory index that candidate discovery matches against. A matched record's journal is existence-tested at lookup so mid-pass removals stay visible, and the revalidation and post-closure guards still re-read live files, so the cleanup guards and quarantine semantics are unchanged. Measured on this home's journal corpus (68 journals, 34 grammar-matching candidate workspaces, all preserved): 466s before, 21s after. --- bin/fm-herdr-session-cleanup.sh | 85 +++++++++++++++++++++++--- tests/fm-herdr-session-cleanup.test.sh | 38 ++++++++++++ 2 files changed, 114 insertions(+), 9 deletions(-) diff --git a/bin/fm-herdr-session-cleanup.sh b/bin/fm-herdr-session-cleanup.sh index 259969bbf22..7a23dd78393 100755 --- a/bin/fm-herdr-session-cleanup.sh +++ b/bin/fm-herdr-session-cleanup.sh @@ -15,6 +15,8 @@ # one pane, absent task metadata, no registered agent, and a process proof that # the pane contains only one idle recognized shell with no child process. A # version 2 journal must also bind the exact workspace, tab, and pane. +# Every home-local journal is parsed once per cleanup pass into a title-keyed +# index used for candidate discovery; the mutation guards re-read live state. # Topology is first checked from one locked API snapshot, then every mutation # prerequisite is immediately rechecked before the existing exact-pane # focus-preserving close helper is called. @@ -62,20 +64,31 @@ fm_herdr_cleanup_home_identity() { (cd "$FM_HOME" 2>/dev/null && pwd -P) } +# One journal parse per cleanup pass: every candidate journal is snapshotted +# once into a title-keyed in-memory index, and each candidate workspace +# matches against that index instead of rescanning every journal per workspace. +# A matched record's journal is existence-tested at lookup time so mid-pass +# removals stay visible; content drift after the index build is still caught +# by the live revalidation and post-closure rechecks, which re-read files. +fm_herdr_cleanup_journal_validate() { # + local journal=$1 id=$2 session=$3 home_real=$4 journal_home + fm_backend_herdr_projection_journal_snapshot "$journal" "$id" || return 1 + if [ "$FM_BACKEND_HERDR_JOURNAL_VERSION" = 2 ]; then + journal_home=$(fm_backend_herdr_projection_home_identity \ + "$FM_BACKEND_HERDR_JOURNAL_HOME" 2>/dev/null) || return 1 + [ "$journal_home" = "$home_real" ] \ + && [ "$FM_BACKEND_HERDR_JOURNAL_SESSION" = "$session" ] || return 1 + fi +} + fm_herdr_cleanup_journal_matches() { # <session> <home-real> - local title=$1 session=$2 home_real=$3 journal id expected journal_home + local title=$1 session=$2 home_real=$3 journal id expected [ -d "$STATE" ] && [ ! -L "$STATE" ] || return 1 for journal in "$STATE"/*"$FM_BACKEND_HERDR_PRESENTATION_JOURNAL_SUFFIX"; do [ -f "$journal" ] && [ ! -L "$journal" ] || continue id=$(basename "$journal" "$FM_BACKEND_HERDR_PRESENTATION_JOURNAL_SUFFIX") fm_task_id_creation_valid "$id" || continue - fm_backend_herdr_projection_journal_snapshot "$journal" "$id" || continue - if [ "$FM_BACKEND_HERDR_JOURNAL_VERSION" = 2 ]; then - journal_home=$(fm_backend_herdr_projection_home_identity \ - "$FM_BACKEND_HERDR_JOURNAL_HOME" 2>/dev/null) || continue - [ "$journal_home" = "$home_real" ] \ - && [ "$FM_BACKEND_HERDR_JOURNAL_SESSION" = "$session" ] || continue - fi + fm_herdr_cleanup_journal_validate "$journal" "$id" "$session" "$home_real" || continue expected=$(fm_backend_herdr_projection_workspace_label \ "$id" "$FM_BACKEND_HERDR_JOURNAL_PROJECTION_ID") [ "$expected" = "$title" ] || continue @@ -114,6 +127,58 @@ fm_herdr_cleanup_unique_match() { # <title> <session> <home-real> fi } +fm_herdr_cleanup_index_build() { # <session> <home-real> + local session=$1 home_real=$2 journal id label + FM_HERDR_CLEANUP_INDEX= + [ -d "$STATE" ] && [ ! -L "$STATE" ] || return 0 + for journal in "$STATE"/*"$FM_BACKEND_HERDR_PRESENTATION_JOURNAL_SUFFIX"; do + [ -f "$journal" ] && [ ! -L "$journal" ] || continue + id=$(basename "$journal" "$FM_BACKEND_HERDR_PRESENTATION_JOURNAL_SUFFIX") + fm_task_id_creation_valid "$id" || continue + fm_herdr_cleanup_journal_validate "$journal" "$id" "$session" "$home_real" || continue + label=$(fm_backend_herdr_projection_workspace_label \ + "$id" "$FM_BACKEND_HERDR_JOURNAL_PROJECTION_ID") + FM_HERDR_CLEANUP_INDEX+="$label"$'\t'"$journal"$'\t'"$id"$'\t' + FM_HERDR_CLEANUP_INDEX+="${FM_BACKEND_HERDR_JOURNAL_PROJECTION_ID}"$'\t' + FM_HERDR_CLEANUP_INDEX+="${FM_BACKEND_HERDR_JOURNAL_VERSION}"$'\t' + FM_HERDR_CLEANUP_INDEX+="${FM_BACKEND_HERDR_JOURNAL_WORKSPACE_ID}"$'\t' + FM_HERDR_CLEANUP_INDEX+="${FM_BACKEND_HERDR_JOURNAL_TAB_ID}"$'\t' + FM_HERDR_CLEANUP_INDEX+="${FM_BACKEND_HERDR_JOURNAL_PANE_ID}"$'\n' + done +} + +fm_herdr_cleanup_index_match() { # <title> <session> <home-real> + local title=$1 session=$2 home_real=$3 + local label journal id token version bound_workspace bound_tab bound_pane matches=0 + FM_HERDR_CLEANUP_JOURNAL= + FM_HERDR_CLEANUP_ID= + FM_HERDR_CLEANUP_TOKEN= + FM_HERDR_CLEANUP_VERSION= + FM_HERDR_CLEANUP_BOUND_WORKSPACE= + FM_HERDR_CLEANUP_BOUND_TAB= + FM_HERDR_CLEANUP_BOUND_PANE= + if [ "${FM_HERDR_CLEANUP_INDEX_READY:-0}" != 1 ]; then + fm_herdr_cleanup_index_build "$session" "$home_real" + FM_HERDR_CLEANUP_INDEX_READY=1 + fi + while IFS=$'\t' read -r label journal id token version bound_workspace bound_tab bound_pane; do + [ "$label" = "$title" ] || continue + [ -f "$journal" ] && [ ! -L "$journal" ] || continue + matches=$((matches + 1)) + FM_HERDR_CLEANUP_JOURNAL=$journal + FM_HERDR_CLEANUP_ID=$id + FM_HERDR_CLEANUP_TOKEN=$token + FM_HERDR_CLEANUP_VERSION=$version + FM_HERDR_CLEANUP_BOUND_WORKSPACE=$bound_workspace + FM_HERDR_CLEANUP_BOUND_TAB=$bound_tab + FM_HERDR_CLEANUP_BOUND_PANE=$bound_pane + done <<< "$FM_HERDR_CLEANUP_INDEX" + [ "$matches" -eq 1 ] || return 1 + [ -n "$FM_HERDR_CLEANUP_JOURNAL" ] \ + && [ -n "$FM_HERDR_CLEANUP_ID" ] \ + && [ -n "$FM_HERDR_CLEANUP_TOKEN" ] || return 1 +} + fm_herdr_cleanup_snapshot_candidate() { # <snapshot> <workspace> <title> <token> <bound-workspace> <bound-tab> <bound-pane> local snapshot=$1 workspace=$2 title=$3 token=$4 local bound_workspace=$5 bound_tab=$6 bound_pane=$7 record @@ -204,7 +269,7 @@ fm_herdr_cleanup_one() { # <session> <workspace> <title> <home-real> local version bound_workspace bound_tab bound_pane presentation_lock snapshot local tab pane state close_status=0 token=$(fm_herdr_cleanup_title_token "$title") || return 0 - if ! fm_herdr_cleanup_unique_match "$title" "$session" "$home_real"; then + if ! fm_herdr_cleanup_index_match "$title" "$session" "$home_real"; then return 0 fi journal=$FM_HERDR_CLEANUP_JOURNAL @@ -294,6 +359,8 @@ fm_herdr_cleanup_one() { # <session> <workspace> <title> <home-real> fm_herdr_session_cleanup() { local session home_real list candidates workspace title journal found=0 + FM_HERDR_CLEANUP_INDEX= + FM_HERDR_CLEANUP_INDEX_READY=0 [ -d "$STATE" ] && [ ! -L "$STATE" ] || return 0 for journal in "$STATE"/*"$FM_BACKEND_HERDR_PRESENTATION_JOURNAL_SUFFIX"; do if [ -f "$journal" ] && [ ! -L "$journal" ]; then diff --git a/tests/fm-herdr-session-cleanup.test.sh b/tests/fm-herdr-session-cleanup.test.sh index 99cf4b49a1c..02b30b19cf5 100755 --- a/tests/fm-herdr-session-cleanup.test.sh +++ b/tests/fm-herdr-session-cleanup.test.sh @@ -282,6 +282,44 @@ reset_fixture; : > "$FIXTURE_DIR/race"; assert_preserved "revalidation race" reset_fixture; printf '%s\n' "$TAB" > "$FIXTURE_DIR/active-tab"; assert_preserved "active target" reset_fixture; : > "$FIXTURE_DIR/focus-refuse"; assert_preserved "focus refusal" +# Candidate discovery must parse each home-local journal exactly once per +# cleanup pass however many grammar-matching candidate workspaces exist. +# Wrapping the journal snapshot backend function counts parses through the +# public data path without touching its validated semantics. +eval "$(declare -f fm_backend_herdr_projection_journal_snapshot \ + | sed '1s/^fm_backend_herdr_projection_journal_snapshot/_cleanup_journal_parse_real/')" +_parse_calls=0 +fm_backend_herdr_projection_journal_snapshot() { + _parse_calls=$((_parse_calls + 1)) + _cleanup_journal_parse_real "$@" +} +rm -f "${FM_STATE_OVERRIDE:?}"/*herdr-presentation +write_v1 probe-a ZzZzZzZzZzZzZzZzZzZzA0 +write_v1 probe-b ZzZzZzZzZzZzZzZzZzZzB0 +write_v1 probe-c ZzZzZzZzZzZzZzZzZzZzC0 +write_v1 probe-d ZzZzZzZzZzZzZzZzZzZzD0 +fm_backend_herdr_cli() { + local _session=$1; shift + [ "${1:-} ${2:-}" = "workspace list" ] || return 1 + printf '%s\n' '{"result":{"workspaces":[' + printf '%s\n' '{"workspace_id":"w1","label":"firstmate","focused":true,"active_tab_id":"w1:t1","tab_count":1,"pane_count":1},' + printf '%s\n' '{"workspace_id":"wx1","label":"└ sonar-1 · p:Sonar0000000000000000A","focused":false,"active_tab_id":"wx1:t1","tab_count":1,"pane_count":1},' + printf '%s\n' '{"workspace_id":"wx2","label":"└ sonar-2 · p:Sonar0000000000000000B","focused":false,"active_tab_id":"wx2:t1","tab_count":1,"pane_count":1},' + printf '%s\n' '{"workspace_id":"wx3","label":"└ sonar-3 · p:Sonar0000000000000000C","focused":false,"active_tab_id":"wx3:t1","tab_count":1,"pane_count":1},' + printf '%s\n' '{"workspace_id":"wx4","label":"└ sonar-4 · p:Sonar0000000000000000D","focused":false,"active_tab_id":"wx4:t1","tab_count":1,"pane_count":1}]}}' +} +_parse_calls=0 +fm_herdr_session_cleanup >/dev/null 2>&1 +_journals_left=0 +for _journal in "${FM_STATE_OVERRIDE:?}"/*herdr-presentation; do + [ -f "$_journal" ] && _journals_left=$((_journals_left + 1)) +done +[ "$_journals_left" = 4 ] \ + || fail "parse-count probe pass removed journals" +[ "$_parse_calls" = 4 ] \ + || fail "candidate discovery parsed journals per candidate ($_parse_calls) instead of once per pass" +pass "four grammar candidates parse four journals exactly once per pass" + INTEGRATION_ROOT="$TMP_ROOT/bootstrap-integration" mkdir -p "$INTEGRATION_ROOT/home/state" "$INTEGRATION_ROOT/home/data" "$INTEGRATION_ROOT/home/config" cp -R "$ROOT/bin" "$INTEGRATION_ROOT/bin" From cbd21d700aae215c4b73e06e7428fd74f4d6c227 Mon Sep 17 00:00:00 2001 From: Ardy Dedase <ardy.dedase@gmail.com> Date: Fri, 23 Oct 2026 12:25:18 +0700 Subject: [PATCH 2/6] no-mistakes(review): replace guard rescans with cksum-reconciled journal index refresh --- bin/fm-herdr-session-cleanup.sh | 153 ++++++++++++++++--------- tests/fm-herdr-session-cleanup.test.sh | 66 ++++++++++- 2 files changed, 159 insertions(+), 60 deletions(-) diff --git a/bin/fm-herdr-session-cleanup.sh b/bin/fm-herdr-session-cleanup.sh index 7a23dd78393..e79a9705c39 100755 --- a/bin/fm-herdr-session-cleanup.sh +++ b/bin/fm-herdr-session-cleanup.sh @@ -16,7 +16,9 @@ # the pane contains only one idle recognized shell with no child process. A # version 2 journal must also bind the exact workspace, tab, and pane. # Every home-local journal is parsed once per cleanup pass into a title-keyed -# index used for candidate discovery; the mutation guards re-read live state. +# index; the mutation guards re-read live state by digesting the whole journal +# directory once per check and re-parsing exactly any journal whose bytes +# changed since the index, so no guard ever rescans every journal. # Topology is first checked from one locked API snapshot, then every mutation # prerequisite is immediately rechecked before the existing exact-pane # focus-preserving close helper is called. @@ -67,9 +69,18 @@ fm_herdr_cleanup_home_identity() { # One journal parse per cleanup pass: every candidate journal is snapshotted # once into a title-keyed in-memory index, and each candidate workspace # matches against that index instead of rescanning every journal per workspace. -# A matched record's journal is existence-tested at lookup time so mid-pass -# removals stay visible; content drift after the index build is still caught -# by the live revalidation and post-closure rechecks, which re-read files. +# A matched record's journal stays existence-tested at lookup time, and the +# mutation guards reconcile the index with live bytes instead of rescanning: +# a matching whole-directory digest proves every row still mirrors its journal +# byte for byte, while any digest divergence names exactly the journals that +# were added, rewritten, or removed, and only those are re-parsed. Anything +# stale or contested therefore degrades to conservative preservation without +# a per-mutation directory rescan. +fm_herdr_cleanup_fingerprint() { + [ -d "$STATE" ] && [ ! -L "$STATE" ] || return 0 + cksum "$STATE"/*"$FM_BACKEND_HERDR_PRESENTATION_JOURNAL_SUFFIX" 2>/dev/null +} + fm_herdr_cleanup_journal_validate() { # <journal> <id> <session> <home-real> local journal=$1 id=$2 session=$3 home_real=$4 journal_home fm_backend_herdr_projection_journal_snapshot "$journal" "$id" || return 1 @@ -81,72 +92,99 @@ fm_herdr_cleanup_journal_validate() { # <journal> <id> <session> <home-real> fi } -fm_herdr_cleanup_journal_matches() { # <title> <session> <home-real> - local title=$1 session=$2 home_real=$3 journal id expected - [ -d "$STATE" ] && [ ! -L "$STATE" ] || return 1 - for journal in "$STATE"/*"$FM_BACKEND_HERDR_PRESENTATION_JOURNAL_SUFFIX"; do - [ -f "$journal" ] && [ ! -L "$journal" ] || continue - id=$(basename "$journal" "$FM_BACKEND_HERDR_PRESENTATION_JOURNAL_SUFFIX") - fm_task_id_creation_valid "$id" || continue - fm_herdr_cleanup_journal_validate "$journal" "$id" "$session" "$home_real" || continue - expected=$(fm_backend_herdr_projection_workspace_label \ - "$id" "$FM_BACKEND_HERDR_JOURNAL_PROJECTION_ID") - [ "$expected" = "$title" ] || continue - printf '%s\t%s\t%s\n' "$journal" "$id" "$FM_BACKEND_HERDR_JOURNAL_PROJECTION_ID" - done -} - +# Guard-grade unique match: reconcile the index against the live journal +# directory first (one digest pass, re-parsing only journals whose bytes +# changed so the rows are provably current), then resolve the one journal +# still matching the title from that reconciled index. fm_herdr_cleanup_unique_match() { # <title> <session> <home-real> - local title=$1 session=$2 home_real=$3 matches count record - FM_HERDR_CLEANUP_JOURNAL= - FM_HERDR_CLEANUP_ID= - FM_HERDR_CLEANUP_TOKEN= - FM_HERDR_CLEANUP_VERSION= - FM_HERDR_CLEANUP_BOUND_WORKSPACE= - FM_HERDR_CLEANUP_BOUND_TAB= - FM_HERDR_CLEANUP_BOUND_PANE= - matches=$(fm_herdr_cleanup_journal_matches "$title" "$session" "$home_real") || return 1 - count=$(printf '%s\n' "$matches" | awk 'NF { n++ } END { print n+0 }') - [ "$count" -eq 1 ] || return 1 - record=$(printf '%s\n' "$matches" | awk 'NF { print; exit }') - FM_HERDR_CLEANUP_JOURNAL=${record%%$'\t'*} - record=${record#*$'\t'} - FM_HERDR_CLEANUP_ID=${record%%$'\t'*} - FM_HERDR_CLEANUP_TOKEN=${record#*$'\t'} - [ -n "$FM_HERDR_CLEANUP_JOURNAL" ] \ - && [ -n "$FM_HERDR_CLEANUP_ID" ] \ - && [ -n "$FM_HERDR_CLEANUP_TOKEN" ] || return 1 - fm_backend_herdr_projection_journal_snapshot \ - "$FM_HERDR_CLEANUP_JOURNAL" "$FM_HERDR_CLEANUP_ID" || return 1 - [ "$FM_BACKEND_HERDR_JOURNAL_PROJECTION_ID" = "$FM_HERDR_CLEANUP_TOKEN" ] || return 1 - FM_HERDR_CLEANUP_VERSION=$FM_BACKEND_HERDR_JOURNAL_VERSION - if [ "$FM_HERDR_CLEANUP_VERSION" = 2 ]; then - FM_HERDR_CLEANUP_BOUND_WORKSPACE=$FM_BACKEND_HERDR_JOURNAL_WORKSPACE_ID - FM_HERDR_CLEANUP_BOUND_TAB=$FM_BACKEND_HERDR_JOURNAL_TAB_ID - FM_HERDR_CLEANUP_BOUND_PANE=$FM_BACKEND_HERDR_JOURNAL_PANE_ID - fi + local title=$1 session=$2 home_real=$3 + fm_herdr_cleanup_index_refresh "$session" "$home_real" + fm_herdr_cleanup_index_match "$title" "$session" "$home_real" } fm_herdr_cleanup_index_build() { # <session> <home-real> - local session=$1 home_real=$2 journal id label + local session=$1 home_real=$2 journal id FM_HERDR_CLEANUP_INDEX= + FM_HERDR_CLEANUP_FINGERPRINT=$(fm_herdr_cleanup_fingerprint) [ -d "$STATE" ] && [ ! -L "$STATE" ] || return 0 for journal in "$STATE"/*"$FM_BACKEND_HERDR_PRESENTATION_JOURNAL_SUFFIX"; do [ -f "$journal" ] && [ ! -L "$journal" ] || continue id=$(basename "$journal" "$FM_BACKEND_HERDR_PRESENTATION_JOURNAL_SUFFIX") fm_task_id_creation_valid "$id" || continue - fm_herdr_cleanup_journal_validate "$journal" "$id" "$session" "$home_real" || continue - label=$(fm_backend_herdr_projection_workspace_label \ - "$id" "$FM_BACKEND_HERDR_JOURNAL_PROJECTION_ID") - FM_HERDR_CLEANUP_INDEX+="$label"$'\t'"$journal"$'\t'"$id"$'\t' - FM_HERDR_CLEANUP_INDEX+="${FM_BACKEND_HERDR_JOURNAL_PROJECTION_ID}"$'\t' - FM_HERDR_CLEANUP_INDEX+="${FM_BACKEND_HERDR_JOURNAL_VERSION}"$'\t' - FM_HERDR_CLEANUP_INDEX+="${FM_BACKEND_HERDR_JOURNAL_WORKSPACE_ID}"$'\t' - FM_HERDR_CLEANUP_INDEX+="${FM_BACKEND_HERDR_JOURNAL_TAB_ID}"$'\t' - FM_HERDR_CLEANUP_INDEX+="${FM_BACKEND_HERDR_JOURNAL_PANE_ID}"$'\n' + fm_herdr_cleanup_index_row_append "$journal" "$id" "$session" "$home_real" || continue done } +# Parse one journal once into a fresh index row. Shared by the index build and +# the digest-driven reconciliation so both produce byte-identical rows. +fm_herdr_cleanup_index_row_append() { # <journal> <id> <session> <home-real> + local journal=$1 id=$2 session=$3 home_real=$4 label + fm_herdr_cleanup_journal_validate "$journal" "$id" "$session" "$home_real" || return 1 + label=$(fm_backend_herdr_projection_workspace_label \ + "$id" "$FM_BACKEND_HERDR_JOURNAL_PROJECTION_ID") + FM_HERDR_CLEANUP_INDEX+="$label"$'\t'"$journal"$'\t'"$id"$'\t' + FM_HERDR_CLEANUP_INDEX+="${FM_BACKEND_HERDR_JOURNAL_PROJECTION_ID}"$'\t' + FM_HERDR_CLEANUP_INDEX+="${FM_BACKEND_HERDR_JOURNAL_VERSION}"$'\t' + FM_HERDR_CLEANUP_INDEX+="${FM_BACKEND_HERDR_JOURNAL_WORKSPACE_ID}"$'\t' + FM_HERDR_CLEANUP_INDEX+="${FM_BACKEND_HERDR_JOURNAL_TAB_ID}"$'\t' + FM_HERDR_CLEANUP_INDEX+="${FM_BACKEND_HERDR_JOURNAL_PANE_ID}"$'\n' +} + +# Refresh exactly one journal path from live bytes: its previous rows are +# dropped, then the journal is re-parsed into fresh rows only while it is +# still an ordinary readable file with a valid task id and payload. A removed +# or invalid journal ends with no rows at all. +fm_herdr_cleanup_index_row_replace() { # <journal> <session> <home-real> + local journal=$1 session=$2 home_real=$3 id + local r_label r_journal r_id r_token r_version r_workspace r_tab r_pane + local index= + while IFS=$'\t' read -r r_label r_journal r_id r_token r_version r_workspace r_tab r_pane; do + [ -n "$r_journal" ] || continue + [ "$r_journal" = "$journal" ] && continue + index+="$r_label"$'\t'"$r_journal"$'\t'"$r_id"$'\t'"$r_token"$'\t' + index+="$r_version"$'\t'"$r_workspace"$'\t'"$r_tab"$'\t'"$r_pane"$'\n' + done <<< "$FM_HERDR_CLEANUP_INDEX" + FM_HERDR_CLEANUP_INDEX=$index + [ -f "$journal" ] && [ ! -L "$journal" ] || return 0 + id=$(basename "$journal" "$FM_BACKEND_HERDR_PRESENTATION_JOURNAL_SUFFIX") + fm_task_id_creation_valid "$id" || return 0 + fm_herdr_cleanup_index_row_append "$journal" "$id" "$session" "$home_real" || return 0 + return 0 +} + +# Reconcile the index with the live directory before a guard relies on it. +# A matching digest proves every row still mirrors the live bytes; otherwise +# exactly the digest lines that differ name their journals, and each one is +# re-read. An unparseable digest line falls back to a full index rebuild so a +# change can never be silently missed. +fm_herdr_cleanup_index_refresh() { # <session> <home-real> + local session=$1 home_real=$2 next changed line journal seen + local recorded=${FM_HERDR_CLEANUP_FINGERPRINT:-} + local cksum_re='^([0-9]+)[[:space:]]([0-9]+)[[:space:]]+(.*)$' + local NLPAD=$'\n' + next=$(fm_herdr_cleanup_fingerprint) + [ "$next" = "$recorded" ] && return 0 + changed=$( + { + printf '%s\n' "$recorded" + printf '%s\n' "$next" + } | LC_ALL=C sort | LC_ALL=C uniq -u + ) + FM_HERDR_CLEANUP_FINGERPRINT=$next + seen=$NLPAD + while IFS= read -r line; do + [ -n "$line" ] || continue + if ! [[ "$line" =~ $cksum_re ]]; then + fm_herdr_cleanup_index_build "$session" "$home_real" + return 0 + fi + journal=${BASH_REMATCH[3]} + case "$seen" in *"$NLPAD$journal$NLPAD"*) continue ;; esac + seen+="$journal$NLPAD" + fm_herdr_cleanup_index_row_replace "$journal" "$session" "$home_real" + done <<< "$changed" +} + fm_herdr_cleanup_index_match() { # <title> <session> <home-real> local title=$1 session=$2 home_real=$3 local label journal id token version bound_workspace bound_tab bound_pane matches=0 @@ -361,6 +399,7 @@ fm_herdr_session_cleanup() { local session home_real list candidates workspace title journal found=0 FM_HERDR_CLEANUP_INDEX= FM_HERDR_CLEANUP_INDEX_READY=0 + FM_HERDR_CLEANUP_FINGERPRINT= [ -d "$STATE" ] && [ ! -L "$STATE" ] || return 0 for journal in "$STATE"/*"$FM_BACKEND_HERDR_PRESENTATION_JOURNAL_SUFFIX"; do if [ -f "$journal" ] && [ ! -L "$journal" ]; then diff --git a/tests/fm-herdr-session-cleanup.test.sh b/tests/fm-herdr-session-cleanup.test.sh index 02b30b19cf5..63feb2db397 100755 --- a/tests/fm-herdr-session-cleanup.test.sh +++ b/tests/fm-herdr-session-cleanup.test.sh @@ -170,6 +170,14 @@ fm_backend_herdr_cli() { ;; "api snapshot") : > "$FIXTURE_DIR/snapshotted" + if [ -e "$FIXTURE_DIR/late-duplicate" ]; then + rm -f "$FIXTURE_DIR/late-duplicate" + write_v1 fm-task + fi + if [ -e "$FIXTURE_DIR/late-extra" ]; then + rm -f "$FIXTURE_DIR/late-extra" + write_v1 probe-extra QyQyQyQyQyQyQyQyQyQyQy0 + fi printf '{"result":{"snapshot":{"focused_workspace_id":"w1","focused_tab_id":"%s","focused_pane_id":"w1:p1","workspaces":' "$(cat "$FIXTURE_DIR/active-tab")" fixture_workspaces printf ',"tabs":'; fixture_tabs @@ -282,10 +290,9 @@ reset_fixture; : > "$FIXTURE_DIR/race"; assert_preserved "revalidation race" reset_fixture; printf '%s\n' "$TAB" > "$FIXTURE_DIR/active-tab"; assert_preserved "active target" reset_fixture; : > "$FIXTURE_DIR/focus-refuse"; assert_preserved "focus refusal" -# Candidate discovery must parse each home-local journal exactly once per -# cleanup pass however many grammar-matching candidate workspaces exist. # Wrapping the journal snapshot backend function counts parses through the -# public data path without touching its validated semantics. +# public data path without touching its validated semantics; installed once +# for the mutation-guard and discovery parse budgets below. eval "$(declare -f fm_backend_herdr_projection_journal_snapshot \ | sed '1s/^fm_backend_herdr_projection_journal_snapshot/_cleanup_journal_parse_real/')" _parse_calls=0 @@ -293,6 +300,59 @@ fm_backend_herdr_projection_journal_snapshot() { _parse_calls=$((_parse_calls + 1)) _cleanup_journal_parse_real "$@" } + +# A completed close keeps its whole-pass parse budget at one parse per +# present journal: the index build parses both journals, and the immediate +# revalidation plus post-closure journal recheck re-read live state by +# digesting the journal directory once per check and re-parsing only the +# journals whose bytes changed since the index. +reset_fixture +write_v1 probe-idle XyXyXyXyXyXyXyXyXyXyX0 +_parse_calls=0 +fm_herdr_session_cleanup >/dev/null 2>&1 +[ ! -e "$FM_STATE_OVERRIDE/$ID.herdr-presentation" ] \ + || fail "closed candidate's journal was not retired" +[ "$(wc -l < "$CLOSE_LOG" | tr -d ' ')" = 1 ] \ + || fail "guard-budget close did not close exactly once" +[ -f "$FM_STATE_OVERRIDE/probe-idle.herdr-presentation" ] \ + || fail "guard-budget close removed an unmatched journal" +[ "$_parse_calls" = 2 ] \ + || fail "closing one candidate parsed journals $_parse_calls times instead of once per journal per pass" +pass "closing one candidate guards via digest without a per-mutation rescan" + +# A journal that appears after discovery must still be caught the same way a +# fresh rescan would: the revalidation guard reconciles the index with the live +# directory, so a duplicate title appearing mid-pass preserves the candidate. +reset_fixture +: > "$FIXTURE_DIR/late-duplicate" +fm_herdr_session_cleanup >/dev/null 2>&1 +[ -f "$FM_STATE_OVERRIDE/$ID.herdr-presentation" ] \ + || fail "mid-pass duplicate journal retired the candidate" +[ -f "$FM_STATE_OVERRIDE/fm-task.herdr-presentation" ] \ + || fail "mid-pass duplicate journal was itself retired" +[ ! -s "$CLOSE_LOG" ] \ + || fail "mid-pass duplicate journal closed the pane" +pass "guards catch a journal that appears after discovery" + +# An unrelated journal that appears after discovery is re-parsed by the guards +# alone, and a still-unique candidate completes its exact close anyway. +reset_fixture +write_v1 probe-idle XyXyXyXyXyXyXyXyXyXyX0 +: > "$FIXTURE_DIR/late-extra" +_parse_calls=0 +fm_herdr_session_cleanup >/dev/null 2>&1 +[ ! -e "$FM_STATE_OVERRIDE/$ID.herdr-presentation" ] \ + || fail "mid-pass unrelated journal change preserved a closable candidate" +[ -f "$FM_STATE_OVERRIDE/probe-extra.herdr-presentation" ] \ + || fail "mid-pass unrelated journal change retired the wrong journal" +[ "$(wc -l < "$CLOSE_LOG" | tr -d ' ')" = 1 ] \ + || fail "mid-pass unrelated journal change did not close exactly once" +[ "$_parse_calls" = 3 ] \ + || fail "mid-pass change parsed journals $_parse_calls times instead of exactly the changed journal" +pass "guards re-parse only the changed journal and still close" + +# Candidate discovery must parse each home-local journal exactly once per +# cleanup pass however many grammar-matching candidate workspaces exist. rm -f "${FM_STATE_OVERRIDE:?}"/*herdr-presentation write_v1 probe-a ZzZzZzZzZzZzZzZzZzZzA0 write_v1 probe-b ZzZzZzZzZzZzZzZzZzZzB0 From 721d783f00d0185d2872e8938b5ac4ba27bd7fc9 Mon Sep 17 00:00:00 2001 From: Ardy Dedase <ardy.dedase@gmail.com> Date: Fri, 23 Oct 2026 13:36:27 +0700 Subject: [PATCH 3/6] no-mistakes(document): Clarify herdr cleanup guard comments for digest-build fallback --- bin/fm-herdr-session-cleanup.sh | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/bin/fm-herdr-session-cleanup.sh b/bin/fm-herdr-session-cleanup.sh index e79a9705c39..02d22f06356 100755 --- a/bin/fm-herdr-session-cleanup.sh +++ b/bin/fm-herdr-session-cleanup.sh @@ -18,7 +18,9 @@ # Every home-local journal is parsed once per cleanup pass into a title-keyed # index; the mutation guards re-read live state by digesting the whole journal # directory once per check and re-parsing exactly any journal whose bytes -# changed since the index, so no guard ever rescans every journal. +# changed since the index, so no guard rescans every journal; +# fm_herdr_cleanup_index_refresh documents its one deliberate exception, the +# full rebuild on an unparseable digest line. # Topology is first checked from one locked API snapshot, then every mutation # prerequisite is immediately rechecked before the existing exact-pane # focus-preserving close helper is called. @@ -74,8 +76,8 @@ fm_herdr_cleanup_home_identity() { # a matching whole-directory digest proves every row still mirrors its journal # byte for byte, while any digest divergence names exactly the journals that # were added, rewritten, or removed, and only those are re-parsed. Anything -# stale or contested therefore degrades to conservative preservation without -# a per-mutation directory rescan. +# stale or contested therefore degrades to conservative preservation, never +# to a silently stale row. fm_herdr_cleanup_fingerprint() { [ -d "$STATE" ] && [ ! -L "$STATE" ] || return 0 cksum "$STATE"/*"$FM_BACKEND_HERDR_PRESENTATION_JOURNAL_SUFFIX" 2>/dev/null From efe03382e20898bdaefa64b9d7f0901984499596 Mon Sep 17 00:00:00 2001 From: Ardy Dedase <ardy.dedase@gmail.com> Date: Fri, 23 Oct 2026 14:15:00 +0700 Subject: [PATCH 4/6] no-mistakes(review): Include journal eligibility and state-dir validity in cleanup digest --- bin/fm-herdr-session-cleanup.sh | 36 +++++++++++++------ tests/fm-herdr-session-cleanup.test.sh | 50 ++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 11 deletions(-) diff --git a/bin/fm-herdr-session-cleanup.sh b/bin/fm-herdr-session-cleanup.sh index 02d22f06356..1429b125309 100755 --- a/bin/fm-herdr-session-cleanup.sh +++ b/bin/fm-herdr-session-cleanup.sh @@ -16,9 +16,11 @@ # the pane contains only one idle recognized shell with no child process. A # version 2 journal must also bind the exact workspace, tab, and pane. # Every home-local journal is parsed once per cleanup pass into a title-keyed -# index; the mutation guards re-read live state by digesting the whole journal -# directory once per check and re-parsing exactly any journal whose bytes -# changed since the index, so no guard rescans every journal; +# index; the mutation guards re-read live state by digesting once per check +# exactly the ordinary journal files, plus one deliberately unparseable line +# while the state directory is missing or a symlink, and re-parsing exactly +# any journal whose digest changed since the index, so no guard rescans every +# journal; # fm_herdr_cleanup_index_refresh documents its one deliberate exception, the # full rebuild on an unparseable digest line. # Topology is first checked from one locked API snapshot, then every mutation @@ -72,15 +74,27 @@ fm_herdr_cleanup_home_identity() { # once into a title-keyed in-memory index, and each candidate workspace # matches against that index instead of rescanning every journal per workspace. # A matched record's journal stays existence-tested at lookup time, and the -# mutation guards reconcile the index with live bytes instead of rescanning: -# a matching whole-directory digest proves every row still mirrors its journal -# byte for byte, while any digest divergence names exactly the journals that -# were added, rewritten, or removed, and only those are re-parsed. Anything -# stale or contested therefore degrades to conservative preservation, never -# to a silently stale row. +# mutation guards reconcile the index with the live journal directory instead +# of rescanning: the digest checksums only ordinary journal files and gains a +# deliberately unparseable line whenever the state directory stops being an +# ordinary directory, so every file-type or directory transition either +# changes a digest line or forces the full index rebuild on that line. A +# matching digest therefore proves every row still mirrors its journal byte +# for byte, while any divergence names exactly the journals that must be +# re-read, and only those are re-parsed. Anything stale or contested +# degrades to conservative preservation, never to a silently stale row. fm_herdr_cleanup_fingerprint() { - [ -d "$STATE" ] && [ ! -L "$STATE" ] || return 0 - cksum "$STATE"/*"$FM_BACKEND_HERDR_PRESENTATION_JOURNAL_SUFFIX" 2>/dev/null + local journal + set -- + [ -d "$STATE" ] && [ ! -L "$STATE" ] || { + printf '%s\n' 'fm-herdr-session-cleanup: state directory is missing or a symlink' + return 0 + } + for journal in "$STATE"/*"$FM_BACKEND_HERDR_PRESENTATION_JOURNAL_SUFFIX"; do + [ -f "$journal" ] && [ ! -L "$journal" ] || continue + set -- "$@" "$journal" + done + [ "$#" -eq 0 ] || cksum "$@" 2>/dev/null } fm_herdr_cleanup_journal_validate() { # <journal> <id> <session> <home-real> diff --git a/tests/fm-herdr-session-cleanup.test.sh b/tests/fm-herdr-session-cleanup.test.sh index 63feb2db397..abf5f931029 100755 --- a/tests/fm-herdr-session-cleanup.test.sh +++ b/tests/fm-herdr-session-cleanup.test.sh @@ -62,6 +62,7 @@ TAB=w2:t1 PANE=w2:p1 TITLE="└ task · p:$TOKEN" FIXTURE_DIR="$TMP_ROOT/fixture" +OUTSIDE_HOME_STATE="$TMP_ROOT/outside-home-state" LOCK_LOG="$TMP_ROOT/locks.log" CLOSE_LOG="$TMP_ROOT/closes.log" mkdir -p "$FIXTURE_DIR" @@ -178,6 +179,16 @@ fm_backend_herdr_cli() { rm -f "$FIXTURE_DIR/late-extra" write_v1 probe-extra QyQyQyQyQyQyQyQyQyQyQy0 fi + if [ -e "$FIXTURE_DIR/late-materialized-journal" ]; then + rm -f "$FIXTURE_DIR/late-materialized-journal" + rm -f "$FM_STATE_OVERRIDE/fm-task.herdr-presentation" + cp "$FIXTURE_DIR/mirror-journal" "$FM_STATE_OVERRIDE/fm-task.herdr-presentation" + fi + if [ -e "$FIXTURE_DIR/late-state-symlink" ]; then + rm -f "$FIXTURE_DIR/late-state-symlink" + mv "$FM_STATE_OVERRIDE" "$FM_STATE_OVERRIDE.preserved" + ln -s "$OUTSIDE_HOME_STATE" "$FM_STATE_OVERRIDE" + fi printf '{"result":{"snapshot":{"focused_workspace_id":"w1","focused_tab_id":"%s","focused_pane_id":"w1:p1","workspaces":' "$(cat "$FIXTURE_DIR/active-tab")" fixture_workspaces printf ',"tabs":'; fixture_tabs @@ -351,6 +362,45 @@ fm_herdr_session_cleanup >/dev/null 2>&1 || fail "mid-pass change parsed journals $_parse_calls times instead of exactly the changed journal" pass "guards re-parse only the changed journal and still close" +# A journal parked as a symlink at pass start has no row, but the digest must +# make its byte-identical materialization a visible change the same way a +# rewrite is visible, so the guards preserve the pane while two same-titled +# journals exist instead of closing on the one indexed row. +reset_fixture +write_v1 fm-task +cp "$FM_STATE_OVERRIDE/fm-task.herdr-presentation" "$FIXTURE_DIR/mirror-journal" +rm -f "$FM_STATE_OVERRIDE/fm-task.herdr-presentation" +ln -s "$FIXTURE_DIR/mirror-journal" "$FM_STATE_OVERRIDE/fm-task.herdr-presentation" +: > "$FIXTURE_DIR/late-materialized-journal" +fm_herdr_session_cleanup >/dev/null 2>&1 +[ -f "$FM_STATE_OVERRIDE/$ID.herdr-presentation" ] \ + || fail "byte-identical materialized journal retired the candidate" +[ -f "$FM_STATE_OVERRIDE/fm-task.herdr-presentation" ] \ + || fail "byte-identical materialized journal was itself retired" +[ ! -s "$CLOSE_LOG" ] \ + || fail "byte-identical materialized journal closed the pane" +pass "guards catch a byte-identical journal materializing from a symlink" + +# The state directory itself swapping mid-pass for a symlink to byte-identical +# journal copies outside the effective FM_HOME invalidates the reconciled +# index: the guards must preserve the candidate and leave both journal stores +# untouched, the same way the base per-guard live rescan skipped a symlinked +# state directory. +reset_fixture +mkdir -p "$OUTSIDE_HOME_STATE" +cp "$FM_STATE_OVERRIDE/$ID.herdr-presentation" "$OUTSIDE_HOME_STATE/$ID.herdr-presentation" +: > "$FIXTURE_DIR/late-state-symlink" +fm_herdr_session_cleanup >/dev/null 2>&1 +[ -f "$FM_STATE_OVERRIDE.preserved/$ID.herdr-presentation" ] \ + || fail "symlinked state directory retired the home journal" +[ -f "$OUTSIDE_HOME_STATE/$ID.herdr-presentation" ] \ + || fail "symlinked state directory acted on a journal outside the effective FM_HOME" +[ ! -s "$CLOSE_LOG" ] \ + || fail "symlinked state directory closed the pane" +rm -f "$FM_STATE_OVERRIDE" +mv "$FM_STATE_OVERRIDE.preserved" "$FM_STATE_OVERRIDE" +pass "guards preserve both stores when the state directory becomes a symlink" + # Candidate discovery must parse each home-local journal exactly once per # cleanup pass however many grammar-matching candidate workspaces exist. rm -f "${FM_STATE_OVERRIDE:?}"/*herdr-presentation From 10e39360d0d8f35317c2a41ca160ef12e822d6e4 Mon Sep 17 00:00:00 2001 From: Ardy Dedase <ardy.dedase@gmail.com> Date: Tue, 27 Oct 2026 09:48:20 +0700 Subject: [PATCH 5/6] no-mistakes(document): Document cleanup test parse budget coverage --- docs/herdr-backend.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/herdr-backend.md b/docs/herdr-backend.md index 242e2fcf7dc..56edf934b91 100644 --- a/docs/herdr-backend.md +++ b/docs/herdr-backend.md @@ -481,7 +481,7 @@ Any of these preserves the candidate and lets session startup continue with at m | Test | What it covers | | --- | --- | | `tests/fm-backend-herdr-presentation-e2e.test.sh` | Multi-home ordering, concurrency, lock contention, legacy coexistence, focus preservation, exact same-identity restart replacement, ambiguous bindings and tokens, and exact-pane cleanup through the guarded lab path. | -| `tests/fm-herdr-session-cleanup.test.sh` | Every discovery, ownership, topology, process, locking, revalidation, focus, retirement, and continue-on-error boundary. | +| `tests/fm-herdr-session-cleanup.test.sh` | Every discovery, ownership, topology, process, locking, revalidation, focus, retirement, and continue-on-error boundary, plus the per-pass journal parse budget. | | `tests/fm-herdr-session-cleanup-e2e.test.sh` | The restored-shell cleanup in a guarded non-default named lab. | | `tests/fm-backend-herdr-focus-flash-e2e.test.sh` | Reproduces the raw explicit-close focus steal on the installed release, and proves the focus-safe emptying-close plan removes a doomed workspace with no wrong-focus interval. | | `tests/fm-backend-herdr-stale-active-tab-e2e.test.sh` | Proves a persisted-focused tab still closes when no foreground client is attached. | From 4403712e9d976883b0e1d5926e95198c2be499f9 Mon Sep 17 00:00:00 2001 From: Ardy Dedase <ardy.dedase@gmail.com> Date: Tue, 27 Oct 2026 10:42:24 +0700 Subject: [PATCH 6/6] =?UTF-8?q?no-mistakes(ci):=20Fixed=20the=20genuine=20?= =?UTF-8?q?PR-caused=20CI=20defect:=20the=20PR's=20new=20parse-budget=20sc?= =?UTF-8?q?enario=20added=20a=20second=20fm=5Fbackend=5Fherdr=5Fcli=20fixt?= =?UTF-8?q?ure=20stub=20in=20tests/fm-herdr-session-cleanup.test.sh,=20mak?= =?UTF-8?q?ing=20the=20shared=20stub=20at=20line=20132=20provably=20dead?= =?UTF-8?q?=20code=20under=20ShellCheck's=20SC2329=20analysis,=20which=20f?= =?UTF-8?q?ails=20CI's=20Lint=20partition=20job=20(bin/fm-lint.sh=20--part?= =?UTF-8?q?ition=201of2=20lints=20tests/*.sh=20with=20--norc=20--external-?= =?UTF-8?q?sources=20and=20does=20not=20exclude=20SC2329;=20verified=20bas?= =?UTF-8?q?e=20commit=20passes=20the=20exact=20invocation,=20PR=20head=20f?= =?UTF-8?q?ailed=20with=20exit=201).=20Applied=20the=20repo's=20establishe?= =?UTF-8?q?d=20convention:=20a=20one-line=20'#=20shellcheck=20disable=3DSC?= =?UTF-8?q?2329'=20directive=20with=20rationale=20above=20the=20stub,=20ma?= =?UTF-8?q?tching=20the=20same=20file's=20lines=2043/49=20and=20~20=20othe?= =?UTF-8?q?r=20test=20files=20for=20indirectly-invoked=20runtime=20overrid?= =?UTF-8?q?es.=20Verified:=20partition-mode=20shellcheck=20exit=200=20on?= =?UTF-8?q?=20both=20changed=20shell=20files,=20default=20changed-file=20l?= =?UTF-8?q?int=20with=20workflow=20validation=20exit=200,=20all=2028=20beh?= =?UTF-8?q?avior=20scenarios=20pass,=20real-Herdr=20e2e=20passes.=20The=20?= =?UTF-8?q?other=20reported=20failures=20are=20not=20code-caused:=20the=20?= =?UTF-8?q?coverage=20guard=20(--check-coverage=20'portable=20serial=20sha?= =?UTF-8?q?rds=20share=20scripts')=20reproduces=20identically=20on=20the?= =?UTF-8?q?=20base=20commit=2046df1608=20(which=20merged=20green)=20in=20t?= =?UTF-8?q?his=20environment,=20with=20the=20serial-lane=20listing=20itsel?= =?UTF-8?q?f=20nondeterministically=20truncating=20under=20local=20bash=20?= =?UTF-8?q?3.2.57=20(226/227/250=20lines=20for=20a=20stable=20292-file=20i?= =?UTF-8?q?nventory;=20bin/fm-test-run.sh=20untouched=20by=20this=20PR,=20?= =?UTF-8?q?CI=20runs=20ubuntu-24.04)=20=E2=80=94=20an=20environment=20arti?= =?UTF-8?q?fact;=20and=20'Require=20no-mistakes'=20is=20a=20PR-attestation?= =?UTF-8?q?=20check=20(action=5Frequired=20after=20later=20pipeline=20push?= =?UTF-8?q?es=20moved=20the=20head),=20owned=20by=20the=20outer=20executor?= =?UTF-8?q?,=20not=20fixable=20in=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/fm-herdr-session-cleanup.test.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/fm-herdr-session-cleanup.test.sh b/tests/fm-herdr-session-cleanup.test.sh index abf5f931029..c11bfd71e68 100755 --- a/tests/fm-herdr-session-cleanup.test.sh +++ b/tests/fm-herdr-session-cleanup.test.sh @@ -129,6 +129,7 @@ fixture_panes() { printf ']' } +# shellcheck disable=SC2329 # Invoked indirectly by the sourced cleanup script; the parse-budget scenario below redefines it. fm_backend_herdr_cli() { local _session=$1 first=${2:-} second=${3:-} title tabs panes shift