mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-13 17:16:05 +00:00
Summary: - The branch updates the Codex plugin native subagent parser, monitor, and tests so successful null or blank c ... final result and transcript reconciliation can override early empty notifications before fallback delivery. - PR surface: Source +92, Tests +176. Total +268 across 4 files. - Reproducibility: yes. at source level: current main maps successful null/blank Codex completions to `(no out ... n recover final text. I did not run a live current-main Telegram/Codex reproduction in this read-only pass. Automerge notes: - PR branch already contained follow-up commit before automerge: fix(codex): preserve native subagent completion results Validation: - ClawSweeper review passed for headf9270c28e7. - Required merge gates passed before the squash merge. Prepared head SHA:f9270c28e7Review: https://github.com/openclaw/openclaw/pull/91235#issuecomment-4643854708 Co-authored-by: 宇宙熊Yzx <53250620+849261680@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>
259 lines
7.5 KiB
TypeScript
259 lines
7.5 KiB
TypeScript
/**
|
|
* Extracts native Codex subagent completion notifications from trusted
|
|
* inter-agent commentary messages emitted by the app-server.
|
|
*/
|
|
import type { CodexServerNotification, JsonObject, JsonValue } from "./protocol.js";
|
|
import { isJsonObject } from "./protocol.js";
|
|
|
|
const CODEX_SUBAGENT_NOTIFICATION_START = "<subagent_notification>";
|
|
const CODEX_SUBAGENT_NOTIFICATION_END = "</subagent_notification>";
|
|
|
|
/** Terminal status values OpenClaw accepts for Codex native subagent completion. */
|
|
export type CodexNativeSubagentCompletionStatus = "succeeded" | "failed" | "cancelled";
|
|
|
|
type CodexNativeSubagentCompletionDetails = {
|
|
status: CodexNativeSubagentCompletionStatus;
|
|
statusLabel: string;
|
|
result: string;
|
|
};
|
|
|
|
/** Completion associated with a resolved child thread id. */
|
|
export type CodexNativeSubagentCompletion = CodexNativeSubagentCompletionDetails & {
|
|
childThreadId: string;
|
|
};
|
|
|
|
/** Completion parsed from a notification payload before agent-path matching resolves the thread. */
|
|
export type CodexNativeSubagentNotificationCompletion = CodexNativeSubagentCompletionDetails & {
|
|
agentPath: string;
|
|
};
|
|
|
|
/** Extracts trusted subagent completion payloads from a Codex server notification. */
|
|
export function extractCodexNativeSubagentCompletions(
|
|
notification: CodexServerNotification,
|
|
): CodexNativeSubagentNotificationCompletion[] {
|
|
const params = isJsonObject(notification.params) ? notification.params : undefined;
|
|
if (!params) {
|
|
return [];
|
|
}
|
|
const item = isJsonObject(params.item) ? params.item : undefined;
|
|
if (!item) {
|
|
return [];
|
|
}
|
|
const text = readTrustedInterAgentCommunicationContent(item);
|
|
if (!text) {
|
|
return [];
|
|
}
|
|
const author = readTrustedInterAgentCommunicationAuthor(item);
|
|
return extractCodexNativeSubagentCompletionsFromText(text).filter(
|
|
(completion) => completion.agentPath === author,
|
|
);
|
|
}
|
|
|
|
/** Parses one or more tagged subagent completion payloads from commentary text. */
|
|
export function extractCodexNativeSubagentCompletionsFromText(
|
|
text: string,
|
|
): CodexNativeSubagentNotificationCompletion[] {
|
|
const completions: CodexNativeSubagentNotificationCompletion[] = [];
|
|
let cursor = 0;
|
|
while (cursor < text.length) {
|
|
const start = text.indexOf(CODEX_SUBAGENT_NOTIFICATION_START, cursor);
|
|
if (start < 0) {
|
|
break;
|
|
}
|
|
const bodyStart = start + CODEX_SUBAGENT_NOTIFICATION_START.length;
|
|
const end = text.indexOf(CODEX_SUBAGENT_NOTIFICATION_END, bodyStart);
|
|
if (end < 0) {
|
|
break;
|
|
}
|
|
const parsed = parseCodexNativeSubagentNotificationBody(text.slice(bodyStart, end));
|
|
if (parsed) {
|
|
completions.push(parsed);
|
|
}
|
|
cursor = end + CODEX_SUBAGENT_NOTIFICATION_END.length;
|
|
}
|
|
return completions;
|
|
}
|
|
|
|
function parseCodexNativeSubagentNotificationBody(
|
|
body: string,
|
|
): CodexNativeSubagentNotificationCompletion | undefined {
|
|
let payload: JsonValue;
|
|
try {
|
|
payload = JSON.parse(body.trim());
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
if (!isJsonObject(payload)) {
|
|
return undefined;
|
|
}
|
|
const agentPath = readString(payload, "agent_path")?.trim();
|
|
const status = isJsonObject(payload.status) ? payload.status : undefined;
|
|
if (!agentPath || !status) {
|
|
return undefined;
|
|
}
|
|
const statusEntry = readCompletionStatus(status);
|
|
if (!statusEntry) {
|
|
return undefined;
|
|
}
|
|
return {
|
|
agentPath,
|
|
status: statusEntry.status,
|
|
statusLabel: statusEntry.label,
|
|
result: statusEntry.result,
|
|
};
|
|
}
|
|
|
|
function readCompletionStatus(status: JsonObject):
|
|
| {
|
|
status: CodexNativeSubagentCompletionStatus;
|
|
label: string;
|
|
result: string;
|
|
}
|
|
| undefined {
|
|
for (const [rawKey, value] of Object.entries(status)) {
|
|
const normalized = normalizeStatusKey(rawKey);
|
|
const mappedStatus = mapCompletionStatus(normalized);
|
|
if (!mappedStatus) {
|
|
continue;
|
|
}
|
|
const result = stringifyResult(value, mappedStatus);
|
|
const noFinalAssistantMessage =
|
|
mappedStatus === "succeeded" && result.kind === "no_final_assistant_message";
|
|
return {
|
|
status: mappedStatus,
|
|
label: noFinalAssistantMessage ? "completed_without_final_message" : rawKey,
|
|
result: result.text,
|
|
};
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function mapCompletionStatus(value: string): CodexNativeSubagentCompletionStatus | undefined {
|
|
if (value === "completed" || value === "succeeded" || value === "success") {
|
|
return "succeeded";
|
|
}
|
|
if (
|
|
value === "cancelled" ||
|
|
value === "canceled" ||
|
|
value === "interrupted" ||
|
|
value === "shutdown"
|
|
) {
|
|
return "cancelled";
|
|
}
|
|
if (
|
|
value === "failed" ||
|
|
value === "error" ||
|
|
value === "errored" ||
|
|
value === "systemerror" ||
|
|
value === "notfound"
|
|
) {
|
|
return "failed";
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function stringifyResult(
|
|
value: JsonValue | undefined,
|
|
status: CodexNativeSubagentCompletionStatus,
|
|
): {
|
|
text: string;
|
|
kind?: "no_final_assistant_message";
|
|
} {
|
|
if (typeof value === "string") {
|
|
const text = value.trim();
|
|
if (text) {
|
|
return { text };
|
|
}
|
|
return status === "succeeded"
|
|
? completedWithoutFinalAssistantMessage()
|
|
: { text: "(no output)" };
|
|
}
|
|
if (value === null || value === undefined) {
|
|
return status === "succeeded"
|
|
? completedWithoutFinalAssistantMessage()
|
|
: { text: "(no output)" };
|
|
}
|
|
try {
|
|
return { text: JSON.stringify(value) };
|
|
} catch {
|
|
return { text: "(unserializable output)" };
|
|
}
|
|
}
|
|
|
|
function completedWithoutFinalAssistantMessage(): {
|
|
text: string;
|
|
kind: "no_final_assistant_message";
|
|
} {
|
|
return {
|
|
text: "Codex native subagent completed without a final assistant message.",
|
|
kind: "no_final_assistant_message",
|
|
};
|
|
}
|
|
|
|
function readTrustedInterAgentCommunicationContent(item: JsonObject): string | undefined {
|
|
const communication = readTrustedInterAgentCommunication(item);
|
|
return typeof communication?.content === "string" ? communication.content : undefined;
|
|
}
|
|
|
|
function readTrustedInterAgentCommunicationAuthor(item: JsonObject): string | undefined {
|
|
const communication = readTrustedInterAgentCommunication(item);
|
|
return typeof communication?.author === "string" ? communication.author : undefined;
|
|
}
|
|
|
|
function readTrustedInterAgentCommunication(item: JsonObject): JsonObject | undefined {
|
|
if (
|
|
readString(item, "type") !== "message" ||
|
|
readString(item, "role") !== "assistant" ||
|
|
readString(item, "phase") !== "commentary"
|
|
) {
|
|
return undefined;
|
|
}
|
|
const text = extractSingleTextPart(item);
|
|
if (!text) {
|
|
return undefined;
|
|
}
|
|
let parsed: JsonValue;
|
|
try {
|
|
parsed = JSON.parse(text);
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
if (!isJsonObject(parsed)) {
|
|
return undefined;
|
|
}
|
|
if (
|
|
typeof parsed.author !== "string" ||
|
|
typeof parsed.recipient !== "string" ||
|
|
typeof parsed.content !== "string" ||
|
|
parsed.trigger_turn !== false
|
|
) {
|
|
return undefined;
|
|
}
|
|
return parsed;
|
|
}
|
|
|
|
function extractSingleTextPart(item: JsonObject): string | undefined {
|
|
const content = item.content;
|
|
if (!Array.isArray(content) || content.length !== 1) {
|
|
return undefined;
|
|
}
|
|
const [entry] = content;
|
|
if (!isJsonObject(entry)) {
|
|
return undefined;
|
|
}
|
|
const type = readString(entry, "type");
|
|
if (type !== "output_text" && type !== "text") {
|
|
return undefined;
|
|
}
|
|
return readString(entry, "text")?.trim();
|
|
}
|
|
|
|
function readString(record: JsonObject, key: string): string | undefined {
|
|
const value = record[key];
|
|
return typeof value === "string" ? value : undefined;
|
|
}
|
|
|
|
function normalizeStatusKey(value: string): string {
|
|
return value.replace(/[^a-z0-9]/giu, "").toLowerCase();
|
|
}
|