Skip to content

Timeout middleware: per-tool deadline (kills a hung bash)#5

Merged
EdmondDantes merged 2 commits into
mainfrom
feat/timeout-middleware
Jun 14, 2026
Merged

Timeout middleware: per-tool deadline (kills a hung bash)#5
EdmondDantes merged 2 commits into
mainfrom
feat/timeout-middleware

Conversation

@EdmondDantes

Copy link
Copy Markdown
Contributor

What

Adds TimeoutMiddleware to the executor chain — the second slice of the executor arc (after Permission + Audit in #4).

  • Runs the tool in a child coroutine and awaits it with Async\timeout().
  • On the deadline it cancels the coroutine — TrueAsync propagates the cancellation into a running bash subprocess and kills it — and returns an error tool_result (timed out after Ns).
  • Sits innermost in the chain, so a slow tool is capped but the user's approval prompt is not.

Session gains an optional toolTimeoutMs (0 = off, so existing tests are unaffected); bin/claw wires it from CLAW_TURN_TIMEOUT_MS.

Why

Closes a real gap now that the bot is remotely reachable: a trusted user approving a command that hangs (e.g. sleep 999) would otherwise wedge that chat forever.

Still deferred

/stop (user-initiated turn cancellation) — that needs a turn-loop restructure to watch input mid-turn, not just a middleware.

Tests

tests/Exec/TimeoutMiddlewareTest.php — error result when the tool exceeds the deadline; pass-through when fast enough.

90 tests pass, PHPStan level 8 clean, php-cs-fixer clean.

TimeoutMiddleware runs the tool in a child coroutine and awaits it with Async\timeout(); on the deadline it cancels the coroutine (TrueAsync propagates the cancellation into a running bash subprocess and kills it) and returns an error result. Sits innermost, so a slow tool is capped but the user's approval prompt is not.

Session adds an optional toolTimeoutMs (0 = off, so existing tests are unaffected); bin/claw wires it from CLAW_TURN_TIMEOUT_MS. 90 tests, PHPStan level 8, php-cs-fixer — all green.
The static-analysis job runs PHPStan without the true_async extension, so the ide-helper stub for Async\OperationCanceledException has no Throwable parent and 'catch (OperationCanceledException)' tripped catch.notThrowable. Catch \Throwable and compare $e::class instead (rethrowing anything that isn't the cancellation) — env-independent and also stops a genuine tool error being masked as a timeout.
@EdmondDantes EdmondDantes merged commit ae3e950 into main Jun 14, 2026
2 checks passed
@EdmondDantes EdmondDantes deleted the feat/timeout-middleware branch June 14, 2026 19:29
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.

1 participant