tick: hold a due spawn while it would take the host under its memory floor #1

Merged
caoer115 merged 5 commits from spawn-memory-gate into main 2026-10-20 06:36:47 +00:00
Owner

timeline-lax (7.9 GB) dipped under its 3.5 GB MemAvailable backstop at 04:05Z 10-20 while timeline seats started on top of mrd's resident swing.

Change: before each spawn the tick asks memgate: MemAvailable must stay ≥ spawnFloorMB after reserving seatMB for this seat and for each seat already fired that tick. A held key writes no ledger row, so it stays due and fires on a later tick (catch_up: once jobs keep their latest slot; items stay due 6 h; a catch_up: skip job is dropped after its 2 min grace). The tick logs deferred <kind> <slug>: <why>, state.json carries deferred_since, and both boards show "spawns held for memory since …" / 「内存不足,自 … 起推迟启动」 while it is set.

Module defaults: spawnFloorMB = 3500, seatMB = 500. The binary's default floor is 0, which turns the gate off.

Tests: memgate (arithmetic, off/unreadable, meminfo parse), engine TestAdmitDefersUntilAdmitted (no ledger row on hold, Admit sees the tick's fired count, the held key fires once admitted), and the board line in TestRenderFlagsAndEscapes. go vet ./... is clean, go test ./... gives 42 PASS and 0 FAIL, and nix build .#timeline builds (vendorHash unchanged).

Deploy: merge, bump the agent-timeline input in osfiles, then leader-osfiles switches timeline-lax. The defaults need no osfiles config change.

Evidence: sessions card year=2026/month=10/06-20-tooling-dev/tasks/timeline-lax-item-seats-memory-bound.md § Findings.

timeline-lax (7.9 GB) dipped under its 3.5 GB MemAvailable backstop at 04:05Z 10-20 while timeline seats started on top of mrd's resident swing. **Change:** before each spawn the tick asks `memgate`: MemAvailable must stay ≥ `spawnFloorMB` after reserving `seatMB` for this seat and for each seat already fired that tick. A held key writes no ledger row, so it stays due and fires on a later tick (`catch_up: once` jobs keep their latest slot; items stay due 6 h; a `catch_up: skip` job is dropped after its 2 min grace). The tick logs `deferred <kind> <slug>: <why>`, `state.json` carries `deferred_since`, and both boards show "spawns held for memory since …" / 「内存不足,自 … 起推迟启动」 while it is set. Module defaults: `spawnFloorMB = 3500`, `seatMB = 500`. The binary's default floor is 0, which turns the gate off. **Tests:** `memgate` (arithmetic, off/unreadable, meminfo parse), engine `TestAdmitDefersUntilAdmitted` (no ledger row on hold, Admit sees the tick's fired count, the held key fires once admitted), and the board line in `TestRenderFlagsAndEscapes`. `go vet ./...` is clean, `go test ./...` gives 42 PASS and 0 FAIL, and `nix build .#timeline` builds (vendorHash unchanged). **Deploy:** merge, bump the agent-timeline input in osfiles, then leader-osfiles switches timeline-lax. The defaults need no osfiles config change. Evidence: sessions card `year=2026/month=10/06-20-tooling-dev/tasks/timeline-lax-item-seats-memory-bound.md` § Findings.
Each spawn now asks memgate first: MemAvailable must stay at or above the
floor after reserving seatMB for this seat and for each seat the tick already
fired. A held key writes no ledger row, so it stays due and fires on a later
tick (catch_up: once jobs keep their latest slot, items stay due 6 h). The
tick prints `deferred <kind> <slug>: <why>`; state.json carries deferred_since
and both boards show "spawns held for memory since …" while it is set.

Module defaults: spawnFloorMB 3500 (timeline-lax's 3.5 GB backstop),
seatMB 500 (claude seats measured at 0.25-0.5 GB within ~12 s of spawn).
Binary default floor 0: no gate unless configured.
Review rv-at1 and the SA's ruling on PR 1:
- state.json `held` carries each held key's first hold across the oneshot
  ticks; a key past its bound (items and `audience: zt` 10 min, the rest
  60 min) fires anyway, its ledger rows carry held_since, and the ## Fires
  line, timeline.json and both boards say it started under low memory.
  A job's newer slot inherits the hold of the slot it replaces.
- `audience: zt|housekeeping` (absent: by type) is validated and settable
  with --audience; ZT-facing keys are admitted ahead of the rest.
- A held catch_up: skip slot whose grace ends writes an error row
  (`dropped: held for memory N min`) and a journal line; a held entry that
  is cancelled or edited ends its hold with a journal line.
- MemAvailable is read once per tick; an unreadable read is said once per
  tick and admits; the hold line names the key.
- Each held entry is marked on the boards (item line, job card).
A tick stopped after the wiki commit but before state.json is saved left
held keys that already fired; the next tick wrote a false 'dropped: held
for memory' error row for each (re-review rv-at1b finding 1). slotStands
now requires a job slot to be after last_fired.

inheritHolds passed an old hold to a newly due catch_up: skip slot, which
then fired at once under low memory; a skip slot is now held from its own
first deferral and the superseded slot gets its drop row (finding 3).

TestValidateRefusals gains the audience row (finding 2).
A skip job's superseded slot dropped in the tick its new slot fires wrote
its drop row after the fire rows, and the job card (the slug's last row)
showed a failure for the run that started. The drop now lands first.

A drop already in the ledger (its tick lost the held map) is not written
again, and a job slot its every no longer yields (an edit moved the
schedule) ends the hold as an entry change, not a memory drop.
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
caoer115/agent-timeline!1
No description provided.