Bug type
Behavior bug (incorrect output/state without crash)
Beta release blocker
No
Summary
openclaw doctor mangles long file paths inside note(...) boxes by re-wrapping copy-sensitive tokens (paths, URLs) at the box width even though wrapNoteMessage correctly preserved them as a single unbroken token. The mangled line cannot be copy-pasted, which defeats the entire reason the path is being surfaced.
Steps to reproduce
- Create a real or fake session lock so that
Session locks has something to report. Easy fake:
mkdir -p ~/.openclaw/agents/main/sessions
touch ~/.openclaw/agents/main/sessions/9c2acae5-841f-4aea-936b-fdb513b60202.jsonl.lock
- Run
openclaw doctor in an 80-column terminal (default in many CI / Docker / Pi setups).
- Observe the
Session locks panel — the lock path is split mid-extension.
Or, isolated repro that does not require a running gateway — exercises the exact same @clack/prompts.note() path used by src/commands/doctor-session-locks.ts:
mkdir /tmp/note-wrap-repro && cd /tmp/note-wrap-repro
npm init -y >/dev/null && npm install @clack/prompts >/dev/null
COLUMNS=80 node -e "
process.stdout.columns = 80;
import('@clack/prompts').then(m => {
m.note(
'- Found 1 session lock file.\n' +
'- ~/.openclaw/agents/main/sessions/9c2acae5-841f-4aea-936b-fdb513b60202.jsonl.lock pid=86519 (alive) age=2m47s stale=no',
'Session locks'
);
});
"
Expected
The lock file path is rendered as a single unbroken token so the user can:
- Visually identify the lock file
- Triple-click / copy-paste it into a follow-up
ls, cat, rm, or lsof command
Either the path stays on its own line (overflowing the box if needed), or the box widens to fit it, or it gets a clearly-marked continuation glyph (e.g. trailing ↵) so the reader knows the break is cosmetic. Splitting .jsonl.lock into .js + onl.lock is the worst outcome.
Actual
◇ Session locks ──────────────────────────────────────────────────────────────╮
│ │
│ - Found 1 session lock file. │
│ - ~/.openclaw/agents/main/sessions/9c2acae5-841f-4aea-936b-fdb513b60202.js │
│ onl.lock pid=86519 (alive) age=2m47s stale=no │
│ │
├──────────────────────────────────────────────────────────────────────────────╯
Note .js at the end of one line and onl.lock at the start of the next. Reproduced verbatim with openclaw@2026.4.15 against my real gateway, and against bare @clack/prompts@<latest> in an isolated /tmp directory (no OpenClaw runtime involved).
Affected versions
- Locally observed on
openclaw@2026.4.15 (041266a).
- Source still vulnerable on
main (HEAD c33cec04d9): packages/terminal-core/src/note.ts calls clackNote(...) after wrapping, and clackNote does its own width-based wrapping inside the box.
Root cause (source-level)
packages/terminal-core/src/note.ts correctly handles copy-sensitive tokens in its own wrapLine function — isCopySensitiveToken (line 37) returns true for words starting with ~/, /, ./, ../, URLs, Windows drives, etc., and pushWrappedWordSegments is skipped for those tokens at lines 99–101 and 129–131. The token is allowed to overflow the chosen maxWidth rather than be broken mid-word.
The bug is that wrapNoteMessage is only the first pass. The function then calls clackNote(message, title) (line ~205), which is @clack/prompts.note. Clack performs its own width-based wrapping for the bordered box and is unaware of isCopySensitiveToken semantics — it just word-breaks/character-breaks at the box width. With the default MIN_NOTE_COLUMNS = 80, the 80-character path lands exactly at the box's right edge and gets broken inside .jsonl.lock.
The unit-test file packages/terminal-core/src/note.test.ts does not exist; there is no regression test covering the interaction between wrapNoteMessage and the downstream clackNote render. (Adjacent files in packages/terminal-core/src/*.test.ts do exist for ansi, links, table, etc., so adding one is in-keeping with the package convention.)
Suggested fix
A few options, ranked rough-best to rough-worst by effort × impact:
-
Special-case copy-sensitive lines. Before handing the message to clackNote, detect lines whose only oversized token is copy-sensitive and emit them on their own line inside the box (i.e. let them overflow the box right edge, or render them outside the box prefix/suffix). This matches what wrapNoteMessage was already trying to do.
-
Stop relying on clack's box wrap. Build the bordered box in packages/terminal-core/src/note.ts directly so OpenClaw owns the wrapping decisions end-to-end. The bordered output is already largely OpenClaw-controlled (the title, the ◇/├/╮/╯ glyphs, the column resolution) — this would be a continuation of that direction.
-
Shorten the path proactively. For known patterns (session lock files), use a … middle-truncation that fits the box, and print the full path separately when --verbose is on. Lossy and arguably worse — the path is supposed to be actionable — but minimally invasive.
I think option 1 is the smallest change that preserves the user-facing contract (isCopySensitiveToken already exists and is the right concept to extend).
Notes
- 🦞 Diamond-lobster shape: source-reproducible (file + line), isolated repro without the gateway, narrow blast radius (one rendering helper, one downstream
clackNote call).
- Same code path is used by other doctor sections that print paths (
note() is used in src/commands/doctor-state-integrity.ts, doctor-volatile-fs.ts, etc.), so this fix benefits multiple panels at once.
- AI-assisted issue (Claude). Repro verified end-to-end against both my installed
openclaw doctor and bare @clack/prompts in /tmp.
Bug type
Behavior bug (incorrect output/state without crash)
Beta release blocker
No
Summary
openclaw doctormangles long file paths insidenote(...)boxes by re-wrapping copy-sensitive tokens (paths, URLs) at the box width even thoughwrapNoteMessagecorrectly preserved them as a single unbroken token. The mangled line cannot be copy-pasted, which defeats the entire reason the path is being surfaced.Steps to reproduce
Session lockshas something to report. Easy fake:openclaw doctorin an 80-column terminal (default in many CI / Docker / Pi setups).Session lockspanel — the lock path is split mid-extension.Or, isolated repro that does not require a running gateway — exercises the exact same
@clack/prompts.note()path used bysrc/commands/doctor-session-locks.ts:Expected
The lock file path is rendered as a single unbroken token so the user can:
ls,cat,rm, orlsofcommandEither the path stays on its own line (overflowing the box if needed), or the box widens to fit it, or it gets a clearly-marked continuation glyph (e.g. trailing
↵) so the reader knows the break is cosmetic. Splitting.jsonl.lockinto.js+onl.lockis the worst outcome.Actual
Note
.jsat the end of one line andonl.lockat the start of the next. Reproduced verbatim withopenclaw@2026.4.15against my real gateway, and against bare@clack/prompts@<latest>in an isolated/tmpdirectory (no OpenClaw runtime involved).Affected versions
openclaw@2026.4.15(041266a).main(HEADc33cec04d9):packages/terminal-core/src/note.tscallsclackNote(...)after wrapping, andclackNotedoes its own width-based wrapping inside the box.Root cause (source-level)
packages/terminal-core/src/note.tscorrectly handles copy-sensitive tokens in its ownwrapLinefunction —isCopySensitiveToken(line 37) returnstruefor words starting with~/,/,./,../, URLs, Windows drives, etc., andpushWrappedWordSegmentsis skipped for those tokens at lines 99–101 and 129–131. The token is allowed to overflow the chosenmaxWidthrather than be broken mid-word.The bug is that
wrapNoteMessageis only the first pass. The function then callsclackNote(message, title)(line ~205), which is@clack/prompts.note. Clack performs its own width-based wrapping for the bordered box and is unaware ofisCopySensitiveTokensemantics — it just word-breaks/character-breaks at the box width. With the defaultMIN_NOTE_COLUMNS = 80, the 80-character path lands exactly at the box's right edge and gets broken inside.jsonl.lock.The unit-test file
packages/terminal-core/src/note.test.tsdoes not exist; there is no regression test covering the interaction betweenwrapNoteMessageand the downstreamclackNoterender. (Adjacent files inpackages/terminal-core/src/*.test.tsdo exist for ansi, links, table, etc., so adding one is in-keeping with the package convention.)Suggested fix
A few options, ranked rough-best to rough-worst by effort × impact:
Special-case copy-sensitive lines. Before handing the message to
clackNote, detect lines whose only oversized token is copy-sensitive and emit them on their own line inside the box (i.e. let them overflow the box right edge, or render them outside the box prefix/suffix). This matches whatwrapNoteMessagewas already trying to do.Stop relying on
clack's box wrap. Build the bordered box inpackages/terminal-core/src/note.tsdirectly so OpenClaw owns the wrapping decisions end-to-end. The bordered output is already largely OpenClaw-controlled (the title, the◇/├/╮/╯glyphs, the column resolution) — this would be a continuation of that direction.Shorten the path proactively. For known patterns (session lock files), use a
…middle-truncation that fits the box, and print the full path separately when--verboseis on. Lossy and arguably worse — the path is supposed to be actionable — but minimally invasive.I think option 1 is the smallest change that preserves the user-facing contract (
isCopySensitiveTokenalready exists and is the right concept to extend).Notes
clackNotecall).note()is used insrc/commands/doctor-state-integrity.ts,doctor-volatile-fs.ts, etc.), so this fix benefits multiple panels at once.openclaw doctorand bare@clack/promptsin/tmp.