Skip to content

fix(AlgorithmNudge): latch sweep-delegation to once per inline stretch - #2123

Open
pai-scaffolde wants to merge 1 commit into
danielmiessler:mainfrom
pai-scaffolde:fix/s-nudge-sweep-delegation-latch
Open

pai-scaffolde wants to merge 1 commit into
danielmiessler:mainfrom
pai-scaffolde:fix/s-nudge-sweep-delegation-latch

Conversation

@pai-scaffolde

Copy link
Copy Markdown

Reproduced on a fresh LifeOS 7.40.4 install (macOS, Claude Code in the Claude desktop app); the fix was applied to that install and run there before filing.

Observed

During a long verification sweep — Bash/Read/Grep, no dispatches — the sweep-delegation row kept coming back: once at 20 inline calls, then again every 20 calls once the 5-minute cooldown lapsed. Simulated against the unpatched hook, 60 inline calls produce 3 fires of the same question:

AlgorithmNudge.base.hook.ts: 60 inline calls -> 3 sweep-delegation fire(s); dispatch; 60 more -> 3 fire(s)

Root cause

AlgorithmNudge.hook.ts:822-823 bounds the row with cooled(state, 'sweep-delegation', now) and a counter reset. A cooldown is a clock; what the row asks about is a stretch. The first ask is legitimate — "20+ consecutive inline calls" is an aggregate the model never counted. Every later ask in the same stretch is about calls the model has just made and can still see in its own context, which is the bound this file already states for itself and the reason the principal row was cut on 2026-07-30: a row may only ask about state the model CANNOT observe. The clock also gets it wrong in the other direction — a dispatch followed by 20 fresh inline calls inside five minutes is exactly when the question is live, and the cooldown silences it.

Fix

Latch instead of cool. A sweepDelegationFired flag is set when the row fires and cleared only by an Agent dispatch, alongside the existing buildCallsSinceDispatch / workCallsSinceDispatch resets. One question per inline stretch, and a new stretch always re-arms it. cooled() is untouched and still bounds every other row.

How tested

Simulation harness: run(input) called directly with PostToolUse payloads under a temp HOME (the hook derives its state dir from homedir(), so state lands in the temp tree, not the install). 60 Bash calls, then one Agent dispatch, then 60 more; every 20 calls the recorded nudge timestamps in the state file are rewound 6 minutes, which is the 5-minute cooldown elapsing during a real sweep. Counted lines containing "consecutive inline tool calls, zero dispatches":

AlgorithmNudge.base.hook.ts: 60 inline calls -> 3 sweep-delegation fire(s); dispatch; 60 more -> 3 fire(s)
AlgorithmNudge.hook.ts:      60 inline calls -> 1 sweep-delegation fire(s); dispatch; 60 more -> 1 fire(s)

Transpiles (bun build --target=bun --no-bundle LifeOS/install/hooks/AlgorithmNudge.hook.ts, exit 0). Running on the install since 2026-09-14.

Independent of #2083 (late-ISA re-arm): different state field, different block; both apply cleanly in either order. Verified by three-way applying each patch on top of the other — the only conflict is the @version line (3.2.3 here vs 3.3.0 there), identical in both directions; every other hunk lands, the merged file transpiles, and the simulation above still reports 1 fire per stretch on it.

Repro on a clean tree: check out upstream/main, point HOME at a temp dir, and drive run() with 60 PostToolUse payloads for a non-dispatch tool on one session_id, rewinding lastNudgeAt by 6 minutes every 20 calls; the row fires 3 times before this change and once after.

🤖 Generated with Claude Code

Through a long verification sweep the sweep-delegation row fired again
every 20 tool calls once the 5-minute cooldown lapsed — three times in 60
inline calls in simulation. The cooldown was never the right bound: it is
a clock, and the thing the row asks about is a stretch. Every re-ask after
the first is about calls the model has just made and can still see, which
is the same "state the model cannot observe" rule that retired the
`principal` row.

Latch the row instead: a new `sweepDelegationFired` flag is set when it
fires and cleared only by an Agent dispatch, so each inline stretch is
asked about exactly once and a genuinely new stretch is never silenced by
a clock that happens to still be running.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant