Commit graph

2476 commits

Author SHA1 Message Date
755230705c
feat(scheduler-core): define new event payload types (task 7.2)
Adds JobStateChangeEvent (jobid/oldState/newState/exitCode) and
JobUpdateEvent (jobid/changedFields/job: Job) to controller.capnp,
matching design.md's spec verbatim. JobUpdateEvent.job reuses the
existing full 12-field Job/xce_job_t record, verified distinct from
the narrower SchedJob struct sharing the same mapname.

Extends ReplayEvent.body's union (task 7.1) with jobStateChange@10/
jobUpdate@11 -- the only two valid next ordinals, since capnp requires
strictly sequential ordinals with no gaps across a struct and task
7.1 deliberately stopped at @9. Existing @0-@9 fields and every other
struct in the schema are byte-identical.

Deep round-trip tests in test_e2e_capnp.c exercise every field with
non-default values, including the 3-level nested Job substructs
(submitData/allocs/pidinfo/pendreason). JOB_F_* bitmask bit values
deliberately left undefined -- a later task's job, not this
schema-only one's.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019dw5WnN3DXStaEaPYfACU1
2026-08-04 10:17:57 +00:00
lei.chen
2fa9bedcd3 fix(controller): persist_job_hook_fun OOB read on RUSAGE_UPDATE hook (bug-1502)
The RUSAGE_UPDATE job hook fires with (cluster_t*, xce_job_rusages_t*) as
its varargs — the apiserver and scheduler subscribers parse exactly that
contract — but persist_job_hook_fun's default case treated the second arg
as a job_t* and read j->job.state at offset 56. A xce_job_rusages_t is 56
bytes and shares only jobid@0 with xce_job_t (which is why the jobid read
coincidentally worked), so the state read landed one byte past the struct:
ASAN heap-buffer-overflow (READ of size 4) in thread T7 whenever a job runs
and the worker pushes rusage updates. This was the recurring controller
crash (term 3/4/5) blocking job-PENDING verification.

Add a dedicated HOOK_DETAIL_JOB_RUSAGE_UPDATE case that reads jobid from
the xce_job_rusages_t* and bakes the job's current state from a jobmgr
key_find on the vararg cluster_t* (race-free: the sole caller
update_job_rusages() holds that job's write lock while firing the
AFTER_EVENT hook; re-locking the job inside the hook would deadlock).

Regression test: test_job_hook_rusage_writes_wal_entry now passes a real
56-byte xce_job_rusages_t instead of a full job_t (which masked the
overflow) — ASAN stack-buffer-overflow under the old code, PASS under the
fix. Full controller unit suite: 458 tests, 0 failures.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-04 09:39:57 +00:00
68a6196517
feat(scheduler-core): define replay stream envelope schema (task 7.1)
Adds ReplayCluster/ReplayHeader/SnapshotEntry/ReplayEvent/
SessionSummary/ReplaySession to controller.capnp, additively, first
task of Phase 3 (Foundation: Replay Wire Format).

Requirements.md's AC 20 capnp sketch isn't self-contained -- ReplayEvent's
union references JobStateChangeEvent/JobUpdateEvent (task 7.2's job),
SnapshotEntry.data/ReplayDecision's union reference SnapshotData/
DispatchDecision/PendDecision/PreemptDecision (task 7.3's job), none
of which exist yet. Resolved by verifying capnp's real constraints
against the toolchain directly: SnapshotData is an empty {} placeholder
(task 7.3 replaces its mapname'd C struct wholesale when adding the
real ~11 fields), ReplayDecision holds only {ts} (task 7.3 appends a
body union at new ordinals), and ReplayEvent's union holds only the 9
members with pre-existing backing types (Conf4Sched/SchedJobData/
WorkerData/ResourceData/ResValList/QueueConf/SchedJobRusages/Void),
deferring jobStateChange/jobUpdate to task 7.2 as new ordinals --
capnp requires strictly sequential ordinals with no holes, so this
split is compiler-mandated, not stylistic.

Round-trip test added to test_e2e_capnp.c. Full project build (all
binaries sharing this schema) and 1075/1075 controller_unit_test
verified clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019dw5WnN3DXStaEaPYfACU1
2026-08-04 09:38:13 +00:00
lei.chen
ff2d784ca9 fix(apiserver): don't free client config C-strings that the C library retains (bug-1501)
tryConnect built the xce_client_config_t with C.CString(host/from/key), then a
defer freed all three. But xcecli_ctx_init stores the config POINTER
(ctx->conf = config), not a copy — client_failover.c dereferences
config->masters/from/key for the client's ENTIRE lifetime (e.g.
xce_failover_next_target strncpy's a master host on every write-fail/redirect).
Freeing the strings left config->masters[0] dangling → ASAN heap-use-after-free
(strncpy in the background C thread) after a few submits.

Fix: keep the three C strings for the process lifetime (tryConnect runs once at
startup, gCtx is a process-global); free them only on the init-failure path.

Verified: 6 consecutive job submits, apiserver stays up, 0 ASAN errors
(previously crashed after ~2-3 submits).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-04 08:59:06 +00:00
d39ffc1b4e
chore(scheduler-core): close Phase 2 HA soak regression gate (task 6)
Sequential HA soak (xce-test-ha skill), 15 rounds, 48 jobs, zero job
loss, all 3 nodes led as primary, committed-prefix walcompare
consistent every round. Confirms tasks 4.1-5.2's src/scheduler/
rewrite (core extraction + normal-mode driver wiring into main.c)
introduced no controller-side regression -- consistent with every
task's independent review confirming zero src/controller/ changes
throughout Group 4/5. Closes out Phase 2 entirely; gates Phase 2 ->
Phase 3.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019dw5WnN3DXStaEaPYfACU1
2026-08-04 08:14:48 +00:00
062cf40d29
feat(scheduler-core): wire decision publishing and mode-selection into main.c (task 5.2)
Rewires main.c's live scheduling loop onto the new core-driven
architecture: xce_sched_driver_translate_pending_events() (task 5.1)
-> xce_core_begin_session() -> plugin_begin_session()+sched_account_jobs()
-> xce_core_signal_go() -> decision-drain via the registered
xce_core_decision_cb_t -> plugin_end_session() -> xce_core_end_session(),
all in the new exported xce_sched_driver_run_normal_mode_round()
(driver.c), replacing the old inline sched_update()+sched_begin_session()
+sched_go()+switch+sched_end_session() sequence.

xce_core_go() (task 4.4) is split into xce_core_begin_session()/
xce_core_signal_go() -- a pure decomposition (xce_core_go() itself is
now defined in terms of both, task 4.4's approved test suite passes
unmodified) -- specifically so the driver can interleave
plugin_begin_session()/sched_account_jobs() in the exact safety-critical
window task 4.4 deliberately excluded from pure-core scope: after the
resval reset, strictly before the picker thread is signaled. This is
empirically proven by test_driver_session_ordering.c via real
cross-thread happens-before assertions, not just documented.

xce_sched_driver_on_decision() is the new registered callback:
DISPATCH/PEND forward to the existing sched_publish_dispatch()/
sched_publish_pend_reason(); PREEMPT fails loudly (-ENOSYS, logged)
since no xcecli_preempt() client API exists and nothing in the live
allocator produces one yet (scheduler-preemption's territory).

Adds a --sim CLI flag (parse-only, loud not-yet-implemented failure,
since SimDriver is Phase 3).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019dw5WnN3DXStaEaPYfACU1
2026-08-04 08:05:57 +00:00
lei.chen
5b47ee603f fix(apiserver): carry exec_user on job submit so jobs don't run as nobody (bug-1500)
modelJobToSubmit built the C job_startinfo_t without setting execuser, so the
controller forwarded a NULL execuser in the startjob request and the worker
ran every job as "nobody". Now the submit carries the resolved exec user: an
explicit exec_user wins, otherwise the job runs as the submitting user.

- add resolveExecUser() (plain-Go, unit-testable) + TestResolveExecUser
- set si.execuser in modelJobToSubmit, free it in freeJobSubmit

Note: deploying this exposed an ASAN heap-use-after-free in the apiserver's
submit/notification path (strncpy reading freed memory); the fix is committed
but needs the memory-lifecycle investigation before it can ship safely.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-04 07:38:43 +00:00
dceafc6b12
feat(scheduler-core): build normal-mode driver event translation path (task 5.1)
Adds driver.h/driver.c: xce_sched_driver_translate_pending_events()
drains core->pend_updates via the existing dataq-swap pattern (same
mechanism sched_update() uses), translates all 10 xce_sched_update_
request_t op types into the matching xce_core_event_t, and pushes the
batch through xce_core_push_events(). Scoped to xce_sched_core_t*
alone -- pend_updates already lives on core since task 4.1, and this
task is translation-only (no decision publishing, no mode-selection,
no recording -- all later work). handler.c (the enqueue side) is
untouched. First task in Group 5 (Core: Normal-Mode Driver).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019dw5WnN3DXStaEaPYfACU1
2026-08-04 04:39:17 +00:00
19d8df3532
test(scheduler-core): add core contract test for full push-go-drain-end cycle (task 4.6)
Adds test_core_full_cycle.c: drives xce_core_push_events() -> xce_core_go()
-> decision drain -> xce_core_end_session() end-to-end against a real
picker pthread (running the real xce_job_picker_iface_t hooks on a
test-local stub picker) and a main-thread stand-in allocator, since
sched_allocator() is xce_scheduler_t*-coupled and out of pure-core
scope. Proves both preconditions (core_go reentrancy, push_events
mid-session) reject against a genuinely active session and recover
once it ends. Closes out group 4 (Foundation: Scheduler Core
Extraction) -- tasks 4.1-4.6 all complete.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019dw5WnN3DXStaEaPYfACU1
2026-08-04 04:05:57 +00:00
c5432f7786
feat(scheduler-core): implement clock virtualization and session timing (task 4.5)
Adds xce_core_now() (new core_clock.c, kept separate from core.c so
test_core_isolation.c's wall-clock scan still enforces zero direct
clock_gettime()/time() calls elsewhere in the core): the two-anchor
plus speed formula (virtual_clock_base + (elapsed * speed_num) /
speed_den), covering all four modes (normal, discrete/real-time/fast-
slow simulation) via four new atomic fields on xce_sched_core_t,
defaulted to normal-mode (1/1 speed) in xce_sched_core_init().

requirements.md's literal tlog_reg_format_func(fn, core_ptr) call
shape doesn't exist in the real vendored tlog library (registration
takes only the callback; userptr is per-call-site, not bindable at
registration) -- resolved with a module-level active-core pointer read
by a new core-scoped format function, not wired into mylog_init()/
main.c yet. xce_core_go()/xce_core_end_session() (task 4.4) now stamp
session_start/compute session_duration_ns via xce_core_now(), with
their existing session-active/reentrancy behavior untouched.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019dw5WnN3DXStaEaPYfACU1
2026-08-04 03:38:58 +00:00
de69b6288d
feat(scheduler-core): implement session-start signal + decision callback (task 4.4)
Adds xce_core_go()/xce_core_end_session() (new core_session.c):
xce_core_go() is non-blocking, performs begin-session state reset
(select-eval-ctx, resval resets), signals the picker thread via wbuf,
and returns -EINVAL on a reentrant call while a session is already
active (proven a true no-op, not just an early return). Draining
core->decisions and invoking the driver's on_decision callback is the
driver's own responsibility between core_go() and core_end_session()
(confirmed driver-owned, not core-owned, per the Driver <-> Core
protocol). xce_core_set_decision_cb() registers the callback + driver
context as a separate setter, keeping xce_sched_core_init()'s live
sched_init() call site untouched. Adds XCE_DECISION_PREEMPT to
decision.h (jobid/victims[]/reason) alongside the existing DISPATCH/
PEND types. Additive only — sched_go/sched_begin_session/
sched_end_session/sched_allocator/main.c remain untouched.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019dw5WnN3DXStaEaPYfACU1
2026-08-04 03:08:18 +00:00
lei.chen
ee7dee0ed4 fix(scheduler): stop re-dispatching running jobs (bug-1498) + resval double-free (bug-1497)
bug-1498 (re-dispatch): sched_job_is_ready() was a stub returning 1, so any
job lingering in the fcfs pendJobs queue was always elected. A dispatched job
was never marked non-PEND (the controller defers its schedStartJob notify to
the not-yet-implemented apply pump, controller/job.c), so fcfs_update_job
re-enqueued it on every job update and the election thread re-dispatched the
running job — the worker got conflicting signals and the job was SIGTERM'd.

- sched_job_is_ready: elect only PEND, non-suspended jobs
- sched_commit_decision (dispatch): mark the job JOB_STAT_RUN on dispatch so
  it leaves the PEND set and is no longer re-electable
- fcfs_update_job: dedup via ordq_peek before enqueue

bug-1497 (double-free): sched_update's resval paths transferred ownership of
rv->val to sharedInst/w->resvals but left the request pointer set, so
sched_pend_update_killer double-freed it -> use-after-free in
build_host_eval_ctx. Clear rv->val = NULL on transfer.

Tests: new scheduler/testsuite/test_redispatch.c (8 cases): sched_job_is_ready
PEND gating + dispatch-commit marks the job RUN. Full scheduler meson suite
green. Live-verified: a `sleep 60` job is elected exactly once, runs the full
duration and exits 0 (`completed successfully`); before the fix it was
re-dispatched at ~11s and killed (SIGTERM 143).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-04 01:20:38 +00:00
e6c0a2448a
feat(scheduler-core): implement core event batch push interface (task 4.3)
Defines xce_core_event_type_t/xce_core_event_t (9 event kinds: config,
job, worker, resource, resval full/delta, job state change, job update,
queue update, job rusage) in core.h. Implements xce_core_push_events()
in a new core_events.c (split out of core.c to stay under the 800-line
cap): rejects -EBUSY mid-session, otherwise applies each event's core-
state mutation and fires the corresponding picker hook synchronously,
exactly once per event, before returning. CONFIG_PUSH decomposes the
pushed config's embedded queues/resources/resvals via shared helpers
(also reused by QUEUE_UPDATE's upsert path), mirroring the existing
sched_init_config()/sched_init_resources()/sched_init_sharedInst()/
sched_init_queues(). Narrows sched_resolve_job_picker() to take
xce_sched_core_t* (pure type change, zero behavior change). Purely
additive — not wired into the live scheduling path yet (task 5.x).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019dw5WnN3DXStaEaPYfACU1
2026-08-03 16:05:33 +00:00
e9b5c3f589
feat(scheduler-core): pin jbuf capacity + record kvmgr determinism audit (task 4.2)
Hardcodes jbuf's cbuf capacity to the literal 1 in xce_sched_core_init()
(no parameter path, no local variable to drift), backed by a regression
test. Audits every kvmgr iteration reachable from the scheduling loop
(jobs/queues/users/projects): only jobs is ever iterated, via xzarray's
deterministic ascending-slot walk with no process-random seeding —
findings recorded durably as a comment above xce_sched_core_init().
core->workers iteration-order stability is flagged as an open follow-up
(scheduler-allocation's territory), not this task's scope.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019dw5WnN3DXStaEaPYfACU1
2026-08-03 14:09:42 +00:00
7c229284e2
feat(scheduler-core): extract xce_sched_core_t from monolithic scheduler struct (task 4.1)
Splits xce_scheduler_t along the controller-I/O seam: xce_sched_core_t
(new core.h/core.c) now holds all scheduling state (jobs/queues/workers/
users/projects/resource managers/plugins/three-thread-cbuf pipeline),
while xce_scheduler_t shrinks to {core; apictx; cluster_cfg}. Proven via
an nm-based link-time isolation test (zero netio/xcecli/idgen/delaymgr
symbols reachable from core.c) plus a reproducible wall-clock-call guard.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019dw5WnN3DXStaEaPYfACU1
2026-08-03 10:55:58 +00:00
604a564904
test(scheduler-core): audit data-sharing invariants across the picker rework (task 2.6)
Closes out group 2 (Core: Builtin FCFS Picker). Confirms builtin_fcfs's
ordering matches legacy fcfs_elect_job (byte-identical comparators, same
sched_queue_is_ready/sched_job_is_ready gates, provably equivalent jobid
sequence), confirms tasks 1.1-2.5 left every pre-existing test file
unmodified, and confirms resreq immutability, pend-reason factory-only
allocation, and select_eval_ctx_t memoization all hold across every file
this spec has touched so far. Adds a durable regression lock for the
pend-reason factory invariant (no direct calloc(pendreason_t) outside
make_pendreason()).
2026-08-03 09:35:21 +00:00
9d91d470b4
test(scheduler-core): guard core scheduling loop against ordering-logic regressions (task 2.5)
Task 2.4 already made sched_job_picker() delegate entirely to the picker
interface with zero direct queue/job traversal, satisfying Req 1 AC 1/AC 7
as a byproduct. This adds a source-text regression test locking that
property in place: asserts sched_job_picker()'s body contains none of the
forbidden direct-traversal symbols and does contain the expected
picker-interface call sites.
2026-08-03 09:11:55 +00:00
7ee9a16f53
feat(scheduler-core): wire builtin_fcfs as registered default picker (task 2.4)
Registers builtin_fcfs at scheduler startup and makes it the default
job_pick_plugin, replacing the hardcoded "xce_sched_fcfs" literal in
sched_job_picker() with a fail-loud lookup through sched_resolve_job_picker().
The picker thread now branches legacy job_elect_fn plugins (delegated
unchanged, preserving their self-contained wbuf-driven loop) from native
xce_job_picker_iface_t pickers (driven by a new per-session loop), and the
allocator now calls on_dispatch/on_pend for every picker as Req 1 AC 4
requires. Closes task 2.3's flagged picker/allocator concurrency race with a
mutex on fcfs_plugin_data_t, and adds cancel points to next_job().
2026-08-03 08:51:45 +00:00
fcb32a8b30 feat(scheduler-core): implement builtin_fcfs session and post-allocation hooks (task 2.3)
Adds begin_session/next_job/end_session/on_dispatch/on_pend, driving a
dual-cursor walk (queue then job) over builtin_fcfs's private ordq_t
state. next_job() returns candidates in (queue priority desc, job
priority desc, queue_time asc) order and NULL once exhausted;
on_dispatch removes permanently, on_pend is a true no-op.

Review flagged an unsynchronized cross-thread access risk between
next_job() (picker thread) and on_dispatch/on_pend (allocator thread)
once wired live in task 2.4 — recorded in tasks.md Implementation Notes
for that task to close before registering builtin_fcfs as the default.

Requirements: 3.2, 3.4, 3.6
2026-08-03 07:18:39 +00:00
b7214ebe88 feat(scheduler-core): implement builtin_fcfs state-change hooks (task 2.2)
Adds on_queue_update, on_job_push, on_job_state_change, and on_job_update
to builtin_fcfs, driving the picker's private ordq_t candidate sets
directly (no core/sched queries), matching fcfs.c's existing ordering
semantics on the new finer-grained hook interface.

Requirements: 3.2
2026-08-03 06:55:33 +00:00
lei.chen
07ab05671d fix(scheduler): put_resvals treats resource idx 0 as invalid → SEGV
put_resvals (resource.c) used `idxs[i] <= 0` to skip "invalid" resource
indexes, but index 0 IS valid — it's the first resource in the table
(e.g. "slots"). For idx 0 the val was never allocated (xvals[i] stayed
NULL) and the write loop then SEGV'd. bug-1495.

Fix: skip only negative indexes (`idxs[i] < 0`), and NULL-guard the
write loop defensively.

Verified end-to-end: a locally-built scheduler now connects to the t1
controller and dispatches all 5 test jobs to node t1 (exec_hosts
populated, jobs run and exit). This was the final blocker for job
scheduling (bug-1494: all jobs PEND).

Also: add the t1 host definition to hosts.yaml (worker join was rejected
with -EINVAL because no matching hostconfig existed for hostname "t1").

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-03 06:53:14 +00:00
f7921b961a feat(scheduler-core): implement builtin_fcfs lifecycle and private state (task 2.1)
New src/scheduler/src/plugins/builtin_fcfs.c implements
builtin_fcfs_init()/fini() and the private fcfs_plugin_data_t (ordq_t
queues, per-queue ordq_t pendJobs) that all later builtin_fcfs hooks
build on. fcfs.c (today's live job_elect_fn-based picker) is untouched
-- still production, until task 2.4 flips the registration.

picker == xce_sched_plugin_t* calling convention (private state stored
in plugin->data) confirmed consistent with task 1.2's legacy wrapper.

Requirements: 3.1 (context), 3.3
2026-08-03 06:03:49 +00:00
0d63a789a1 feat(scheduler-core): add picker configuration fields (task 1.3)
Adds xce_scheduler_t.job_pick_plugin (global picker name), and on
xce_sched_queue_t: project_name + job_pick_plugin (per-queue override,
NULL = inherit global). Adds xce_sched_project_t.priority (scaffolding
only, no quota logic). sched_set_default_job_pick_plugin() is added but
not yet wired into sched_init() -- task 2.4 owns that, alongside
replacing the hardcoded "xce_sched_fcfs" literal.

Requirements: 2.1, 2.2, 2.3, 2.4
2026-08-03 05:45:53 +00:00
4d8d72142e feat(scheduler-core): register picker plugin category + job_elect_fn compat wrapper (task 1.2)
plugin_register() now adapts an old-style job_elect_fn-only plugin to
xce_job_picker_iface_t via legacy_picker_wrapper_iface (next_job()
forwards to job_elect_fn; other hooks are safe no-ops covered elsewhere
in the existing lifecycle), logging a one-time deprecation warning. A
plugin that already sets picker_iface directly skips the wrapper and
the warning. Reuses the existing plugin_register()/key_find() mechanism
-- no parallel registration system.

Requirements: 1.6, 2.5
2026-08-03 05:23:53 +00:00
065025a3a6 feat(scheduler-core): define job picker plugin contract (task 1.1)
Add xce_job_picker_iface_t to plugin.h with all 13 hooks across
lifecycle/state-change/session/post-allocation/optional categories,
matching design.md's literal struct. Documents per-category thread
assignment, next_job() cancel-safety, and begin_session() session-reset
requirements. Forward-declares xce_sched_core_t (built in a later task).

Requirements: 1.2, 1.5, 2.6
2026-08-03 04:49:55 +00:00
lei.chen
62dee0edc1 fix(configs): add worker-builtin load indices missing from resources.yaml
worker/src/resource_sync.c init_builtin_load_indices registers 11 load
indices (r15s/r1m/r15m/ut/pg/ls/it/swp/mem/tmp/io) but resources.yaml only
defined r1m/r15s/mem/tmp. The 7 missing ones (r15m/ut/pg/ls/it/swp/io) caused
controller "update_resval: Success" ERRORs and scheduler "unknown resource in
per-worker resvals update" warnings, leaving the worker resource view
incomplete. Add the 7 definitions matching the existing r1m/r15s pattern.

Docs: log bug-1494 (all jobs PEND, scheduler never dispatches; after this
config fix jobs still PEND — remaining suspect is the scheduler→controller
join path after restart), update design doc + verification manual known-issue
tables and root CLAUDE.md cluster-verification status.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-02 07:36:03 +00:00
lei.chen
226259a52b fix(schedulerclient): parse apiserver OrderMap columnar rows (bug-1491)
The apiserver returns list rows in OrderMap columnar form —
{"Fields":["col",...],"Data":{"col":val,...}} — but the read clients
unmarshalled each row as a flat object, so every field came back empty
(counts were right, values were all zero/"").

Fix: decode rows as apiRow{Fields, Data map[string]json.RawMessage} and
pull named fields out of Data via rowStr/rowInt/rowInt64/rowFloat helpers.
ListNodes, ListQueues, and getJobs (ListJobs/GetJob) all use apiRow now.

TDD: 3 new ordermap_test.go tests built from live-captured apiserver
response shapes (red: fields empty → green: parsed). Live-verified against
t1: node name=t1 cores=8, 4 queues active, 3 jobs with real fields.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-02 05:01:16 +00:00
4153b35b22 chore(wolf): update buglog and session state
buglog.json: corrected bug-1480/1481 (stale "not yet fixed" entries, code
landed same day the entries were written); bug-1483/1484 fixed; bug-1485
investigated, downgraded to low-priority/environment-flake.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LXkenK8rqE8mdS1YKjsq78
2026-08-02 04:37:36 +00:00
d683f5fc44 docs: add Jul27-Aug01 2026 project status report
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LXkenK8rqE8mdS1YKjsq78
2026-08-02 04:37:28 +00:00
a5560256ea test(xceadmin): capture live HA E2E stderr instead of DEVNULL (bug-1485 investigation)
bug-1485 (flaky test_live_config_push_against_primary_reaches_success_path,
~24/25 fail rate under pytest, primary process going <defunct> after
promotion) could never be diagnosed because controller stderr was
DEVNULL'd -- no ASAN report or signal was ever captured from a real
failure.

Redirects each node's stderr to deploy_dir/logs/<name>.stderr.log and adds
an XCE_KEEP_DEPLOY_DIR=1 debug toggle to preserve the deploy dir across
fixture cleanup for inspection. Ran the live test 40 times in this
environment (25 idle + 15 under concurrent build contention): 40/40
passed, 0/120 captured stderr logs non-empty -- could not reproduce.
Downgraded to low-priority/environment-flake, left open; the capture
infrastructure stays in place for the next real failure.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LXkenK8rqE8mdS1YKjsq78
2026-08-02 04:36:57 +00:00
8e75b0c10c test(ha): add term-adoption regression tests for heartbeat/WAL-NACK vectors (bug-1484)
bug-1421 added two term-discovery fallback vectors so a stale-term primary
steps down instead of retrying AppendEntries forever: heartbeat-reply
state-report term adoption and WAL-NACK reply_term adoption, both inside
repl_ack_decode_and_dispatch. Neither had a test driving the real
production dispatch path and asserting raft_on_higher_term_observed
actually fires.

Adds two tests mirroring test_config_repl.c's existing config-push term
test, driving peer_ctrl_msg_handler -> repl_ack_decode_and_dispatch via
wire-shape capnp packets. Both pass immediately -- the underlying bug-1421
code was already correct, just unverified end-to-end -- closing
raft-adapter-conformance's Requirement 7.5 unverified claim.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LXkenK8rqE8mdS1YKjsq78
2026-08-02 04:36:46 +00:00
f16733ee61 fix(ha): free liveness poll ctx on failed/skipped delaymgr schedule (bug-1483)
ha_arm_heartbeat_timeout_vtask published a calloc'd liveness_poll_ctx_t
into s_liveness_ctx unconditionally but only ever freed it via
liveness_poll_fn's cancelled-check path, which never runs if
delay_task_ms was never called (c==NULL, delaymgr not running) or itself
failed. Both no-schedule cases now free the ctx immediately, establishing
the invariant that s_liveness_ctx is non-NULL only when a delaymgr task
actually holds a live reference to it.

Regression test drives a real LOST PreVote round without stubbing
ha_prevote_rearm_liveness_hook (every other LOST-path test in the file
stubs it specifically to avoid this code path) and asserts the ctx isn't
left dangling.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LXkenK8rqE8mdS1YKjsq78
2026-08-02 04:36:37 +00:00
lei.chen
4a4ae16725 docs(scheduler-agent): add Claude Code integration guide to verification manual
New §5 in src/docs/chatbot-verification.md: how to connect Claude Code to
the scheduler-agent MCP server. Covers building the mcp-server binary,
`claude mcp add` CLI registration (verified against installed CLI
v2.1.220), the .mcp.json project config schema, the three scopes
(local/user/project), the first-run approval gate, verification commands,
and stdio/env gotchas. Original §5-§7 renumbered to §6-§8.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-02 04:26:23 +00:00
lei.chen
56da673b22 docs(scheduler-agent): add cluster deployment + chatbot verification manual
New src/docs/chatbot-verification.md: reproducible steps to deploy the
scheduler-agent stack on the t1/t2/t3 test cluster (AppImage build, store
sync, controller/apiserver/scheduler/worker startup) and verify Phase 0-1
(login, mcp_operator API key, read ops, submit). Documents the vcpus quirk,
setsid launch pattern, conf/current pointer mechanism, and known bugs
(job-control HTTP 500 = bug-1490, HA election deadlock).

Also add the manual to the design doc's related-documents index.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-01 09:57:44 +00:00
lei.chen
9fe14c1512 docs(scheduler-agent): update design doc with cluster verification results
- Design doc v1.2: Phase 0-1 cluster verification status (read + submit OK,
  control HTTP 500 open bug-1490), bug table for whitelist/job_status fixes,
  mcp_operator queue:control note.
- Root + frontend CLAUDE.md: reflect verified state and apiserver bugs found.
- wolf memory/anatomy/buglog: log deployment session, bug-1489/bug-1490.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-01 09:54:49 +00:00
lei.chen
e36ed63ca0 fix(apiserver): apikeys whitelist + job_status type + mcp_operator queue:control
Three fixes found during scheduler-agent Phase 1 cluster verification:

- whitelist.go: add /xce/v1/apikeys to WhitelistPOSTURL (Phase 0 added the
  route but never whitelisted it → 403 "URL is not allowed"). bug-1489.
- jobs.go: fix job_status int64 assertion in job control. store.Jobs returns
  status as int/float64; the `.(int64)` assertion broke control with
  "job_status is not an int64". Add normalizeJobStatus() accepting int/int32/
  int64/float64/json.Number.
- rbac.yaml: mcp_operator role gains queue:control (the job-control handler
  also checks queue:control on the job's queue); add rbac_mcp_operator_test.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-01 09:54:44 +00:00
lei.chen
35416d5cf4 feat(scheduler-agent): Phase 1 write operations — submit/cancel/hold/release + templates
Add write operations to the scheduler-agent MCP server (Phase 1):

- schedulerclient: SubmitJob/CancelJob/HoldJob/ReleaseJob + JobSpec/
  SubmitJobResult/JobControlRequest/ControlResult types + doPost helper
  (HTTP 200 + envelope-code dual check). TDD: 12 new tests.
- mcp-server: 4 write tools (scheduler_submit/cancel/hold/release) with
  destructive annotations; scheduler_submit has dry_run review mode +
  general_serial (通用串行脚本) job template registry. Fix the 6 read-only
  tools' misleading destructive annotations. TDD: 14 new tests.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-01 09:54:38 +00:00
Lei Chen
2e299f4a76 docs: add Unified Scheduler Agent Framework design document
Complete design spec for the natural-language scheduler agent:
- Layered architecture (UI/LLM/Agent core/Adapter/Scheduler)
- Unified Resource Model (URM) types and principles
- SchedulerAdapter interface + per-backend strategy
- Language selection strategy (Go primary)
- MCP protocol tool set (read/write/advanced)
- Security model (write-protected by default, API Key auth)
- Event stream design with current-state research
- Implementation roadmap Phase 0-4
- Testing strategy with golden tests
- Design principles + ADR decision records
- Current progress status and blockers

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-01 12:18:39 +08:00
lei.chen
7e17c5e403 Merge branch 'working' into chatbot
# Conflicts:
#	.wolf/memory.md
2026-08-01 03:43:59 +00:00
lei.chen
b20f2abb27 chore(wolf): record rule — never push to Rongsong's branch without permission
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-01 03:41:47 +00:00
lei.chen
d6ae04f47f chore(wolf): log bug-1488, record merge session
- buglog: trim/mtime test failures (pre-existing, fixed in f2993d1)
- memory: merge of origin/working + capnpc override dedup

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-01 03:34:58 +00:00
lei.chen
f2993d1efc fix(controller): trim config versions by ns mtime; unique test tmpdir
Two pre-existing failures (2026-05-13, commit 491c255) surfaced by the
controller unit-test suite (1070 Tests 2 Failures), both in the config
version storage path:

1. conf_trim_old_versions sorted cluster.bin.* versions by
   second-granularity st_mtime. Versions saved within the same second
   (rapid replication, or the trim test's 15ms-spaced saves) tie, glibc's
   stable mergesort keeps creation order, and the newest (current-pointer)
   entry lands at the protected keep_n index — so nothing is pruned and
   keep_n+1 files remain. Store st_mtim (struct timespec) and compare
   tv_sec then tv_nsec.

2. cr_setup() built its tmpdir path from (pid, time(NULL)) at second
   granularity; a Unity assertion failure longjmps past cr_teardown(),
   leaking the dir, and the next test (same pid+second) silently inherited
   the leftover conf/current — cascading a second failure
   (test_conf_get_saved_tag_no_config read a real tag instead of 0). Add a
   per-invocation counter to the path so no two tests share a dir.

Verified: nix develop ./src --command bash src/tools/xce-build.sh all →
meson 9/9 Ok 0 Fail, controller 1070 Tests 0 Failures, xceadmin 172
passed, frontend Go all ok.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-01 03:34:55 +00:00
lei.chen
84fc46b5fe chore(nix): drop redundant capnpc override from flake.nix overlay
The merged origin/working commit e0c5e8b pins capnpc's source to an
immutable commit directly in src/nixSupport/xce.nix (bug-918 canonical
fix, verified with a full controller_unit_test build: 978 Tests 0
Failures). The flake.nix overlay from 98066db solved the same bug with a
ghfast.top-proxied fetchurl tarball; keeping both is redundant and the
proxied tarball hash differs from GitHub's git-archive. Keep the
fetchFromGitHub pin in xce.nix (idiomatic, no third-party mirror,
scoped to the build), drop the global pkgs overlay.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-01 03:04:10 +00:00
lei.chen
93ebea572f Merge remote-tracking branch 'origin/working' into working 2026-08-01 03:02:49 +00:00
b17d272f1d add wolf state 2026-07-31 17:42:34 +08:00
5e8a20178b add scheduler related projects 2026-07-31 17:42:13 +08:00
986474eb65 docs(config-upgrade-snapshot): add host admin-state frames, capnp migration, clear bug-1418 blocker
- Host admin-state (open/closed/locked) now in scope for the upgrade
  snapshot; host and queue admin-state frames migrate to independently-
  extensible Cap'n Proto structs (one format-version bump covers both)
- bug-1418 blocker cleared: fixed 2026-07-09 via raft-apply-decoupling,
  independently re-verified 2026-07-23 with a clean 10-round chaotic soak
- New Design Invariants section (6 standing rules) and Requirement 7
  (regression coverage for host-placeholder/rejoin, overlapping upgrades,
  format-mismatch rejection, WAL-trim-under-chaos)
2026-07-31 09:32:24 +00:00
d968bbd722 chore(wolf): update session memory, anatomy, and token ledger 2026-07-31 09:29:48 +00:00
d4b80010b1 docs(raft-adapter-conformance): commit final design.md/research.md and requirements/spec.json state
Spec reached GO with 15/15 tasks complete (see prior task-confirmation
commits); these doc artifacts were generated during the design/research
phases but never separately staged.
2026-07-31 09:29:35 +00:00
a86de77f91 fix(waldump): decode WAL_CONFIG_MARKER, distinguish trimmed gaps from real loss
wal_type_name() never had a case for WAL_CONFIG_MARKER (type 50) -- every
config-change marker entry printed as UNKNOWN instead of a real, defined
type. Added the case plus payload decode (tag/sha256) matching config_repl.c's
config_marker_encode/_decode byte layout, hand-decoded here rather than
linking config_repl.c into xce_test's link graph.

Gap detection also had no way to tell "committed prefix missing!" (real
loss) apart from a normal checkpoint-triggered WAL trim (persist.c's
trim_wal_segments, floor-guarded so it never removes a segment a standby
still needs) -- every long-running node's WAL, once checkpointed, would
false-alarm on every inspection. Cross-checks the sibling data/snap/
directory (snapshot LSN is literally in the filename, snap-<hex>.capnp)
and downgrades the message when a snapshot covers the gap.

Adds waldump_stats_t.leading_gap_explained and 3 new tests plus one
assertion on the existing gap test.
2026-07-31 09:05:22 +00:00