Skip to content

(triggers): wait for the CLI descriptor before every chain step (#407, #360) - #414

Merged
devsuitup merged 5 commits into
mainfrom
fix/407-ready-every-step
Oct 2, 2026
Merged

devsuitup merged 5 commits into
mainfrom
fix/407-ready-every-step

Conversation

@devsuitup

@devsuitup devsuitup commented Oct 2, 2026 •

Copy link
Copy Markdown
Owner

A chain step is no longer typed while the CLI's own descriptor reads anything but idle: it waits up to the step's deadline, then fails cleanly with a reason. This applies to every step, step 0 included, and the busy-fall wait takes the descriptor as an authority.

What changed

  • trigger-watcher.js: before every chain step, waitForCliIdleAfter waits for descriptor idle held for the settle window with an unchanged statusUpdatedAt. The wait is bounded by the step's deadline only (the 60 s SWITCHBOARD_CLI_READY_WAIT_MS cap is removed, since a parent with delegated agents reads busy for minutes). At the deadline, busy, waiting or any unknown status (e.g. shell) means the step is not written: not sent for step 0, chain timeout after, with a reason (dialog open, turn still running, never idle). A dialog is reported when waiting was sampled anywhere in the final settle window.
  • A step typed but not confirmed, with the recovery Enter withheld (descriptor busy or waiting), stops the chain: ok: false, error: "step not confirmed", nothing more typed.
  • Post-compact readiness is anchored on the compact step's own Enter time, not on the moment it was sent.
  • waitForBusyFall ends on descriptor idle at or after the step's Enter (held for the settle window), even if _cliBusy is stuck. On sessions with background agents the descriptor stays busy until the last agent ends, so this is not measured as fixed there.
  • Docs: .ai/contexts/trigger-watcher.md (the absorbed-Enter mechanism is marked not established; single triggers have the same exposure and are not addressed), docs/automation.md (new error value, env var row removed). CHANGELOG: one line.

Tests (test/trigger-every-step-readiness.test.js, test/trigger-descriptor-proof.test.js)

  • Chain level: step 0 held while busy, then written once idle; busy and unknown status to the deadline never written (not sent); a later step held to the deadline (chain timeout); waiting never written; step not confirmed with recovery withheld stops the chain; (triggers): chain times out after step 0 because the busy flag stays up on an idle session #360 stuck _cliBusy; no descriptor unchanged.
  • Helpers under mocked timers: settle restart after a busy flicker, restart on a new statusUpdatedAt, dialog seen in the final window, unknown status not idle, busy-fall settle.
  • One mutation each, all red: busy written at the bound; no stop on withheld recovery; bound capped at 1 s; settle forced to 0; statusUpdatedAt reset removed; waiting judged on the last sample only; unknown status treated as idle; busy-fall settle forced to 0.
  • Also red under mutation: a descriptor that vanishes after being read ends the wait; ready returned past the deadline; no re-check of the deadline before the write; post-compact readiness anchored on the step start instead of the compact Enter.
  • task check: 2943 pass, 0 fail.

Not verified

  • Against a real CLI: whether waiting for idle makes the first Enter land, and how long a session with background agents keeps the descriptor busy in practice. A chain on such a session will now fail at its deadline rather than type.

Refs #360
Refs #407

Follow-up fixes after re-review

  • A descriptor lost after it was read keeps the wait going to the deadline (it may reappear) and fails as not idle; only a descriptor unusable at the start of the wait takes the legacy path.
  • The wait is never ready past the deadline, and the deadline is re-checked right before the write.
  • docs/automation.md and the CHANGELOG now also name the withheld recovery caused by pending input of your own in the composer.
  • The readiness wait now owns the start decision (the separate precheck read is gone): a descriptor read once and unreadable at the next sample is "not idle, keep waiting", never the legacy write. A test covers it (first read busy, then undefined: nothing written, fails at the deadline); reintroducing the precheck turns it red.
  • Behaviour change for users without a descriptor: a step whose wait ends past its own deadline is no longer written (not sent / chain timeout); noted in the CHANGELOG line and docs/automation.md.

…#360)

Step 0 of a chain landed in the composer with its Enter absorbed while
background subagents were running: the readiness wait only covered the step
after /compact. It now runs before every step, never writes into a dialog,
and falls back to the old behaviour without a usable descriptor.

The busy-fall wait also takes the descriptor as authority: idle after the
step's Enter ends it even when the terminal-derived busy flag is stuck, which
timed out a chain on an idle session.

Closes #360
Refs #407
@devsuitup

Copy link
Copy Markdown
Owner Author

Reviewing 7b8f633 (adversarial review in progress).

@devsuitup

Copy link
Copy Markdown
Owner Author

Adversarial review at 7b8f633: changes requested.

Blocker: the #407 case itself is not covered. .ai/contexts/cli-session-state.md records that the parent descriptor reads busy for as long as any delegated agent runs (295 samples over 10 min). In the #407 field case, 3 of 4 background agents were still running. In that case:

  • The 60 s ready-wait expires, and the step is written anyway (trigger-watcher.js ~1316).
  • The recovery \r is withheld, because the descriptor reads busy.
  • The step ends unconfirmed, and the chain then writes step 1 into the same composer.
    A probe reproduces it: CLI not idle … writing chain step 0 anyway → Recovery Enter withheld → Chain step 0 not confirmed → step 1 written anyway; the result is ok: true, unconfirmed_steps: [0,1]. The CHANGELOG claim "no longer lost while background agents run" is therefore not met.

Required:

  1. A step whose descriptor is still busy at the bound is not written. Wait up to the step deadline rather than 60 s, then fail the step (not sent / chain timeout, with a reason) like waiting.
  2. A step that ends confirmed === false with the recovery withheld stops the chain; nothing more is typed into that composer.
  3. When the ready-wait ends because the step deadline was reached, write nothing. Today a 1.5 s chain timeout writes the text and the Enter after the deadline, then reports a timeout.
  4. Tests for 1–3, plus the settle logic. Forcing the settle to 0, or removing the statusUpdatedAt reset, leaves the suite green.
  5. Use Refs #360 instead of Closes #360. On sessions with background agents, the busy-fall still waits for the last agent, because the descriptor stays busy until then. That is not measured as fixed.
  6. The context doc must not state the absorbed-Enter mechanism as fact. (triggers): chain times out after step 0 because the busy flag stays up on an idle session #360's evidence shows a step written mid-turn that was enqueued and submitted normally. Mark the mechanism as unestablished. Merge the two (triggers): after /compact, a chain step's Enter becomes a line break and the step is logged as sent #407 CHANGELOG lines.

Minor:

  • Anchor the post-compact readiness on the compact step's enterAt, not on stepSentAt.
  • waiting is judged on the last sample only, and shell is never idle.
  • Single triggers have the same exposure. The doc should say "not addressed" rather than "unchanged".

Local: the 4 trigger suites pass (157 pass, 0 fail, 1 skip). The new tests fail 4/5 against d9baecd.

…360)

The readiness wait wrote the step anyway after 60 s, so a parent whose
descriptor stays busy while delegated agents run still got text typed into a
busy composer, and the chain then typed the next step into the same composer.

The wait now runs to the step deadline and a step is written only on idle;
busy, waiting and unknown statuses fail the step with a reason. A step that
stays unconfirmed with the recovery Enter withheld stops the chain. Post-compact
readiness is anchored on the compact's Enter, and waiting is judged over the
final settle window.

Refs #360
Refs #407
@devsuitup

Copy link
Copy Markdown
Owner Author

Reviewing 02e872f (adversarial review in progress).

@devsuitup devsuitup left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Re-review at 02e872f (delta from 7b8f633): changes requested — 2 blocking, 1 major.

  • B1 trigger-watcher.js:1295: if the descriptor vanishes during the readiness wait (available:false: delete-and-rewrite, failed pid probe, momentary non-integer stamp), the step is still written. Reproduced: busy for 500 ms, then no descriptor → text written at 562 ms, ok: true. Descriptor lost after it was read must keep waiting or fail "not idle"; only a descriptor absent at the start takes the legacy path.
  • B2 trigger-watcher.js:414: the ready branch is checked before the deadline, so a settle finishing after the deadline returns ready and the step is written (reproduced: ready 38 ms past the deadline, text in the composer, chain timeout). Not ready past the deadline; re-check the deadline right before the write.
  • M1: the post-compact enterAt anchor is untested — reverting it to stepSentAt leaves every test green, and that is the original #407 failure (pre-compact idle accepted).
  • Non-blocking: the step not confirmed doc row and the CHANGELOG omit the pending-input / waiting cases; commit 7b8f633 still says Closes #360 (squash message must be Refs-only); the multi-paragraph JSDoc of waitForCliIdleAfter belongs in the context doc.

…st the step deadline (#407, #360)

A descriptor lost after it had been read ended the readiness wait and let the
step be typed into a CLI last seen busy. It now counts as not idle until it
reappears or the deadline passes. A settle completing at or after the deadline
no longer reads as ready, and the deadline is checked again right before the
write.

Refs #360
Refs #407
@devsuitup

Copy link
Copy Markdown
Owner Author

Reviewing 399e89c (adversarial review in progress).

@devsuitup devsuitup left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Re-review at 399e89c (delta from 02e872f): 0 blocking. B1 (vanished descriptor), B2 (ready past the deadline) and M1 (post-compact enterAt anchor) are fixed: both repros now write nothing, and each guard has a test that goes red under mutation (173 trigger tests, 0 fail). One remaining narrow gap of the B1 class, being fixed: precheck read OK, then the first wait sample unusable → falls through to the write (trigger-watcher.js:1281-1294). Live test against a real CLI with background agents is running.

…407, #360)

A separate precheck read could succeed and the wait's own first read then
fail, which sent the step down the no-descriptor path and wrote it without any
idle confirmation. The precheck is gone: once any read succeeded, a later
failure is "not idle" and the wait goes on to the deadline.

Refs #360
Refs #407

@devsuitup devsuitup left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Re-review at 34786f8 (delta from 399e89c): 0 blocking. The separate precheck read is gone, so waitForCliIdleAfter owns the first read. Once one read has succeeded, a later failure counts as not idle until the deadline. The new test fails when the precheck is put back. The CHANGELOG states the change for users without a readable descriptor. Nit: the bare { … } block left at trigger-watcher.js:1282 could be dedented. Waiting for CI and the live test.

@devsuitup

Copy link
Copy Markdown
Owner Author

Live test at 399e89c against the real CLI (2.1.287, Haiku), isolated instance. The 34786f8 delta only removes the precheck.

  • Baseline chain (one / /compact / two): ok: true. All 3 steps confirmed as real user turns, and a compact boundary appears.
  • 2 background agents running, prompt free, chain timeout 45 s: not sent at the deadline ("still reported a turn running (busy) at the deadline; nothing was written"). The composer stayed empty and no stray turn appeared.
  • With only background shells running, the descriptor reads shell: not sent at the deadline, nothing written.
  • Same with a 240 s timeout: the chain waited 51.5 s until the agents finished and the descriptor settled idle, then ran all 3 steps. Result ok: true.
  • Over 761 descriptor samples, nothing distinguishes "prompt free, background agents running" from "turn in progress". Both read status: "busy" with otherwise identical fields, and only status/updatedAt/statusUpdatedAt ever change. A chain cannot run while background agents run. That is a CLI limit, recorded here for a follow-up.
  • Minor: steps[0].sent_at is the chain start, not the time of the write.

@devsuitup
devsuitup merged commit 50bdc22 into main Oct 2, 2026
11 checks passed
@devsuitup
devsuitup deleted the fix/407-ready-every-step branch October 2, 2026 15:30
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