You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
fix(skills/bounding-heavy-commands): show commands that wait for memory in the check - #91
A bounded command that waited for free memory held its slots, but --check showed no line for it, because the held lines come from the scopes of running commands. So --check could show taken slots with no held line to explain them, and the user could not see what held the queue. Now the wrapper holds a wait file while it waits for memory, and --check prints a waiting line for each such command.
This PR is visibility only. An earlier version also gave the slots back while a command waited for memory, so that a smaller command that fits could start. That part was dropped to keep the line fair: a large waiter would lose its turn, and smaller commands could take the memory each time it came free, so the large one could wait without end. The case where the queue was blocked with no bounded command running already ends with exit 75 since #85. The wait that is left lasts until a running bounded command frees its memory, and holding the slots there keeps the line fair.
Changes
bounded_run.py: while it waits for memory, the wrapper holds a locked wait-<unit>.lock file with the budget, the slots that it holds, the start time, the directory, and the command (no process ID). It writes the file once, before the first sleep of the wait, through a new on_wait callback of wait_for_memory, and removes it when the wait ends, before the command starts.
--check prints bounded-run: check waiting unit=<name> for=memory budget=<n>M slots=<n> age=<n>s dir=<directory> command=<command> for each wait file, also without the hard cap. It removes a wait file that no process holds.
SKILL.md: the check-line formats; how to read a waiting line (a command that had its turn, keeps its slots, and waits for the memory that a held command frees when it ends, because programs outside the queue hold most of it); the report after a long wait; the lock-file rule (a deleted wait file hides a waiter from --check); and the verbatim subagent rule now name the waiting lines.
The script tests pass: 374 tests, run through the bounded wrapper at --memory 512M. The wait_for_memory tests of main are unchanged. A process test runs a 6G waiter beside a 2G holder on 5 slots. --check prints free-slots=1 and a waiting line with the waiter's unit, budget, slots=3, directory, and command. When the holder ends, the waiter exits 75 and leaves no wait file. Unit tests cover the on_wait callback, the wait file's fields and cleanup, the removal of the file before the command starts, and the waiting line of --check.
The skill text went through RED and GREEN with isolated subagents. With the final text, 3 of 3 agents that read a check with one held line and one waiting line said that the waiting command holds its slots and keeps its turn, and that it starts when the held command ends.
…waits for memory
The wrapper took its slots, and then it waited until the free memory held its budget. While it waited, it kept the slots but had no scope. So `--check` showed no line for it, and a smaller command that fit in the free memory waited behind slots that nothing used.
Now the wrapper gives back its slots while the free memory does not hold its budget. When the memory looks free again, it takes slots through the line and reads the memory again under the memory lock before it starts. So two waiters that see the same free memory cannot both start on it. The wait has the same no-memory exit 75 when no other bounded command runs.
While it waits for memory, the wrapper holds a wait file locked in the lock directory. The file names the budget, the slots, the start time, the directory, and the command. It holds no process ID. `--check` prints one `check waiting ... for=memory` line for each wait file, also without the hard cap, and removes a wait file that no process holds.
Fixes#80.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mand that waits for memory
The wrapper now gives back its slots while it waits for free memory, and `--check` prints a `check waiting ... for=memory` line for each such command. The skill did not say how to read that line.
The skill now gives the line format. It says that a waiting command holds no slot and has no place in the line, and that the line means the programs outside the queue hold the memory, not the slots. The report after a long wait, and the rule that a dispatcher gives its subagents, now name the waiting lines too. The lock files now carry no claim that they are empty, because a wait file names its command.
In a baseline, agents that read a check with two waiting lines took them as an order in the line ("head of the line", "behind us"). One blamed the only running bounded command for the shortage. With the new text, each agent read the lines correctly and told its dispatcher that programs outside the queue hold the memory.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… a stale name
A waiter puts its wait file at the same name each time it gives back its slots. A check can lock a stale file at that name, or create an empty one there in the moment after the waiter removed its old file. The waiter's rename can then put its new, held file at the name before the check removes the name, so the check deleted the live file and the waiter dropped out of `--check`.
The check now removes the name only when the file at the name is still the file that it locked. Otherwise it skips the name in this pass. Run files need no such change: each unit makes its run file once and never makes it again under the same name.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cross its waits
A waiter wrote a new start time to its wait file each time it gave back its slots. When the memory went away again after the slots came back, the `age` on its `check waiting` line started again at 0, and a command that waited for a long time looked new.
The wrapper now keeps the time of its first wait for memory and writes it to each new wait file.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The rule about the lock files said that a deleted lock file makes two commands hold one slot. That is true of a slot file only. A deleted wait file hides a command that waits for memory from `--check`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This synchronization point fires before the waiter releases its slots and creates the wait file: wait_for_memory logs ; waiting before calling release(). The following --check can therefore run first and intermittently miss the expected waiting line. Wait for the wait-*.lock file to appear before returning from this helper.
…mory-waiter tests
The test helper waited for the "; waiting" line of the large command. The wrapper prints that line before it gives back its slots and writes its wait file, so a check that ran at once could miss the `waiting` line. The helper now also waits until the wait file exists.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review round 2 (review 5426377966, head 1ae738a). Review base: 282db8d.
The review posted no inline findings. Its body has one "Previously missed" finding, which has no thread to reply in:
Synchronize after creating the wait file (test_bounded_run.py:3181): applied in d39bd63. The memory_waiter helper now waits until the wait-*.lock file exists, as well as the "; waiting" line. The wrapper prints that line before it gives back its slots and writes the file, so a --check that ran at once could miss the waiting line.
…ry wait's docstring
The docstring of `wait_for_memory` said that the call returns at once when the free memory cannot be read. Since the wait gives back its slots, the call first takes them back with `hold`, which can wait in the line. It also said that NoMemory leaves the slots as they are, which a caller can read as "still held". NoMemory can come after the call gave the slots back.
The docstring now says that the call returns without a wait for memory when the free memory cannot be read, and that NoMemory can come after the call gave the slots back.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review round 3 (review 5426419391, head d39bd63): "Approval recommended", no findings, inline or in the body.
Before handing back, I ran the comment gate against origin/main. It flagged two docstrings that this PR grew. A fresh evaluator judged both:
Block
Lines (base → now)
Sentence
Verdict
Source / caller code
Judge
module usage text (text that users read)
90 → 94
"one waiting line for each bounded command that waits for memory"
KEEP
check() prints the waiting lines between the held and budget lines; a parser must expect them
evaluator
module usage text
90 → 94
the check waiting format line
KEEP
the format string in memory_waiters
evaluator
module usage text
90 → 94
"A command that waits for memory holds none of the slots of its waiting line, …"
KEEP
give_back_slots releases before it writes the wait file
evaluator
wait_for_memory docstring
8 → 14
"Returns at once when the free memory cannot be read." (from main)
CUT, false now: after a give-back the call runs hold() first. Replaced in e20798e with "When the free memory cannot be read, the call returns without a wait for memory." (KEEP)
fits = available is None or … then return
evaluator
wait_for_memory docstring
8 → 14
"NoMemory leaves the slots as they are."
CUT, false: the slots can already be given back. Replaced in e20798e with "NoMemory can come after the call gave the slots back." (KEEP)
the NoMemory handler cannot assume that the slots are still held
evaluator
wait_for_memory docstring
8 → 14
"…so the caller gives back only the slots that it still holds."
ASK, no source: removed
evaluator
wait_for_memory docstring
8 → 14
"…so that a smaller command can start with them", "…and reads the memory again before it returns", "hold can wait in the line for minutes"
Proposals only (CUT/MOVE as descriptions of the body). Not applied: this is true text that the PR wrote before the review
evaluator
The PR body is reconciled: it now has the kept start time, the wait-file sentence of the lock-file rule, and 379 tests.
The reason will be displayed to describe this comment to others. Learn more.
Copilot review overview
🟢 Approval recommended
The implementation satisfies the issue criteria with comprehensive unit and process-level regression coverage.
Review effort: Balanced Findings: None
sourcehawk
changed the title
fix(skills/bounding-heavy-commands): stop memory waiters from holding slots
fix(skills/bounding-heavy-commands): show commands that wait for memory in the check
Oct 6, 2026
… waits for memory
Giving back the slots while a command waits for memory breaks the fairness of the line. The large waiter loses its turn, and smaller commands that fit can take the memory each time it comes free, so the large one can wait without end. The case that blocked the queue with no bounded command running already ends with exit 75. The wait that is left lasts until a running bounded command frees its memory, and holding the slots there keeps the line fair.
A command that waits for memory now keeps its slots and its turn, as before this branch. `wait_for_memory` has the contract and tests that it had on main, plus one `on_wait` callback that runs before each sleep. The wrapper uses it to write its wait file once, and removes the file when the wait ends. So `--check` still prints a `waiting` line, and its `slots` are the slots that the waiter holds. Because the wait file is now written once for each wait, the start time needs no copy and the reader needs no check that the name still holds the file that it locked.
The process test now checks that `free-slots` counts the slots of the waiter beside a holder, and the test that a smaller command starts before the waiter is gone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The skill said that a command that waits for memory gives its slots back, leaves the line, and lets smaller commands start before it. The wrapper no longer does that.
The skill now says that a `waiting` line is a command that had its turn and keeps its slots while it waits, so it does not lose its turn to a command behind it. It waits for the memory that a running bounded command of a `held` line frees when it ends, because the programs outside the queue hold most of the memory. It starts when that memory is free, or exits 75 when no bounded command runs any more. The report after a long wait and the rule for subagents say that the machine is short of memory, not of slots.
With this text, isolated agents that read a check with one `held` line and one `waiting` line each said that the waiting command holds its slots and keeps its turn, and that it starts when the held command ends.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Scope change (ce84ad9, fdbf4b9): this PR is now visibility only. A command that waits for memory keeps its slots and its turn, as on main. The give-back of slots was dropped to keep the line fair; the PR body says why. The new commits are added on top, with no force-push, so the review history stays.
Comment gate against origin/main: 2 flagged. A fresh evaluator judged the new sentences:
Block
Sentence
Verdict
Source / caller code
Judge
module usage text (text that users read)
"A waiting line is a command that holds its slots and its place in the line, and waits for memory that a running bounded command will free."
CUT, false: the wait file is written after reserve() returns, when the waiter has already released line.lock. Replaced with "A waiting line is a command that holds its slots and waits for memory." (KEEP)
reservation = reserve(...) held until reservation.release(); mark_waiting runs only from on_wait
evaluator
module usage text
"…that a running bounded command will free."
ASK, no source (read_running can be None): removed
evaluator
wait_for_memory docstring
"on_wait is called, with lock released, before each sleep of the wait."
KEEP
unlock(); on_wait(); sleep(...); the caller must make it safe to call many times and must not need the lock
evaluator
The same correction went into SKILL.md. The waiter "keeps its slots while it waits, so it does not lose its turn to a command behind it in the line". GREEN ran again on that text: 3 of 3 isolated agents said the waiter holds its slots and keeps its turn.
…mand, which has a held line only under the cap
The skill said that a waiting command waits for the memory of a running command of a `held` line, and that the waiting commands start when the `held` commands end. Without the hard cap there are no `held` lines, but `--check` still prints the `waiting` lines.
The skill now says that the waiter waits for a running bounded command, and that this command has a `held` line where the hard cap is available.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review round (review 5430138547, head fdbf4b9): 1 inline finding, none in the body.
Clarify the waiting dependency where no hard cap means no held lines: applied in 1fedfd5, at lines 68 and 162. Line 196 already did not depend on a held line; the thread reply says why.
The review overview also says that the PR closes #80 without meeting all its acceptance criteria. That is deliberate: criterion 2 (a smaller command that fits starts before a waiter) was dropped to keep the line fair, as the Description says. The decision about #80 is with the maintainer.
PR closes #80 while its acceptance criterion remains unresolved
skills/bounding-heavy-commands/SKILL.md:68
This now codifies the opposite of issue #80's second acceptance criterion: the waiter keeps its slots, so a smaller command that fits can still be blocked. Because the PR says Fixes #80, merging it would close the issue while that required behavior remains unresolved. Either update/supersede that acceptance criterion before merge, or stop closing #80 and track this visibility-only change separately.
Follow-up to the "Previously missed" finding of review 5430194018 (PR closes #80 while its acceptance criterion remains unresolved): #80 is updated. Its criterion 2 is dropped, and #80 (comment) records why and links ce84ad9. The criteria are now the waiting line of --check, and the skill text about how to read it. This PR meets both, so Fixes #80 stays. The PR body already matches the new criteria, so it does not change.
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
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
Fixes #80
A bounded command that waited for free memory held its slots, but
--checkshowed no line for it, because theheldlines come from the scopes of running commands. So--checkcould show taken slots with noheldline to explain them, and the user could not see what held the queue. Now the wrapper holds a wait file while it waits for memory, and--checkprints awaitingline for each such command.This PR is visibility only. An earlier version also gave the slots back while a command waited for memory, so that a smaller command that fits could start. That part was dropped to keep the line fair: a large waiter would lose its turn, and smaller commands could take the memory each time it came free, so the large one could wait without end. The case where the queue was blocked with no bounded command running already ends with exit 75 since #85. The wait that is left lasts until a running bounded command frees its memory, and holding the slots there keeps the line fair.
Changes
bounded_run.py: while it waits for memory, the wrapper holds a lockedwait-<unit>.lockfile with the budget, the slots that it holds, the start time, the directory, and the command (no process ID). It writes the file once, before the first sleep of the wait, through a newon_waitcallback ofwait_for_memory, and removes it when the wait ends, before the command starts.--checkprintsbounded-run: check waiting unit=<name> for=memory budget=<n>M slots=<n> age=<n>s dir=<directory> command=<command>for each wait file, also without the hard cap. It removes a wait file that no process holds.SKILL.md: the check-line formats; how to read awaitingline (a command that had its turn, keeps its slots, and waits for the memory that aheldcommand frees when it ends, because programs outside the queue hold most of it); the report after a long wait; the lock-file rule (a deleted wait file hides a waiter from--check); and the verbatim subagent rule now name thewaitinglines.Related
Testing
The script tests pass: 374 tests, run through the bounded wrapper at
--memory 512M. Thewait_for_memorytests of main are unchanged. A process test runs a 6G waiter beside a 2G holder on 5 slots.--checkprintsfree-slots=1and awaitingline with the waiter's unit, budget,slots=3, directory, and command. When the holder ends, the waiter exits 75 and leaves no wait file. Unit tests cover theon_waitcallback, the wait file's fields and cleanup, the removal of the file before the command starts, and thewaitingline of--check.The skill text went through RED and GREEN with isolated subagents. With the final text, 3 of 3 agents that read a check with one
heldline and onewaitingline said that the waiting command holds its slots and keeps its turn, and that it starts when the held command ends.🤖 Generated with Claude Code