[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
205
infrastructure/ai/requestPayloadCompression.ts
Normal file
205
infrastructure/ai/requestPayloadCompression.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
import type { ModelMessage } from "ai";
|
||||
|
||||
const RETRY_MAX_TOOL_RESULT_CHARS = 4_000;
|
||||
const RETRY_MAX_MESSAGE_TEXT_CHARS = 8_000;
|
||||
const TRUNCATION_MARKER = "\n\n[... output truncated for request size ...]\n\n";
|
||||
const HEAD_CHARS = 800;
|
||||
const TAIL_CHARS = 4_000;
|
||||
const MAX_VERBOSE_LINE_CHARS = 2_000;
|
||||
// Terminal streams contain control bytes by design; these patterns remove them before model use.
|
||||
// eslint-disable-next-line no-control-regex
|
||||
const ANSI_ESCAPE_PATTERN = /[\u001b\u009b][[\]()#;?]*(?:(?:(?:[a-zA-Z\d]*(?:;[-a-zA-Z\d/#&.:=?%@~_]+)*)?\u0007)|(?:(?:\d{1,4}(?:[;:]\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))/g;
|
||||
const LARGE_BASE64_PATTERN = /(?:[A-Za-z0-9+/]{4}){125,}(?:==|=)?/g;
|
||||
|
||||
export interface CompressMessagesForRequestTooLargeRetryResult {
|
||||
messages: ModelMessage[];
|
||||
didAdjust: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapse noisy terminal/build output.
|
||||
* Keeps semantics while removing repeated blank lines and long duplicate runs.
|
||||
*/
|
||||
export function compressVerboseText(value: string): string {
|
||||
if (!value) return value;
|
||||
|
||||
let compressed = value.replace(ANSI_ESCAPE_PATTERN, "");
|
||||
compressed = compressed.replace(/\r\n/g, "\n");
|
||||
compressed = collapseCarriageReturnFrames(compressed);
|
||||
// eslint-disable-next-line no-control-regex
|
||||
compressed = compressed.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, "");
|
||||
compressed = compressed.replace(LARGE_BASE64_PATTERN, match => `[base64-like payload omitted: ${match.length} chars]`);
|
||||
compressed = compressed.replace(/\n{4,}/g, "\n\n\n");
|
||||
|
||||
const lines = compressed.split("\n").map(shortenVerboseLine);
|
||||
const deduped: string[] = [];
|
||||
let repeatCount = 0;
|
||||
for (const line of lines) {
|
||||
const previous = deduped[deduped.length - 1];
|
||||
if (previous === line) {
|
||||
repeatCount += 1;
|
||||
if (repeatCount <= 2) deduped.push(line);
|
||||
continue;
|
||||
}
|
||||
repeatCount = 0;
|
||||
deduped.push(line);
|
||||
}
|
||||
|
||||
return deduped.join("\n");
|
||||
}
|
||||
|
||||
function collapseCarriageReturnFrames(value: string): string {
|
||||
if (!value.includes("\r")) return value;
|
||||
const output: string[] = [];
|
||||
let current = "";
|
||||
for (const char of value) {
|
||||
if (char === "\r") {
|
||||
current = "";
|
||||
} else if (char === "\n") {
|
||||
output.push(current);
|
||||
current = "";
|
||||
} else {
|
||||
current += char;
|
||||
}
|
||||
}
|
||||
output.push(current);
|
||||
return output.join("\n");
|
||||
}
|
||||
|
||||
function shortenVerboseLine(line: string): string {
|
||||
if (line.length <= MAX_VERBOSE_LINE_CHARS) return line;
|
||||
const edge = Math.floor((MAX_VERBOSE_LINE_CHARS - 80) / 2);
|
||||
return `${line.slice(0, edge)}[... long line shortened: ${line.length} chars ...]${line.slice(-edge)}`;
|
||||
}
|
||||
|
||||
export function truncateTextWithHeadAndTail(
|
||||
value: string,
|
||||
maxChars: number,
|
||||
{
|
||||
headChars = HEAD_CHARS,
|
||||
tailChars = TAIL_CHARS,
|
||||
marker = TRUNCATION_MARKER,
|
||||
}: {
|
||||
headChars?: number;
|
||||
tailChars?: number;
|
||||
marker?: string;
|
||||
} = {},
|
||||
): string {
|
||||
if (value.length <= maxChars) return value;
|
||||
if (maxChars <= marker.length + 16) {
|
||||
return value.slice(0, maxChars);
|
||||
}
|
||||
|
||||
const budget = maxChars - marker.length;
|
||||
const head = Math.min(headChars, budget);
|
||||
let tail = Math.min(tailChars, Math.max(0, budget - head));
|
||||
if (head + tail > budget) {
|
||||
tail = Math.max(0, budget - head);
|
||||
}
|
||||
if (head + tail >= value.length) {
|
||||
return value.slice(0, maxChars);
|
||||
}
|
||||
if (head + tail <= 0) {
|
||||
return value.slice(0, maxChars);
|
||||
}
|
||||
|
||||
return `${value.slice(0, head).trimEnd()}${marker}${value.slice(-tail).trimStart()}`;
|
||||
}
|
||||
|
||||
export function compressMessagesForRequestTooLargeRetry(
|
||||
messages: ModelMessage[],
|
||||
): CompressMessagesForRequestTooLargeRetryResult {
|
||||
let didAdjust = false;
|
||||
const compressedMessages = messages.map((message) => {
|
||||
const compressed = compressModelMessageForRequestRetry(message);
|
||||
if (compressed !== message) didAdjust = true;
|
||||
return compressed;
|
||||
});
|
||||
|
||||
return {
|
||||
messages: didAdjust ? compressedMessages : messages,
|
||||
didAdjust,
|
||||
};
|
||||
}
|
||||
|
||||
function compressModelMessageForRequestRetry(message: ModelMessage): ModelMessage {
|
||||
if (typeof message.content === "string") {
|
||||
// User text is an instruction or exact input, not terminal output. Lossy
|
||||
// cleanup can alter commands even when the request is far below its limit.
|
||||
if (message.role === "user") return message;
|
||||
const content = compressAndTruncateText(message.content, RETRY_MAX_MESSAGE_TEXT_CHARS);
|
||||
return content === message.content ? message : ({ ...message, content } as ModelMessage);
|
||||
}
|
||||
|
||||
if (!Array.isArray(message.content)) return message;
|
||||
|
||||
let didAdjust = false;
|
||||
const content = message.content.map((part) => {
|
||||
if (message.role === "user" && part.type === "text") return part;
|
||||
const compressed = compressContentPartForRequestRetry(part);
|
||||
if (compressed !== part) didAdjust = true;
|
||||
return compressed;
|
||||
});
|
||||
|
||||
return didAdjust ? ({ ...message, content } as ModelMessage) : message;
|
||||
}
|
||||
|
||||
function compressContentPartForRequestRetry(part: unknown): unknown {
|
||||
if (!part || typeof part !== "object") return part;
|
||||
const record = part as Record<string, unknown>;
|
||||
const type = record.type;
|
||||
|
||||
if (type === "text" && typeof record.text === "string") {
|
||||
const text = compressAndTruncateText(record.text, RETRY_MAX_MESSAGE_TEXT_CHARS);
|
||||
return text === record.text ? part : { ...record, text };
|
||||
}
|
||||
|
||||
if (type === "tool-result") {
|
||||
const output = record.output;
|
||||
if (output && typeof output === "object") {
|
||||
const outputRecord = output as Record<string, unknown>;
|
||||
if (outputRecord.type === "text" && typeof outputRecord.value === "string") {
|
||||
const value = compressAndTruncateText(outputRecord.value, RETRY_MAX_TOOL_RESULT_CHARS);
|
||||
if (value === outputRecord.value) return part;
|
||||
return {
|
||||
...record,
|
||||
output: {
|
||||
...outputRecord,
|
||||
value,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (type === "image" && typeof record.image === "string") {
|
||||
return omittedAttachmentTextPart("image", record.image, record);
|
||||
}
|
||||
|
||||
if (type === "file" && typeof record.data === "string") {
|
||||
return omittedAttachmentTextPart("file", record.data, record);
|
||||
}
|
||||
|
||||
return part;
|
||||
}
|
||||
|
||||
function compressAndTruncateText(value: string, maxChars: number): string {
|
||||
return truncateTextWithHeadAndTail(compressVerboseText(value), maxChars);
|
||||
}
|
||||
|
||||
function omittedAttachmentTextPart(
|
||||
label: "image" | "file",
|
||||
payload: string,
|
||||
record: Record<string, unknown>,
|
||||
): { type: "text"; text: string } {
|
||||
const details = [
|
||||
typeof record.filename === "string" ? `filename=${record.filename}` : undefined,
|
||||
typeof record.mediaType === "string" ? `mediaType=${record.mediaType}` : undefined,
|
||||
`${payload.length} chars`,
|
||||
].filter(Boolean).join(", ");
|
||||
|
||||
return {
|
||||
type: "text",
|
||||
text: `[${label} attachment omitted to keep the AI request small: ${details}]`,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user