Repository navigation
Conversation
…w to The doc comment on Entry::follow and LongLived::steady_mem_kb, and a test comment, still said a steady job keeps what its past runs grew to after their start-up. Since 0.7.1 it books what it holds now plus a quarter, which the test already checks.
outside_admit lets a job start that only programs outside taskguard keep out, while memory stays under pause_at (92%). A job it would let start is over mem_max already, so with mem_max = 95 (the MacBook) it never let anything start. The ceiling is now halfway from mem_max to all of RAM when mem_max is at or above pause_at.
src/stall.rs replays the MacBook morning second by second through the real decide() and Entry::follow(): two agents bring up a dev stack each and queue their browser run once it is up, give up on the run after 10 minutes and take the stack down, while an older turbo run keeps queueing lints. It also replays Omarchy's head of line: a second stack that does not fit, queued in front of 25 jobs. With today's rules the runs are given up (5 to 8 of about 12 in two hours), and the second Omarchy stack starts at once to 114% of RAM.
…no one back for a job that cannot With nothing running, or nothing but long-lived jobs past their start-up, the head of the line started whatever the readings said. On the MacBook that head was a second dev stack (priority 10) that did not fit: it started over memory while the first stack's run waited, and the run waited until its caller gave up. At macOS warning the rule was skipped altogether. The rule now starts the first job in line that fits: a long-lived job under mem_max (it stays), any other job up to all of RAM. A job that does not fit holds nobody back. At warning the rule still decides when only long-lived jobs run; critical still stops everything. Head of line: a waiting job that would not fit by that rule even once every job that ends by itself has ended can only start when a long-lived job is stopped. It no longer holds others back by priority or reservation. On Omarchy a 5.5 GB lint that had waited 55 minutes kept 23 jobs out, among them a 189 MB test. Entry gains long_lived (serde default), set by the owner from the config. The NEXT line names a long-lived holder as one that must be stopped.
…ork goes first A dev stack and the run it exists for were two jobs nothing related: the caller queued the run only once the stack was up, minutes later, so taskguard admitted the stack without room for its run and let other jobs, or a second stack, take that room. The caller can now declare the work: --expect WORK on the stack's job (repeatable), or TASKGUARD_EXPECT=WORK,WORK in its environment, which DALP's devenv up passes through as it is. WORK is a pool name, a job key, or a command pattern. The stack books each item's expected need, learned as the work itself would learn it, and starts only with room for itself and its work. A job in the same checkout that matches links to the stack: it goes one priority above it (an ordinary priority, which older versions honour), takes over the booked room when it starts (the stack drops it on a nudge), and waits, holding nobody back, while its stack still waits. Status shows "(for stack KEY)" on the work and "(expects WORK, books SIZE)" on the stack. Data formats, all additive with serde defaults: queue::Entry gains expects and work_for, queue::Nudge gains work_started. No change to the machine sample, the database schema or job keys. In the stall model, declaring the browser run clears the MacBook morning at 11, 12.8 and 14.5 GB outside: no run given up, worst run wait 9 s at 12.8 and 14.5 GB (CPU-bound 120 s at 11 GB), at the cost of one stack at a time.
…ueued for it goes first The captain could not tell whether a stack and the run that needs it arrive together. When they do, the stack should start with room for both. A long-lived job that has reason to expect work now waits up to expect_wait (5 s, 0 = off, at most 60) before it starts, for that work to queue, and starts as soon as it has. The reason is a declaration (--expect), or the job history for that checkout: at least two of the stack's last five runs there, and on at least half of them, a job of the same pool (or key, outside any pool) queued there while it ran. Work the history implies books nothing and only links while the stack waits; once it queues, the stack books its need. A stack with neither reason starts at once. A stack with work queued for it, and work whose stack runs, now hold their turn as a strict reservation instead of by backfill, as long as they could fit once the jobs that end by themselves have ended. Without that, a stream of lints kept taking the room a stack and its run needed together until the caller gave up (17 of 23 runs at 14.5 GB outside in the model). Entry gains expect_until, Expect gains implied (serde defaults). New setting expect_wait. The database gains no column: the history comes from runs.
The MacBook morning: a caller that declares its run never gives it up, at 11, 12.8 and 14.5 GB held outside taskguard, and memory stays under the limit; without a declaration the progress rule alone clears 11 and 12.8 GB. A stack and its run queued together: with a declaration or the history, no run is given up and none starts before its stack. Omarchy: the second stack stays out and the small jobs behind it start within minutes. The scout's fallback (a steady stack books what it holds, without room to grow) stays a printed comparison only: it gives up 5 runs at 12.8 GB.
…follows it, as a bounded guess The captain: "would be cool if we could grab something from the context to determine that there will indeed be work incoming for the dev stack. And ideally we know what work so we know the full scope of the load on the machine." A long-lived job with no declared work now looks at its last infer_work_of (5) runs in its checkout. Work of one kind (a pool, or the key of a job outside any pool) that queued there within infer_work_within (900) seconds of their start on at least infer_work_min (3) of them is expected again: the stack books its usual need (the median need it queued with), status shows it as "from history", and the work links and goes first as declared work. This replaces the looser history rule that only drove expect_wait. Bounds, so a wrong guess cannot hurt: - the booking ends twice the usual delay after the stack started, ten minutes at most, and once work for the stack starts; - the rule for nothing running ignores a guess, so the queue never stops for one, and a job waits for it at most up to all of RAM; - a stack whose caller declared no work ignores guesses, its own included: it starts as it would without the history; - a declaration wins, --expect none expects nothing, infer_work = false turns it off; - once the guessed work queues, its room is booked as declared work. In the model the undeclared MacBook morning now clears at 14.5 GB too (no run given up), and a bad guess (no run ever follows) starves nothing: no lint or stack waits more than a minute longer than without the guess, though about a quarter fewer lints run at 14.5 GB. Data formats, additive: Expect gains expire_after_s and expired. New settings infer_work, infer_work_min, infer_work_of, infer_work_within. No database change: the history query reads runs as they are.
…side_admit lone-stack ceiling
…ing; apply rustfmt
… a full macOS runner On macos-latest the test waited 195 s against its 15 s bound: the stack and its run are first runs that book new_job_mem (1.5 GB) each, and the runner reports most of its 7 GB in use. A lone stack that does not fit in all of RAM waits by the ruled hard ceiling, so the test measured the host's memory instead of the expect_wait logic. It now books 64 MB each.
Restore still_expected to '!x.implied || x.open()': once the run that took over a history guess has started, a second matching run in the checkout keeps its own priority instead of linking to the stack and holding its turn. taskguard status no longer labels a guess that came true as "from history".
The keys keep their room: the version shows as 'taskguard VERSION', or 'vVERSION' when narrow, and not at all when the keys fill the line.
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.
Why
Idle dev stacks were blocking the browser runs they exist for. On the MacBook (10 cores, 32 GB, mem_max 95), the caller queues a stack's run only once the stack is up, about 6 minutes later, so taskguard could not know the run was coming. Two things then went wrong:
The first stack's run then waited until the agent's 10-minute command timeout gave up and took the stack down: 7 of 97 browser runs in three days, 6 of them at exactly 10.0 minutes. On Omarchy, one 9.1 GB stack that was 5.4 GB short held back 25 other jobs for 27 minutes.
The captain's ask: queue work for a stack, so the manager knows to prioritise it and knows the full load on the machine. Use context to tell whether work is coming and what it is. A stack with no work must stay possible.
What changed
mem_max(+noise_mem), or under theoutside_admitceiling when only programs outside taskguard keep it out. When nothing of taskguard runs, a lone job (stack or not) may go up to 100% of RAM, so outside programs can never keep it waiting forever. A job that does not fit holds nobody back. The rule also decides at macOS warning; critical still stops everything.--expect WORK(repeatable) orTASKGUARD_EXPECT=WORK,WORKon a long-lived job, where WORK is a pool name, job key or command pattern. DALP can pass the env var today throughdevenv up.ea52172).work_for) and goes to stack priority + 1. It takes over the booked room when it starts, and while its stack still waits it waits too, holding nobody back.(for stack KEY)on the work,(expects WORK, books SIZE)on the stack.infer_work, on by default).infer_work_min(3) of its lastinfer_work_of(5) runs in the checkout had work of one kind (a pool, else a key) queue withininfer_work_within(900 s) of their start.(expects browser from history, books …).--expect noneexpects nothing, andinfer_work = falseturns inference off.expect_wait(5 s; 0 turns it off). A stack that expects work, declared or inferred, waits up to that long before starting for the work to queue, and starts as soon as it has. A stack that expects nothing starts at once.outside_admitunder a highmem_max. The ceiling waspause_at(92), undermem_max95, sooutside_admitcould never admit anything on the MacBook. It is now halfway frommem_maxto 100% whenmem_max >= pause_at(97.5 for 95).steady_mem_kbdoc, test comment and README sentence fixed: a steady job books what it holds + 25%.src/stall.rs: a test-only model driving the realdecide(),Entry::follow()andlink_work()second by second. It asserts the scenarios below.Data formats (Mixed versions): all additive, with serde defaults
queue::Entry:long_lived,expects: Vec<Expect>(work, mem_kb, from, started, implied, expire_after_s, expired),work_for: Option<WorkFor>,expect_until.queue::Nudge:work_started.machine::MachineSample, the database schema (the history query reads existingrunscolumns), and job keys.expect_wait,infer_work,infer_work_min,infer_work_of,infer_work_within. Older versions warn about them and ignore them.expect_waitas stalled after 10 s, so keepexpect_waitunder 10 while they share a machine.Evidence for "work is incoming"
expect_wait. A stack with neither, or with--expect none, starts at once.Model results (
src/stall.rs)Setup: 2 h, MacBook limits (mem_max 95, max_backfill 1800, warn_admit), outside load 11 / 12.8 / 14.5 GB. Two agents each bring up a stack, queue their browser run (3 cores, 4.3 GB) once it is up, and give the run up after 10 minutes. An older turbo run keeps queueing lints.
Columns: runs done · runs given up · worst run wait · stack idle while its run waited · max stacks at once · worst memory · seconds over the limit · lints done · worst stack wait.
Before (master, 0.10.0 rules):
After (head 965cdaa):
Stack and run queued together (the run 2 s after the stack):
Bad guess (history infers a run after every stack, but the agents only boot stacks, 20 min each, and never queue a run):
Which scenarios clear:
TASKGUARD_EXPECT=browser): clears at all three loads. At 12.8 and 14.5 GB the run starts within 9 s of being queued. The cost: one stack at a time, so 11–13 runs in 2 h, and a second stack waits up to ~12 min.infer_work = false): the progress-rule fix alone clears 11 and 12.8 GB, but not 14.5 GB (6 given up).Validation
cargo fmt --checkok,cargo clippy --all-targets -- -D warningsok,cargo test: 197 unit + 31 CLI tests passed.744ec32,460f763).ea52172); and a stack-hold test was aligned with the ruled lone-job ceiling (8d5e037). It then drove 7 live CLI scenarios against the real binary, all passing.c87d101).a_stack_waits_for_the_work_it_expects_when_both_queue_togetherwaited 195 s against its 15 s bound. Its first-run bookings (1.5 GB each) did not fit in all of RAM on the 7 GB runner, which is the ruled hard ceiling, so the stack waited for memory. Fixed in the test, which now books 64 MB (50795e8).965cdaa).db82278. Its review fixed the explain memory line (3a73eed); its document step reflowed README and CHANGELOG (db82278).Residual fixed, and pre-release 0.11.0-rc.1
0e96cc0):still_expectedis!x.implied || x.open()again. Once the run that took over a history guess has started, a second matching run in the same checkout no longer links to the stack, gets no priority lift and does not hold its turn; one that linked earlier lets go of its link. Declared work still links a second run. Test:a_second_run_does_not_ride_on_a_guess_the_first_run_made_come_true. The stall model's tables are unchanged by it.0e96cc0):taskguard statusshows "from history" on a guess only until work queues for it (testa_guess_reads_from_history_until_work_queues_for_it).3a73eed): the memory check inexplain/status leaves out a waiting undeclared stack's own guessed booking, as the scheduler does, so it no longer shows a failure for a stack that would start.79beb9e): a tag with a suffix (v0.11.0-rc.1) is published as a pre-release and never as latest; the release skill notes this.ff7e3b5):0.11.0-rc.1, with itsCHANGELOG.mdsection. This PR stays unmerged; the pre-releasev0.11.0-rc.1is tagged on its head.Version in
taskguard top, and pre-release 0.11.0-rc.22c5c3c6:taskguard topshows the binary's version (CARGO_PKG_VERSION; the build exposes no git describe and no build script was added) at the right of the key bar, in the room the keys leave:taskguard VERSION,vVERSIONwhen narrow, nothing when the keys fill the line, so it never hides a key. Test:the_footer_shows_the_version_at_the_right_when_the_keys_leave_room.4ae9d40: version0.11.0-rc.2, with itsCHANGELOG.mdsection; the pre-releasev0.11.0-rc.2is tagged on this head. Validated by a fourth no-mistakes run (review clean, test, document, lint, push) with no fixes.Not in this PR
master.TASKGUARD_EXPECT=browserwhere an agent brings a stack up for a browser run; with history inference this is no longer required.