refactor(agents): share log windowing and UTF-16 budget clamping

This commit is contained in:
Gustavo Madeira Santana
2026-02-14 18:27:20 -05:00
parent 020e635ded
commit 8e1635fa3f
2 changed files with 31 additions and 32 deletions

View File

@@ -32,6 +32,24 @@ type WritableStdin = {
};
const DEFAULT_LOG_TAIL_LINES = 200;
function resolveLogSliceWindow(offset?: number, limit?: number) {
const usingDefaultTail = offset === undefined && limit === undefined;
const effectiveLimit =
typeof limit === "number" && Number.isFinite(limit)
? limit
: usingDefaultTail
? DEFAULT_LOG_TAIL_LINES
: undefined;
return { effectiveOffset: offset, effectiveLimit, usingDefaultTail };
}
function defaultTailNote(totalLines: number, usingDefaultTail: boolean) {
if (!usingDefaultTail || totalLines <= DEFAULT_LOG_TAIL_LINES) {
return "";
}
return `\n\n[showing last ${DEFAULT_LOG_TAIL_LINES} of ${totalLines} lines; pass offset/limit to page]`;
}
const processSchema = Type.Object({
action: Type.String({ description: "Process action" }),
sessionId: Type.Optional(Type.String({ description: "Session id for actions other than list" })),
@@ -295,25 +313,15 @@ export function createProcessTool(
details: { status: "failed" },
};
}
const effectiveOffset = params.offset;
const usingDefaultTail = params.offset === undefined && params.limit === undefined;
const effectiveLimit =
typeof params.limit === "number" && Number.isFinite(params.limit)
? params.limit
: usingDefaultTail
? DEFAULT_LOG_TAIL_LINES
: undefined;
const window = resolveLogSliceWindow(params.offset, params.limit);
const { slice, totalLines, totalChars } = sliceLogLines(
scopedSession.aggregated,
effectiveOffset,
effectiveLimit,
window.effectiveOffset,
window.effectiveLimit,
);
const defaultTailNote =
usingDefaultTail && totalLines > DEFAULT_LOG_TAIL_LINES
? `\n\n[showing last ${DEFAULT_LOG_TAIL_LINES} of ${totalLines} lines; pass offset/limit to page]`
: "";
const logDefaultTailNote = defaultTailNote(totalLines, window.usingDefaultTail);
return {
content: [{ type: "text", text: (slice || "(no output yet)") + defaultTailNote }],
content: [{ type: "text", text: (slice || "(no output yet)") + logDefaultTailNote }],
details: {
status: scopedSession.exited ? "completed" : "running",
sessionId: params.sessionId,
@@ -326,27 +334,17 @@ export function createProcessTool(
};
}
if (scopedFinished) {
const effectiveOffset = params.offset;
const usingDefaultTail = params.offset === undefined && params.limit === undefined;
const effectiveLimit =
typeof params.limit === "number" && Number.isFinite(params.limit)
? params.limit
: usingDefaultTail
? DEFAULT_LOG_TAIL_LINES
: undefined;
const window = resolveLogSliceWindow(params.offset, params.limit);
const { slice, totalLines, totalChars } = sliceLogLines(
scopedFinished.aggregated,
effectiveOffset,
effectiveLimit,
window.effectiveOffset,
window.effectiveLimit,
);
const status = scopedFinished.status === "completed" ? "completed" : "failed";
const defaultTailNote =
usingDefaultTail && totalLines > DEFAULT_LOG_TAIL_LINES
? `\n\n[showing last ${DEFAULT_LOG_TAIL_LINES} of ${totalLines} lines; pass offset/limit to page]`
: "";
const logDefaultTailNote = defaultTailNote(totalLines, window.usingDefaultTail);
return {
content: [
{ type: "text", text: (slice || "(no output recorded)") + defaultTailNote },
{ type: "text", text: (slice || "(no output recorded)") + logDefaultTailNote },
],
details: {
status,

View File

@@ -4,6 +4,7 @@ import path from "node:path";
import type { OpenClawConfig } from "../../config/config.js";
import type { WorkspaceBootstrapFile } from "../workspace.js";
import type { EmbeddedContextFile } from "./types.js";
import { truncateUtf16Safe } from "../../utils.js";
type ContentBlockWithSignature = {
thought_signature?: unknown;
@@ -153,10 +154,10 @@ function clampToBudget(content: string, budget: number): string {
return content;
}
if (budget <= 3) {
return content.slice(0, budget);
return truncateUtf16Safe(content, budget);
}
const safe = Math.max(1, budget - 1);
return `${content.slice(0, safe)}`;
return `${truncateUtf16Safe(content, safe)}`;
}
export async function ensureSessionHeader(params: {