Skip to content

fix(config): show live Herdr sidebar size for uncommitted worker changes - #21

Merged
undeemed merged 3 commits into
mainfrom
fm/cf-sidebar-live-stats-s3
Oct 1, 2026
Merged

undeemed merged 3 commits into
mainfrom
fm/cf-sidebar-live-stats-s3

Conversation

@undeemed

@undeemed undeemed commented Oct 1, 2026

Copy link
Copy Markdown
Owner

Intent

"why does status never update live and only after a stage/commit?"

"why does the stats not appear under the [worker] spawns???"

Context for reading those asks: the Herdr Agents sidebar shows a size line under each worker (+<added> · −<deleted> · ✎ <files>), fed by the omp extension config/herdr-sidebar.ts (installed as ~/.omp/agent/extensions/code-factory-herdr-sidebar.ts). Two workers were running when the second ask came in, one with commits and one with only uncommitted edits, and neither had a size line under it. The current code:

  • computes the size from git diff --shortstat <base>...HEAD, so only committed work counts and a staged or edited file changes nothing
  • clears the size when a worker has no pull request and no commit yet (if (!pr && !stat.files) stat = {})
  • refreshes only on session_start and agent_end, and a worker usually stays inside one long turn for most of its task, so the line freezes until that turn ends

What Changed

  • config/herdr-sidebar.ts: the size line now comes from git diff --shortstat --merge-base <base>, so it counts committed, staged and unstaged tracked changes. A new diffStat adds untracked, non-ignored files, counting lines the way git does (one for a symlink, none for a binary file). It tries origin/main and then origin/master when the base ref is missing. It no longer clears the size for a worker with no pull request and no commit yet. Instead, an empty diff or no base ref gives no size. The diff runs with diff.autorefreshindex=false so it doesn't rewrite the index.
  • config/herdr-sidebar.ts: a 10-second timer started on session_start (cleared on session_shutdown) refreshes the size during a long turn. These ticks skip the PR and issue lookup and report only when the size differs from the last one reported successfully, so a failed report is retried on the next tick. run() also gets a 64 MiB maxBuffer.
  • docs/herdr.md documents the new size source and refresh cadence. tests/test_herdr_sidebar.py is reworked and adds cases for uncommitted, staged and untracked work, the origin/main fallback, no base ref, an empty diff, the index staying unchanged, size updates without a turn end, and retry after a failed report.

Risk Assessment

⚠️ Medium: The change satisfies all three intent asks (working-tree counting, no clearing on an empty-commit worker, 10 s refresh), and its logic traces correctly for the normal cases. The follow-up risks are a background git diff that can contend for the worker's index.lock, an unbounded untracked-file scan on a 10 s loop, a missed retry after a failed report, and an unrequested base-ref fallback.

Testing

The 69 existing sidebar tests pass. I then drove every scenario under real omp 18.4.2 in an isolated real Herdr server and compared each size with git's own count. This covered uncommitted, staged, untracked and committed work updating live in one idle session, and clearing on an empty diff. It also covered the index-untouched guard, retry after a failed report, git-equivalent untracked counting, the base-ref fallback and the home-pane no-size guard. The base-commit extension showed no size line in the same setup. The sidebar evidence is a text dump of the real Herdr TUI rendered from a pty (pyte). I captured no PNG screenshot, so there is no image of the sidebar. The dump shows the size row under └ work. Not exercised: a real model turn ending (agent_end), because the isolated omp has no model credentials. The 10-second timer and session_start paths are what the change relies on and were driven.

  • Live validation: ✅ go - 11 of 11 scenarios driven live against the product
Scenario Result Live Evidence
Worker with no PR and no commit makes only unstaged edits to tracked files; under real omp the size line +2 · −1 · ✎ 2 appears under └ work in the Herdr Agents sidebar within ~2s, no turn run ✅ pass live real-omp-A-sequence-sidebar.txt (sidebar dump after A1, matches git oracle); real-omp-herdr-tui-full-screen.txt
In the same idle real omp session the worker stages a file and adds an untracked file, commits locally, then edits more; the sidebar size follows each state live (+5 −1 ✎3, unchanged after commit, the… ✅ pass live real-omp-A-sequence-sidebar.txt (A2, A3, A4, each matching the git oracle)
Worker resets to origin/main and cleans the tree; the size tokens and the sidebar line clear ✅ pass live real-omp-A-sequence-sidebar.txt (A5)
Worker with only a local commit and a clean tree shows its size at session start under real omp (+1 ✎1, matching git) ✅ pass live real-omp-scenarios-B-F.txt (B)
Before the change: the base-commit extension under real omp shows no size under the worker for uncommitted edits (25s) or for a local commit in the same session (25s) ✅ pass live real-omp-baseline-pre-change.txt
Adversarial: a tracked file with stale stat info and about 25s of 10s ticks under real omp leave .git/index byte-identical, with no index.lock, and the worker's own git add succeeds ✅ pass live real-omp-scenarios-C-D-E-G.txt (the control shows the plain git diff form rewrites the index)
Adversarial: herdr report-metadata fails while the size changes under real omp; tokens stay empty, then the next tick reports the size with no new edit (6s after recovery) ✅ pass live real-omp-scenarios-C-D-E-G.txt
Adversarial: an untracked ignored file, binary, symlinks (including to a directory and /dev/zero), nested repo and a file without a trailing newline are counted exactly as git counts them (+11, 8 file… ✅ pass live real-omp-scenarios-C-D-E-G.txt
Guard: a home pane (no FM_TASK_ID, no PR) with a dirty checkout and timer ticks reports only who, never a size, under real omp ✅ pass live real-omp-scenarios-C-D-E-G.txt
Base ref (documented behaviour; the review item about the fallback was declined and left to the author): without origin/HEAD the size falls back to origin/main, and with no base ref at all no size is… ✅ pass live real-omp-scenarios-B-F.txt (F1, F2)
Quit: /exit in real omp clears the tokens and removes the sidebar entry, and no omp process remains ✅ pass live real-omp-shutdown.txt; process-list check
Evidence: Real omp + real Herdr TUI: edit/stage/commit/reset sequence with sidebar dump after each step
=== A (REAL omp 18.4.2 in a real herdr pane; real herdr TUI sidebar; extension loaded with `--no-extensions -e <worktree>/config/herdr-sidebar.ts`) ===
omp alive: True | tokens (clean checkout): {}
--- Agents sidebar, clean checkout ---
 spaces                                      │
                                             │
 ○ worker                                    │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
 new                                     menu│
─────────────────────────────────────────────│
 agents                               grouped│
                                             │
 ○ └ work                                    │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                            «│
A1 unstaged edit of two tracked files (no commit, no stage, omp idle)
  [  2.0s] A1 (git oracle {'add': '+2', 'del': '−1', 'files': '✎ 2'}): {'add': '··+2', 'del': '−1', 'files': '✎ 2'}
A2 stage one + untracked file
  [  8.7s] A2 (git oracle {'add': '+5', 'del': '−1', 'files': '✎ 3'}): {'add': '··+5', 'del': '−1', 'files': '✎ 3'}
A3 commit locally (no push / no PR)
  after commit +12s: {'add': '··+5', 'del': '−1', 'files': '✎ 3'} oracle {'add': '+5', 'del': '−1', 'files': '✎ 3'}
A4 more edits on top of the commit
  [  5.5s] A4 (git oracle {'add': '+8', 'del': '−1', 'files': '✎ 3'}): {'add': '··+8', 'del': '−1', 'files': '✎ 3'}
A5 reset --hard origin/main && clean -fd
  [  8.6s] A5 cleared: {}
--- Agents sidebar after A1 ---
 spaces                                      │
                                             │
 ○ worker                                    │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
 new                                     menu│
─────────────────────────────────────────────│
 agents                               grouped│
                                             │
 ○ └ work                                    │
   ⠀⠀+2 · −1 · ✎ 2                           │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                            «│
--- Agents sidebar after A2 ---
 spaces                                      │
                                             │
 ○ worker                                    │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
 new                                     menu│
─────────────────────────────────────────────│
 agents                               grouped│
                                             │
 ○ └ work                                    │
   ⠀⠀+5 · −1 · ✎ 3                           │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                            «│
--- Agents sidebar after A3 ---
 spaces                                      │
                                             │
 ○ worker                                    │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
 new                                     menu│
─────────────────────────────────────────────│
 agents                               grouped│
                                             │
 ○ └ work                                    │
   ⠀⠀+5 · −1 · ✎ 3                           │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                            «│
--- Agents sidebar after A4 ---
 spaces                                      │
                                             │
 ○ worker                                    │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
 new                                     menu│
─────────────────────────────────────────────│
 agents                               grouped│
                                             │
 ○ └ work                                    │
   ⠀⠀+8 · −1 · ✎ 3                           │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                            «│
--- Agents sidebar after A5 ---
 spaces                                      │
                                             │
 ○ worker                                    │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
 new                                     menu│
─────────────────────────────────────────────│
 agents                               grouped│
                                             │
 ○ └ work                                    │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                            «│
{"A0_clean_no_size": true, "A1": {"got": {"add": "+2", "del": "−1", "files": "✎ 2"}, "git": {"add": "+2", "del": "−1", "files": "✎ 2"}, "match": true, "omp_alive": true}, "A2": {"got": {"add": "+5", "del": "−1", "files": "✎ 3"}, "git": {"add": "+5", "del": "−1", "files": "✎ 3"}, "match": true, "omp_alive": true}, "A3": {"got": {"add": "+5", "del": "−1", "files": "✎ 3"}, "git": {"add": "+5", "del": "−1", "files": "✎ 3"}, "match": true}, "A4": {"got": {"add": "+8", "del": "−1", "files": "✎ 3"}, "git": {"add": "+8", "del": "−1", "files": "✎ 3"}, "match": true, "omp_alive": true}, "A5_cleared": true}
Evidence: Full Herdr TUI screen (150x40 pty): Agents sidebar with the size line under the worker, beside the real omp session
 spaces                                      │   1      +
                                             │
 ○ worker                                    │╭─── omp v18.4.2 ──────────────────────────────────────────────────────────────────────────────────╮
                                             ││                          │ Tips                                                                  │
                                             ││      Welcome back!       │ # for prompt actions                                                  │
                                             ││                          │ / for commands                                                        │
                                             ││       ████████████       │ ! to run bash                                                         │
                                             ││          ██  ██          │ $ to run python                                                       │
                                             ││          ██  ██          │ ───────────────────────────────────────────────────────────────────── │
                                             ││          ▒▒  ██          │ LSP Servers                                                           │
                                             ││              ██          │ ● vscode-html-language-server .html .htm                              │
                                             ││                          │ ● vscode-css-language-server .css .scss .sass                         │
                                             ││         Unknown          │ ● vscode-json-language-server .json .jsonc                            │
                                             ││         Unknown          │ ● bashls .sh .bash .zsh                                               │
                                             ││                          │ ───────────────────────────────────────────────────────────────────── │
                                             ││                          │ Recent sessions                                                       │
                                             ││                          │ No recent sessions                                                    │
                                             ││                          │                                                                       │
                                             ││                          │                                                                       │
 new                                     menu││                          │                                                                       │
─────────────────────────────────────────────││                          │                                                                       │
 agents                               grouped│╰──────────────────────────┴───────────────────────────────────────────────────────────────────────╯
                                             │ Tip: Please use nerdfont 😭.
 ○ └ work                                    │
   ⠀⠀+2 · −1 · ✎ 2                           │ Warning: No models available. Use /login or set an API key environment variable. Then use /model to
                                             │ select a model.
                                             │
                                             │───────────────────────────────────────────────────────────────────────────────────────────────────────
                                             │ Update Available
                                             │ New version 18.4.5 is available. Run: omp update
                                             │───────────────────────────────────────────────────────────────────────────────────────────────────────
                                             │
                                             │ π > ⬢ no-model > 🗑 hs/repos/RA/work > ⑂ fm/fix-x > ◫ 6.5K/? ⟲ ▶───────────────────────────────────────
                                             │╰─                                                                         ⇧⇥ to change thinking effort
                                             │
                                             │
                                             │
                                             │
                                             │
                                            «│
Evidence: Real omp: index untouched, failed-report retry, untracked parity, home-pane guard
=== C (real omp, adversarial): stale stat info; the 10s timer must not rewrite .git/index ===
  control: plain `git diff --merge-base` rewrote the index on a copy: True
omp main prompt reached: True pane w3:p1
  [  0.0s] C size shown: {'add': '··+1', 'del': '−0', 'files': '✎ 1'}
  after ~25s of timer ticks (real omp alive: True ): size {'add': '··+1', 'del': '−0', 'files': '✎ 1'} | index unchanged: True | index.lock: False
  worker `git add` rc: 0
after /exit tokens: {}
=== D (real omp, adversarial): herdr report-metadata fails while the size changes; next tick retries ===
omp main prompt reached: True pane w4:p1
  armed herdr failure + edit (+2); waiting 14s
  tokens during failure: {}
  herdr healthy; no further edit
  [  6.0s] D size appears from retry alone: {'add': '··+2', 'del': '−0', 'files': '✎ 1'}
after /exit tokens: {}
=== E (real omp): untracked parity with git (ignored, binary, symlinks, nested repo, no final newline) ===
omp main prompt reached: True pane w5:p1
  [  0.0s] E size: {'add': '··+11', 'del': '−0', 'files': '✎ 8'}
  git oracle: {'add': '+11', 'del': '−0', 'files': '✎ 8'} | extension: {'add': '+11', 'del': '−0', 'files': '✎ 8'}
after /exit tokens: {}
=== G (real omp, guard): home pane (no FM_TASK_ID, no PR) with dirty checkout never shows size ===
omp main prompt reached: True pane w6:p1
  home pane tokens after >1 tick: {'who': 'home-G'}
after /exit tokens: {}
{"control_plain_diff_rewrites_index": true, "index_unchanged_after_ticks": true, "worker_git_add_ok": true, "D_not_reported_while_failing": true, "D_retried": true, "E_parity": true, "G_home_no_size": true}
Evidence: Real omp: commit-only worker at session start; base-ref fallback and no-base cases
=== B (real omp): worker with only a local commit; size shows at session start ===
omp main prompt reached: True pane w1:p1
  [  0.0s] B size from committed work only (nothing uncommitted): {'add': '··+1', 'del': '−0', 'files': '✎ 1'}
  git oracle: {'add': '+1', 'del': '−0', 'files': '✎ 1'} | status: clean
after /exit tokens: {}
=== F1 (real omp): no origin/HEAD, origin/main present -> falls back to origin/main ===
  refs: ['refs/remotes/origin/main']
omp main prompt reached: True pane w2:p1
  [  0.0s] F1 size via origin/main fallback: {'add': '··+1', 'del': '−0', 'files': '✎ 1'}
after /exit tokens: {}
=== F2 (real omp): no origin/HEAD and no origin/main/master -> no size ===
  refs: none
omp main prompt reached: True pane w3:p1
  tokens after >1 tick: {} | omp alive: True
after /exit tokens: {}
{"B": true, "F1": true, "F2": true}
Evidence: Real omp: base-commit extension shows no size line (before the change)
=== BASELINE under REAL omp: base-commit extension (f1a6d76) loaded with `--no-extensions -e <copy>` ===
omp main prompt reached: True pane w2:p1
edited two tracked files (uncommitted); waiting 25s (> the new 10s tick)
  tokens: {}
--- sidebar (old code, uncommitted edits) ---
 spaces                                      │
 · worker                                    │
 ○ worker-old                                │
 new                                     menu│
─────────────────────────────────────────────│
 agents                               grouped│
 ○ └ work                                    │
                                            «│
committed locally; waiting 25s inside the same live session
  tokens: {}
--- sidebar (old code, after commit) ---
 spaces                                      │
 · worker                                    │
 ○ worker-old                                │
 new                                     menu│
─────────────────────────────────────────────│
 agents                               grouped│
 ○ └ work                                    │
                                            «│
after /exit tokens: {}
{"old_uncommitted_shows_size": false, "old_commit_shows_size_live": false}
Evidence: Real omp: /exit clears tokens and removes the sidebar entry
 spaces                                      │   1      +
                                             │
 ○ worker                                    │╭─── omp v18.4.2 ──────────────────────────────────────────────────────────────────────────────────╮
                                             ││                          │ Tips                                                                  │
                                             ││      Welcome back!       │ # for prompt actions                                                  │
                                             ││                          │ / for commands                                                        │
                                             ││       ████████████       │ ! to run bash                                                         │
                                             ││          ██  ██          │ $ to run python                                                       │
                                             ││          ██  ██          │ ───────────────────────────────────────────────────────────────────── │
                                             ││          ▒▒  ██          │ LSP Servers                                                           │
                                             ││              ██          │ ● vscode-html-language-server .html .htm                              │
                                             ││                          │ ● vscode-css-language-server .css .scss .sass                         │
                                             ││         Unknown          │ ● vscode-json-language-server .json .jsonc                            │
                                             ││         Unknown          │ ● bashls .sh .bash .zsh                                               │
                                             ││                          │ ───────────────────────────────────────────────────────────────────── │
                                             ││                          │ Recent sessions                                                       │
                                             ││                          │ No recent sessions                                                    │
                                             ││                          │                                                                       │
                                             ││                          │                                                                       │
 new                                     menu││                          │                                                                       │
─────────────────────────────────────────────││                          │                                                                       │
 agents                               grouped│╰──────────────────────────┴───────────────────────────────────────────────────────────────────────╯
                                             │ Tip: Please use nerdfont 😭.
 ○ └ work                                    │
   ⠀⠀+2 · −1 · ✎ 2                           │ Warning: No models available. Use /login or set an API key environment variable. Then use /model to
                                             │ select a model.
                                             │
                                             │───────────────────────────────────────────────────────────────────────────────────────────────────────
                                             │ Update Available
                                             │ New version 18.4.5 is available. Run: omp update
                                             │───────────────────────────────────────────────────────────────────────────────────────────────────────
                                             │
                                             │ π > ⬢ no-model > 🗑 hs/repos/RA/work > ⑂ fm/fix-x > ◫ 6.5K/? ⟲ ▶───────────────────────────────────────
                                             │╰─                                                                         ⇧⇥ to change thinking effort
                                             │
                                             │
                                             │
                                             │
                                             │
                                            «│

quitting omp with /exit
omp UI gone: False | screen tail: ────────────── ⏎ ╰─                                                                         ⇧⇥ to change thinking effort ⏎ $
pane process-info: {}
tokens after real omp exit: {}
sidebar after exit:
 spaces                                      │
                                             │
 · worker                                    │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
 new                                     menu│
─────────────────────────────────────────────│
 agents                               grouped│
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                             │
                                            «│
  • Evidence: Worker-size pytest results (8 passed) (local file: ~/.no-mistakes/evidence/01M3TDR33V67QD2E73F3588E0V/pytest-worker-tests.txt)
  • Evidence: Earlier stub-host runs (fake pi events, real extension/Herdr/git), corroboration only (local file: ~/.no-mistakes/evidence/01M3TDR33V67QD2E73F3588E0V/scenario-A-live-updates.txt)
Evidence: Agents sidebar rendered by the real Herdr TUI with real omp, worker with uncommitted edits only
○ └ work
⠀⠀+2 · −1 · ✎ 2
(clean checkout: line absent; base-commit extension: line absent; after reset --hard + clean: line removed)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 5 warnings
  • ⚠️ config/herdr-sidebar.ts:86 - The size now comes from git diff --shortstat --merge-base &lt;ref&gt;, which compares against the working tree. It runs every 10 s inside every worker's checkout. The old &lt;base&gt;...HEAD form never touched the index or working tree. I checked this in a scratch repo: after touch f on a tracked file, this git diff rewrites .git/index, so it must take .git/index.lock. It does so whenever tracked files have stale stat info, such as after a touch, checkout, rebase or stash, or just after a git add (racy timestamps). A worker's own git add, git commit or git stash that lands in that window fails with fatal: Unable to create &#39;.git/index.lock&#39;: File exists. The background timer, not the worker, causes that failure. git --no-optional-locks and GIT_OPTIONAL_LOCKS=0 do not prevent it for diff; I verified both still rewrite the index. -c diff.autorefreshindex=false does prevent it. Fix: run the diff as git -c diff.autorefreshindex=false diff --shortstat --merge-base &lt;ref&gt;. ls-files --others and rev-parse do not write the index, so line 86 is the only site.
  • ⚠️ config/herdr-sidebar.ts:234 - stat is assigned at line 232 before report() runs. The timer tick then decides whether to report by comparing JSON.stringify(stat) with before. Sequence: a long turn, the size changes, the 10 s tick sets stat and calls report(), and report() throws (Herdr restarting or timing out). The outer catch swallows the error. At the next tick before already equals stat, so nothing is reported. The sidebar keeps the old size until the size changes again or the turn ends, which is the 'does not update live' symptom this change is meant to fix. Fix: keep a reported snapshot that is set only after await report() succeeds, and compare stat against it rather than against the pre-refresh value. The turn-end and lookup paths report every time, so they are unaffected.
  • ⚠️ config/herdr-sidebar.ts:91 - The new untracked-file counting runs every 10 s per worker instead of once per turn end, and three things go wrong with it. (1) readFile loads each whole file, including large binaries, and only afterwards checks the first 8000 bytes for a NUL. A worker with a few hundred MB of unignored output (screenshots, logs, downloads) therefore re-reads all of it every cycle, then toString(&#39;latin1&#39;).split(&#39;\n&#39;) allocates a string and array the size of each text file (line 93). The ponytail: comment on line 88 names this ceiling, but the timer multiplies its cost. (2) readFile follows symlinks. git counts an untracked symlink as 1 file with 1 line, which I checked with git add -N and --shortstat. The code counts the target's line count instead. A symlink to a directory hits EISDIR and counts 0 lines. A symlink to a huge file or a special file reads the target in full. A node_modules/ ignore pattern does not match a symlink, so this case is reachable. (3) run uses execFile's default 1 MiB maxBuffer. A large untracked listing (a tree with roughly 17k or more unignored files) makes ls-files reject. The .catch(() =&gt; stat) at line 232 then silently freezes the old size indefinitely. Behavior-preserving fix: lstat first and count a symlink as 1 line; open each file and read only the first 8000 bytes for the binary check, so binaries are never fully read; raise maxBuffer or stream the ls-files output. A hard cap on bytes or files would change the displayed counts, so that is the author's decision. The docs/herdr.md sentence 'counted as a new file (its lines, or none for a binary file)' and the comment on line 87 claim git parity that symlinks break.
  • ⚠️ config/herdr-sidebar.ts:79 - Component not required by the intent: the origin/main, then origin/master base fallback in the for (const r of [base, &#34;origin/main&#34;, &#34;origin/master&#34;]) loop. The intent names three defects: committed-only counting, clearing the size when there is no PR or commit, and refreshing only on session_start and agent_end. None of them asks for a different base-ref resolution. The change also reverses the existing documented contract (a checkout without origin/HEAD shows no counts until its PR opens; git remote set-head origin --auto adds the ref). It rewrites test_worker_without_origin_head_shows_no_size into ..._falls_back_to_origin_main (tests/test_herdr_sidebar.py:456) and adds a no-base test (tests/test_herdr_sidebar.py:462). The fallback can also compare against a stale or unrelated origin/main when the repo's default branch is something else. Smallest honest remedy: remove the fallback so base is the only ref, restore the old no-origin/HEAD test and doc sentence, and drop the extra test. This needs the author's decision because it was a deliberate behavior change.
  • ⚠️ tests/test_herdr_sidebar.py:478 - The 10 s refresh is the fix for the first ask ('status never update live'). No added test drives it. worker_report calls session_start once and reads the last report. Every new test passes through the session_start refresh, which the old code also ran, so only the working-tree counting is covered. Deleting setInterval at config/herdr-sidebar.ts:248, or breaking the lookup=false skip or the report-on-change gate at line 234, would leave the whole suite green. The reported failure (the size line frozen inside one long turn) is not reproduced. Add a test that runs the real extension: call session_start, edit or stage a file in the checkout, wait past one tick (about 11 s, or use Bun fake timers), and assert a second report-metadata call carries the new counts. A tick with no change should add no report.

🔧 Fix applied.
5 warnings still open:

  • ⚠️ config/herdr-sidebar.ts:86 - The size now comes from git diff --shortstat --merge-base &lt;ref&gt;, which compares against the working tree. It runs every 10 s inside every worker's checkout. The old &lt;base&gt;...HEAD form never touched the index or working tree. I checked this in a scratch repo: after touch f on a tracked file, this git diff rewrites .git/index, so it must take .git/index.lock. It does so whenever tracked files have stale stat info, such as after a touch, checkout, rebase or stash, or just after a git add (racy timestamps). A worker's own git add, git commit or git stash that lands in that window fails with fatal: Unable to create &#39;.git/index.lock&#39;: File exists. The background timer, not the worker, causes that failure. git --no-optional-locks and GIT_OPTIONAL_LOCKS=0 do not prevent it for diff; I verified both still rewrite the index. -c diff.autorefreshindex=false does prevent it. Fix: run the diff as git -c diff.autorefreshindex=false diff --shortstat --merge-base &lt;ref&gt;. ls-files --others and rev-parse do not write the index, so line 86 is the only site.
  • ⚠️ config/herdr-sidebar.ts:234 - stat is assigned at line 232 before report() runs. The timer tick then decides whether to report by comparing JSON.stringify(stat) with before. Sequence: a long turn, the size changes, the 10 s tick sets stat and calls report(), and report() throws (Herdr restarting or timing out). The outer catch swallows the error. At the next tick before already equals stat, so nothing is reported. The sidebar keeps the old size until the size changes again or the turn ends, which is the 'does not update live' symptom this change is meant to fix. Fix: keep a reported snapshot that is set only after await report() succeeds, and compare stat against it rather than against the pre-refresh value. The turn-end and lookup paths report every time, so they are unaffected.
  • ⚠️ config/herdr-sidebar.ts:91 - The new untracked-file counting runs every 10 s per worker instead of once per turn end, and three things go wrong with it. (1) readFile loads each whole file, including large binaries, and only afterwards checks the first 8000 bytes for a NUL. A worker with a few hundred MB of unignored output (screenshots, logs, downloads) therefore re-reads all of it every cycle, then toString(&#39;latin1&#39;).split(&#39;\n&#39;) allocates a string and array the size of each text file (line 93). The ponytail: comment on line 88 names this ceiling, but the timer multiplies its cost. (2) readFile follows symlinks. git counts an untracked symlink as 1 file with 1 line, which I checked with git add -N and --shortstat. The code counts the target's line count instead. A symlink to a directory hits EISDIR and counts 0 lines. A symlink to a huge file or a special file reads the target in full. A node_modules/ ignore pattern does not match a symlink, so this case is reachable. (3) run uses execFile's default 1 MiB maxBuffer. A large untracked listing (a tree with roughly 17k or more unignored files) makes ls-files reject. The .catch(() =&gt; stat) at line 232 then silently freezes the old size indefinitely. Behavior-preserving fix: lstat first and count a symlink as 1 line; open each file and read only the first 8000 bytes for the binary check, so binaries are never fully read; raise maxBuffer or stream the ls-files output. A hard cap on bytes or files would change the displayed counts, so that is the author's decision. The docs/herdr.md sentence 'counted as a new file (its lines, or none for a binary file)' and the comment on line 87 claim git parity that symlinks break.
  • ⚠️ config/herdr-sidebar.ts:79 - Component not required by the intent: the origin/main, then origin/master base fallback in the for (const r of [base, &#34;origin/main&#34;, &#34;origin/master&#34;]) loop. The intent names three defects: committed-only counting, clearing the size when there is no PR or commit, and refreshing only on session_start and agent_end. None of them asks for a different base-ref resolution. The change also reverses the existing documented contract (a checkout without origin/HEAD shows no counts until its PR opens; git remote set-head origin --auto adds the ref). It rewrites test_worker_without_origin_head_shows_no_size into ..._falls_back_to_origin_main (tests/test_herdr_sidebar.py:456) and adds a no-base test (tests/test_herdr_sidebar.py:462). The fallback can also compare against a stale or unrelated origin/main when the repo's default branch is something else. Smallest honest remedy: remove the fallback so base is the only ref, restore the old no-origin/HEAD test and doc sentence, and drop the extra test. This needs the author's decision because it was a deliberate behavior change.
  • ⚠️ config/herdr-sidebar.ts:103 - Carried over from round 1 and not yet decided. The origin/main, then origin/master fallback in for (const r of [base, &#34;origin/main&#34;, &#34;origin/master&#34;]) is not needed by the intent. The intent names three defects: committed-only counting, clearing the size when there is no PR or commit, and refreshing only on session_start and agent_end. None of them asks for a different base-ref resolution. The fallback also reverses the documented contract that a checkout without origin/HEAD shows no counts until its PR opens, and docs/herdr.md and the header comment now describe it. It can give a silently wrong number. Take a repo whose default branch is develop, with no origin/HEAD but a stale origin/main. The diff is then taken against that stale branch and the sidebar shows inflated counts. A PR whose base ref was never fetched falls through to origin/main the same way, where before it kept the previous value. Tests tied to the fallback: test_worker_without_origin_head_falls_back_to_origin_main (tests/test_herdr_sidebar.py:482) and test_worker_without_any_base_ref_shows_no_size (tests/test_herdr_sidebar.py:488). Smallest remedy: use base as the only ref. Restore the original no-origin/HEAD test and the doc sentence, and drop the extra test. The remedy changes deliberate behavior, so the author must decide.
✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 11 of 11 scenarios driven live against the product
Scenario Result Live Evidence
Worker with no PR and no commit makes only unstaged edits to tracked files; under real omp the size line +2 · −1 · ✎ 2 appears under └ work in the Herdr Agents sidebar within ~2s, no turn run ✅ pass live real-omp-A-sequence-sidebar.txt (sidebar dump after A1, matches git oracle); real-omp-herdr-tui-full-screen.txt
In the same idle real omp session the worker stages a file and adds an untracked file, commits locally, then edits more; the sidebar size follows each state live (+5 −1 ✎3, unchanged after commit, the… ✅ pass live real-omp-A-sequence-sidebar.txt (A2, A3, A4, each matching the git oracle)
Worker resets to origin/main and cleans the tree; the size tokens and the sidebar line clear ✅ pass live real-omp-A-sequence-sidebar.txt (A5)
Worker with only a local commit and a clean tree shows its size at session start under real omp (+1 ✎1, matching git) ✅ pass live real-omp-scenarios-B-F.txt (B)
Before the change: the base-commit extension under real omp shows no size under the worker for uncommitted edits (25s) or for a local commit in the same session (25s) ✅ pass live real-omp-baseline-pre-change.txt
Adversarial: a tracked file with stale stat info and about 25s of 10s ticks under real omp leave .git/index byte-identical, with no index.lock, and the worker's own git add succeeds ✅ pass live real-omp-scenarios-C-D-E-G.txt (the control shows the plain git diff form rewrites the index)
Adversarial: herdr report-metadata fails while the size changes under real omp; tokens stay empty, then the next tick reports the size with no new edit (6s after recovery) ✅ pass live real-omp-scenarios-C-D-E-G.txt
Adversarial: an untracked ignored file, binary, symlinks (including to a directory and /dev/zero), nested repo and a file without a trailing newline are counted exactly as git counts them (+11, 8 file… ✅ pass live real-omp-scenarios-C-D-E-G.txt
Guard: a home pane (no FM_TASK_ID, no PR) with a dirty checkout and timer ticks reports only who, never a size, under real omp ✅ pass live real-omp-scenarios-C-D-E-G.txt
Base ref (documented behaviour; the review item about the fallback was declined and left to the author): without origin/HEAD the size falls back to origin/main, and with no base ref at all no size is… ✅ pass live real-omp-scenarios-B-F.txt (F1, F2)
Quit: /exit in real omp clears the tokens and removes the sidebar entry, and no omp process remains ✅ pass live real-omp-shutdown.txt; process-list check
  • python3 -m pytest tests/test_herdr_sidebar.py -q (69 passed)
  • python3 -m pytest tests/test_herdr_sidebar.py -k worker -v (8 worker-size tests passed)
  • Real omp 18.4.2 run live in an isolated real Herdr 0.9.3 server (temp HOME/XDG, Agents layout from config/default.yml). The extension was loaded with omp --no-extensions -e &lt;worktree&gt;/config/herdr-sidebar.ts, with FM_TASK_ID set, in scratch git clones. Tokens were read with herdr pane get and compared with a git oracle (git add -N + git diff --shortstat). A real Herdr TUI client ran in a 150x40 pty (TIOCSWINSZ set before attaching), rendered through pyte from uv's ephemeral cache.
  • Real-omp A: unstaged edit, stage plus untracked file, local commit, more edits, then reset --hard + clean. Sizes were +2 −1 ✎2, then +5 −1 ✎3, then the same after the commit, then +8 −1 ✎3, then cleared. The sidebar line appeared and disappeared with them.
  • Real-omp B: a worker with only a local commit shows +1 ✎1 at session start, matching the oracle with a clean working tree.
  • Real-omp C: with a stale mtime on a tracked file, a control showed plain git diff --merge-base rewrites .git/index. During about 25s of timer ticks the index was byte-identical, with no index.lock, and the worker's git add succeeded.
  • Real-omp D: with HERDR_BIN_PATH pointing at a wrapper that fails report-metadata, a size edit was reported nowhere for 14s. After recovery the size appeared 6s later with no new edit.
  • Real-omp E: untracked parity with git for an ignored file, a binary, symlinks (including to a directory and /dev/zero), a nested repo and a file without a final newline. The extension and the oracle both gave +11, 8 files.
  • Real-omp F1/F2: with no origin/HEAD the size comes from origin/main (+1 ✎1). With no base ref at all there are no tokens.
  • Real-omp G: a home pane (no FM_TASK_ID, no PR) with a dirty checkout reported only who, never a size.
  • Real-omp baseline: the extension from f1a6d76 loaded the same way showed no tokens and no sidebar line for uncommitted edits (25s), and none after a local commit in the same session (25s).
  • Quit and cleanup: /exit returned each pane to a shell prompt and cleared the tokens, and the worker entry left the sidebar. A ps check found no omp, host.ts or TUI-capture process from my runs. I stopped the isolated Herdr servers and removed /tmp/hs. The worktree git status --ignored is clean, including the ignored pytest and pycache directories I removed.
  • Earlier stub-host runs (host.ts) loaded the real extension and fired fake pi events against real Herdr and git. They are corroboration only; every scenario above was then re-driven under real omp.
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

@undeemed
undeemed merged commit 10758a5 into main Oct 1, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant