test(runtime): guard late ACP updates (#252)

This commit is contained in:
Peter Steinberger 2026-04-25 06:41:02 +01:00
parent c5dea03e96
commit 051d5e25ff
6 changed files with 16 additions and 20 deletions

View File

@ -6,6 +6,7 @@ Repo: https://github.com/openclaw/acpx
### Changes
- Conformance/ACP: add a post-success drain case that catches late tool updates emitted after `session/prompt` resolves. (#252) Thanks @logofet85-ai.
- Conformance/ACP: add a data-driven ACP core v1 conformance suite with CI smoke coverage, nightly coverage, and a hardened runner that reports startup failures cleanly and scopes filesystem checks to the session cwd. (#130) Thanks @lynnzc.
- CLI/prompts: add `--prompt-retries` to retry transient prompt failures with exponential backoff while preserving strict JSON behavior and avoiding replay after prompt side effects. (#142) Thanks @lupuletic and @dutifulbob.
- Output: add `--suppress-reads` to mask raw file-read bodies in text and JSON output while keeping normal tool activity visible. (#136) Thanks @hayatosc.

View File

@ -36,7 +36,7 @@
},
{
"type": "updates_text_includes",
"text": "сейчас пишу"
"text": "writing now"
},
{
"type": "updates_session_update_includes",

View File

@ -26,20 +26,15 @@ export async function runPromptTurn(params: {
const promptPromise = params.client.prompt(params.sessionId, params.prompt);
await params.onPromptStarted?.();
const response = await withTimeout(promptPromise, params.timeoutMs);
if (
params.promptMessageId &&
!hasAgentReplyAfterPrompt(params.conversation, params.promptMessageId)
) {
await params.client
.waitForSessionUpdatesIdle?.({
idleMs: SESSION_REPLY_IDLE_MS,
timeoutMs: SESSION_REPLY_DRAIN_TIMEOUT_MS,
})
.catch(() => {
// Best effort. The prompt already completed successfully, so keep the
// original stop reason if late update draining itself times out.
});
}
await params.client
.waitForSessionUpdatesIdle?.({
idleMs: SESSION_REPLY_IDLE_MS,
timeoutMs: SESSION_REPLY_DRAIN_TIMEOUT_MS,
})
.catch(() => {
// Best effort. The prompt already completed successfully, so keep the
// original stop reason if late update draining itself times out.
});
return {
stopReason: response.stopReason,
source: "rpc",

View File

@ -239,7 +239,7 @@ test("runner observes late post-success tool updates after settle timeout", asyn
},
{
type: "updates_text_includes",
text: "сейчас пишу",
text: "writing now",
},
{
type: "updates_session_update_includes",

View File

@ -2071,7 +2071,7 @@ test("integration: late post-success tool updates are rendered before prompt exi
homeDir,
);
assert.equal(result.code, 0, result.stderr);
assert.match(result.stdout, /сейчас пишу/);
assert.match(result.stdout, /writing now/);
assert.match(result.stdout, /\[tool\] LateTool/);
assert.match(result.stdout, /follow-up/);
@ -3599,7 +3599,7 @@ test("runPromptTurn: missing waitForSessionUpdatesIdle still returns cleanly on
assert.equal(result.stopReason, "end_turn");
});
test("runPromptTurn: existing agent reply skips post-success drain", async () => {
test("runPromptTurn: existing agent reply still allows post-success drain", async () => {
const calls: string[] = [];
const conversation = createSessionConversation();
const promptMessageId = recordPromptSubmission(conversation, "hello");
@ -3631,5 +3631,5 @@ test("runPromptTurn: existing agent reply skips post-success drain", async () =>
assert.equal(result.source, "rpc");
assert.equal(result.stopReason, "end_turn");
assert.deepEqual(calls, ["prompt"]);
assert.deepEqual(calls, ["prompt", "drain"]);
});

View File

@ -843,7 +843,7 @@ class MockAgent implements Agent {
throw new Error("Usage: late-tool <milliseconds> <text>");
}
await this.sendAssistantMessage(sessionId, "сейчас пишу");
await this.sendAssistantMessage(sessionId, "writing now");
this.emitLateToolCall(sessionId, delayMs, lateText);
return `late-tool scheduled: ${lateText}`;
}