The buglog was one 27K-line JSON file whose fields covered only symptom,
root cause and fix, as free text: 2% of entries named the code version a
bug was found on, 12% the build profile, 36% a way to reproduce, and 58% of
the file was OpenWolf hook auto-detections ("Missing guard clause",
"Function not marked async" on .md files).
- .claude/skills/buglog/bug-record.md: the record standard. Eight
sections (Summary as the title, Description, Environment, Reproduction;
Root cause, Fix, Verification, References once fixed), each with the
content it must contain -- e.g. Environment needs the OS, the libc the
failing binary loads (from its ELF interpreter: glibc 2.42 in the nix
shell vs 2.40 on the host), toolchain, build profile and commit.
- .wolf/bugs/<id>.md: 500 records migrated from buglog.json; every field
verified to round-trip. They carry legacy: true and an explicit "not
recorded" where the old log had no such field; component derived from
file paths, severity from keywords (else unknown), fix_commits from
hashes in status_note/fix that git resolves (91 of 463 fixed).
- .wolf/buglog-archive.json: the 710 hook auto-detections, verbatim.
- .wolf/buglog.json is now an index generated from the records, keeping
the fields OpenWolf's hooks and `openwolf bug search` read.
- scripts/buglog.py rewritten: new (template + Environment collected from
the machine and --binary), check, status (refuses a status the record
doesn't support; fixed needs Root cause, Fix, Verification and
--commit hashes git resolves), note, recur, set, show, list, triage,
env, index. 29 unittest cases in scripts/tests/test_buglog.py.
- scripts/git-hooks/pre-commit runs `check --staged`: incomplete staged
records, a legacy record hand-changed to fixed without the fix-time
sections, a new record claiming legacy: true, or a stale index refuse
the commit.
- scripts/gitea-sync.py reads the records: issue body = the record's
sections plus linked fix commits (origin remote, GITEA_COMMIT_REPO to
override), and a body hash so an issue's text is refreshed when the
record changes.
- bug-1866 rewritten to the full standard as the worked example.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
38 lines
5.1 KiB
Markdown
38 lines
5.1 KiB
Markdown
---
|
|
id: "bug-1748"
|
|
status: "fixed"
|
|
status_updated: "2026-09-24"
|
|
status_note: ""
|
|
component: "controller"
|
|
severity: "unknown"
|
|
timestamp: "2026-09-01T02:00:00Z"
|
|
last_seen: "2026-09-19T09:00:00Z"
|
|
occurrences: 1
|
|
files: ["src/controller/resync.c"]
|
|
tags: ["worker-rejoin-resync", "task-3.2", "resync-c", "requirement-4.4", "requirement-4.8", "resync_apply_running_entry", "record_job_reconcile_assign_fact", "guard-logic", "short-circuit", "review-caught", "test-baked-in-bug"]
|
|
related: ["bug-1747", "bug-1697", "bug-1631"]
|
|
legacy: true
|
|
---
|
|
|
|
# bug-1748: worker-rejoin-resync task 3.2 (resync_apply_running_entry, resync.c), round-2 independent review…
|
|
|
|
## Description
|
|
worker-rejoin-resync task 3.2 (resync_apply_running_entry, resync.c), round-2 independent review: the round-1 fix for the PEND/unassigned-job regression (bug-1747-adjacent, not itself in bug-1747) introduced a NEW Requirement 4.4 violation. The round-1 guard `if (!resync_job_is_terminal(job->job.state) && resync_job_assigned_to_host(job, w->host.name)) { no-op } else { record_job_reconcile_assign_fact(...) }` short-circuits the `&&` to false whenever job->job.state IS terminal, falling into the `else` branch and calling record_job_reconcile_assign_fact() on an ALREADY-TERMINAL (DONE/EXIT) job -- which has no backward-transition guard of its own and unconditionally sets job->job.state = JOB_STAT_RUN, clobbering a terminal job back to RUN. Confirmed live and, worse, CERTIFIED AS CORRECT by the round-1 implementation's own test: test_apply_report_all_five_outcomes_updates_job_table seeded j_extra at JOB_STAT_DONE with a comment claiming this 'satisfies Req 4.8's trigger,' then asserted j_extra->job.state == JOB_STAT_RUN afterward and the suite reported PASS -- the test baked the bug in as an expected outcome instead of catching it.
|
|
|
|
## Environment
|
|
_Not recorded (migrated from buglog.json)._
|
|
|
|
## Reproduction
|
|
_Not recorded (migrated from buglog.json)._
|
|
|
|
## Root cause
|
|
Requirement 4.8's actual trigger condition is 'no existing non-terminal record of that job assigned elsewhere' (requirements.md AC 4.8; record_job_reconcile_assign_fact's own doc comment and design.md's Service Interface Preconditions: 'job is non-terminal ... or has no existing non-terminal assignment'). Terminal-vs-non-terminal is NOT the axis that decides assign-eligibility on its own -- Requirement 4.4 ('shall not overwrite that state with a less final or older report') independently and unconditionally forbids ever assign-routing a terminal job, regardless of host. The round-1 guard conflated two orthogonal conditions (terminal-ness, and host-assignment) into a single boolean expression via `&&`, which is only correct when BOTH must jointly hold for the no-op case -- but Req 4.4's terminal exclusion needs to apply UNCONDITIONALLY, independent of the host check, not only when the host check also happens to be true. A two-way boolean cannot express three independent outcomes (terminal-always-noop; non-terminal-but-already-assigned-noop; non-terminal-and-unassigned-assign) without this exact class of short-circuit bug.
|
|
|
|
## Fix
|
|
FIXED (2026-09-01, round-2 review, task 3.2). resync_apply_running_entry() rewritten as a genuine three-way `if`/`else if`/`else` split, not a boolean guard: (a) resync_job_is_terminal(job->job.state) -> ALWAYS a no-op, checked FIRST and independently, regardless of host (closes the Req 4.4 violation). (b) non-terminal AND job->job.allocs != NULL (already has SOME live non-terminal host assignment -- to the reporting worker's own host, or to a different one, it does not matter which) -> a no-op; also tightened from round 1's own host-specific check (resync_job_assigned_to_host(job, w->host.name)) to a host-agnostic allocs!=NULL check, since round-1's version would have ALSO wrongly routed a job assigned to a DIFFERENT host into the assign branch, resetting its start_time and re-appending a spurious WAL_JOB_DISPATCH fact for a job that was never actually lost. (c) non-terminal AND allocs == NULL (genuinely unassigned) -> record_job_reconcile_assign_fact(), Req 4.8's real trigger. Verified: (1) test_apply_report_all_five_outcomes_updates_job_table's j_extra is now seeded TERMINAL and asserted COMPLETELY UNTOUCHED (state stays DONE, no WAL append) -- the exact inversion of the round-1 assertion that had certified the bug. (2) test_apply_report_unqueried_extra_already_tracked_is_noop (branch (b), assigned to a DIFFERENT host) strengthened with a start_time-unchanged assertion (record_job_reconcile_assign_fact unconditionally stamps time(NULL), bug-1697 -- invisible to a bare state/allocs check since the job was already RUN either way) and an LSN-unchanged assertion (no spurious WAL_JOB_DISPATCH append) -- round 1's version of this test could not have caught either defect. (3) test_apply_report_unqueried_extra_unassigned_pend_job_gets_assigned (branch (c), genuinely unassigned/PEND) already covers the real assign-success case with full restart-replay rigor, added in the same round-1 fix pass. Full controller test suite (meson test -C src/xce.build 'controller test'): all green.
|
|
|
|
## Verification
|
|
_Not recorded (migrated from buglog.json)._
|
|
|
|
## References
|
|
- Related: bug-1747, bug-1697, bug-1631
|