Skip to content

[bug] Navigating back loses 'working on it' — the run is still going and the API already reports it #252

Closed

Description

@serge-ivo

The symptom

Start work, navigate away, come back — "working on it" is gone. The agent may still be working; the
console simply stops claiming so. There is no way to tell a finished run from a running one.

It is entirely client-side, and the data already exists

The thinking indicator is local React state. pages/InstanceDetail.tsx:

:80    const [thinking, setThinking] = useState(false);
:327   setThinking(true);      // on send
:352   setThinking(false);     // when the reply lands

Never seeded from the server. Unmount discards it; remount starts at false.

The loop watcher only knows about loops this tab started. pollLoop opens with:

if (!id || !loopRunIdRef.current) return;

loopRunIdRef is set when you press Loop in that tab. After a remount it is null, so the
watcher returns immediately every tick — even with a run active.

But the server can answer this already:

Source What it gives
GET /v1/instances/:id/loop (routes/tools.ts:457 → listLoopRuns) every run for the instance, with status + iteration
GET /v1/instances/:id/loop/:runId (:460) one run
WS broadcast already emits {type:"status", status:"thinking"}
AgentDO state records status: "thinking" (readable via /state)

Nothing needs building on the backend. The Assistant tab just never asks on mount.

And the one UI that does show runs is in the wrong place

LoopRunsSection — the component that lists loop runs — is rendered only in
SettingsTab.tsx:1145. So the answer to "is my agent working?" lives in Settings, several clicks
from the Assistant tab where the work was started and where the user is looking.

Proposal

  • On mount, call GET /v1/instances/:id/loop; if a run is running, adopt its id into
    loopRunIdRef and resume the existing watcher. The polling machinery already works — it is only
    missing its starting value.
  • Show a persistent indicator on the Assistant tab whenever a run is active, with the iteration
    count that pollLoop already reads.
  • For a plain chat turn, seed the thinking state from the DO's own status rather than assuming
    idle. Note ensureStateDefaults (agent-do-prompt.ts:63) resets thinking/error → idle
    when the DO reloads, which is a sensible self-heal but means status is only trustworthy while
    the object is warm — so treat it as a hint, and let [bug] A chat turn is bound to the request — navigating away can lose the reply while its tool side effects persist #251 decide what "in flight" should durably
    mean for chat.
  • Consider surfacing LoopRunsSection (or a compact form of it) where work happens, not only in
    Settings.

Verification

  • Start a loop, navigate to another agent and back: the indicator and iteration count return
    without a page reload.
  • Two tabs open on the same instance both show the run — neither "owns" it.
  • A finished run clears on return rather than showing stale "working".

Activity

  1. serge-ivo commented on Aug 6, 2026

    @serge-ivo
    ContributorAuthor

    Fixed in 534db6d.

    Verified true as written (line numbers had drifted ~+30, and it was worse than stated: the watcher effect at :509 also returns early on loopOn, which is likewise false after a remount — so it was doubly dead, not just returning every tick). Also confirmed: the console has no WebSocket client at all, so the WS {type:"status"} broadcast is not a drop-in; the poll path is.

    Shipped:

    • lib/workInFlight.ts (pure + 11 unit tests): adoptableRun (newest running run — the real status vocabulary is running/completed/failed/needs_human/cancelled), shouldAdopt, isChatWorking.
    • checkWork in InstanceDetail polls /state + /loop every 10s (skipped while the tab is hidden), adopts a running run into loopRunIdRef/loopOn — the existing watcher only ever lacked its starting value — and restores objective, iteration and max, so the Stop button and its iteration badge come back on the Assistant tab, where the work was started.
    • Thinking state is seeded from the server, not assumed idle: the pill reads thinking || remoteWork. Per the ticket's own caveat, state.status is NOT trusted (ensureStateDefaults scrubs a stale thinking on reload) — the answer comes from the DO's in-flight turn markers added in [bug] A chat turn is bound to the request — navigating away can lose the reply while its tool side effects persist #251.
    • A finished run clears on return rather than showing stale "working", and when a turn we didn't start completes, the transcript is refreshed.
    • An adopted run's completion notice is local-only (emitSystemChat(…, false)) — otherwise every tab watching the same run would write one into the transcript.

    Not done, deliberately: moving LoopRunsSection out of Settings. The adopted run now shows where the work happens, which is the part that mattered; a second listing there can be its own change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions