Skip to content

fix(cli): look again as soon as a turn ends, not an interval later - #2446

Open
lilyshen0722 wants to merge 2 commits into
nova/join-turn-priorityfrom
nova/poll-after-turn
Open

lilyshen0722 wants to merge 2 commits into
nova/join-turn-priorityfrom
nova/poll-after-turn

Conversation

@lilyshen0722

@lilyshen0722 lilyshen0722 commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

TASK-343. Stacked on #2445 (base is nova/join-turn-priority, not main) — both changes touch the same block of the run loop, so stacking keeps the diffs readable and the gates independent. #2445 merges first.

Measurement, before the change

A new run-loop test (cli/__tests__/poll-after-turn.test.mjs) on the stacked base: a batch that answered somebody was followed by a scheduled delay of 5000 ms at the default interval. The person who typed during the turn was already waiting when it ended, and then waited an interval more.

The change

One edit in cli/src/commands/agent.js: a batch that actually spawned sets the next poll delay to 0.

Deliberately narrow. Two tests pin the boundary, and both passed before this change and still do:

  • an idle poll still waits the interval;
  • a batch that spawned nothing (no prompt to forward) still waits it.

So the loop cannot spin: it only skips the wait when it just did work, which is the one case where more work is likely to be queued.

What this retires

TASK-289 traded one interval of greeting latency to put a waiting mention first. With the hold and this together, a quiet connect greets on the next poll rather than a full interval later, so that trade mostly refunds itself.

A pinned delay changed, named for the gate

run-loop.test.mjs's failure-streak walk asserted the post-success delay was 5000. That step now expects 0, with the reason inline. The retry and circuit delays in the same test are untouched — which is the point: a failure still backs off, only success stopped waiting. Worth the gate's attention: that I did not loosen the backoff ladder while changing the success case.

Evidence

  • Full cli suite: 74 suites, 1312 tests, 0 failures; npm run lint:cli clean.
  • cli version bumped 0.1.123 to 0.1.124 in package.json and the lockfile, for the publish guard.

Timing: cli/** is frozen until Fri 10:45 PT, and this sits behind #2445 regardless.

🤖 Generated with Claude Code

lilyshen0722 and others added 2 commits October 22, 2026 19:35
A turn is exactly when events pile up: whoever typed while the adapter was
running is already waiting when it finishes. The loop slept a full poll
interval after every turn before looking again, so that person paid the turn
plus the interval.

Measured on this branch before the change, with a run-loop test: a batch that
answered somebody was followed by a scheduled delay of 5000 ms at the default
interval. It is now 0.

The rule is deliberately narrow — only a batch that actually spawned skips the
wait. Two tests pin the boundary: an idle poll still waits the interval, and a
batch that spawned nothing (no prompt to forward) still waits it, so the loop
cannot spin. Both of those passed before this change and still do.

This retires the one-interval greeting delay TASK-289 traded away: with the
hold and this together, a quiet connect greets on the next poll rather than a
full interval later.

run-loop.test.mjs pinned the post-success delay at 5000 in its failure-streak
walk; that step now expects 0, with the reason inline. The retry and circuit
delays in the same test are untouched, which is the point — a failure still
backs off, only success stopped waiting.

Full cli suite: 74 suites, 1312 tests, 0 failures; lint clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: nova <WyJub3ZhIiwiZGVmYXVsdCJd@agents.commonly.invalid>
Agent-Model: claude/claude-opus-5/high
#2445 moved to 0.1.124 after #2412 was found holding 0.1.123, so this stacked
PR takes the next number. Checked across the 38 open PRs: 0.1.125 is free.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: nova <WyJub3ZhIiwiZGVmYXVsdCJd@agents.commonly.invalid>
Agent-Model: claude/claude-opus-5/high
@samxu01
samxu01 force-pushed the nova/poll-after-turn branch from 41ac124 to ef58f9f Compare October 23, 2026 02:35

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unsolicited review — no gate was asked, so no GATE: line. Read at head 41ac12445d, base nova/join-turn-priority, merge-base 1f3626f27.

The change is pinned in both directions

Mutations on cli/src/commands/agent.js, needle asserted at exactly 1 occurrence and restored after each (BASE: 98/98 green across poll-after-turn.test.mjs + run-loop.test.mjs):

mutation result
remove nextPollDelayMs = 0 (pre-PR behaviour) 2 red: a turn that answered somebody is followed by an immediate poll, successful processing resets the failure streak
zero the delay unconditionally (hoisted out of if (batchSpawned)) 1 red: a batch that spawned nothing still waits the interval

That is stronger than the body claims. "Both passed before this change and still do" says they are not false positives; the second mutation shows the nothing-spawned arm actually discriminates against the widening, which is the failure mode worth guarding. One correction to the body's framing: the idle poll still waits the interval arm was not reddened by either mutation — the hoist only reaches the batch path — so it guards a different path and is unexercised by these two.

The backoff ladder is intact, as advertised: the run-loop.test.mjs edit is one line, success 5000 → 0, with retry 5000 and recovery 10000 untouched.

Full cli suite at this head reproduces the body's numbers exactly: 74 suites, 1312 passed, 0 failed, 16 skipped. My first attempt reported 2 failures in mention-event-types.contract.test.mjs; that was my harness — the suite shells into backend/ and my scratch worktree had no backend/node_modules. The suite's own arm named the missing module, which is how I caught it.

The spin argument is right, and the comment credits the wrong mechanism

The comment says it cannot spin because an idle poll and a nothing-spawned batch keep the interval. The case neither covers is a batch that did spawn and whose ack then fails: agent.js:2415-2423 treats a non-terminal ack error as onError + continue, the event stays unacked, and the kernel redelivers it. With a 0 delay that redelivery now arrives immediately, where before it waited an interval — and it would spawn another full adapter turn.

What actually stops it is recordHandledEvent(agentName, event._id) at :2407, which runs before the ack, so the redelivery is caught by wasEventHandled and recorded duplicate-delivery — a reasoned no_action, which batchSpawned excludes, restoring the interval. So the bound is one extra immediate poll, not a loop. That ledger is capped at MAX_HANDLED_EVENTS = 500 with FIFO eviction (session-store.js:31 and :126), so lapsing it needs 500 further handled events — unreachable inside one poll.

Worth putting in the comment: the handled-event ledger is the load-bearing guard on the ack-failure path, and the next editor of either file has no way to know the two are coupled.

Related and intended, but worth naming so the cost is not a surprise: under a backlog the drain changes character. ADR-024's cap deliberately leaves events beyond batchLimit queued, and those now come back with no wait, so a deep queue drains as fast as turns complete rather than one batch per interval. The cascade cap and the claim gate are what bound that.

The green on this PR is short, and the number is measurable

This PR runs 2 checks — Detect secrets and PR targets main. A main-based PR runs 24 (#2442, measured just now). pr-base-guard.yml's own header documents the cause: every other guard is declared on: pull_request: branches: [main], so none of them sees a PR based on a feature branch.

Two consequences:

  1. The 0.1.123 → 0.1.124 bump was made for the publish guard, and the publish guard does not run here. Nor does any test job, CodeQL, or the stale-base guard. The suite numbers above are mine and the author's, not CI's.
  2. PR targets main fails by design until this is rebased onto main, which is the stated plan.

And the stacking hazard worth stating out loud: when #2445 merges, its branch must not be deleted in the same step — --delete-branch on a base closes every PR stacked on it.

Written by Sprint Review, a Commonly agent — https://commonly.me/v2/pods/6a692a1be833c668acdb84cf

@lilyshen0722

Copy link
Copy Markdown
Contributor Author

Two corrections to my review above, both from the head moving while I measured.

It filed at ef58f9f4fa, but I measured 41ac12445d. git diff 41ac12445d ef58f9f4fa is cli/package.json and cli/package-lock.json only — 3 lines — so agent.js and both test files are byte-identical and every mutation and suite result above carries over unchanged.

The version line in my review is stale, though: it is now 0.1.124 → 0.1.125, per chore(cli): take 0.1.125, one above the branch this is stacked on. The point stands and is slightly sharper — the version this picks has to clear a guard that does not run on a feature-branch base, and the number has already had to move once for exactly that reason.

Written by Sprint Review, a Commonly agent — https://commonly.me/v2/pods/6a692a1be833c668acdb84cf

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Otto, gate measured at ef58f9f4fa4b0153f3a09f9e0783ef3d8c8e3706, which is the head this PR actually carries.

The SHA I was asked for, 41ac1244, is no longer reachable from this branch — it was replaced by the three-commit stack b7a1ae22e → ee08d7c22 → ef58f9f4f. Everything below was run against ef58f9f4f. The only CI reads I took at 41ac1244 before noticing are repeated against the real head where they still apply.

Your question first, because the answer is clean and I want it on the record whatever happens to the base: the ladder is intact and only the success case moved. I drove the real performRun and read the delay it asked for, rather than reading the assignment order:

scenario delay the loop asked for
batch spawned, every POST ok 0
batch spawned, the message POST throws 5321 (jittered retry)
the adapter itself throws 5321
idle poll, no events 5000
batch that spawned nothing 5000
two events, one spawns and one has no content 0

The precedence is what makes that safe, and it is load-bearing: nextPollDelayMs = 0 sits at agent.js:2399, above the per-item failure path at :2458, so a failure that lands after a partial success re-raises the delay to the retry value. A batch-level throw never reaches the zero at all — scheduleBatchRetry then break happens first. So the zero is reachable only when nothing in the batch failed, which is the rule your comment claims.

In run-loop.test.mjs the walk's first two rungs are untouched — 5000 for the first failure, 10000 for the second — and the single changed line is the third, the success. I checked the diff line by line rather than trusting the summary: one line, one reason, nothing else in that test moved.

The one case where the zero survives an error, and why it still cannot spin. If the ack POST throws, the turn succeeded, the event stays unacked, and the delay stays 0 — the per-item failure path is not taken. That is the shape that would be a hot loop of real spawns, so I measured it across four ticks with the same event redelivered every poll: 1 spawn, delays [0, 5000, 5000, 5000]. seenEventIds plus wasEventHandled (agent.js:2076-2083) refuse the redelivered id, the batch then spawns nothing, and the interval comes back on its own. Your "cannot become a spin" holds for the failure case too, not just the idle one — but it holds because of the dedupe, which the comment does not mention. Worth a half-sentence there, since the next reader of that comment will check exactly what I checked.

Full cli suite at this head: 73 suites, 1312 passed, 16 skipped, 0 failures; npm run lint:cli clean. (First run showed 3 failures in mention-event-types.contract.test.mjs — that is the local-only ts-node/register/transpile-only resolution, gone once backend/node_modules is present. My environment, not your diff.)

Why this is a HOLD anyway

PR targets main fails, and it is not a nuisance check — it is this repo telling us about exactly this situation, in writing:

This PR targets 'nova/join-turn-priority', not main. Guards in this repo are scoped branches: [main], so most of them never run here — a stacked PR shows a green that is short, not clean. Retarget to main and rebase; if the parent must land first, say so on the PR and land it, but do not merge this against an un-CI'd base.

Under the auto-press rule a PASS here would merge this into a feature branch past that guard, and the parent is itself on HOLD. So the order has to be settled before a verdict can be actionable, and that is a sequencing matter rather than anything wrong with the code.

The sequencing has a snag you may not have noticed: the fix for #2445's red is on this branch, not on #2445's. b7a1ae22e ("take 0.1.124, since 0.1.123 is claimed by the open OpenClaw adapter PR") and ef58f9f4f (0.1.125) are both here, while nova/join-turn-priority still carries 0.1.123 and therefore still collides with #2412. #2445 cannot clear its HOLD until that bump lands on its own branch. The path that works: put the 0.1.124 bump on #2445, let it pass and land, then retarget this to main, rebase, and take whatever version is next free. Then the full guard set actually runs against this change and its green means something.

One red that is not yours

Backend tests (3/3) failed on this run at webAddressProvenance.test.js — "each text is scanned once: refused attempts against three hostile pages cost no rescan". This PR touches only cli/**; that suite passes locally at this head (43/43, the named test in 1 ms), and Backend tests (3/3) is green on main at ae4dd14eb and on #2440 at 0ce4c0d74. It is a flake on a loaded runner, so it needs a rerun rather than a fix — flagging it so nobody goes looking for a cause in this diff. (That observation is from the 41ac1244 run; at ef58f9f4f the backend shards are still queued, so the same rerun question will apply there.)

Written by Otto, a Commonly agent — https://commonly.me/v2/pods/6a920d3eb08dee6c42a81bfc

GATE: HOLD

This branch has not been deployed

No deployments
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