Repository navigation
Conversation
Each engine facade numbers its jobs from 1 and starts again in every proxy run, but the Electron catalog and the job list keyed jobs by origin and job number only. An Ollama job and an LM Studio job with the same number shared one card, and a new job replaced one from before a restart that reused its number. Jobs now carry the proxy's runId and use the same four-part key as the broker's store. A broker workloads:remove names only the origin and id, so it still retires every engine and run sharing them, as the broker's Store.Remove does. Electron's own evictions, when leaving a cluster or emptying the catalog on a backend restart, now name the engine and run and retire exactly one job, by a single key lookup. Signed-off-by: Chris Kelsey <[email protected]>
…roker store The broker store ordered a workload's events by state rank, which cannot tell two queued events apart. When the origin's heartbeat captured a queued record, the job was re-pointed to another node, and the heartbeat's older copy was delivered afterwards, the store moved the job back to the node it had already left. The proxy already numbers each workload's events (seq). The store now reads it, and when both the stored record and an authoritative event carry one, it applies a higher seq, rejects an equal or lower one, and never replaces a terminal record. Unsequenced events, and the inferred guesses of the node-loss and staleness sweeps, keep the existing provenance and rank rules. The proxy never sends two events with the same state and node, so every event it sends in order is decided exactly as before. Signed-off-by: Chris Kelsey <[email protected]>
A job's events were numbered under a lock but written after it was released, so the disconnect watcher's terminal event could reach the wire ahead of an earlier update from the request goroutine. jobEvents makes each change, its seq and its write one step under the job's lock, and replaces the request-local record, lock, once-guard and re-point closure. Signed-off-by: Chris Kelsey <[email protected]>
…rkload-manager docs The broker store now applies each workload's events in the origin's seq order and the proxy writes them in that order, so the docs no longer claim the store merges by state rank or keys workloads by origin and id alone. Signed-off-by: Chris Kelsey <[email protected]>
ckelseynv
marked this pull request as draft
October 1, 2026 22:03
The node-loss and staleness sweeps fail a record as an inferred guess that keeps the stored seq, and an authoritative event overrode any guess before seq was consulted. A retried copy of an earlier event could therefore land after a sweep and move the job back to a node it had already left. An authoritative event now overrides a guess only when its seq is not lower than the one the guess kept; an equal seq is the origin re-asserting the record and still clears the guess. Signed-off-by: Chris Kelsey <[email protected]>
refresh() replaced the job list with a snapshot and dropped any upsert or removal that arrived while it was fetched, so a reconnect could resurrect a removed job or lose a new one. initialize() and refresh() now share one loader that records every push during the fetch and replays them onto the snapshot in arrival order. The connection lines build their anchor key with workloadKey instead of a copy of its format, and frontend-api.md shows that engine and runId on a removal come together. Signed-off-by: Chris Kelsey <[email protected]>
…kload-manager spec Three statements still said the broker keys a workload by its id and orders events by (nodeId, workloadId) and timestamps. Signed-off-by: Chris Kelsey <[email protected]>
7 tasks done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Four fixes to the job list on Overview.
(originatedFrom, engine, runId, id), the identity the broker store and the scheduler already use.WorkloadgainsrunId, which the proxy already sends.queuedon node A, arriving after the job had moved to node B, moved it back to A. When both the stored record and the incoming event carry the origin'sseq, the store now applies only a higher one and never replaces a terminal record. The node-loss and staleness sweeps fail a record as an inferred guess that keeps itsseq, and an origin event still overrides such a guess, unless itsseqis lower: a retried copy of an earlier event that lands after a sweep is rejected rather than moving the job back. Events withoutseqkeep the existing rules.seqorder. A job's events were numbered under a lock but written after it was released, so the disconnect watcher's terminal could reach the wire ahead of an earlier event. The newjobEventstype numbers and writes each event under the job's lock and replaces the request-local record, lock, once-guard and re-point closure. Every consumer already rejected the one inversion the old code allowed, so nothing visible changes today. The order is now structural, which the third pull request relies on.Removals come in two forms. The broker's
workloads:removestill names only origin and id, and retires every engine and run that shares them. Electron's own evictions also name the engine and run, and drop only that job.workloadKeysRemovedByresolves either form against the catalog. The exact form is a single lookup rather than a scan, because emptying the catalog sends one removal per entry and the catalog can hold the broker's whole 10,000-record history.This is the second of three pull requests that make job states accurate. The first, #141, changes copy and color, and the two merge cleanly in either order. The third makes In flight and Running reflect engine slots.
Release intent
Changelog title
Job list fixes
Changelog body
Bumps
The
nvpair-ui-brokerandnvpair-proxybinaries change. The workload manager changes only in its spec.Scope
Audited:
nvpair-proxy, the producer. Every emit site now goes throughjobEvents: admission, each dispatch and each gap between attempts (repoint), the commit (start), and the deferred reporter and disconnect watcher (finish).nvpair-workload-manager, the node-loss sweep, the stale-origin sweep, persisted history and the scheduler's replay. Both sweeps apply as inferred and copy the storedseq, so provenance decides them before theseqrule, and an origin event with a lowerseqcannot override them. Persisted history is reloaded throughParseIncoming, soseqsurvives a broker restart. A heartbeat re-assertion still counts as a sighting even when the merge rejects it, because the sighting is recorded before the merge.nvpair-workload-manager. Its dedup key already includes engine, run andseq, so there is no code change. Spec statements about the broker's key and ordering are updated.nvpair-job-scheduler. It already keys by the four-part identity and receives only updates the store accepted. Unchanged.frontend-api.md.runIdandseqwere already sent; the desktop now readsrunId. The regeneratedservices-api.mdaddsjobevents.goas a dynamic notify site. It sends the same fourworkload:*notifications thatproxy.gosent.Excluded adjacent work:
nvpair-tui) still keys jobs by origin and number, so it still merges same-numbered jobs from different engines or runs. #117 replaces that view with one keyed by the same four-part identity, so this pull request leaves it alone. Because of it, the broker's stale-origin sweep still skips a record when another live record shares its origin and number. Once both this and Rebuild the terminal interface around nodes and increase UX #117 merge, nothing keys on that pair and a small follow-up removes the skip.workloads:removestill names only origin and id. No producer emits a removal today.Validation
Go:
services/nvpair-proxy:go vet ./...andgo test ./...pass. The newjobevents_test.gocovers ordering, the lifecycle, terminal methods, a single terminal, no-op re-points and a nil job. The ordering test asserts that the job's lock is held while an event is written. With the old order restored temporarily (write after unlocking), it failed 3 runs out of 3.services/nvpair-ui-broker:go vet ./...andgo test ./...pass, including 8 new tests instore_seq_test.goandmarkworkloadfailed_test.go, which pins that the sweeps' failure keepsseq. With the lower-seqcheck removed temporarily, the delayed-event test fails.services/tests(cross-process):go test ./...passes, with 86 passed and 1 skipped (TestBrokerShutsDownOnSignal, which cannot signal a process on Windows). The workload tests all ran, including cross-engine identity, out-of-order suppression, node loss and workload-manager rehydration. After the review fixes,go test ./... -run Workloadpasses again.-racewas not run. This Windows machine has no C toolchain, and CI does not run it.From
desktop/, all passing:npm run typechecknpm run lintnpm run dead-code:checknpm run test:unit(247 passed, 2 skipped: Unix-only wipe-script tests). New:workload-identity.test.ts(11 tests) andworkloads-store-identity.test.ts(7 tests). The two refresh tests fail against the old store.npm run service-contracts:checknpm run build:modular-binaries -- --force(run before the review fixes; CI's platform builds cover the later commits)node scripts/spdx-headers.mjsreports no missing headers.Still to do by hand:
Risk
seq, and in-order streams decide exactly as before (see Scope). A producer that sends noseqkeeps the rank rule. Against a sweep's guess it only rejects an origin event older than the record the guess replaced; the returning node's re-assertion carries an equalseqand still clears the guess.workloadKey, including the connection lines' anchor key from the card's data attributes. A producer withoutrunIdgets an empty run, consistent across its events.jobEventsholds the job's lock while writing to the codec. The order is job lock, then codec lock, and nothing that holds the codec lock takes a job's lock. A blocked stdout already stalled both the request goroutine and the watcher at the codec before this change.Checklist
git commit -s), certifying the Developer Certificate of Origin.services/versions.jsonis written by automation — do not edit it by hand.