Skip to content

fix(threads): show tool calls a server restart killed as stopped - #532

Merged
incognitojam merged 1 commit into
mainfrom
styal/stop-tool-calls-killed-by-restart
Sep 29, 2026
Merged

incognitojam merged 1 commit into
mainfrom
styal/stop-tool-calls-killed-by-restart

Conversation

@incognitojam

@incognitojam incognitojam commented Sep 29, 2026 •

Copy link
Copy Markdown
Owner

When the server restarts while an agent is running a command, the command's process dies with the server and never reports a result. Its row stayed "in progress" forever, and because both clients hide in-progress tool rows once a turn has ended, the command disappeared from the thread. The fold header still counted it ("1 terminal command"), but expanding the fold showed nothing.

What changed

  • Server: startup reconciliation already finds threads whose turn was running when the server stopped. For each one, it now records a tool.completed activity with status stopped for every tool call in that turn that has no completion. This happens whether the thread is then continued or settled with an error. Threads whose provider session is still live are left alone.
    • The stopped row copies the call's last payload, so the command, title and tool data are unchanged.
    • Its timestamp is the call's last update, the latest time the call is known to have been running. It sorts with the work it ended, not at the moment the server came back.
    • A new repository query, listUnfinishedToolCalls, reads only the start and update rows of that turn's unfinished calls.
  • Web and mobile: a tool call whose completion is stopped is now shown rather than hidden as running, with a stopped heading: "Command stopped", "Edit stopped", and so on, matching "Command failed". The rule lives in workEntryIsStoppedToolCall in client-runtime and the stopped flag of the shared tool-row presentation. Background tasks that report cancelled or interrupted progress are unchanged and stay hidden.
  • Ledger: adds a restart-stopped-tool-calls entry.

No provider reports a stopped tool status today. The live database I checked has none, so the client change affects only the rows this fix records.

Both shots show the same thread with the "Worked for" fold opened. The agent started a 60-second command, and the server restarted while the command was running:

Before After
Before: the opened fold goes straight from the agent's first message to the restart note, with no sign of the command After: the opened fold shows Command stopped with the command, then the restart note

Before, the command is missing even though the fold's header counts one terminal command. After, it shows as "Command stopped". The final reply is the agent's own report after it was continued.

Validation

  • Real restart: on an isolated dev server, a Claude thread ran node -e "setTimeout(…, 60000)" in the foreground, and I restarted the server while the command was running. On startup the server recorded one tool.completed row with status stopped for that call, on the interrupted turn and at the call's last update time, and then continued the thread. The web app shows "Command stopped - node -e …" inside the turn's fold, with no page errors. The screenshots above are from that thread; the before shot uses the same data with the client rule turned off, which is what current clients render.
  • Server tests:
    • serverRuntimeStartup.reconcile.test.ts: an orphaned running thread gets exactly one stopped completion, for its open call, using the call's latest payload and time. A call whose last update already failed is skipped, and a thread with a live session is untouched. The test fails with the reconcile step disabled.
    • ProjectionThreadActivities.test.ts: runs the query against SQLite. It returns only rows for that turn's calls that have no tool.completed, and skips rows without a call ID.
  • Client tests:
    • session-logic.test.ts (web) and threadActivity.test.ts (mobile): a stopped tool completion stays visible, and the mobile row reads "Command stopped". Both fail with the shared rule disabled. The existing test that keeps cancelled background tasks hidden still passes.
    • toolRowPresentation.test.ts covers the "Command stopped" heading.

Not verified

  • Mobile on a device: covered only by the feed tests. When the command is the turn's only work, the collapsed group on mobile shows the command without saying it stopped. Expanding the group shows "Command stopped".
  • Other providers: only Claude was restarted for real. The server step doesn't depend on the provider, because it reads the recorded activities.

Related upstream work: pingdotgg#9391 settles background agent tasks the same way when a session ends. It does not cover tool calls.


Written by an agent (Claude Code, claude-opus-5-5).

A command running when the server restarted never got a completion row,
because its provider process died with the server. Both clients hide a tool
row that is still in progress once its turn ends, so the command vanished
from the thread while the fold still counted it.

Startup reconciliation now records a stopped completion for every unfinished
tool call in the turn the restart interrupted, and web and mobile show a
stopped tool call with a "stopped" heading instead of hiding it.
@incognitojam
incognitojam force-pushed the styal/stop-tool-calls-killed-by-restart branch from c4b6a36 to ee0acee Compare September 29, 2026 11:11
@incognitojam
incognitojam enabled auto-merge (squash) September 29, 2026 11:12
@incognitojam
incognitojam merged commit 344ee95 into main Sep 29, 2026
24 checks passed
@incognitojam
incognitojam deleted the styal/stop-tool-calls-killed-by-restart branch September 29, 2026 11:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant