Root Cause
The error "No response received from Claude." is returned by consumeResponse() in src/claude/response.ts whenever the SDK generator finishes without ever yielding a type === "result" message.
This is almost always triggered by the 3-minute inactivity timeout in withInactivityTimeout():
const INACTIVITY_TIMEOUT_MS = 3 * 60 * 1000; // 3 minutes
This function races every iterator.next() call against a setTimeout. If the Claude SDK goes silent for 3 minutes — no messages of any kind — it stops iteration early, result stays undefined, and the error is returned.
Why does the SDK go silent? The SDK only emits messages between tool calls. A single long-running tool call (slow shell command, large file operation, network request, npm install, model training run, etc.) produces complete silence for its entire duration. If that exceeds 3 minutes, the timeout fires mid-tool-call and the response is lost.
Secondary failure mode (silent)
In src/slack/events.ts, consumeResponse is wrapped in .catch(() => null). If it throws (rather than returning the error object), the failure is swallowed silently — the thinking indicator disappears but nothing is posted to the user.
Recommended Fix
The core tension: we want a timeout to prevent genuine doom loops (stuck processes, hung sessions), but we don't want to interrupt legitimate long-running work.
Proposed approach: distinguish total timeout from inactivity timeout
Keep two separate timeout values:
| Timeout |
Purpose |
Suggested default |
| Inactivity timeout |
No SDK messages at all — likely hung |
10–15 min |
| Hard cap (optional) |
Absolute ceiling regardless of activity |
e.g. 30–60 min, or disabled |
The inactivity timeout already exists — just increase INACTIVITY_TIMEOUT_MS from 3 min to something more generous (10–15 min covers nearly all realistic tool calls without allowing true doom loops to run forever).
Allow per-channel/per-message override via env var or config
Add an env var that operators can set when deploying:
const INACTIVITY_TIMEOUT_MS =
parseInt(process.env.CLAUDE_INACTIVITY_TIMEOUT_MS ?? "") ||
10 * 60 * 1000; // default: 10 minutes
Power users doing long jobs (training runs, big refactors) can bump it; cautious deployments can lower it.
Fix the silent failure path
The .catch(() => null) in events.ts should post an error message rather than silently swallowing the failure:
const consumePromise = consumeResponse(gen).catch((err) => {
console.warn(`[${threadKey}] consumeResponse error: ${err}`);
return { sessionId: "", text: `Error: ${err instanceof Error ? err.message : String(err)}`, isError: true, costUsd: 0 };
});
Summary of recommended changes
- Increase default
INACTIVITY_TIMEOUT_MS from 3 min → 10–15 min
- Make it configurable via
CLAUDE_INACTIVITY_TIMEOUT_MS env var
- Fix the silent
.catch(() => null) to surface errors to the user
Root Cause
The error
"No response received from Claude."is returned byconsumeResponse()insrc/claude/response.tswhenever the SDK generator finishes without ever yielding atype === "result"message.This is almost always triggered by the 3-minute inactivity timeout in
withInactivityTimeout():This function races every
iterator.next()call against asetTimeout. If the Claude SDK goes silent for 3 minutes — no messages of any kind — it stops iteration early,resultstaysundefined, and the error is returned.Why does the SDK go silent? The SDK only emits messages between tool calls. A single long-running tool call (slow shell command, large file operation, network request,
npm install, model training run, etc.) produces complete silence for its entire duration. If that exceeds 3 minutes, the timeout fires mid-tool-call and the response is lost.Secondary failure mode (silent)
In
src/slack/events.ts,consumeResponseis wrapped in.catch(() => null). If it throws (rather than returning the error object), the failure is swallowed silently — the thinking indicator disappears but nothing is posted to the user.Recommended Fix
The core tension: we want a timeout to prevent genuine doom loops (stuck processes, hung sessions), but we don't want to interrupt legitimate long-running work.
Proposed approach: distinguish total timeout from inactivity timeout
Keep two separate timeout values:
The inactivity timeout already exists — just increase
INACTIVITY_TIMEOUT_MSfrom 3 min to something more generous (10–15 min covers nearly all realistic tool calls without allowing true doom loops to run forever).Allow per-channel/per-message override via env var or config
Add an env var that operators can set when deploying:
Power users doing long jobs (training runs, big refactors) can bump it; cautious deployments can lower it.
Fix the silent failure path
The
.catch(() => null)inevents.tsshould post an error message rather than silently swallowing the failure:Summary of recommended changes
INACTIVITY_TIMEOUT_MSfrom 3 min → 10–15 minCLAUDE_INACTIVITY_TIMEOUT_MSenv var.catch(() => null)to surface errors to the user