mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-08 08:29:42 +02:00
fix(input): an oversized paste no longer poisons the durable input queue (#484)
A single input over MAX_INPUT_LENGTH (64 KiB) was queued for reliable
delivery, refused by both transports (the WebSocket silently, POST with a
400), and never dropped: the client treated the 400 as transient, so the
frame was re-sent every 2 s forever, blocked every later input for that
session, and came back from localStorage on every reload.
- Client: a paste over the frame limit is split into in-limit frames
(never cutting a surrogate pair) delivered in seq order; over 1 MiB, or
an oversized mux write, it is refused with a toast and never queued.
- Client: the POST drain drops a frame answered 400/413; a WS error ACK
drops it too; frames over the limit persisted by an older build are
pruned on load.
- Server: the WebSocket answers an oversized sequenced frame with
{t:'ia',seq,err:'too_large',max} instead of silence (an older client
reads that as a plain ACK and drops it); the POST schema uses
MAX_INPUT_LENGTH instead of a second 100000 limit.
Verified end to end on an isolated instance: a 110 KB paste reached the
PTY byte-identical over both the WebSocket and the POST path, a poisoned
120 KB persisted frame was pruned on load, and a 2 MB paste showed the
refusal toast with nothing queued.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -762,6 +762,49 @@ function resolveTerminalFontWeights(settings) {
|
||||
// without a terminal, a clipboard, or a browser.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Largest single input frame the server accepts, in UTF-16 code units.
|
||||
* ⚠️ Must equal MAX_INPUT_LENGTH in src/config/terminal-limits.ts (pinned by
|
||||
* test/input-size-limit.test.ts). Both transports reject a longer frame, and
|
||||
* before issue #484 the durable input queue retried such a frame forever.
|
||||
*/
|
||||
const INPUT_FRAME_MAX_CHARS = 64 * 1024;
|
||||
|
||||
/**
|
||||
* Largest paste the client will deliver at all. Anything up to this is split
|
||||
* into INPUT_FRAME_MAX_CHARS frames that go out in seq order, so the PTY sees
|
||||
* one contiguous byte stream (bracketed-paste markers included). Past it the
|
||||
* input is refused with a toast rather than queued: every frame is persisted
|
||||
* and retried until ACKed, so a multi-megabyte paste would pin the queue.
|
||||
*/
|
||||
const INPUT_PASTE_MAX_CHARS = 1024 * 1024;
|
||||
|
||||
/**
|
||||
* Split input into frames no longer than `max` code units, never cutting a
|
||||
* surrogate pair in half (a lone surrogate reaches the PTY as U+FFFD).
|
||||
*
|
||||
* @param {string} data
|
||||
* @param {number} [max]
|
||||
* @returns {string[]}
|
||||
*/
|
||||
function splitInputFrames(data, max = INPUT_FRAME_MAX_CHARS) {
|
||||
if (typeof data !== 'string' || data.length === 0) return [];
|
||||
if (!(max >= 2)) max = 2;
|
||||
if (data.length <= max) return [data];
|
||||
const frames = [];
|
||||
let start = 0;
|
||||
while (start < data.length) {
|
||||
let end = Math.min(start + max, data.length);
|
||||
if (end < data.length) {
|
||||
const code = data.charCodeAt(end - 1);
|
||||
if (code >= 0xd800 && code <= 0xdbff) end--; // keep the pair together
|
||||
}
|
||||
frames.push(data.slice(start, end));
|
||||
start = end;
|
||||
}
|
||||
return frames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upper bound on an AUTO-copied selection.
|
||||
*
|
||||
@@ -940,6 +983,11 @@ if (typeof window !== 'undefined') {
|
||||
compare: compareSessionActivity,
|
||||
sort: sortSessionsByActivity,
|
||||
};
|
||||
window.CodemanInputLimit = {
|
||||
FRAME_MAX_CHARS: INPUT_FRAME_MAX_CHARS,
|
||||
PASTE_MAX_CHARS: INPUT_PASTE_MAX_CHARS,
|
||||
split: splitInputFrames,
|
||||
};
|
||||
window.CodemanAutoCopy = {
|
||||
decide: decideAutoCopy,
|
||||
MAX_CHARS: AUTO_COPY_MAX_CHARS,
|
||||
|
||||
Reference in New Issue
Block a user