Skip to main content

A nullable-field fix stopped at two of three consumers, and the third crash was reported as success

lesson · created 2026-09-27 · actor brain-distill · confidence high · status active tags: portfolio null-handling silent-failure subprocess exit-code cron

Observed 2026-09-27 05:00-05:20 AEST on Zappo (macbook-pro-3).

On 2026-09-20 entry was allowed to become null in ~/clawd/skills/portfolio-tracker/data/holdings.json to carry a CBA Employee Share Plan holding whose cost basis the registry does not state. Commit 7953f95 fixed two consumers, portfolio-pricer.py and vix_portfolio_advisor.py, and both are still healthy (live re-run 2026-09-27: pricer exits 0, advisor still blocks trade advice for CXO/ILU/MAET).

A third consumer was missed. ~/clawd/scripts/UnifiedPortfolio.py:36 copies h["entry"] into a working row and :104 computes pnl = (price - entry) * qty, so a null basis raises on the first MUFG row. Live reproduction 2026-09-27 with OUTPUT_FILE redirected:

TypeError: unsupported operand type(s) for *: 'int' and 'NoneType' (exit 1)

Skipping rows whose entry is None makes the same file exit 0 and write a snapshot, so the null is the sole cause.

The crash then hid behind a success report. github-sync-orchestrator.py:193 launches it as a bare subprocess.run([...]) and never checks the return code, so its own exit is 0 (live 2026-09-27) while the child's traceback goes to stderr. The Hermes wrapper ~/.hermes/scripts/github_sync_report.py gates on the orchestrator's exit code and on an error/failed/leak substring in stdout, so the nightly nightly-github-sync job (c737c1447aa8, 02:00) prints "synced successfully" and its output file carries no traceback.

Result: memory/unified-portfolio.json has not changed since 2026-09-20T02:00:58.245383, while the holding that breaks it landed at 13:17 that day. The nightly job has run seven times since (2026-09-21..2026-09-27) and its generated artifact has been byte-identical across every one, including the 2026-09-27 02:00 commit 0ceb0be.

Rule: when a field is allowed to become null, enumerate every consumer before shipping the fix, and make each one reject the null loudly. A nested subprocess without a return-code check converts a hard failure into a reported success.

Do not assume "the two scripts I fixed" is the full set: grep for the field, not for the call sites you remember.

Sources: live Python and orchestrator runs 2026-09-27; ~/clawd/scripts/UnifiedPortfolio.py (lines 11, 36, 91, 104); ~/clawd/scripts/github-sync-orchestrator.py (line 193); ~/.hermes/scripts/github_sync_report.py (lines 24-43); ~/clawd/memory/unified-portfolio.json (timestamp 2026-09-20T02:00:58.245383); ~/.hermes/cron/output/c737c1447aa8/ (seven runs); clawd git 0ceb0be, 2c7909a, 7953f95; ~/.hermes/cron/jobs.json job c737c1447aa8.


Sources: /Users/dwayne/clawd/scripts/UnifiedPortfolio.py · /Users/dwayne/clawd/scripts/github-sync-orchestrator.py · /Users/dwayne/.hermes/scripts/github_sync_report.py · /Users/dwayne/clawd/memory/unified-portfolio.json · /Users/dwayne/.hermes/cron/output/c737c1447aa8 · /Users/dwayne/clawd/skills/portfolio-tracker/data/holdings.json