Skip to content

Six octal reads, four of which delete the loudest error report in the file - #331

Merged
fdaviddpt merged 1 commit into
mainfrom
fix/324-327
Aug 8, 2026
Merged

Six octal reads, four of which delete the loudest error report in the file#331
fdaviddpt merged 1 commit into
mainfrom
fix/324-327

Conversation

@fdaviddpt

@fdaviddpt fdaviddpt commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Closes #324
Closes #327

Both issues were partly wrong. The code fixes are real and are wider than either described.

#324 — the headline claim does not reproduce

Filed as: the test writes the marker to a path the hook never reads, so the cooldown block is never entered and every corrupt value passes trivially.

Measured, both spellings behave identically:

WHERE= dotted-root RC= 0 COMMITTED= True
STDERR= '.../50-git-backup.sh: line 172: 08: value too great for base (error token is "08")'
legacy exists after: False
WHERE= state-dir   RC= 0 COMMITTED= True
STDERR= '.../50-git-backup.sh: line 172: 08: value too great for base (error token is "08")'

The cause is the #261 legacy carry-forward at 50-git-backup.sh:138-147, which copies $REPO_ROOT/.last-git-backup-ts into $GB_STATE_DIR/last-git-backup-ts before the cooldown block, and consumes the dotted file doing it. That is also why the reporter's glob showed legacy dotted path exists? False — they read it as "the hook writes elsewhere and never saw mine", when it had moved theirs and read it.

So the four existing parameters were exercising the guard. Three narrower things were wrong and are fixed:

Transport was the shim, not the feature The carry-forward exists in order to be deleted. The day it goes, all four cases go vacuous and nothing turns red. The marker is now written through the existing hook_state() helper, resolved via git rev-parse --git-common-dir.
08 / 09 were absent Added — the values the guard is actually about.
The assertions could not fail rc == 0 and _slug_is_committed are both true with the octal error on this platform. The stderr assertion is the reporter's second point, and the only observable that separates broken from fixed on every bash.

Their second point was right and is the one that mattered.

#327 — right about the sites, wrong about the severity

The issue claimed the hook dies and the git backup is stopped permanently, class fails-to-preserve. Its repro is a flattened command list; the hook's actual shape is if [ -f "$COOLDOWN_MARKER" ]; then … fi, and on bash 3.2.57 that difference decides the outcome:

$ /bin/bash -c 'set -u; LAST_MOD=08; case ...; ELAPSED=$((...)); [ "$ELAPSED" -lt 900 ]'
/bin/bash: line 2: 08: value too great for base
/bin/bash: line 4: ELAPSED: unbound variable         exit=127   <- the issue's shape

$ ... wrapped in if [ -f "$MARK" ]; then ... fi
/bin/bash: line 4: 08: value too great for base
PROCEEDED-TO-COMMIT                                   exit=0    <- the hook's shape

The marker also self-heals — state marker after: True 1786191071. So it is one skipped cooldown plus unexplained stderr, not a permanent stop. misreports, not fails-to-preserve. #329's own docstring already recorded this, measured on 3.2.57 and 5.2.37; the issue contradicted a note that was already correct.

The sites, and there are seven

Site Consequence of the octal read
50-git-backup.sh cooldown marker skipped cooldown + unexplained stderr
50-git-restore.sh _spawn_fetch an in-window fetch gets a second one stacked on it
50-git-restore.sh _fetch_health falls through to the rc branch with _rc empty, so "abandoned" is reported as "FAILED (rc=unknown)" — a remedy offered for a failure that did not happen
git-backup-rejected counter log "ERROR: push REJECTED …" never runs
git-backup-commit-failed counter log "ERROR: commit FAILED …" never runs
git-backup-no-remote counter the no-remote report never runs
git-restore-diverged counter log "ERROR: … DIVERGED …" never runs

The four counters were in neither issue and are the worse half. (Count corrected during review: seven sites, not six — the table below always had seven rows and the sentence above it said six. The two files also spell the guard differently, a 3-line case/esac block in 50-git-backup.sh and a single line in 50-git-restore.sh, so each was verified independently rather than assumed from the first.) A corrupt counter does not mis-count — it deletes the loudest report in the file. That is #257 reached through the counter instead of through exit 0, and it is invisible, because the arithmetic error goes to the detached worker's stderr rather than the hook's. The missing log line is the only detector.

Bounded, which is what stops the sweep spreading: [ "$x" -lt "$y" ] parses base 10 ([ 08 -lt 9 ] is true, no diagnostic). $(( )) is the only sink.

The base was stale, and it changed both issues

main in the shared clone was two commits behind origin/main — missing eeb7a62 (0.17.0) and 7d6f030 (#329). Rebased onto origin/main rather than building on it. Consequences:

Without the rebase, test_the_per_tool_call_path_is_not_touched fails on any branch cut from that clone — it diffs the working tree against origin/main, so a stale local main makes it red for unrelated reasons. That would have been a phantom finding on this PR.

Tests

RED, hooks stashed, on the rebased base:

FAILED tests/test_git_backup_silent_stops_257.py::test_a_corrupt_cooldown_marker_does_not_kill_the_hook[08]
FAILED tests/test_git_backup_silent_stops_257.py::test_a_corrupt_cooldown_marker_does_not_kill_the_hook[09]
FAILED tests/test_git_backup_silent_stops_257.py::test_a_corrupt_failure_counter_does_not_silence_the_commit_failed_report
FAILED tests/test_git_restore_hook_253.py::TestCouldNotCheckIsItsOwnState::test_a_leading_zero_start_time_is_read_as_decimal
4 failed, 4 passed in 39.57s
E  AssertionError: the commit failed and the hook said nothing — a corrupt failure counter suppressed
E    the one report that tells a user their memory is in no git history at all

GREEN: 8 passed in 10.52s. Full suite run: 1523 passed, 43 skipped in 854.17s. Both touched test files carry skipif(sys.platform == "win32"), so the twelve-leg matrix is the only real Windows signal — unverified locally.

Consolidation was considered and deliberately deferred

Seven more case-guarded-then-$(( )) sites remain outside these two hooks: post-tool-hook.sh:302, session-start-hook.sh:875, save-session.sh:342, run-consolidation.sh:213,218, log.sh:1213, lib-lock.sh:191, doctor.sh:261.

That is fifteen sites across four issues, each fixing a subset by hand — which is the mechanism that produced #327 in the first place. It is not done here because a shared _remember_epoch_or_zero helper belongs in log.sh, and changing the sourcing contract for every hook in a repo that has taken ten Windows issues from seven reporters is the wrong risk to bundle into a two-issue PR. Filed separately, and the right shape is a lint rule as much as a helper — $(( )) reaching a case-guarded variable without 10# is mechanically greppable, and ShellCheck does not flag it.

…he hook reads

#327 asked for one 10# in 50-git-backup.sh. The sweep found six sinks across the
two git hooks: the backup cooldown marker, the restore hook's two fetch-state
reads, and all four consecutive-failure counters, whose failing increment
abandons the branch holding the ERROR log.

#327's severity claim does not hold: measured on bash 3.2.57 in the shape the
hook has, the failing arithmetic abandons the if body and the hook exits 0,
commits, and re-stamps -- misreports, not fails-to-preserve. Its docstring half
was already fixed by #329; that note is updated instead.

#324's premise that the test asserts nothing is also wrong -- the #261 legacy
carry-forward transports the dotted marker into the state dir before the
cooldown block. The test now writes through hook_state anyway, because the shim
is not the feature. What was genuinely missing is 08/09 and an assertion on
stderr.

Co-Authored-By: Max <noreply>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant