A short writeup of how I tracked down nexu-io/open-design#1265.
When using Open Design with Devin for Terminal as the ACP agent, after Devin finished responding:
- The
Done · X.Xs · N outindicator 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).
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 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.
All changes coordinated around the same "SIGTERM is a clean exit when we asked for it" invariant:
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.apps/daemon/src/acp.ts— newcompletedSuccessfully()method on the session handle returnsfinished && !fatal && !aborted. Lets consumers distinguish "clean signal-driven exit after prompt success" from "genuine signal-driven failure".apps/daemon/src/server.ts—child.on('close')treats a SIGTERM exit as'succeeded'whenacpSession.completedSuccessfully()is true.apps/daemon/src/connectionTest.ts— same fix insidetestAgentConnectionInternal(added after Codex bot review caught that the daemon connection-test path would mis-classify SIGTERM exits asagent_spawn_failed).apps/web/src/providers/daemon.ts— trust the server's authoritativeendStatus. 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).
pnpm guardcleanpnpm --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
- Issue: nexu-io/open-design#1265
- PR: nexu-io/open-design#1286
- Related model-picker fix (prerequisite): #1200 → #1208
- Related badge stale-detail fix (post-merge follow-up): #1410 → #1413