Merge remote-tracking branch 'origin/main' into fix/inline-slash-skills

This commit is contained in:
Jesse Merhi
2026-08-22 13:46:46 +10:00
8 changed files with 83 additions and 17 deletions
+1
View File
@@ -47,6 +47,7 @@ function prepare(root) {
...packageJson.scripts,
openclaw: "node openclaw.mjs",
};
delete packageJson.scripts.postinstall;
const aiRuntimeSource = path.join(root, "node_modules", "@openclaw", "ai");
const aiRuntimePackageJson = path.join(aiRuntimeSource, "package.json");
if (!fs.existsSync(aiRuntimePackageJson)) {
+4 -1
View File
@@ -140,7 +140,10 @@ export function normalizeClaudeCliStreamJsonRecord(
return normalized ? { line: JSON.stringify(parsed), omittedRawChars } : undefined;
}
function streamJsonOutputLimitErrorText(kind: "raw" | "line" | "lines", limit: number): string {
export function streamJsonOutputLimitErrorText(
kind: "raw" | "line" | "lines",
limit: number,
): string {
if (kind === "line") {
return `CLI JSONL line exceeded ${limit} characters; refusing to parse output.`;
}
+25 -4
View File
@@ -847,7 +847,7 @@ describe("Claude live turn output bounds and result projection", () => {
});
await expect(startLiveTurn("run-live-oversized-line", false)).rejects.toThrow(
"Claude CLI JSONL line exceeded output limit.",
"CLI JSONL line exceeded 8388608 characters; refusing to parse output.",
);
});
@@ -855,14 +855,17 @@ describe("Claude live turn output bounds and result projection", () => {
{
name: "a coalesced blank-frame flood",
createChunk: () => "\n".repeat(20_001),
expectedError: "CLI JSONL output exceeded 20000 lines; refusing to parse output.",
},
{
name: "whitespace-only records exceeding the raw budget",
createChunk: () => `${" ".repeat(4_300_000)}\n${" ".repeat(4_300_000)}\n`,
expectedError: "CLI JSONL output exceeded 8388608 characters; refusing to parse output.",
},
{
name: "valid JSON padded beyond the raw budget",
createChunk: () => `${" ".repeat(4_300_000)}{}\n${" ".repeat(4_300_000)}{}\n`,
expectedError: "CLI JSONL output exceeded 8388608 characters; refusing to parse output.",
},
{
name: "internal formatting around compacted Claude media",
@@ -886,14 +889,32 @@ describe("Claude live turn output bounds and result projection", () => {
}).replace('"message":', `"message":${" ".repeat(4_300_000)}`);
return `${line}\n${line}\n`;
},
expectedError: "CLI JSONL output exceeded 8388608 characters; refusing to parse output.",
},
])("rejects $name from the managed Claude live session", async ({ createChunk }) => {
])("reports the exact limit for $name", async ({ createChunk, expectedError }) => {
const live: ReturnType<typeof mockClaudeLiveRun> = mockClaudeLiveRun(supervisorSpawnMock, {
onWrite: () => live.spawnInput.onStdout?.(createChunk()),
});
await expect(startLiveTurn("run-live-output-budget", false)).rejects.toThrow(
"Claude CLI turn output exceeded limit.",
await expect(startLiveTurn("run-live-output-budget", false)).rejects.toThrow(expectedError);
});
it("reports backend JSONL parser failures without relabeling them as output limits", async () => {
mockClaudeLiveRun(supervisorSpawnMock, {
events: [{ type: "system", subtype: "init", session_id: "live-parser-error" }],
});
await expectRejectsWithFields(
startLiveTurn("run-live-parser-error", false, {
parseJsonlEvent: () => {
throw new Error("invalid custom event");
},
}),
{
name: "FailoverError",
reason: "format",
message: "CLI backend claude-cli JSONL parser failed: invalid custom event",
},
);
});
+8 -3
View File
@@ -26,6 +26,7 @@ import {
createCliJsonlStreamingParser,
frameBoundedCliJsonlChunk,
normalizeClaudeCliStreamJsonRecord,
streamJsonOutputLimitErrorText,
} from "../cli-output-stream.js";
import { parseCliOutput } from "../cli-output.js";
import type { FailoverError } from "../failover-error.js";
@@ -360,10 +361,11 @@ function applyBackgroundTasksChanged(
function pushTurnLine(host: ClaudeLiveTurnHost, turn: ClaudeLiveTurn, line: string): boolean {
turn.streamingParser.push(`${line}\n`);
if (!turn.streamingParser.getErrorText()) {
const errorText = turn.streamingParser.getErrorText();
if (!errorText) {
return true;
}
host.close("abort", createClaudeOutputLimitError(host, "Claude CLI turn output exceeded limit."));
host.close("abort", createClaudeOutputLimitError(host, errorText));
return false;
}
@@ -492,7 +494,10 @@ export function acceptClaudeStdout(host: ClaudeLiveTurnHost, chunk: string): voi
) {
host.close(
"abort",
createClaudeOutputLimitError(host, "Claude CLI JSONL line exceeded output limit."),
createClaudeOutputLimitError(
host,
streamJsonOutputLimitErrorText("line", maxPendingLineChars),
),
);
}
} catch (error) {
@@ -231,7 +231,10 @@ function requireManifestRegistryLoadParams(index = 0): Record<string, unknown> {
return call[0];
}
function expectManifestRegistryLoad(index: number, config: OpenClawConfig | Record<string, never>) {
function expectManifestRegistryLoad(
index: number,
config: OpenClawConfig | Record<string, never> | undefined,
) {
const params = requireManifestRegistryLoadParams(index);
expect(params.config).toEqual(config);
expect(params.env).toBe(process.env);
@@ -1669,7 +1672,7 @@ describe("resolvePluginCapabilityProviders", () => {
const providers = resolvePluginCapabilityProviders({ key: "mediaUnderstandingProviders" });
expectResolvedCapabilityProviderIds(providers, ["google"]);
expectManifestRegistryLoad(0, {});
expectManifestRegistryLoad(0, undefined);
expectActiveRegistryLookup(["google"]);
});
@@ -17,6 +17,30 @@ function osc8Targets(raw: string) {
}
describe("HyperlinkMarkdown", () => {
it("does not reallocate prepared lines for an unchanged same-width redraw", () => {
const markdown = new HyperlinkMarkdown(
"مرحبا [docs](https://example.test/path)",
0,
0,
markdownTheme,
);
const first = markdown.render(80);
expect(markdown.render(80)).toBe(first);
const resized = markdown.render(40);
expect(resized).not.toBe(first);
expect(markdown.render(40)).toBe(resized);
markdown.setText("updated");
const updated = markdown.render(40);
expect(updated).not.toBe(resized);
expect(markdown.render(40)).toBe(updated);
markdown.invalidate();
expect(markdown.render(40)).not.toBe(updated);
});
it("moves dunder identifiers intact across fenced code wrap boundaries", () => {
const markdown = new HyperlinkMarkdown(
["```python", 'if __name__ == "__main__":', "```"].join("\n"),
+11 -1
View File
@@ -24,6 +24,7 @@ function sanitizeMarkdownDisplayText(text: string): string {
export class HyperlinkMarkdown implements Component {
private inner: Markdown;
private urls: string[];
private cachedRender?: { width: number; lines: string[] };
constructor(
text: string,
@@ -39,16 +40,25 @@ export class HyperlinkMarkdown implements Component {
}
render(width: number): string[] {
return addOsc8Hyperlinks(this.inner.render(width), this.urls).map(isolateRtlRenderedLine);
if (this.cachedRender?.width === width) {
return this.cachedRender.lines;
}
const lines = addOsc8Hyperlinks(this.inner.render(width), this.urls).map(
isolateRtlRenderedLine,
);
this.cachedRender = { width, lines };
return lines;
}
setText(text: string): void {
const displayText = sanitizeMarkdownDisplayText(text);
this.inner.setText(displayText);
this.urls = extractUrls(displayText);
this.cachedRender = undefined;
}
invalidate(): void {
this.inner.invalidate();
this.cachedRender = undefined;
}
}
+5 -6
View File
@@ -21,6 +21,7 @@ describe("package git fixture", () => {
scripts: {
build: "node build.mjs",
openclaw: "node scripts/run-node.mjs",
postinstall: "node scripts/postinstall-bundled-plugins.mjs",
},
},
null,
@@ -98,6 +99,7 @@ describe("package git fixture", () => {
scripts: {
lint: "node lint.mjs",
openclaw: "node scripts/run-node.mjs",
postinstall: "node scripts/postinstall-bundled-plugins.mjs",
},
},
null,
@@ -113,12 +115,9 @@ describe("package git fixture", () => {
expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(0);
const packageJson = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8"));
expect(packageJson).toMatchObject({
dependencies: { chalk: "5.6.2" },
scripts: {
lint: "node lint.mjs",
openclaw: "node openclaw.mjs",
},
expect(packageJson.scripts).toEqual({
lint: "node lint.mjs",
openclaw: "node openclaw.mjs",
});
expect(packageJson.dependencies).not.toHaveProperty("@openclaw/ai");
});