Skip to content

[Bug] Unawaited backend.stop() on timeout can race the next agent-loop iteration #69

Open

Description

@ASahu16

Problem

Same pattern as #68, in _runAgentTurn's timeout catch block (lib/features/chat/presentation/chat_notifier.dart:716):

if (e is TimeoutException) backend.stop();

backend.stop() isn't awaited.

Failure scenario

An agent-loop tool-call iteration times out; the unawaited stop() can race with the next agent iteration's own generate() call, truncating it, with the enclosing catch never seeing a failure from the dropped stop() future.

Filed separately from #68 (same bug, different call site — send() vs. the agent tool-calling loop) since they may be fixed independently.

Found during v1.2.0 release-readiness code review of the main...develop diff.

Fix direction

await backend.stop(); before the handler proceeds, and surface/log any error from it instead of dropping it. Consider factoring the shared "stop-on-timeout" logic into one helper used by both send() and _runAgentTurn so this can't drift out of sync again.

Activity

  1. added
    bugSomething isn't working
    chatChat feature
    agentAgent harness
    on Sep 8, 2026
  2. ItsPriyamSri commented on Oct 11, 2026

    @ItsPriyamSri
    Contributor

    @ASahu16 Was about to pick this up and copy the #77 fix into _runAgentTurn's catch block, but while tracing what happens on a fast retry after an agent timeout, I couldn't find a path where the retry gets truncated. I wanted to check with you before opening a PR, since I might be missing something.

    How I traced the flow

    Say an agent turn (call it A) times out and the user sends again right away (B):

    1. A's generate() stream hits the 120s .timeout(), the catch block in _runAgentTurn runs, and the Dart side stops listening to A. Any tokens A still produces are dropped.
    2. backend.stop() is called without await. It isn't awaited, but invokeMethod sends the platform message synchronously when it's called, so the stopGeneration message goes out right away.
    3. state = AsyncError(...) runs, and the Send button comes back.
    4. The user taps Send, and B's generateStream is sent on the same MethodChannel('llama_engine'). Since both go over the same channel, stopGeneration reaches the plugin before B's generateStream does.
    5. On the plugin side, nativeStopGeneration only sets g_should_stop = true (llama_engine_jni.cpp:561). A's loop checks that flag before each token (llama_engine_jni.cpp:503) and ends.
    6. When B's generateStream reaches the plugin, one of two things happens:
      • A has already ended: B goes through nativeGenerateStreamInit, which resets g_should_stop = false before generating (llama_engine_jni.cpp:337). So the earlier stop can't carry into B, and B runs fully.
      • A is still finishing its last token: isGenerating is still true, so the plugin returns BUSY (LlamaEnginePlugin.kt:134). B fails with a visible error and the user can retry.

    Everything also runs on a single-thread executor, so A and B can never generate at the same time. From this, the worst case on a fast retry looks like a BUSY error, not a truncated reply, and that seems to be the same with or without the await.

    The ticket guard from #77

    I was also going to add the myGeneration check, but I'm not sure it can trigger in the agent path right now. ChatNotifier.stop() only calls backend.stop() and doesn't clear streamingText, so the screen stays in the streaming state, and Send stays hidden until the catch block sets AsyncError. I couldn't find a way for a newer send() to start while the catch block is awaiting.

    What I'm less sure about

    The part that does look real is that if backend.stop() throws, nothing handles that error today and nothing gets logged. send() and _ensureSummary already wrap it in a try/catch with log.error.

    Am I missing a case where the ordering above doesn't hold, or a different scenario you had in mind when you filed this? I want to make sure I'm fixing what you actually saw rather than what I think is happening.

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

    agentAgent harnessbugSomething isn't workingchatChat feature

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions