mirror of https://github.com/openclaw/openclaw.git
fix(agents): restore log paging and strict bootstrap cap
This commit is contained in:
parent
e7b484dae6
commit
b4766f8743
|
|
@ -261,6 +261,29 @@ describe("exec tool backgrounding", () => {
|
|||
expect(normalizeText(textBlock?.text)).toBe("beta");
|
||||
});
|
||||
|
||||
it("keeps offset-only log requests unbounded by default tail mode", async () => {
|
||||
const lines = Array.from({ length: 260 }, (_value, index) => `line-${index + 1}`);
|
||||
const result = await execTool.execute("call1", {
|
||||
command: echoLines(lines),
|
||||
background: true,
|
||||
});
|
||||
const sessionId = (result.details as { sessionId: string }).sessionId;
|
||||
await waitForCompletion(sessionId);
|
||||
|
||||
const log = await processTool.execute("call2", {
|
||||
action: "log",
|
||||
sessionId,
|
||||
offset: 30,
|
||||
});
|
||||
|
||||
const textBlock = log.content.find((c) => c.type === "text")?.text ?? "";
|
||||
const renderedLines = textBlock.split("\n");
|
||||
expect(renderedLines[0]?.trim()).toBe("line-31");
|
||||
expect(renderedLines[renderedLines.length - 1]?.trim()).toBe("line-260");
|
||||
expect(textBlock).not.toContain("showing last 200");
|
||||
expect((log.details as { totalLines?: number }).totalLines).toBe(260);
|
||||
});
|
||||
|
||||
it("scopes process sessions by scopeKey", async () => {
|
||||
const bashA = createExecTool({ backgroundMs: 10, scopeKey: "agent:alpha" });
|
||||
const processA = createProcessTool({ scopeKey: "agent:alpha" });
|
||||
|
|
|
|||
|
|
@ -296,16 +296,18 @@ export function createProcessTool(
|
|||
};
|
||||
}
|
||||
const effectiveOffset = params.offset;
|
||||
const usingDefaultTail = params.offset === undefined && params.limit === undefined;
|
||||
const effectiveLimit =
|
||||
typeof params.limit === "number" && Number.isFinite(params.limit)
|
||||
? params.limit
|
||||
: DEFAULT_LOG_TAIL_LINES;
|
||||
: usingDefaultTail
|
||||
? DEFAULT_LOG_TAIL_LINES
|
||||
: undefined;
|
||||
const { slice, totalLines, totalChars } = sliceLogLines(
|
||||
scopedSession.aggregated,
|
||||
effectiveOffset,
|
||||
effectiveLimit,
|
||||
);
|
||||
const usingDefaultTail = params.offset === undefined && params.limit === undefined;
|
||||
const defaultTailNote =
|
||||
usingDefaultTail && totalLines > DEFAULT_LOG_TAIL_LINES
|
||||
? `\n\n[showing last ${DEFAULT_LOG_TAIL_LINES} of ${totalLines} lines; pass offset/limit to page]`
|
||||
|
|
@ -325,17 +327,19 @@ 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
|
||||
: DEFAULT_LOG_TAIL_LINES;
|
||||
: usingDefaultTail
|
||||
? DEFAULT_LOG_TAIL_LINES
|
||||
: undefined;
|
||||
const { slice, totalLines, totalChars } = sliceLogLines(
|
||||
scopedFinished.aggregated,
|
||||
effectiveOffset,
|
||||
effectiveLimit,
|
||||
);
|
||||
const status = scopedFinished.status === "completed" ? "completed" : "failed";
|
||||
const usingDefaultTail = params.offset === undefined && params.limit === undefined;
|
||||
const defaultTailNote =
|
||||
usingDefaultTail && totalLines > DEFAULT_LOG_TAIL_LINES
|
||||
? `\n\n[showing last ${DEFAULT_LOG_TAIL_LINES} of ${totalLines} lines; pass offset/limit to page]`
|
||||
|
|
|
|||
|
|
@ -67,4 +67,26 @@ describe("buildBootstrapContextFiles", () => {
|
|||
expect(result).toHaveLength(3);
|
||||
expect(result[2]?.content).toContain("[...truncated, read USER.md for full content...]");
|
||||
});
|
||||
|
||||
it("enforces strict total cap even when truncation markers are present", () => {
|
||||
const files = [
|
||||
makeFile({ name: "AGENTS.md", content: "a".repeat(1_000) }),
|
||||
makeFile({ name: "SOUL.md", path: "/tmp/SOUL.md", content: "b".repeat(1_000) }),
|
||||
];
|
||||
const result = buildBootstrapContextFiles(files, {
|
||||
maxChars: 100,
|
||||
totalMaxChars: 150,
|
||||
});
|
||||
const totalChars = result.reduce((sum, entry) => sum + entry.content.length, 0);
|
||||
expect(totalChars).toBeLessThanOrEqual(150);
|
||||
});
|
||||
|
||||
it("skips bootstrap injection when remaining total budget is too small", () => {
|
||||
const files = [makeFile({ name: "AGENTS.md", content: "a".repeat(1_000) })];
|
||||
const result = buildBootstrapContextFiles(files, {
|
||||
maxChars: 200,
|
||||
totalMaxChars: 40,
|
||||
});
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@ export function stripThoughtSignatures<T>(
|
|||
|
||||
export const DEFAULT_BOOTSTRAP_MAX_CHARS = 20_000;
|
||||
export const DEFAULT_BOOTSTRAP_TOTAL_MAX_CHARS = 24_000;
|
||||
const MIN_BOOTSTRAP_FILE_BUDGET_CHARS = 64;
|
||||
const BOOTSTRAP_HEAD_RATIO = 0.7;
|
||||
const BOOTSTRAP_TAIL_RATIO = 0.2;
|
||||
|
||||
|
|
@ -144,6 +145,20 @@ function trimBootstrapContent(
|
|||
};
|
||||
}
|
||||
|
||||
function clampToBudget(content: string, budget: number): string {
|
||||
if (budget <= 0) {
|
||||
return "";
|
||||
}
|
||||
if (content.length <= budget) {
|
||||
return content;
|
||||
}
|
||||
if (budget <= 3) {
|
||||
return content.slice(0, budget);
|
||||
}
|
||||
const safe = Math.max(1, budget - 1);
|
||||
return `${content.slice(0, safe)}…`;
|
||||
}
|
||||
|
||||
export async function ensureSessionHeader(params: {
|
||||
sessionFile: string;
|
||||
sessionId: string;
|
||||
|
|
@ -183,27 +198,40 @@ export function buildBootstrapContextFiles(
|
|||
if (remainingTotalChars <= 0) {
|
||||
break;
|
||||
}
|
||||
if (remainingTotalChars < MIN_BOOTSTRAP_FILE_BUDGET_CHARS) {
|
||||
opts?.warn?.(
|
||||
`remaining bootstrap budget is ${remainingTotalChars} chars (<${MIN_BOOTSTRAP_FILE_BUDGET_CHARS}); skipping additional bootstrap files`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
if (file.missing) {
|
||||
const missingText = `[MISSING] Expected at: ${file.path}`;
|
||||
const cappedMissingText = clampToBudget(missingText, remainingTotalChars);
|
||||
if (!cappedMissingText) {
|
||||
break;
|
||||
}
|
||||
remainingTotalChars = Math.max(0, remainingTotalChars - cappedMissingText.length);
|
||||
result.push({
|
||||
path: file.path,
|
||||
content: `[MISSING] Expected at: ${file.path}`,
|
||||
content: cappedMissingText,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const fileMaxChars = Math.max(1, Math.min(maxChars, remainingTotalChars));
|
||||
const trimmed = trimBootstrapContent(file.content ?? "", file.name, fileMaxChars);
|
||||
if (!trimmed.content) {
|
||||
const contentWithinBudget = clampToBudget(trimmed.content, remainingTotalChars);
|
||||
if (!contentWithinBudget) {
|
||||
continue;
|
||||
}
|
||||
if (trimmed.truncated) {
|
||||
if (trimmed.truncated || contentWithinBudget.length < trimmed.content.length) {
|
||||
opts?.warn?.(
|
||||
`workspace bootstrap file ${file.name} is ${trimmed.originalLength} chars (limit ${trimmed.maxChars}); truncating in injected context`,
|
||||
);
|
||||
}
|
||||
remainingTotalChars = Math.max(0, remainingTotalChars - trimmed.content.length);
|
||||
remainingTotalChars = Math.max(0, remainingTotalChars - contentWithinBudget.length);
|
||||
result.push({
|
||||
path: file.path,
|
||||
content: trimmed.content,
|
||||
content: contentWithinBudget,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
|
|
|
|||
Loading…
Reference in New Issue