myxce/.wolf/bugs/bug-1825.md
Rongsong Shen d26ff8870d
feat(buglog): one Markdown record per bug, checked against a record standard
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>
2026-09-28 03:40:11 +00:00

48 lines
6.2 KiB
Markdown

---
id: "bug-1825"
status: "fixed"
status_updated: "2026-09-24"
status_note: ""
component: "scheduler"
severity: "crash"
timestamp: "2026-09-17T07:10:00Z"
last_seen: "2026-09-19T09:00:00Z"
occurrences: 1
files: ["src/scheduler/src/sim_picker.c"]
tags: ["scheduler", "sim-mode", "repl", "limit-plugin", "plugin-bootstrap", "architectural-gap", "discovered-during-scheduler-sim-diagnostics", "driver-parity"]
related: []
legacy: true
---
# bug-1825: verified architectural gap discovered while closing out scheduler-sim-diagnostics task 5 (final…
## Description
NOT A CRASH -- significant, verified architectural gap discovered while closing out scheduler-sim-diagnostics task 5 (final validation task for the new `info limit` REPL command). The limit plugin is never registered in ANY simulation-mode driver path, so `info limit <name|all>` will ALWAYS report "no limits configured"/"no such limit" for a real operator running `xce_scheduler -i`/`-s <replay>` against a genuine recording -- even one whose original live run had limits configured and actively enforced. The entire feature built across scheduler-sim-diagnostics tasks 1-5 is correctly implemented, thoroughly tested, and read-only/deterministic as designed, but is currently INERT in real production usage: every positive-display test path (tasks 2-3, showing a limit's configured setting and live usage) only exercises the code via a test-only helper that hand-registers the plugin into core->plugins directly, which has no production equivalent in sim mode.
## Environment
_Not recorded (migrated from buglog.json)._
## Reproduction
_Not recorded (migrated from buglog.json)._
## Root cause
register_limit() (plugins/limit.c:609) has exactly one call site in the entire codebase: scheduler.c:605, inside sched_register_plugins(), which is itself called only from scheduler.c:827 -- the normal-mode (controller-connected, live) driver's sched_init() path. Sim mode's own plugin-bootstrap mechanism, sim_bootstrap_picker() (sim_picker.c), exists specifically to register a picker plugin (builtin_fcfs, via plugin_register_core()) for simulation/replay mode, since sim mode deliberately does not run the full normal-mode init sequence -- but it only ever registers the PICKER plugin category. It has no equivalent step for the limit plugin (or, by the same reasoning, potentially other plugin categories registered by sched_register_plugins() that were never audited for this gap). Confirmed via full-codebase grep: register_limit()/sched_register_plugins() are never referenced anywhere under sim.c/sim_loop.c/sim_picker.c/driver.c/repl*.c, and no write into core->plugins occurs anywhere in the sim-mode driver files. This is the same class of gap scheduler-driver-equivalence.md already documents for OTHER driver-parity properties (e.g. bug-1613's sched_account_jobs() gap) -- a sim-mode driver feature silently missing something the normal-mode driver does, discovered only when a REPL feature tried to read state that assumes it.
## Fix
FIXED (2026-09-17, scheduler-sim-diagnostics task 6). Added limit_new_plugin() factory (extracted from register_limit(), mirroring builtin_fcfs_new_plugin()'s established pattern) and a new sim_bootstrap_limit_plugin() (new file src/scheduler/src/sim_limit.c, mirroring sim_picker.c's structure exactly): registers the limit plugin via plugin_register_core() and seeds it with sim->core.config (the snapshot's own borrowed-reference config, which already carries the original session's limit configuration -- no recording/replay format change needed) via a new limit_seed_config(plugin, xce_sched_core_t* core, config) entry point. Wired into sim_loop.c alongside the existing sim_bootstrap_picker(sim) call.
First attempt (round 1) set plugin->sched = (void*)sim (a disguised xce_sched_sim_driver_t*) to reuse the existing config_hook path -- REJECTED on independent review: this repeats the exact pointer-to-first-member cross-cast pattern that already corrupted memory once in this codebase (see this bug's own root_cause note and core.h's xce_core_cache_clear_cb_t doc comment for that prior incident). Round 2 eliminated the cast entirely: limit_seed_config() takes xce_sched_core_t* core as an honest parameter (a real field both xce_scheduler_t and xce_sched_sim_driver_t genuinely have), delegating to a shared limit_seed_config_impl() also used by normal-mode limit_update_config(). No cast, no aliasing risk -- confirmed structurally sound by two independent reviewers, each reading the actual struct definitions and full call chain rather than trusting the report.
Also closed: sim_limit.c added to sched_sim_isolation_lib's source list (meson.build) so Req 5 AC 17's mechanical controller-I/O-isolation check covers it (round 1 omitted this; round 2 fixed it, sched_sim_isolation_test independently re-verified passing with the new file included).
Ripple: two waves of an identical, minimal link-satisfying stub pattern across the ~24 white-box test files that #include "../src/sim_loop.c" (one for limit_new_plugin(), one for limit_seed_config()) -- same established 'duplicate, don't share' idiom this file family already used for the earlier scheduler-sim-diagnostics tasks' repl_info.c ripple.
TDD: RED->GREEN verified for both the config-seed test and the remediation itself (a temporary limit_seed_config() no-op stub reproduced the exact expected single-test failure, reverted cleanly). Full scheduler suite: 78/78 Ok, 0 Fail; sched_repl_limit_table_test 26/26; sched_limit_plugin_test 5/5 (zero regression to normal-mode registration); sched_sim_isolation_test 5/5 (genuinely covering the new file).
Deliberately NOT fixed (separate, larger, tracked as a distinct follow-up item in .kiro/specs/scheduler-sim-diagnostics/tasks.md, not this bug): limit *matching* (limit_job_election()) is still never invoked during replay -- it's only reachable via plugin_job_election(), which is normal-mode-only (one caller, scheduler.c) and would itself need a core-safe variant before it could be wired into sim_loop.c's decision path. So 'info limit' now shows real CONFIGURED settings against a real replay, but live USAGE will still correctly show 'no active usage instance yet' until that separate, larger fix lands.
## Verification
_Not recorded (migrated from buglog.json)._
## References
_Not recorded (migrated from buglog.json)._