mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-31 09:28:30 +00:00
Summary: - The PR adds shared required-completion classification for ACP/subagent finalization, marks missing, progress-only, and delivery-exhausted completions as blocked, and adds regression tests plus a changelog entry. - Reproducibility: yes. source-reproducible. Current main finalizes the implicated ACP and subagent success pa ... he linked issue supplies production-shaped evidence; this read-only pass did not run a live provider repro. Automerge notes: - PR branch already contained follow-up commit before automerge: fix(codex): preserve final completions after progress - PR branch already contained follow-up commit before automerge: fix(codex): accept progress-prefixed final completions - PR branch already contained follow-up commit before automerge: fix(codex): accept separator-delimited completions - PR branch already contained follow-up commit before automerge: fix(codex): keep follow-up planning blocked - PR branch already contained follow-up commit before automerge: fix(codex): block progress-only completions [AI-assisted] Validation: - ClawSweeper review passed for head21a1159165. - Required merge gates passed before the squash merge. Prepared head SHA:21a1159165Review: https://github.com/openclaw/openclaw/pull/85110#issuecomment-4513104331 Co-authored-by: IWhatsskill <284122573+IWhatsskill@users.noreply.github.com> Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com> Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com> Approved-by: takhoffman Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
93 lines
3.7 KiB
TypeScript
93 lines
3.7 KiB
TypeScript
import type { TaskTerminalOutcome } from "./task-registry.types.js";
|
|
|
|
export type RequiredCompletionTerminalResult = {
|
|
terminalOutcome?: Extract<TaskTerminalOutcome, "blocked">;
|
|
terminalSummary?: string;
|
|
};
|
|
|
|
const PROGRESS_ONLY_PATTERN =
|
|
/^(?:i(?:'|\u2019)ll|i will|i(?:'|\u2019)m|i am|i(?:'|\u2019)m going to|i am going to|let me|i need to)\s+(?:now\s+)?(?:analyz(?:e|ing)|apply|check(?:ing)?|continue|debug(?:ging)?|follow(?:ing)?\s+up|inspect(?:ing)?|investigat(?:e|ing)|look(?:ing)?(?:\s+into)?|map(?:ping)?|open(?:ing)?|read(?:ing)?|report(?:ing)?(?:\s+back)?|review(?:ing)?|run(?:ning)?|start(?:ing)?|test(?:ing)?|trace|trac(?:e|ing)|try(?:ing)?|update|verify(?:ing)?|work(?:ing)?)/i;
|
|
|
|
const BARE_PROGRESS_ONLY_PATTERN =
|
|
/^(?:analyz(?:e|ing)|check(?:ing)?|debug(?:ging)?|inspect(?:ing)?|investigat(?:e|ing)|look(?:ing)?\s+into|map(?:ping)?|read(?:ing)?|report(?:ing)?\s+back|review(?:ing)?|run(?:ning)?|test(?:ing)?|trac(?:e|ing)|verify(?:ing)?|work(?:ing)?\s+on)\b/i;
|
|
|
|
const FOLLOW_UP_PLANNING_PREFIX_PATTERN =
|
|
/^(?:after(?:wards|\s+that)?|from\s+there|next|once\s+(?:done|that(?:'|\u2019)?s\s+done|that\s+is\s+done)|then)[,.\s]+/i;
|
|
|
|
function normalizeCompletionText(value: string | null | undefined): string {
|
|
return value?.replace(/\s+/g, " ").trim() ?? "";
|
|
}
|
|
|
|
function normalizeCompletionFailureReason(value: string | null | undefined): string {
|
|
const normalized = normalizeCompletionText(value);
|
|
if (!normalized) {
|
|
return "";
|
|
}
|
|
return normalized.length <= 160 ? normalized : `${normalized.slice(0, 159)}...`;
|
|
}
|
|
|
|
function matchesProgressOnlyPrefix(value: string): boolean {
|
|
if (PROGRESS_ONLY_PATTERN.test(value) || BARE_PROGRESS_ONLY_PATTERN.test(value)) {
|
|
return true;
|
|
}
|
|
const followup = value.replace(FOLLOW_UP_PLANNING_PREFIX_PATTERN, "").trim();
|
|
return (
|
|
followup !== value &&
|
|
(PROGRESS_ONLY_PATTERN.test(followup) || BARE_PROGRESS_ONLY_PATTERN.test(followup))
|
|
);
|
|
}
|
|
|
|
function hasNonProgressFollowupSentence(value: string): boolean {
|
|
const boundary = /(?:[.!?:]|\s[-\u2013\u2014])\s+\S/.exec(value);
|
|
if (!boundary) {
|
|
return false;
|
|
}
|
|
const separatorEnd = boundary.index + boundary[0].length - 1;
|
|
const firstSentence = value.slice(0, separatorEnd).trim();
|
|
const rest = value.slice(separatorEnd).trim();
|
|
return matchesProgressOnlyPrefix(firstSentence) && !isProgressOnlyCompletionText(rest);
|
|
}
|
|
|
|
export function isProgressOnlyCompletionText(value: string | null | undefined): boolean {
|
|
const normalized = normalizeCompletionText(value);
|
|
if (!normalized) {
|
|
return false;
|
|
}
|
|
if (hasNonProgressFollowupSentence(normalized)) {
|
|
return false;
|
|
}
|
|
return matchesProgressOnlyPrefix(normalized);
|
|
}
|
|
|
|
export function resolveRequiredCompletionTerminalResult(
|
|
resultText: string | null | undefined,
|
|
): RequiredCompletionTerminalResult {
|
|
const normalized = normalizeCompletionText(resultText);
|
|
if (!normalized) {
|
|
return {
|
|
terminalOutcome: "blocked",
|
|
terminalSummary: "Required completion did not produce a final deliverable.",
|
|
};
|
|
}
|
|
if (isProgressOnlyCompletionText(normalized)) {
|
|
return {
|
|
terminalOutcome: "blocked",
|
|
terminalSummary:
|
|
"Required completion ended with progress-only text, not a final deliverable.",
|
|
};
|
|
}
|
|
return {};
|
|
}
|
|
|
|
export function resolveRequiredCompletionDeliveryFailureTerminalResult(
|
|
reason: string | null | undefined,
|
|
): RequiredCompletionTerminalResult {
|
|
const normalizedReason = normalizeCompletionFailureReason(reason);
|
|
return {
|
|
terminalOutcome: "blocked",
|
|
terminalSummary: normalizedReason
|
|
? `Required completion delivery failed before reaching the requester: ${normalizedReason}.`
|
|
: "Required completion delivery failed before reaching the requester.",
|
|
};
|
|
}
|