A nullable-field fix stopped at two of three consumers, and the third crash was reported as success
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