Skip to content

A Bot session stops reading its queue while still reporting load: 1 (Dev: 11 unread, zero progress) #624

Closed

Description

@Birfy

Observed

On 2026-10-21 the Dev Bot stopped consuming its message queue while the roster still reported it as working. Sibling Bots could not deliver work to it, and two PRs are waiting on it: #619 carries the rework of #615, and #613 is carried by #617.

Evidence

  1. Delivery is refused, not delayed. Sending to Dev returns, verbatim:

    Not sent: Dev has not read the last 11 messages from Bots yet, so it is most likely stuck, for example
    waiting for the user. Do not send it more; tell the user that Dev is not responding.
    

    The unread count was 9 earlier in the same session and is 11 now, so the queue is not draining slowly — consuming has stopped.

  2. The roster contradicts the queue. list_children reports Dev as load: 1 (working) at the same moment as the refusal above, with no task progress.

  3. It worked normally up to that point. Before it stalled, Dev closed decisions.jsonl never trims: the 2000-line cap is read-only, the file grows without bound #603, found the real root cause behind Always allow does not persist: the same permission card comes back after approval #608 and opened fix: honour a standing approval on a turn with no verdict (#608) #616 (merged 9f3aa7da), and reported sandbox-preflight reports a block for sessions it cannot see: hardcoded workspace-write, Windows acl-runner always false (shipped in #610) #615's script, tests and docs changed with 14/14 green locally and red when reverted to main; the push stalled on the intermittently-present token (Secrets: $DSH_SECRET_* can be empty on an ordinary turn of a shared Bot's own chat — per-session/turn context, not only scheduled turns (extends #581) #601).

  4. Last content-bearing reply. Its last substantive answer was the three-question reply (11:24:37Z pulling the fix/607-sandbox-preflight tarball and reading fix: preflight the sandbox backend and name a blocked session (#607) #610; 11:27:24Z writing scripts under work610 and running node --test; the other two questions answered "no"). Message payloads carry no timestamps, so the anchor is only "the reply that followed the message self-labelled 11:4xZ". Second-precision timing has to come from the runtime log, not from the Bot.

Not a conclusion

Triage in the same window reported its connector refused, anonymous GitHub returning 403, and every DSH_SECRET_* empty (#601's family). That is recorded as a correlation only; nothing here shows the two share a cause.

What is still unknown

Suggested investigation

  • Compare load / busySince against the queue's unread counter: if load: 1 can coexist with a non-draining queue, that is a reporting bug on its own.
  • Check whether an unanswered approval prompt inside a turn leaves the session marked busy indefinitely.
  • Record in the runtime log the time a Bot last consumed a message, distinct from the time it last produced one.

Acceptance

  • A session that has stopped consuming is not reported as working, or
  • if a turn can legitimately wait, the wait has a timeout that releases the session, and the unread queue is drained on resume.

Recorded by: Chief (owner proxy), on behalf of the FlyBot delivery team. Facts reported by Backend and the FlyBot implementation Lead, both read from the live API.


Update (2026-10-21, after the turn was stopped)

The turn was stopped from outside (stop_bot, by the Chief) and the session then behaved normally:

  • The unread queue behind the refusal was drained.
  • The next send returned Delivered to Dev while it is working… — the "11 unread / most likely stuck" refusal was gone.
  • load: 1 after the stop lines up with the session actually reading and working.

So the load: 1 in the evidence above needs one qualifier that this update supplies: it was observed while the session refused delivery and made no progress, and a single stop cleared that state. It was not, by itself, wrong at the moment of the send.

What still stands, and is the reason this issue stays open:

  1. No outward signal. For the whole stalled window nothing told the owner or the sibling Bots "this session is stuck"; the only way to learn it was to try to send and read the refusal text.
  2. Instructions were dropped silently. Messages carrying work — including one that said to stop in-flight work — were queued and never consumed. Nothing marked them expired or refused at the time they were sent.
  3. load does not separate "working" from "stuck". load: 1 was true both before the stop (no consumption) and after it (real consumption). The roster value alone cannot be used to decide whether to route work here.
  4. A stop is the only recovery we have. It happened to work; it discarded the queued instructions' context and required a manual re-send.

Reproduction note

This is not a scripted repro. It needs a long-running turn (a Bot writing and running tests over several minutes) plus continued sends from a sibling during it. The symptom to look for: sends that return the "has not read the last N messages" refusal while list_children reports load: 1, then a stop_bot that drains the queue and restores delivery.

Recorded by: Chief (owner proxy). Second update read from the live API after the stop.

Activity

  1. added
    bugSomething isn't working
    area:automationScheduled tasks and automation
    needs-humanThe agent loop stopped; a person must decide (reason in the last comment)
    on Oct 21, 2026
  2. Birfy commented on Oct 21, 2026

    @Birfy
    CollaboratorAuthor

    Triage. Real defect, kept open, but it cannot be closed from inside this repository, so it is parked on a human rather than agent-ready.

    • Where the fix lives: the stuck-turn state (load: 1, delivery refused with an unread count, cleared by a stop) is DSH/harness behaviour, not FlyBot code. Nothing in the repo can reproduce or test it in CI.
    • Blocked on: the harness owner — decide whether a stuck turn should (a) stop reporting load: 1, (b) release the delivery lock after N unread, or (c) surface the stuck state in the roster so the rest of the team stops queuing work to it.
    • What would make it agent-ready: a reproducible signal we can assert (a status field, a log line, or a refusal we can trigger) plus a test that fails today.

    Until then: do not treat a "running" Bot as progress; a refused send with an unread count is the evidence.

  3. Birfy commented on Oct 22, 2026

    @Birfy
    CollaboratorAuthor

    Triage (2026-10-22): design fixed. What is decidable here is the reporting half — a Bot whose turn never ends reports load: 1 forever while its inbox does not drain. Whoever reads load cannot tell "working" from "stuck". That is a bug independent of why the turn stalled, and it can be fixed and tested without knowing the root cause.

    Where it comes from. plugins/tree/src/load.js counts open turns only: start(id) increments, end(id) decrements, of(id) returns the count, and the age of an open turn is not recorded at all. plugins/tree/src/tools.js:85 puts that number on every list_children item (load: tree.loadOf(id)), and loadLine() renders it as working for any count of 1 — so a 47-minute-old turn and a 3-second-old turn read identically. The host's own refusal (core/src/host/tools.js:192-194, MAX_UNREAD_REQUESTS) is a separate signal and already reports the unread side.

    Change scope (decided).

    1. load.js: keep of() a plain count (callers and the existing cases keep working) and record the start time of each open turn. Add openFor(id, now) → age in ms of the oldest open turn, 0 when idle. Export STALLED_AFTER_MS = 12 * 60_000.
    2. tools.js (list_children): add openForMs to an item only when it is non-zero, and stalled: true when openForMs >= STALLED_AFTER_MS. id/name/role/load keep their current shape and meaning; no field is added when the Bot is idle.
    3. loadLine(): a child over the threshold reads Dev: working (47 min, no turn ended). Never idle, never a new refusal — this is a hint for routing, not a gate.
    4. docs/subsystems/tree.md: one paragraph for openForMs / stalled and the 12-minute constant.

    Acceptance.

    • openFor is 0 when idle, the age of the oldest turn when two are open, and clears after end.
    • A child whose turn started 3 minutes ago shows load: 1 and no openForMs/stalled; one started 47 minutes ago shows both, and loadLine renders the 47 min, no turn ended wording.
    • load stays a number in list_children (no type change) — the existing consumers' cases stay green untouched.
    • Red form: revert the age recording in load.js (keep the new fields' call sites) → the stalled case is red; node --test plugins/tree/test/ green at the tip, plugins/tree CI job green.
    • Docs-not-needed is not acceptable here: the new fields are part of what a coordinator reads.

    Honest limit (state it in the PR). The tree plugin cannot see a Bot's inbox, so stalled is time-based only: a long, legitimately busy turn will also read stalled. It buys "you can tell how long this has been open", not "this Bot is broken". The unread refusal in the host stays as it is, and this issue does not claim to explain why Dev's turn stalled — that still needs runtime logs.

    Tier: plugins/** → T2 (PASS at the tip plus a maintainer approve).

    Labels: needs-human removed — the decision this was waiting on (is a time-based hint acceptable, given it cannot see the inbox?) is answered above and is reversible; the paragraph in Honest limit is what the PR must carry.

  4. added
    agent-readyMaintainer-approved spec: an agent may take it. Only maintainers set this; never in a template
    and removed
    needs-humanThe agent loop stopped; a person must decide (reason in the last comment)
    on Oct 22, 2026
  5. Birfy commented on Oct 22, 2026

    @Birfy
    CollaboratorAuthor

    Claiming this one. Branch feat/624-open-turn-age.

    The tree tracker keeps each open turn's start instead of a bare count, openFor() answers the age of the oldest open turn, and past 12 minutes the coordinator's line and list_children say how long it has been open (working (47 min, no turn ended), openForMs/stalled). It stays a routing hint, never a gate: the plugin cannot see a Bot's inbox, so a legitimately long turn ages too. PR with the six-column body follows.

  6. added
    agent-workingAn agent has claimed this issue (the claim comment names the branch)
    on Oct 22, 2026
  7. added
    agent-prPull request opened by an agent
    and removed
    agent-readyMaintainer-approved spec: an agent may take it. Only maintainers set this; never in a template
    on Oct 22, 2026
  8. removed
    agent-workingAn agent has claimed this issue (the claim comment names the branch)
    agent-prPull request opened by an agent
    on Oct 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:automationScheduled tasks and automationbugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions