Files
openclaw/src/tasks/task-completion-contract.ts
WhatsSkiLL 36e76ef424 fix(codex): block progress-only completions [AI-assisted] (#85110)
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 head 21a1159165.
- Required merge gates passed before the squash merge.

Prepared head SHA: 21a1159165
Review: 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>
2026-05-22 03:43:28 +00:00

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.",
};
}