Merge pull request #670: fix(input): keep the line breaks of a multi-line mux prompt, pasted as one block

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-10-22 19:23:17 +02:00
commit b67018b318
39 files changed
+618 -135

No files matched your search

+17 -7
View File
@@ -70,7 +70,7 @@ later call opens with, and your first REAL call performs them anyway:
```bash
. "${XDG_CACHE_HOME:-$HOME/.cache}/codeman-agent-$CODEMAN_SESSION_ID.sh" 2>/dev/null
[ "${CODEMAN_PREAMBLE:-}" = 1.42.0 ] || { echo "preamble missing or stale; run the full §0 block"; exit 1; }
[ "${CODEMAN_PREAMBLE:-}" = 1.43.1 ] || { echo "preamble missing or stale; run the full §0 block"; exit 1; }
```
⚠️ **Never spend a Bash call on this check alone.** §1's block opens with this same
@@ -98,8 +98,8 @@ PRE="${XDG_CACHE_HOME:-$HOME/.cache}/codeman-agent-$CODEMAN_SESSION_ID.sh"
mkdir -p "$(dirname "$PRE")"
# Rewrite unless the file already ends with THIS version's stamp, so a stale or a
# half-written file self-heals here instead of costing you a round trip to rm it.
grep -qs '^CODEMAN_PREAMBLE=1.42.0$' "$PRE" || (umask 077; cat > "$PRE" <<'PREAMBLE'
# ---- Codeman agent preamble 1.42.0 (seeded by Codeman at session spawn; the SKILL.md §0 bootstrap rewrites it when missing or stale) ----
grep -qs '^CODEMAN_PREAMBLE=1.43.1$' "$PRE" || (umask 077; cat > "$PRE" <<'PREAMBLE'
# ---- Codeman agent preamble 1.43.1 (seeded by Codeman at session spawn; the SKILL.md §0 bootstrap rewrites it when missing or stale) ----
# Positional parameters are always written with braces, ${1} and never a bare dollar-digit: Claude Code
# replaces a bare one in a skill body with the matching word of the skill's arguments, and §0 is pasted
# from that body (test/agent-skill.test.ts pins it).
@@ -339,6 +339,16 @@ spawn_workers() {
# pane clearly finished means that profile, so switch that worker to markers.
sendwait() {
local sid="${1:?}" p="${2:?}" seq="${3:-$(date +%s)}" body r c head n=0 tmp bg i
# One line, like every other waiting sender (`toSingleLine`, `codeman agent send`,
# the TUI): the server pastes a multi-line prompt as one block, and the loop below,
# like the server's own submit check, cannot recognise a pasted block that is still
# on the composer, so a prompt like `Review this:\n- item 1` would sit unsent for
# the whole re-wait. Each line is trimmed, blank lines are dropped, the rest are
# joined with a space. For a long multi-line task, write a file and send its path.
case "$p" in *$'\n'*|*$'\r'*)
p=$(printf '%s\n' "$p" | tr '\r' '\n' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' -e '/^$/d' | paste -sd ' ' -) ;;
esac
[ -n "$p" ] || { echo "sendwait: empty prompt" >&2; return 1; }
# `wait:"stop,exit"`, never the `wait:true` default set: that set also carries
# `idle`, which is INFERRED from output stabilization and flaps mid-turn. On a
# dsh worker whose TUI repaints rarely the session reads `idle` while the model
@@ -410,10 +420,10 @@ last_text() {
# The stamp is the LAST line on purpose (a truncated write leaves it unset) and is kept
# bare on purpose: the write condition above anchors on it with $, so an inline comment
# here would fail that match and rewrite this file on every single bootstrap.
CODEMAN_PREAMBLE=1.42.0
CODEMAN_PREAMBLE=1.43.1
PREAMBLE
)
. "$PRE"; [ "${CODEMAN_PREAMBLE:-}" = 1.42.0 ] || { echo "preamble at $PRE is stale or truncated: rm it and re-run this block"; exit 1; }
. "$PRE"; [ "${CODEMAN_PREAMBLE:-}" = 1.43.1 ] || { echo "preamble at $PRE is stale or truncated: rm it and re-run this block"; exit 1; }
```
Every later Bash call that touches the API starts with the same two loader lines from
@@ -464,7 +474,7 @@ and no per-call body to hand-build.
```bash
. "${XDG_CACHE_HOME:-$HOME/.cache}/codeman-agent-$CODEMAN_SESSION_ID.sh" 2>/dev/null # §0 loader
[ "${CODEMAN_PREAMBLE:-}" = 1.42.0 ] || { echo "preamble missing or stale; run the full §0 block"; exit 1; }
[ "${CODEMAN_PREAMBLE:-}" = 1.43.1 ] || { echo "preamble missing or stale; run the full §0 block"; exit 1; }
N=(alpha beta) # INVENT one fresh case name per worker; never list cases first
# (a name may carry a mode: `beta:deepseek`, see below)
T=('reply with one line: the absolute path of your working directory'
@@ -587,7 +597,7 @@ One row per job. Acting on this table alone is correct; the §5 links are the de
| know if it is stuck | `GET .../active-tools` and `GET .../run-summary` are structured and free; two `terminal?tail=` samples are the crude fallback | [§5.6](reference/verbs.md#56-alive-and-stuck) |
| make a runaway worker stop | `POST .../input {"input":"\u001b"}` (ESC, **no** `\r`). Deleting the session would destroy the conversation instead | [§5.7](reference/verbs.md#57-interrupt-without-destroying) |
| resume a worker halted on a usage limit | `POST .../auto-resume {"enabled":true}`. Respawn and Ralph are **not** the remedy: respawn runs `/clear` | [§5.8](reference/verbs.md#58-usage-limits) |
| give a worker big input | write a file into its workspace with your own tools and send one short line pointing at it. The composer takes 65536 characters, single-line, newlines stripped | [§5.9](reference/verbs.md#59-big-input-via-the-workspace) |
| give a worker big input | write a file into its workspace with your own tools and send one short line pointing at it. The composer takes 65536 characters, and `send` takes one line | [§5.9](reference/verbs.md#59-big-input-via-the-workspace) |
| watch N workers at once | one in-flight wait per worker (per-session waiter cap 16); fan-out shapes differ for claude and shell | [§5.10](reference/verbs.md#510-fan-out) |
| find yourself, list what exists | `GET /api/v1/sessions`, match your `$SELF` by **prefix** | [§5.11](reference/verbs.md#511-list-and-find-yourself) |
| read or record what the user wants | `GET/PUT .../intent`, and `POST .../readmymind` to predict | [§5.12](reference/verbs.md#512-read-my-mind) |
+12 -2
View File
@@ -1,4 +1,4 @@
# ---- Codeman agent preamble 1.42.0 (seeded by Codeman at session spawn; the SKILL.md §0 bootstrap rewrites it when missing or stale) ----
# ---- Codeman agent preamble 1.43.1 (seeded by Codeman at session spawn; the SKILL.md §0 bootstrap rewrites it when missing or stale) ----
# Positional parameters are always written with braces, ${1} and never a bare dollar-digit: Claude Code
# replaces a bare one in a skill body with the matching word of the skill's arguments, and §0 is pasted
# from that body (test/agent-skill.test.ts pins it).
@@ -238,6 +238,16 @@ spawn_workers() {
# pane clearly finished means that profile, so switch that worker to markers.
sendwait() {
local sid="${1:?}" p="${2:?}" seq="${3:-$(date +%s)}" body r c head n=0 tmp bg i
# One line, like every other waiting sender (`toSingleLine`, `codeman agent send`,
# the TUI): the server pastes a multi-line prompt as one block, and the loop below,
# like the server's own submit check, cannot recognise a pasted block that is still
# on the composer, so a prompt like `Review this:\n- item 1` would sit unsent for
# the whole re-wait. Each line is trimmed, blank lines are dropped, the rest are
# joined with a space. For a long multi-line task, write a file and send its path.
case "$p" in *$'\n'*|*$'\r'*)
p=$(printf '%s\n' "$p" | tr '\r' '\n' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' -e '/^$/d' | paste -sd ' ' -) ;;
esac
[ -n "$p" ] || { echo "sendwait: empty prompt" >&2; return 1; }
# `wait:"stop,exit"`, never the `wait:true` default set: that set also carries
# `idle`, which is INFERRED from output stabilization and flaps mid-turn. On a
# dsh worker whose TUI repaints rarely the session reads `idle` while the model
@@ -309,4 +319,4 @@ last_text() {
# The stamp is the LAST line on purpose (a truncated write leaves it unset) and is kept
# bare on purpose: the write condition above anchors on it with $, so an inline comment
# here would fail that match and rewrite this file on every single bootstrap.
CODEMAN_PREAMBLE=1.42.0
CODEMAN_PREAMBLE=1.43.1
@@ -420,8 +420,9 @@ callers send no body at all.
**or Enter is never sent**: the text is typed onto the worker's prompt and sits
there unsubmitted. This is [symptom 1](#1-deliveredtrue-then-every-wait-times-out),
the number-one silent failure.
- `input` must be single-line (newlines are stripped). To send a bare Enter (confirm
a dialog), send `{"input":"\r"}`.
- With `useMux`, newlines in `input` are kept: the text goes in as one bracketed paste
and the `\r` submits all lines (without `useMux`, multi-line input is written straight
into the pane). To send a bare Enter (confirm a dialog), send `{"input":"\r"}`.
- `input` is capped at **65536** characters. ⚠️ **Two caps disagree and the smaller one
is the real one**: the Zod schema allows 100000 (`schemas.ts:1035`), so a 65537-to-100000
character body passes validation and *then* 400s at the route against
@@ -21,7 +21,7 @@ by sourcing the preamble file the §0 bootstrap wrote, and checking its version
```bash
. "${XDG_CACHE_HOME:-$HOME/.cache}/codeman-agent-$CODEMAN_SESSION_ID.sh" 2>/dev/null
[ "${CODEMAN_PREAMBLE:-}" = 1.42.0 ] || { echo "preamble missing or stale; re-run the §0 bootstrap"; exit 1; }
[ "${CODEMAN_PREAMBLE:-}" = 1.43.1 ] || { echo "preamble missing or stale; re-run the §0 bootstrap"; exit 1; }
```
Do **not** re-paste the preamble body into each call. Sourcing it is what retires the
@@ -304,10 +304,16 @@ duplicate resend costs nothing.
Codeman types the text and sends Enter **only when the input contains a carriage
return**; without it your command sits unsubmitted on the worker's prompt and
everything downstream times out. No response field catches this: `delivered:true`
means "written to the pane", **not** "submitted". Newlines are stripped, so input is
single-line by construction. Build the body with `jq -n` for any prompt you did not
author as a literal, because the inline `-d '{"input":"'"$P"'\r"}'` pattern breaks on
the first double quote, backslash or `$` in a real prompt:
means "written to the pane", **not** "submitted". With `useMux`, newlines are kept: the
text goes in as one paste, and the `\r` submits all of it. **`sendwait` sends one line,
though:** its Enter loop and the server's submit check cannot reliably recognise a
multi-line paste, so a multi-line prompt to a freshly spawned worker could sit unsent in
Claude Code's Enter-ignore window. `sendwait` therefore joins the lines with a space,
which loses their layout; put long multi-line content in a file
([§5.9](#59-big-input-via-the-workspace)). Build the body with `jq -n`
for any prompt you did not author as a literal, because the inline
`-d '{"input":"'"$P"'\r"}'` pattern breaks on the first double quote, backslash or `$`
in a real prompt:
```bash
BODY=$(jq -n --arg p "$PROMPT" '{input:($p+"\r"),useMux:true,clientId:"agent-1",seq:1,wait:true,waitTimeout:60000}')
@@ -588,9 +594,10 @@ allowlist in §4.
### 5.9 Big input via the workspace
The composer is a single line capped at 65536 characters with newlines stripped, which
makes it a bad channel for a spec, a diff or a file list. The workspace is the good
one, and for a local or docker case you are on the same filesystem as the worker.
The composer is capped at 65536 characters, and a multi-line paste cannot be confirmed
as submitted, which makes it a bad channel for a spec, a diff or a file list. The
workspace is the good one, and for a local or docker case you are on the same
filesystem as the worker.
1. Write `TASK.md` into the worker's workspace with your own file tools. The path is
`.data.casePath` from `quick-start`, or the `workingDir` you passed to
@@ -600,10 +607,10 @@ one, and for a local or docker case you are on the same filesystem as the worker
3. Wait on `DONE_<token>` with `wait-output` ([§5.5](#55-markers-for-hook-less-workers)),
then read `RESULT.json` back with your own tools.
This sidesteps the byte cap, the newline stripping and the quoting hazards in one
move, and it makes the marker **split by construction**: the token lives in the file,
never in the line you type, so the echo of your own keystrokes cannot match it. The
worker also gets to re-read the task instead of holding it in one echoed line.
This sidesteps the byte cap, the unconfirmed multi-line paste and the quoting hazards
in one move, and it makes the marker **split by construction**: the token lives in the
file, never in the line you type, so the echo of your own keystrokes cannot match it.
The worker also gets to re-read the task instead of holding it in one echoed line.
⚠️ Two places it does not work: a **remote-SSH case** runs on another host whose
filesystem you cannot see, and any worker **currently editing** the directory you are