Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 

Repository files navigation

Diagnosing a "chat stuck in working" bug in Open Design's ACP transport

A short writeup of how I tracked down nexu-io/open-design#1265.

What was wrong

When using Open Design with Devin for Terminal as the ACP agent, after Devin finished responding:

  • The Done · X.Xs · N out indicator appeared correctly
  • But the chat input stayed disabled in the "working" state
  • The user had to click stop manually before sending the next message

Visible symptom looked Devin-specific. The same build tested with Codex returned to ready normally — but Codex uses streamFormat: 'json-event-stream' which bypasses attachAcpSession entirely. Root cause was in the shared ACP transport, so the fix benefits all 6 ACP agents in the codebase (vibe, kiro, kimi, kilo, hermes, devin).

Why it was broken

Open Design finalizes each chat run via child.on('close') in apps/daemon/src/server.ts. Each turn spawns a fresh agent child process. After a clean session/prompt response, attachAcpSession calls stdin.end() and assumes the child will exit.

Devin doesn't. It keeps the process alive after stdin closes, waiting for the next prompt. child.on('close') never fires, the run never finalizes, and the chat stays stuck.

The gotcha

The exact fix pattern was already in the same file. detectAcpModels() at line ~270 of acp.ts calls child.kill('SIGTERM') right after the clean model-discovery probe completes — because that probe has the same "Devin doesn't exit on its own" issue. The prompt-completion path in attachAcpSession was just missing the same SIGTERM finalization.

Once you saw the pattern, the fix wrote itself. The interesting part was making sure the SIGTERM didn't accidentally fail other code paths that check code === 0 && !signal for exit success.

The fix (5 files)

All changes coordinated around the same "SIGTERM is a clean exit when we asked for it" invariant:

  1. apps/daemon/src/acp.ts — after a clean prompt response, schedule a 500 ms grace period and SIGTERM the child if it hasn't exited. child.once('close', ...) clears the timer so well-behaved agents are unaffected.
  2. apps/daemon/src/acp.ts — new completedSuccessfully() method on the session handle returns finished && !fatal && !aborted. Lets consumers distinguish "clean signal-driven exit after prompt success" from "genuine signal-driven failure".
  3. apps/daemon/src/server.tschild.on('close') treats a SIGTERM exit as 'succeeded' when acpSession.completedSuccessfully() is true.
  4. apps/daemon/src/connectionTest.ts — same fix inside testAgentConnectionInternal (added after Codex bot review caught that the daemon connection-test path would mis-classify SIGTERM exits as agent_spawn_failed).
  5. apps/web/src/providers/daemon.ts — trust the server's authoritative endStatus. The pre-existing signal/non-zero-code "safety net" no longer overrides an explicit 'succeeded' status, so a clean ACP run no longer surfaces a fake error banner (added after Looper bot review).

Verified

  • pnpm guard clean
  • pnpm --filter @open-design/daemon exec vitest run tests/acp.test.ts tests/connection-test.test.ts — 56 tests pass, 3 new regression tests covering the SIGTERM finalization invariant
  • Manual UI verification: built locally, sent a Devin prompt, input correctly returns to ready with no error banner

Links

About

Writeup of how I tracked down and fixed nexu-io/open-design#1265 (chat stuck in 'working' state after ACP agent completed)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors