mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 14:10:51 +00:00
fix(acp): preserve streamed progress chunks (#78383)
Co-authored-by: Alex Knight <15041791+amknight@users.noreply.github.com>
This commit is contained in:
@@ -132,6 +132,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Discord/guilds: route plain text control commands such as `/steer` through the normal authorization and mention gate instead of silently dropping them before an agent session can see them. Fixes #78080. Thanks @ramitrkar-hash.
|
||||
- Control UI/Sessions: make the compaction count a compact `N Checkpoint(s)` disclosure and show expanded session-level details with modern checkpoint history cards across responsive table layouts. Thanks @BunsDev.
|
||||
- Control UI/performance: keep chat and channel tabs responsive while history payloads and channel probes are slow, label partial channel status, and record slow chat/config render timings in the event log. Thanks @BunsDev.
|
||||
- ACP: preserve streamed chunk boundaries in background-task progress summaries so CJK text, paths, URLs, and identifiers are no longer split with synthetic spaces. Fixes #78312. Thanks @amknight.
|
||||
- Control UI/sessions: fire the documented `/new` command and lifecycle hooks only for explicit Control UI session creation, restoring session-memory and custom hook capture without changing SDK parent-session creates. Fixes #76957. Thanks @BunsDev.
|
||||
- Exec approvals: fall back to a guarded copy when Windows rejects rename-overwrite for `exec-approvals.json`, while preserving symlink, hard-link, and owner-only permission safeguards. Fixes #77785. (#77907) Thanks @Alex-Alaniz and @MilleniumGenAI.
|
||||
- Slack: preserve Socket Mode SDK error context and structured Slack API fields in reconnect logs, so startup failures no longer collapse to a bare `unknown error`.
|
||||
|
||||
@@ -98,11 +98,15 @@ function summarizeBackgroundTaskText(text: string): string {
|
||||
}
|
||||
|
||||
function appendBackgroundTaskProgressSummary(current: string, chunk: string): string {
|
||||
const normalizedChunk = normalizeText(chunk)?.replace(/\s+/g, " ");
|
||||
const normalizedChunk = chunk.replace(/\s+/g, " ");
|
||||
if (!normalizedChunk) {
|
||||
return current;
|
||||
}
|
||||
const combined = current ? `${current} ${normalizedChunk}` : normalizedChunk;
|
||||
const chunkToAppend = current ? normalizedChunk : normalizedChunk.trimStart();
|
||||
if (!chunkToAppend) {
|
||||
return current;
|
||||
}
|
||||
const combined = `${current}${chunkToAppend}`.replace(/\s+/g, " ");
|
||||
if (combined.length <= ACP_BACKGROUND_TASK_PROGRESS_MAX_LENGTH) {
|
||||
return combined;
|
||||
}
|
||||
|
||||
@@ -313,7 +313,32 @@ describe("AcpSessionManager", () => {
|
||||
yield {
|
||||
type: "text_delta" as const,
|
||||
stream: "output" as const,
|
||||
text: "Write failed: permission denied for /root/oc-acp-write-should-fail.txt.",
|
||||
text: "Write failed: ",
|
||||
};
|
||||
yield {
|
||||
type: "text_delta" as const,
|
||||
stream: "output" as const,
|
||||
text: "permission ",
|
||||
};
|
||||
yield {
|
||||
type: "text_delta" as const,
|
||||
stream: "output" as const,
|
||||
text: "denied for ",
|
||||
};
|
||||
yield {
|
||||
type: "text_delta" as const,
|
||||
stream: "output" as const,
|
||||
text: "/root/",
|
||||
};
|
||||
yield {
|
||||
type: "text_delta" as const,
|
||||
stream: "output" as const,
|
||||
text: "oc-acp-write-",
|
||||
};
|
||||
yield {
|
||||
type: "text_delta" as const,
|
||||
stream: "output" as const,
|
||||
text: "should-fail.txt.",
|
||||
};
|
||||
yield { type: "done" as const };
|
||||
});
|
||||
@@ -374,6 +399,90 @@ describe("AcpSessionManager", () => {
|
||||
});
|
||||
}, 300_000);
|
||||
|
||||
it("preserves token-streamed ACP progress boundaries in parented task summaries", async () => {
|
||||
await withAcpManagerTaskStateDir(async () => {
|
||||
const runtimeState = createRuntime();
|
||||
const chunks = [
|
||||
"현재 ",
|
||||
"작업 ",
|
||||
"디",
|
||||
"렉토",
|
||||
"리는 ",
|
||||
"/home/",
|
||||
"by",
|
||||
"kim",
|
||||
"0119/",
|
||||
".open",
|
||||
"claw/",
|
||||
"workspace",
|
||||
"\n\t",
|
||||
"입니다",
|
||||
];
|
||||
runtimeState.runTurn.mockImplementation(async function* () {
|
||||
for (const text of chunks) {
|
||||
yield {
|
||||
type: "text_delta" as const,
|
||||
stream: "output" as const,
|
||||
text,
|
||||
};
|
||||
}
|
||||
yield { type: "done" as const };
|
||||
});
|
||||
hoisted.requireAcpRuntimeBackendMock.mockReturnValue({
|
||||
id: "acpx",
|
||||
runtime: runtimeState.runtime,
|
||||
});
|
||||
hoisted.readAcpSessionEntryMock.mockImplementation((paramsUnknown: unknown) => {
|
||||
const sessionKey = (paramsUnknown as { sessionKey?: string }).sessionKey;
|
||||
if (sessionKey === "agent:codex:acp:child-1") {
|
||||
return {
|
||||
sessionKey,
|
||||
storeSessionKey: sessionKey,
|
||||
entry: {
|
||||
sessionId: "child-1",
|
||||
updatedAt: Date.now(),
|
||||
spawnedBy: "agent:quant:telegram:quant:direct:822430204",
|
||||
label: "Korean path",
|
||||
},
|
||||
acp: readySessionMeta(),
|
||||
};
|
||||
}
|
||||
if (sessionKey === "agent:quant:telegram:quant:direct:822430204") {
|
||||
return {
|
||||
sessionKey,
|
||||
storeSessionKey: sessionKey,
|
||||
entry: {
|
||||
sessionId: "parent-1",
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
};
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const manager = new AcpSessionManager();
|
||||
await manager.runTurn({
|
||||
cfg: baseCfg,
|
||||
sessionKey: "agent:codex:acp:child-1",
|
||||
text: "Print the current directory in Korean",
|
||||
mode: "prompt",
|
||||
requestId: "direct-parented-korean-path-run",
|
||||
});
|
||||
await flushMicrotasks();
|
||||
|
||||
expect(findTaskByRunId("direct-parented-korean-path-run")).toMatchObject({
|
||||
runtime: "acp",
|
||||
ownerKey: "agent:quant:telegram:quant:direct:822430204",
|
||||
scopeKind: "session",
|
||||
childSessionKey: "agent:codex:acp:child-1",
|
||||
label: "Korean path",
|
||||
task: "Print the current directory in Korean",
|
||||
status: "succeeded",
|
||||
progressSummary: "현재 작업 디렉토리는 /home/bykim0119/.openclaw/workspace 입니다",
|
||||
});
|
||||
});
|
||||
}, 300_000);
|
||||
|
||||
it("serializes concurrent turns for the same ACP session", async () => {
|
||||
const runtimeState = createRuntime();
|
||||
hoisted.requireAcpRuntimeBackendMock.mockReturnValue({
|
||||
|
||||
Reference in New Issue
Block a user