Skip to content

fix(mcp): use idle timeout instead of fixed caps for read/send/send-image - #26

Open
mortalist wants to merge 2 commits into
channprj:mainfrom
mortalist:fix/mcp-send-idle-timeout
Open

mortalist wants to merge 2 commits into
channprj:mainfrom
mortalist:fix/mcp-send-idle-timeout

Conversation

@mortalist

@mortalist mortalist commented Nov 12, 2026 •

Copy link
Copy Markdown

Summary

The MCP tools that drive the KakaoTalk UI (kmsg_read, kmsg_send, kmsg_send_image) use a single wall-clock cap per call. Every one of these commands spends 5.5–7.8s before its first output (auth/window probing), and a cold run that opens the chat takes 10–18s in total, so the caps fail cold runs across the board. In my testing kmsg_read failed 15/15 cold runs.

This PR replaces the fixed caps for those three tools with an idle timeout (12s without stderr output) plus a hard ceiling (30s).

Fixes #25

Why an idle timeout

The runner only waited for the process to exit and read stderr at the end, so it couldn't tell "slow but progressing" from "stuck". [trace-ax] lines are written with FileHandle.standardError.write (unbuffered), so they work as a heartbeat. Stdout print output is buffered on a pipe and can't be used this way.

Timing also drifts between runs (it climbs ~0.2–0.3s per run, then drops), so any fixed total cap gets crossed sooner or later. The silent gaps are much more stable: the longest one in any successful run was 7.8s.

Measurements (Homebrew v1.260921.0, before this PR)

MCP server, 5 cold runs each (chat window closed), sent to my own "chat with me" room:

Tool Cap (deep off / on) Result
kmsg_read, deep off 8s 5/5 PROCESS_TIMEOUT
kmsg_read, deep on 15s 5/5 PROCESS_TIMEOUT
kmsg_read with chat_id 8s 5/5 PROCESS_TIMEOUT
kmsg_read with background_safe, window already open 8s 5/5 ok (3.8–4.3s)
kmsg_send_image, deep off 12s 5/5 PROCESS_TIMEOUT
kmsg_send_image, deep on 20s 5/5 ok, but 16.5–18.0s (close to cap)
kmsg_send, deep off 10s 2/3 PROCESS_TIMEOUT at 10.07s

CLI timings (no cap), 5 runs each: read cold 15.1–17.5s, read with the window already open 10.6–11.0s, read --chat-id cold 15.3–16.5s, send-image cold 14.2–15.6s, send cold 10.3–15.9s (30 runs), status 10.2–11.4s, chats 6.8–7.3s. In every one, the longest silent gap is the 5.5–7.8s before the first [trace-ax] line. Opening a window that's already open, or skipping search with --chat-id, doesn't remove it.

Changes (Sources/kmsg/Commands/MCPServerCommand.swift only)

  • MCPDataBuffer: add append(_:) and lastWriteDate().
  • KmsgSubprocessRunner.run(): new optional idleTimeoutSec (default nil).
    • stderr is read incrementally (availableData loop) instead of readDataToEndOfFile().
    • When idleTimeoutSec is set, it waits in 0.25s slices and times out if total time > timeoutSec or time since the last stderr output > idleTimeoutSec.
    • When nil, behavior is unchanged (--version and the startup status check keep their current timeouts).
  • New shared constants KmsgSubprocessRunner.uiIdleTimeoutSec = 12 and uiHardTimeoutSec = 30.
  • kmsg_read (including the CHAT_NOT_FOUND deep-recovery retry), kmsg_send, kmsg_send_image: always append --trace-ax (heartbeat) and use the shared limits.
    • stderr is still only surfaced in the response when trace_ax was requested, so response shapes are unchanged.

Testing

No automated suite, so tested manually on an M4 Mac mini (16GB, macOS 27.0.1) with KakaoTalk running, by calling tools/call over ndjson stdio against kmsg mcp-server built from this branch. Same cold conditions as above.

Case Result
kmsg_read, deep off 5/5 ok (15.0–16.1s), 20 messages each, valid JSON
kmsg_read, deep on 5/5 ok (15.9–17.7s)
kmsg_read with chat_id 5/5 ok (20.5–23.0s)
kmsg_read with background_safe 5/5 ok (4.3–4.6s)
kmsg_send_image, deep off 5/5 ok (16.2–21.0s)
kmsg_send_image, deep on 5/5 ok (15.8–18.0s)
kmsg_send, deep off 5/5 ok (13.4–14.6s), plus 30/30 ok in an earlier run (10.0–13.9s)
Child frozen with SIGSTOP at 3s / 7s PROCESS_TIMEOUT at 12.25s / 17.8s (last output + 12s); no leftover process
Response shape without trace_ax read meta background_safe, latency_ms, layout; send / send-image meta latency_ms, stdout; same as before, no trace output

swift build, swift build -c release, and the Python checks from ci.yml pass locally.

Trade-offs / notes

  • --trace-ax is now always passed for these three tools. That's a bit of extra stderr, but it isn't surfaced unless requested.
  • The deep recovery split (8/15, 10/18, 12/20) is dropped, since idle + 30s covers both.
  • kmsg_read with chat_id is the slowest case (20.5–23.0s, climbing across runs), so its margin under the 30s ceiling is the smallest. The ceiling only bounds runs that keep producing output, since stuck runs are caught by the idle timeout, so raising it (e.g. to 45s) would be low-risk if you'd prefer more headroom.
  • A deep-recovery attempt that stays silent for more than 12s and then succeeds would now be cut off. I didn't see that in testing; the one 19s-silent deep-recovery run I hit failed anyway (WINDOW_NOT_READY, likely read: opening a room by title intermittently clicks the wrong search row (WINDOW_NOT_READY) #24).
  • The 5.5–7.8s before the first trace line is the biggest shared cost across commands. Adding trace points there, or making that step faster, would help everything but touches the child side, so I left it out to keep this PR focused.
  • Not changed: tools/kmsg-mcp.py (separate Python implementation with the same constants), and the startup status check (15s cap; status measured 10.2–11.4s).
  • Not tested with an actual locked screen. The SIGSTOP cases simulate the same "no output" condition.

The branch name differs from the one mentioned in #25 (fix/mcp-send-timeout-scale) because the approach changed from raising the cap to an idle timeout.


요약 (한국어)

KakaoTalk UI를 조작하는 MCP 도구(kmsg_read, kmsg_send, kmsg_send_image)는 호출마다 전체 시간 상한 하나만 둡니다. 이 명령들은 모두 첫 출력 전에 5.57.8초를 쓰고, 채팅창을 여는 cold 실행은 총 1018초가 걸려서 상한에 걸려 실패합니다. 테스트에서 kmsg_read는 cold 15/15 실패했습니다.

이 PR은 세 도구의 고정 상한을 **idle timeout(stderr 출력 없이 12초) + hard ceiling(30초)**으로 바꿉니다.

  • [trace-ax]는 stderr로 버퍼링 없이 나오므로 진행 신호로 씁니다.
  • idleTimeoutSec가 nil이면 기존 동작 그대로라, --version과 시작 시 status 확인은 영향이 없습니다.
  • 응답 형태는 바뀌지 않습니다. trace는 trace_ax를 요청한 경우에만 노출됩니다.
  • 수동 테스트: 패치 후 MCP cold 실행 35/35 성공(read 20건, send-image 10건, send 5건) + send 30/30. 자식 프로세스를 멈추면 마지막 출력 후 12초에 종료됩니다.
  • chat_id read가 20.5~23.0초로 가장 느려 30초 상한까지의 여유가 가장 작습니다. 필요하면 상한을 45초로 올려도 위험이 적습니다(멈춘 경우는 idle에서 잡히므로).

🤖 Generated with Claude Code

mortalist and others added 2 commits November 12, 2026 11:30
Cold sends that open the chat via search take 10-16s on an M4 Mac mini,
so the fixed 10s cap (18s with deep recovery) kills sends that are slow
but still progressing. The runner only waited for exit and read stderr at
the end, so it could not tell slow from stuck.

- Read the child's stderr incrementally and track the last write time.
- Add an optional idleTimeoutSec to KmsgSubprocessRunner.run(); when set,
  the run fails after that many seconds without stderr output, under the
  existing timeoutSec as a hard ceiling. When nil, behavior is unchanged.
- kmsg_send always passes --trace-ax so the unbuffered [trace-ax] lines
  act as a heartbeat, and uses idle 12s / hard 30s. stderr is still only
  surfaced in the response when trace_ax was requested.

Refs channprj#25

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every command that drives the KakaoTalk UI spends 5.5-7.8s before its
first output (auth/window probing), so the fixed caps fail cold runs
across the board, not just send. Measured with the Homebrew v1.260921.0
MCP server, 5 cold runs each:

- kmsg_read: 15/15 PROCESS_TIMEOUT (8s cap, 15s with deep recovery,
  and 8s with chat_id)
- kmsg_send_image: 5/5 PROCESS_TIMEOUT without deep recovery (12s cap);
  16.5-18.0s with deep recovery, close to its 20s cap

Use the same heartbeat approach as kmsg_send for read (including the
CHAT_NOT_FOUND deep-recovery retry) and send-image, and share the limits
as KmsgSubprocessRunner.uiIdleTimeoutSec (12s) and uiHardTimeoutSec (30s).

Refs channprj#25

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mortalist mortalist changed the title fix(mcp): use idle timeout for kmsg_send instead of fixed cap fix(mcp): use idle timeout instead of fixed caps for read/send/send-image Nov 12, 2026
@mortalist

Copy link
Copy Markdown
Author

Expanded the scope. After measuring every command, the same problem hits kmsg_read (15/15 cold runs timed out on the current release) and kmsg_send_image (5/5 with deep recovery off), not just send. All three share a 5.5–7.8s silent startup before the first output. The new commit applies the same idle timeout to read (including its retry) and send-image, and moves the limits into two shared constants. I've updated the title and description with the full measurements; with this branch, 35/35 cold MCP runs across the three tools succeed.


범위를 넓혔습니다. 모든 명령을 측정해 보니 send뿐 아니라 kmsg_read(현재 릴리스에서 cold 15/15 타임아웃)와 kmsg_send_image(deep recovery를 끄면 5/5)도 같은 문제가 있습니다. 세 도구 모두 첫 출력 전에 5.5~7.8초 무출력 구간이 있습니다. 새 커밋에서 read(재시도 포함)와 send-image에도 같은 idle timeout을 적용하고, 값을 공유 상수 두 개로 묶었습니다. 제목과 본문을 전체 측정 결과로 업데이트했고, 이 브랜치로는 세 도구의 cold MCP 실행 35/35가 성공합니다.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

kmsg_send via MCP always fails with PROCESS_TIMEOUT when the chat window is closed (10s hard cap < cold chat open time)

1 participant