How to write an issue so good someone else sends the PR
A silent 500-character truncation in OpenClaw's reply-context handler. I filed the issue; someone else opened the PR.
Background
OpenClaw is a 375k-star open source AI assistant framework. I run it in my homelab as a Telegram bot — it handles multi-turn conversations, does analysis, and occasionally asks me “Option A, B, or C — which one?”
That last part is where things broke.
One morning I sent the bot a short reply: "A". It responded as if it had never heard of options A, B, or C. It asked me to re-paste my question. It even introduced itself with a slightly different persona than before.
From the user side, the bot just “got confused.” Easy to blame the model. But I’d seen this pattern before: it always happened after the session had sat idle for a while.
Root cause
OpenClaw has an idle session reset — after session.reset.idleMinutes=15 (default), the session is cleared. When a user then replies to a previous bot message, the prior message is injected into the new session as [reply target] context. This is the only surviving signal from the old conversation.
The problem is in src/auto-reply/reply/inbound-meta.ts:
const MAX_UNTRUSTED_TRANSCRIPT_FIELD_CHARS = 500;
function sanitizeTranscriptField(value) {
// used for ALL fields: sender, message_id, media_type, media_ref... and body
}
// in formatChatWindowMessage:
const body = sanitizeTranscriptField(value["body"]); // ← capped at 500 chars
src/auto-reply/reply/inbound-meta.ts
The same function and the same 500-character constant handle both short metadata fields (sender, message_id) and long-form body content. 500 is fine for a sender name. It is not fine for a multi-paragraph analysis that ends with actionable options.
Real-world numbers from my own deployment on 2026-05-27:
- Original bot message: ~1,800 characters — analysis with options A/B/C at the end
- Injected
[reply target]body in the new session: truncated at ~486 chars - Options A/B/C: never reached
- Bot in new session: claimed a different role, asked user to re-paste context
The issue
I filed Issue #87291 with five sections:
- Summary — what breaks and why it matters
- Repro — exact five-step reproduction, including the idle-reset trigger
- Real-world evidence — my own deployment numbers, redacted
- Root cause — the exact file, constant, and function responsible
- Proposed fix — a concrete diff
The proposed fix was simple: split the body cap from the generic field cap.
8,000 was chosen deliberately: generous enough for multi-paragraph reasoning plus a mid-sized table, still a hard cap against untrusted-content prompt-budget abuse.
I ended with: “Happy to send a PR if direction is acceptable.”
What happened next
ClawSweeper reviewed the issue within hours. Its verdict:
Labels added: clawsweeper:source-repro, clawsweeper:fix-shape-clear, clawsweeper:queueable-fix, issue-rating: 🦞 diamond lobster.
Then PR #87311 was opened, adopting the proposed diff.
On issue authorship
In open source, “Happy to send a PR if direction is acceptable” is an expression of intent, not a claim. Anyone can move faster — and that used to bother me. It doesn’t anymore.
A well-written issue is a standalone contribution: a source-level reproduction, a concrete diff, and real deployment numbers that make the impact undeniable. The diamond lobster rating is ClawSweeper’s signal that the issue itself was high quality — not just a complaint, but a complete diagnosis.
That’s the part worth owning. The commit can be anyone’s.
Takeaways
A good issue is not a bug report. It’s a transfer of understanding. Root cause + proposed fix + real evidence means the PR author’s job is mostly mechanical. If someone can take your issue and open a PR in the same day, you wrote a good issue.
The goal is a working codebase, not credit. If the fix ships without your commit, the users who hit the same bug in their Telegram bot still benefit. That’s the point.
The most dangerous bugs are the ones that look like model failures. No error, no stack trace — just a confused bot and a frustrated user who blames the AI. If you see a behavior that’s too consistent to be random, it’s probably a hard-coded constant somewhere.
Update (June 2026)
The story didn’t end at “PR #87311 opened, adopting the proposed diff.” A month later, the real fix landed via a different path:
- PR #87311 was closed unmerged. Maintainers wanted more than a raised cap. Lifting 500 → 8000 still leaves prefix-only truncation, so the same failure mode reappears at 8000 the moment a body grows past it — same shape of bug, different number.
- I opened PR #87909 with a head+tail truncation variant at a 6,000-char cap. Got an independent review, tightened it twice — but by the time I caught up with main, maintainers had already landed their own version.
- The actual fix is commit
3753c5e2c8(closes #91042) — head+tail truncation at a 2,000-char cap, plus coverage for ReplyChain JSON bodies and Telegram inline ReplyToBody fallback that my own PR didn’t touch.
The split-cap direction from the original issue was adopted. The specific 8,000-char prefix-only mechanism was not — and shouldn’t have been. Maintainers landed a better variant.
This is exactly what “the commit can be anyone’s” means in practice. The issue did its job: transferred enough understanding that someone with deeper context on the surrounding paths could land a more complete fix than I would have written.