diff --git a/.wolf/buglog.json b/.wolf/buglog.json index df7868cd..19deff58 100644 --- a/.wolf/buglog.json +++ b/.wolf/buglog.json @@ -28068,14 +28068,14 @@ "id": "bug-1866", "gitea_synced_status": "open", "gitea_issue": 285, - "status": "open", - "status_updated": "2026-09-25", - "status_note": "remove_worker never frees worker->launch_type", + "status": "fixed", + "status_updated": "2026-09-27", + "status_note": "e84a8d43: remove_worker now also frees launch_type and q_pre/q_post/job_pre/job_post_exec (5 strings leaked per finished job, not just launch_type)", "timestamp": "2026-09-25T15:42:02Z", "error_message": "LeakSanitizer at xcecd_virtual exit (burst run 2026-09-25): 'Direct leak of 12 byte(s) in 12 object(s)' from strndup in add_worker (xcecd_worker_utils.c:140) <- xcecd_process_command <- handle_message_from_queue", "file": "src/worker/src/xcecd_worker_utils.c", "root_cause": "add_worker strndup's startinfo->launch_type into worker->launch_type, but remove_worker frees exechome/execcwd/execuser/job_cmd/pids/job_exdata and never launch_type -- one string leaked per finished job that carried a launch type.", - "fix": "", + "fix": "e84a8d43: remove_worker frees launch_type, q_pre_exec, q_post_exec, job_pre_exec, job_post_exec. Regression test test_remove_worker_frees_every_startinfo_copy (ASAN live-heap byte delta across add/remove; RED leaked 58 B).", "tags": [ "worker", "leak", diff --git a/.wolf/cerebrum.md b/.wolf/cerebrum.md index fda6800d..03d7b28a 100644 --- a/.wolf/cerebrum.md +++ b/.wolf/cerebrum.md @@ -152,6 +152,7 @@ - **(2026-08-26) A hand-rolled multi-connection mock TCP/TLS server in a test file needs a genuinely FINITE accept loop (`for handler in handlers:`), not `while self._handlers:` guarding a list that's never mutated — the latter is an always-true condition that leaves the server thread blocked in `accept()` indefinitely once the test's real client stops connecting, which does not fail the test but silently adds `thread.join(timeout=N)`'s FULL timeout to every such test's teardown (measured: 3 tests each added a flat 5s of pure teardown wait with zero effect on pass/fail).** Match the mock's handler-list length exactly to the number of connections the test actually expects (assert on `connection_count` for the "stops on its own budget" property instead of over-provisioning handlers "just in case") and additionally set a short `socket.settimeout()` on the listener as a safety net so a hypothetical future regression that causes MORE connections than expected still fails within one timeout window instead of silently reintroducing the same multi-second stall. - **[2026-09-24: open/closed now lives in each entry's `status` field (`.claude/rules/buglog-status.md`), not in `tags` or `fix` text.]** **(2026-09-19, `/xce-audit-invariants scheduler` after `scheduler-sim-live-allocation`) A buglog entry's `tags` array can say `"fixed"` while its own `fix` text says `"Not yet fixed"` — a self-contradiction that survives silently unless something actually re-reads the entry, because most tooling in this project checks tags, not the narrative field.** Found on bug-1682 (and its unnoticed duplicate, bug-1492 — same defect, `plugin_end_session()` guarding on `begin_session_fn` instead of `end_session_fn`, independently filed twice 7 weeks apart because bug-1681's investigation never cross-referenced the earlier entry) via an audit subagent that read BOTH the code and the buglog text, not just the tags — it caught that `plugin.c:441` was still visibly broken despite the tags claiming otherwise. **Lesson 1**: when closing out a large feature and bulk-stamping buglog entries as fixed/superseded, verify each stamped entry's `fix` narrative actually matches the tag before writing it — a tag-only update is a claim with no evidence behind it. **Lesson 2**: before filing a new bug for a defect discovered "while fixing X," grep buglog.json for the exact symptom text first (e.g. `grep -i "begin_session_fn.*end_session_fn"`) — this exact one-line guard/call mismatch was independently re-discovered and re-filed as a "new" bug once already in this project's history. - **(2026-09-26, bug-1754) A worker reports a job that exits NONZERO as a FAIL fact, not a FINISH: `xcecd_job_finished()` (worker/src/controller_connector.c) calls `xcecd_notify_job_fail(jobid, stage, exit_code)` whenever `exit_code != 0`, and only sends `xcecli_notify_jobfinish` for exit 0.** So `on_job_finish`/WAL_JOB_FINISH only ever sees successful jobs in production, and anything about a failed job's exit code (exit_info, JOB_FAIL hook payloads, apiserver notify) lives on the `on_job_fail` -> `record_job_fail_fact` -> WAL_JOB_FAIL path, where `job_failinfo_t.code` is the job's real exit code for MAIN-stage failures (but the post-exec script's code for POST_EXEC/Q_POST_EXEC, and a -errno for controller-internal `mark_job_fail` callers). Check both paths before assuming a job-exit fix is complete. +- **(2026-09-27, bug-1866) Worker (and most) meson tests run with `ASAN_OPTIONS=detect_leaks=0`, so a leak regression test can't rely on LeakSanitizer.** Compare `__sanitizer_get_current_allocated_bytes()` before/after the alloc/free round trip instead (after one warm-up round trip to settle lazy allocations), guarded by `#ifdef __SANITIZE_ADDRESS__` with a `TEST_IGNORE_MESSAGE` fallback. The nix GCC toolchain does NOT ship `` — declare `size_t __sanitizer_get_current_allocated_bytes(void);` yourself. A byte-exact delta also sizes the leak: bug-1866's 58 B showed five leaked strings, not the one LSan had reported. ## Do-Not-Repeat - **(2026-09-24) Never hand-edit or re-serialize `.wolf/buglog.json` to log a bug or change its status; use the `buglog` skill (`scripts/buglog.py add | status | note | recur | list | show | triage`).** Each entry's `status` (open / partial / investigating / fixed / cannot-reproduce / wontfix / duplicate, defined in `.claude/rules/buglog-status.md`) is the only source of truth for open/closed. Never infer it from `fix` text or `tags`; both drifted (bug-1772, bug-1682), and gitea-sync closed unfixed bugs by trusting a non-empty `fix` (bug-1855). `add` picks max id + 1. OpenWolf's hook uses entry count + 1, which collides, so its auto-detection is off (bug-1856). After a fix commit: `scripts/buglog.py status fixed --note `.