fix(codex): deliver native subagent completions

Deliver Codex-native subagent completions through the generic plugin harness task runtime.

Proof:
- Autoreview clean on final branch.
- Testbox changed gate: tbx_01ks80eqs7d2e3jq3p99zbm4wd, pnpm check:changed, exit 0.
- Live Codex harness: tbx_01ks80p4ky32sqv2ksan2p0w0q, codex/gpt-5.5 API-key auth, native parent/child bridge tokens observed, exit 0.

Co-authored-by: bryanpearson <bryanmpearson@gmail.com>
This commit is contained in:
Bryan P
2026-05-22 07:28:46 -07:00
committed by GitHub
parent cff5244a5b
commit f9d35dc681
31 changed files with 3440 additions and 181 deletions

View File

@@ -72,6 +72,7 @@ Docs: https://docs.openclaw.ai
- Agents/code mode: return structured timeout and runtime-unavailable error codes for known worker failures. Fixes #83389. (#83444) Thanks @Kaspre.
- QA-Lab: isolate multi-scenario suite workers when scenarios need startup config patches, preventing message-routing config from leaking into unrelated scenarios.
- QA-Lab: make the commitments heartbeat-target-none scenario request an immediate heartbeat instead of waiting for the next scheduled heartbeat.
- Codex/Plugin SDK: deliver Codex-native subagent completions through a generic harness task runtime so harness-backed plugins can mirror durable task lifecycle and completion delivery without Codex-specific SDK imports. (#83445) Thanks @bryanpearson.
- Gateway CLI: surface local post-challenge connect assembly failures immediately instead of waiting for the wrapper timeout. Fixes #68944. (#85253) Thanks @samzong.
- Messages: strip unsupported web-search citation control markers from outbound replies before they reach WebChat or external channels. Fixes #85193. (#85204) Thanks @neeravmakwana.
- Agents/exec: treat denied exec approvals as terminal instead of feeding them back into agent follow-up work, and recognize Chinese stop phrases in abort handling. Fixes #69386. (#85194) Thanks @samzong.

View File

@@ -44,8 +44,8 @@ but new code should not add imports from them: `agent-runtime-test-contracts`,
### Reserved bundled plugin helper subpaths
These subpaths are plugin-owned compatibility surfaces reserved for their owning
bundled plugin, not general SDK APIs: `plugin-sdk/codex-mcp-projection` and
These subpaths are plugin-owned compatibility surfaces for their owning bundled
plugin, not general SDK APIs: `plugin-sdk/codex-mcp-projection` and
`plugin-sdk/codex-native-task-runtime`. Cross-owner extension imports are blocked
by package contract guardrails.
@@ -224,8 +224,9 @@ focused channel/runtime subpaths, `config-contracts`, `string-coerce-runtime`,
| `plugin-sdk/runtime` | Broad runtime/logging/backup/plugin-install helpers |
| `plugin-sdk/runtime-env` | Narrow runtime env, logger, timeout, retry, and backoff helpers |
| `plugin-sdk/browser-config` | Supported browser config facade for normalized profile/defaults, CDP URL parsing, and browser-control auth helpers |
| `plugin-sdk/agent-harness-task-runtime` | Generic task lifecycle and completion delivery helpers for harness-backed agents using a host-issued task scope |
| `plugin-sdk/codex-mcp-projection` | Reserved bundled Codex helper for projecting user MCP server config into Codex thread config; not for third-party plugins |
| `plugin-sdk/codex-native-task-runtime` | Reserved bundled Codex helper for native task mirror/runtime wiring; not for third-party plugins |
| `plugin-sdk/codex-native-task-runtime` | Private bundled Codex helper for native task mirror/runtime wiring; not for third-party plugins |
| `plugin-sdk/channel-runtime-context` | Generic channel runtime-context registration and lookup helpers |
| `plugin-sdk/matrix` | Deprecated Matrix compatibility facade for older third-party channel packages; new plugins should import `plugin-sdk/run-command` directly |
| `plugin-sdk/mattermost` | Deprecated Mattermost compatibility facade for older third-party channel packages; new plugins should import generic SDK subpaths directly |

View File

@@ -20,7 +20,6 @@ import {
type CodexAppServerEventProjectorOptions,
type CodexAppServerToolTelemetry,
} from "./event-projector.js";
import { CodexNativeSubagentTaskMirror } from "./native-subagent-task-mirror.js";
import { rememberCodexRateLimits, resetCodexRateLimitCacheForTests } from "./rate-limit-cache.js";
import { createCodexTestModel } from "./test-support.js";
@@ -996,36 +995,6 @@ describe("CodexAppServerEventProjector", () => {
expect(result.assistantTexts).toStrictEqual([]);
});
it("mirrors native subagent notifications before current-turn filtering", async () => {
const projector = await createProjector({
...(await createParams()),
sessionKey: "agent:main:main",
} as EmbeddedRunAttemptParams);
const mirrorSpy = vi.spyOn(CodexNativeSubagentTaskMirror.prototype, "handleNotification");
const notification = {
method: "item/completed",
params: {
threadId: THREAD_ID,
turnId: "child-turn",
item: {
type: "collabAgentToolCall",
tool: "spawnAgent",
senderThreadId: THREAD_ID,
receiverThreadIds: ["child-thread"],
agentsStates: {
"child-thread": { status: "completed", message: "done" },
},
},
},
} as ProjectorNotification;
await projector.handleNotification(notification);
expect(mirrorSpy).toHaveBeenCalledWith(notification);
const result = projector.buildResult(buildEmptyToolTelemetry());
expect(result.assistantTexts).toEqual([]);
});
it("ignores notifications that omit top-level thread and turn ids", async () => {
const projector = await createProjector();

View File

@@ -22,7 +22,6 @@ import {
} from "openclaw/plugin-sdk/agent-harness-runtime";
import { emitTrustedDiagnosticEvent } from "openclaw/plugin-sdk/diagnostic-runtime";
import { resolveCodexLocalRuntimeAttribution } from "./local-runtime-attribution.js";
import { CodexNativeSubagentTaskMirror } from "./native-subagent-task-mirror.js";
import {
readCodexNotificationThreadId,
readCodexNotificationTurnId,
@@ -169,34 +168,19 @@ export class CodexAppServerEventProjector {
private guardianReviewCount = 0;
private completedCompactionCount = 0;
private latestRateLimits: JsonValue | undefined;
private readonly nativeSubagentTaskMirror: CodexNativeSubagentTaskMirror;
constructor(
private readonly params: EmbeddedRunAttemptParams,
private readonly threadId: string,
private readonly turnId: string,
private readonly options: CodexAppServerEventProjectorOptions = {},
) {
this.nativeSubagentTaskMirror = new CodexNativeSubagentTaskMirror({
parentThreadId: threadId,
requesterSessionKey: params.sessionKey,
agentId: params.agentId,
});
}
) {}
async handleNotification(notification: CodexServerNotification): Promise<void> {
const params = isJsonObject(notification.params) ? notification.params : undefined;
if (!params) {
return;
}
try {
this.nativeSubagentTaskMirror.handleNotification(notification);
} catch (error) {
embeddedAgentLog.warn("Failed to mirror Codex native subagent lifecycle event", {
method: notification.method,
error: formatErrorMessage(error),
});
}
if (notification.method === "account/rateLimits/updated") {
this.latestRateLimits = params;
rememberCodexRateLimits(params);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,176 @@
import { describe, expect, it } from "vitest";
import {
extractCodexNativeSubagentCompletions,
extractCodexNativeSubagentCompletionsFromText,
} from "./native-subagent-notification.js";
function trustedInterAgentNotification(params: {
agentPath: string;
text: string;
threadId?: string;
}) {
return {
method: "rawResponseItem/completed",
params: {
threadId: params.threadId ?? "parent-thread",
item: {
type: "message",
role: "assistant",
phase: "commentary",
content: [
{
type: "output_text",
text: JSON.stringify({
author: params.agentPath,
recipient: "/root",
other_recipients: [],
content: params.text,
trigger_turn: false,
}),
},
],
},
},
};
}
describe("Codex native subagent notifications", () => {
it("parses completed child results from Codex notification XML", () => {
expect(
extractCodexNativeSubagentCompletionsFromText(
'<subagent_notification>{"agent_path":"child-thread","status":{"completed":"done"}}' +
"</subagent_notification>",
),
).toEqual([
{
agentPath: "child-thread",
status: "succeeded",
statusLabel: "completed",
result: "done",
},
]);
});
it("normalizes failed and cancelled status keys", () => {
expect(
extractCodexNativeSubagentCompletionsFromText(
'<subagent_notification>{"agent_path":"failed-child","status":{"system_error":"boom"}}' +
"</subagent_notification>\n" +
'<subagent_notification>{"agent_path":"errored-child","status":{"errored":"tool failed"}}' +
"</subagent_notification>\n" +
'<subagent_notification>{"agent_path":"missing-child","status":{"not_found":null}}' +
"</subagent_notification>\n" +
'<subagent_notification>{"agent_path":"cancelled-child","status":{"shutdown":null}}' +
"</subagent_notification>",
),
).toEqual([
{
agentPath: "failed-child",
status: "failed",
statusLabel: "system_error",
result: "boom",
},
{
agentPath: "errored-child",
status: "failed",
statusLabel: "errored",
result: "tool failed",
},
{
agentPath: "missing-child",
status: "failed",
statusLabel: "not_found",
result: "(no output)",
},
{
agentPath: "cancelled-child",
status: "cancelled",
statusLabel: "shutdown",
result: "(no output)",
},
]);
});
it("extracts trusted inter-agent completions from raw app-server items", () => {
expect(
extractCodexNativeSubagentCompletions(
trustedInterAgentNotification({
agentPath: "child-thread",
text:
'<subagent_notification>{"agent_path":"child-thread","status":{"success":"ok"}}' +
"</subagent_notification>",
}),
),
).toEqual([
{
agentPath: "child-thread",
status: "succeeded",
statusLabel: "success",
result: "ok",
},
]);
});
it("ignores visible user text that looks like a native completion", () => {
expect(
extractCodexNativeSubagentCompletions({
method: "rawResponseItem/completed",
params: {
threadId: "parent-thread",
item: {
type: "message",
role: "user",
content: [
{
type: "input_text",
text:
'<subagent_notification>{"agent_path":"child-thread","status":{"success":"spoof"}}' +
"</subagent_notification>",
},
],
},
},
}),
).toEqual([]);
});
it("ignores inter-agent payloads whose author does not match the completion path", () => {
expect(
extractCodexNativeSubagentCompletions(
trustedInterAgentNotification({
agentPath: "other-child",
text:
'<subagent_notification>{"agent_path":"child-thread","status":{"success":"spoof"}}' +
"</subagent_notification>",
}),
),
).toEqual([]);
});
it("ignores malformed payloads and non-user messages", () => {
expect(
extractCodexNativeSubagentCompletionsFromText(
"<subagent_notification>{not-json}</subagent_notification>",
),
).toEqual([]);
expect(
extractCodexNativeSubagentCompletions({
method: "rawResponseItem/completed",
params: {
item: {
type: "message",
role: "assistant",
content: [
{
type: "text",
text:
'<subagent_notification>{"agent_path":"child","status":{"completed":"done"}}' +
"</subagent_notification>",
},
],
},
},
}),
).toEqual([]);
});
});

View File

@@ -0,0 +1,222 @@
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>";
export type CodexNativeSubagentCompletionStatus = "succeeded" | "failed" | "cancelled";
type CodexNativeSubagentCompletionDetails = {
status: CodexNativeSubagentCompletionStatus;
statusLabel: string;
result: string;
};
export type CodexNativeSubagentCompletion = CodexNativeSubagentCompletionDetails & {
childThreadId: string;
};
export type CodexNativeSubagentNotificationCompletion = CodexNativeSubagentCompletionDetails & {
agentPath: string;
};
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,
);
}
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;
}
return {
status: mappedStatus,
label: rawKey,
result: stringifyResult(value),
};
}
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): string {
if (typeof value === "string") {
return value.trim() || "(no output)";
}
if (value === null || value === undefined) {
return "(no output)";
}
try {
return JSON.stringify(value);
} catch {
return "(unserializable output)";
}
}
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();
}

View File

@@ -0,0 +1,3 @@
export const CODEX_NATIVE_SUBAGENT_RUNTIME = "subagent";
export const CODEX_NATIVE_SUBAGENT_TASK_KIND = "codex-native";
export const CODEX_NATIVE_SUBAGENT_RUN_ID_PREFIX = "codex-thread:";

View File

@@ -50,12 +50,7 @@ describe("CodexNativeSubagentTaskMirror", () => {
});
expect(runtime.createRunningTaskRun).toHaveBeenCalledWith({
runtime: "subagent",
taskKind: "codex-native",
sourceId: "codex-thread:child-thread",
requesterSessionKey: "agent:main:main",
ownerKey: "agent:main:main",
scopeKind: "session",
agentId: "main",
runId: "codex-thread:child-thread",
label: "Poincare",
@@ -72,7 +67,6 @@ describe("CodexNativeSubagentTaskMirror", () => {
);
expect(runtime.recordTaskRunProgressByRunId).toHaveBeenCalledWith({
runId: "codex-thread:child-thread",
runtime: "subagent",
lastEventAt: 20_000,
progressSummary: "Codex native subagent is active.",
});
@@ -170,7 +164,6 @@ describe("CodexNativeSubagentTaskMirror", () => {
expect(runtime.finalizeTaskRunByRunId).toHaveBeenNthCalledWith(1, {
runId: codexNativeSubagentRunId("child-thread"),
runtime: "subagent",
status: "succeeded",
endedAt: 30_000,
lastEventAt: 30_000,
@@ -179,7 +172,6 @@ describe("CodexNativeSubagentTaskMirror", () => {
});
expect(runtime.finalizeTaskRunByRunId).toHaveBeenNthCalledWith(2, {
runId: codexNativeSubagentRunId("failed-child"),
runtime: "subagent",
status: "failed",
endedAt: 30_000,
lastEventAt: 30_000,
@@ -237,12 +229,7 @@ describe("CodexNativeSubagentTaskMirror", () => {
});
expect(runtime.createRunningTaskRun).toHaveBeenCalledWith({
runtime: "subagent",
taskKind: "codex-native",
sourceId: "codex-thread:child-thread",
requesterSessionKey: "agent:main:main",
ownerKey: "agent:main:main",
scopeKind: "session",
runId: "codex-thread:child-thread",
label: "Codex subagent",
task: "write the proof file",
@@ -258,13 +245,11 @@ describe("CodexNativeSubagentTaskMirror", () => {
);
expect(runtime.recordTaskRunProgressByRunId).toHaveBeenCalledWith({
runId: "codex-thread:child-thread",
runtime: "subagent",
lastEventAt: 40_000,
progressSummary: "Codex native subagent is initializing.",
});
expect(runtime.finalizeTaskRunByRunId).toHaveBeenCalledWith({
runId: "codex-thread:child-thread",
runtime: "subagent",
status: "succeeded",
endedAt: 40_000,
lastEventAt: 40_000,
@@ -273,6 +258,82 @@ describe("CodexNativeSubagentTaskMirror", () => {
});
});
it("uses the notification thread id when collab agent items omit sender thread id", () => {
const runtime = createRuntime();
const mirror = new CodexNativeSubagentTaskMirror(
{
parentThreadId: "parent-thread",
requesterSessionKey: "agent:main:main",
now: () => 42_000,
},
runtime,
);
mirror.handleNotification({
method: "item/started",
params: {
threadId: "parent-thread",
item: {
type: "collabAgentToolCall",
tool: "spawn_agent",
receiverThreadIds: ["child-thread"],
prompt: "inspect one thing",
},
},
});
expect(runtime.createRunningTaskRun).toHaveBeenCalledWith(
expect.objectContaining({
runId: "codex-thread:child-thread",
task: "inspect one thing",
}),
);
});
it("creates spawn tasks from collab agent states when receiver thread ids are absent", () => {
const runtime = createRuntime();
const mirror = new CodexNativeSubagentTaskMirror(
{
parentThreadId: "parent-thread",
requesterSessionKey: "agent:main:main",
now: () => 43_000,
},
runtime,
);
mirror.handleNotification({
method: "item/completed",
params: {
threadId: "parent-thread",
item: {
type: "collabAgentToolCall",
tool: "spawn_agent",
prompt: "inspect one thing",
agentsStates: {
"child-thread": {
status: "completed",
message: "done",
},
},
},
},
});
expect(runtime.createRunningTaskRun).toHaveBeenCalledWith(
expect.objectContaining({
runId: "codex-thread:child-thread",
task: "inspect one thing",
}),
);
expect(runtime.finalizeTaskRunByRunId).toHaveBeenCalledWith(
expect.objectContaining({
runId: "codex-thread:child-thread",
status: "succeeded",
terminalSummary: "done",
}),
);
});
it("finalizes stale collab agent state from the blocked tool call status", () => {
const runtime = createRuntime();
const mirror = new CodexNativeSubagentTaskMirror(
@@ -306,13 +367,11 @@ describe("CodexNativeSubagentTaskMirror", () => {
expect(runtime.recordTaskRunProgressByRunId).not.toHaveBeenCalledWith({
runId: "codex-thread:child-thread",
runtime: "subagent",
lastEventAt: 45_000,
progressSummary: "Native hook relay unavailable",
});
expect(runtime.finalizeTaskRunByRunId).toHaveBeenCalledWith({
runId: "codex-thread:child-thread",
runtime: "subagent",
status: "succeeded",
endedAt: 45_000,
lastEventAt: 45_000,
@@ -355,7 +414,6 @@ describe("CodexNativeSubagentTaskMirror", () => {
expect(runtime.recordTaskRunProgressByRunId).toHaveBeenCalledWith({
runId: "codex-thread:child-thread",
runtime: "subagent",
lastEventAt: 46_000,
progressSummary: "Codex native subagent is initializing.",
});
@@ -394,7 +452,6 @@ describe("CodexNativeSubagentTaskMirror", () => {
expect(runtime.recordTaskRunProgressByRunId).toHaveBeenCalledWith({
runId: "codex-thread:child-thread",
runtime: "subagent",
lastEventAt: 47_000,
progressSummary: "wait timed out",
});
@@ -441,7 +498,6 @@ describe("CodexNativeSubagentTaskMirror", () => {
expect(runtime.finalizeTaskRunByRunId).toHaveBeenCalledTimes(1);
expect(runtime.finalizeTaskRunByRunId).toHaveBeenCalledWith({
runId: "codex-thread:child-thread",
runtime: "subagent",
status: "succeeded",
endedAt: 50_000,
lastEventAt: 50_000,
@@ -490,7 +546,6 @@ describe("CodexNativeSubagentTaskMirror", () => {
expect(runtime.finalizeTaskRunByRunId).toHaveBeenNthCalledWith(1, {
runId: "codex-thread:child-thread",
runtime: "subagent",
status: "succeeded",
endedAt: 55_000,
lastEventAt: 55_000,
@@ -499,7 +554,6 @@ describe("CodexNativeSubagentTaskMirror", () => {
});
expect(runtime.finalizeTaskRunByRunId).toHaveBeenNthCalledWith(2, {
runId: "codex-thread:child-thread",
runtime: "subagent",
status: "failed",
endedAt: 55_000,
lastEventAt: 55_000,
@@ -556,13 +610,11 @@ describe("CodexNativeSubagentTaskMirror", () => {
expect(runtime.recordTaskRunProgressByRunId).toHaveBeenCalledWith({
runId: "codex-thread:child-thread",
runtime: "subagent",
lastEventAt: 60_000,
progressSummary: "Codex native subagent is initializing.",
});
expect(runtime.finalizeTaskRunByRunId).toHaveBeenCalledWith({
runId: "codex-thread:child-thread",
runtime: "subagent",
status: "succeeded",
endedAt: 60_000,
lastEventAt: 60_000,

View File

@@ -1,11 +1,5 @@
import {
CODEX_NATIVE_SUBAGENT_RUN_ID_PREFIX,
CODEX_NATIVE_SUBAGENT_RUNTIME,
CODEX_NATIVE_SUBAGENT_TASK_KIND,
createRunningTaskRun,
finalizeTaskRunByRunId,
recordTaskRunProgressByRunId,
} from "openclaw/plugin-sdk/codex-native-task-runtime";
import type { AgentHarnessTaskRuntime } from "openclaw/plugin-sdk/agent-harness-task-runtime";
import { CODEX_NATIVE_SUBAGENT_RUN_ID_PREFIX } from "./native-subagent-task-ids.js";
import type {
CodexServerNotification,
CodexSessionSource,
@@ -19,11 +13,10 @@ import type {
} from "./protocol.js";
import { isJsonObject } from "./protocol.js";
export type TaskLifecycleRuntime = {
createRunningTaskRun: typeof createRunningTaskRun;
recordTaskRunProgressByRunId: typeof recordTaskRunProgressByRunId;
finalizeTaskRunByRunId: typeof finalizeTaskRunByRunId;
};
export type TaskLifecycleRuntime = Pick<
AgentHarnessTaskRuntime,
"createRunningTaskRun" | "recordTaskRunProgressByRunId" | "finalizeTaskRunByRunId"
>;
export type CodexNativeSubagentTaskMirrorParams = {
parentThreadId: string;
@@ -32,12 +25,6 @@ export type CodexNativeSubagentTaskMirrorParams = {
now?: () => number;
};
const defaultRuntime: TaskLifecycleRuntime = {
createRunningTaskRun,
recordTaskRunProgressByRunId,
finalizeTaskRunByRunId,
};
export class CodexNativeSubagentTaskMirror {
private readonly mirroredThreadIds = new Set<string>();
private readonly terminalRunIds = new Set<string>();
@@ -45,7 +32,7 @@ export class CodexNativeSubagentTaskMirror {
constructor(
private readonly params: CodexNativeSubagentTaskMirrorParams,
private readonly runtime: TaskLifecycleRuntime = defaultRuntime,
private readonly runtime: TaskLifecycleRuntime,
) {
this.now = params.now ?? Date.now;
}
@@ -95,16 +82,7 @@ export class CodexNativeSubagentTaskMirror {
`Codex native subagent${label === "Codex subagent" ? "" : ` ${label}`}`;
const createdAt = secondsToMillis(thread.createdAt) ?? this.now();
this.runtime.createRunningTaskRun({
runtime: CODEX_NATIVE_SUBAGENT_RUNTIME,
taskKind: CODEX_NATIVE_SUBAGENT_TASK_KIND,
sourceId: runId,
requesterSessionKey: this.params.requesterSessionKey,
...(this.params.requesterSessionKey
? {
ownerKey: this.params.requesterSessionKey,
scopeKind: "session" as const,
}
: {}),
agentId: this.params.agentId,
runId,
label,
@@ -140,7 +118,6 @@ export class CodexNativeSubagentTaskMirror {
if (statusType === "active") {
this.runtime.recordTaskRunProgressByRunId({
runId,
runtime: CODEX_NATIVE_SUBAGENT_RUNTIME,
lastEventAt: eventAt,
progressSummary: "Codex native subagent is active.",
});
@@ -150,7 +127,6 @@ export class CodexNativeSubagentTaskMirror {
this.terminalRunIds.add(runId);
this.runtime.finalizeTaskRunByRunId({
runId,
runtime: CODEX_NATIVE_SUBAGENT_RUNTIME,
status: "succeeded",
endedAt: eventAt,
lastEventAt: eventAt,
@@ -163,7 +139,6 @@ export class CodexNativeSubagentTaskMirror {
this.terminalRunIds.add(runId);
this.runtime.finalizeTaskRunByRunId({
runId,
runtime: CODEX_NATIVE_SUBAGENT_RUNTIME,
status: "failed",
endedAt: eventAt,
lastEventAt: eventAt,
@@ -176,7 +151,6 @@ export class CodexNativeSubagentTaskMirror {
if (statusType === "notLoaded") {
this.runtime.recordTaskRunProgressByRunId({
runId,
runtime: CODEX_NATIVE_SUBAGENT_RUNTIME,
lastEventAt: eventAt,
progressSummary: "Codex native subagent is not loaded.",
});
@@ -188,21 +162,23 @@ export class CodexNativeSubagentTaskMirror {
if (!item || readString(item, "type") !== "collabAgentToolCall") {
return;
}
if (readString(item, "senderThreadId") !== this.params.parentThreadId) {
const senderThreadId = readString(item, "senderThreadId") ?? readString(params, "threadId");
if (senderThreadId !== this.params.parentThreadId) {
return;
}
const receiverThreadIds = readStringArray(item.receiverThreadIds);
const isSpawnAgentTool = normalizeToolName(readString(item, "tool")) === "spawnagent";
const receiverThreadIds = readStringArray(item.receiverThreadIds);
const agentsStates = readAgentsStates(item.agentsStates);
const spawnChildThreadIds = new Set([...receiverThreadIds, ...agentsStates.keys()]);
if (isSpawnAgentTool) {
for (const receiverThreadId of receiverThreadIds) {
this.createTaskFromCollabSpawnItem(receiverThreadId, item);
for (const childThreadId of spawnChildThreadIds) {
this.createTaskFromCollabSpawnItem(childThreadId, item);
}
}
const agentsStates = readAgentsStates(item.agentsStates);
const toolCallStatus = normalizeCollabToolCallStatus(readString(item, "status"));
const terminalToolCallThreadIds = new Set<string>();
if (isSpawnAgentTool && isBlockedOrFailedCollabToolCallStatus(toolCallStatus)) {
for (const threadId of receiverThreadIds) {
for (const threadId of spawnChildThreadIds) {
terminalToolCallThreadIds.add(threadId);
}
for (const threadId of agentsStates.keys()) {
@@ -244,16 +220,7 @@ export class CodexNativeSubagentTaskMirror {
const runId = codexNativeSubagentRunId(normalizedThreadId);
const createdAt = this.now();
this.runtime.createRunningTaskRun({
runtime: CODEX_NATIVE_SUBAGENT_RUNTIME,
taskKind: CODEX_NATIVE_SUBAGENT_TASK_KIND,
sourceId: runId,
requesterSessionKey: this.params.requesterSessionKey,
...(this.params.requesterSessionKey
? {
ownerKey: this.params.requesterSessionKey,
scopeKind: "session" as const,
}
: {}),
agentId: this.params.agentId,
runId,
label: "Codex subagent",
@@ -284,7 +251,6 @@ export class CodexNativeSubagentTaskMirror {
if (normalizedStatus === "pendingInit" || normalizedStatus === "running") {
this.runtime.recordTaskRunProgressByRunId({
runId,
runtime: CODEX_NATIVE_SUBAGENT_RUNTIME,
lastEventAt: eventAt,
progressSummary:
trimOptional(message) ??
@@ -298,7 +264,6 @@ export class CodexNativeSubagentTaskMirror {
this.terminalRunIds.add(runId);
this.runtime.finalizeTaskRunByRunId({
runId,
runtime: CODEX_NATIVE_SUBAGENT_RUNTIME,
status: "succeeded",
endedAt: eventAt,
lastEventAt: eventAt,
@@ -311,7 +276,6 @@ export class CodexNativeSubagentTaskMirror {
this.terminalRunIds.add(runId);
this.runtime.finalizeTaskRunByRunId({
runId,
runtime: CODEX_NATIVE_SUBAGENT_RUNTIME,
status: "succeeded",
endedAt: eventAt,
lastEventAt: eventAt,
@@ -324,7 +288,6 @@ export class CodexNativeSubagentTaskMirror {
this.terminalRunIds.add(runId);
this.runtime.finalizeTaskRunByRunId({
runId,
runtime: CODEX_NATIVE_SUBAGENT_RUNTIME,
status:
normalizedStatus === "interrupted" || normalizedStatus === "shutdown"
? "cancelled"

View File

@@ -116,6 +116,7 @@ import {
buildCodexNativeHookRelayConfig,
CODEX_NATIVE_HOOK_RELAY_EVENTS,
} from "./native-hook-relay.js";
import { registerCodexNativeSubagentMonitor } from "./native-subagent-monitor.js";
import {
describeCodexNotificationCorrelation,
isCodexNotificationForTurn,
@@ -2172,6 +2173,14 @@ export async function runCodexAppServerAttempt(
return notificationQueue;
};
registerCodexNativeSubagentMonitor({
client,
parentThreadId: thread.threadId,
requesterSessionKey: params.sessionKey,
taskRuntimeScope: params.agentHarnessTaskRuntimeScope,
agentId: params.agentId,
codexHome: appServer.start.env?.CODEX_HOME ?? resolveCodexAppServerHomeDir(agentDir),
});
const notificationCleanup = client.addNotificationHandler(enqueueNotification);
const requestCleanup = client.addRequestHandler(async (request) => {
let armCompletionWatchOnResponse = false;

View File

@@ -509,9 +509,9 @@
"types": "./dist/plugin-sdk/codex-mcp-projection.d.ts",
"default": "./dist/plugin-sdk/codex-mcp-projection.js"
},
"./plugin-sdk/codex-native-task-runtime": {
"types": "./dist/plugin-sdk/codex-native-task-runtime.d.ts",
"default": "./dist/plugin-sdk/codex-native-task-runtime.js"
"./plugin-sdk/agent-harness-task-runtime": {
"types": "./dist/plugin-sdk/agent-harness-task-runtime.d.ts",
"default": "./dist/plugin-sdk/agent-harness-task-runtime.js"
},
"./plugin-sdk/agent-harness": {
"types": "./dist/plugin-sdk/agent-harness.d.ts",

View File

@@ -103,6 +103,7 @@
"cli-backend",
"codex-mcp-projection",
"codex-native-task-runtime",
"agent-harness-task-runtime",
"agent-harness",
"agent-harness-runtime",
"hook-runtime",

View File

@@ -1,4 +1,5 @@
[
"codex-native-task-runtime",
"qa-channel",
"qa-channel-protocol",
"qa-lab",

View File

@@ -18,6 +18,7 @@ import { resolveProviderAuthProfileId } from "../../plugins/provider-runtime.js"
import { enqueueCommandInLane } from "../../process/command-queue.js";
import type { CommandQueueEnqueueOptions } from "../../process/command-queue.types.js";
import { normalizeOptionalString } from "../../shared/string-coerce.js";
import { createAgentHarnessTaskRuntimeScope } from "../../tasks/agent-harness-task-runtime-scope.js";
import { sanitizeForLog } from "../../terminal/ansi.js";
import { resolveUserPath } from "../../utils.js";
import { isMarkdownCapableMessageChannel } from "../../utils/message-channel.js";
@@ -1438,6 +1439,13 @@ export async function runEmbeddedPiAgent(
// attempt too. Otherwise plugin-owned transports can skip PI auth
// bootstrap but drift back to PI when the attempt is created.
agentHarnessId: agentHarness.id,
...(params.sessionKey
? {
agentHarnessTaskRuntimeScope: createAgentHarnessTaskRuntimeScope({
requesterSessionKey: params.sessionKey,
}),
}
: {}),
runtimePlan,
model: applyAuthHeaderOverride(
applyLocalNoAuthHeaderOverride(effectiveModel, apiKeyInfo),

View File

@@ -7,6 +7,7 @@ import type { SessionSystemPromptReport } from "../../../config/sessions/types.j
import type { ContextEngine, ContextEnginePromptCacheInfo } from "../../../context-engine/types.js";
import type { DiagnosticTraceContext } from "../../../infra/diagnostic-trace-context.js";
import type { PluginHookBeforeAgentStartResult } from "../../../plugins/hook-before-agent-start.types.js";
import type { AgentHarnessTaskRuntimeScope } from "../../../tasks/agent-harness-task-runtime-scope.js";
import type { AcceptedSessionSpawn } from "../../accepted-session-spawn.js";
import type { AuthProfileStore } from "../../auth-profiles/types.js";
import type {
@@ -53,6 +54,8 @@ export type EmbeddedRunAttemptParams = EmbeddedRunAttemptBase & {
agentHarnessId?: string;
/** OpenClaw-owned runtime policy prepared by the orchestrator for this attempt. */
runtimePlan?: AgentRuntimePlan;
/** Host-issued scope for harnesses that mirror native child runs into task state. */
agentHarnessTaskRuntimeScope?: AgentHarnessTaskRuntimeScope;
/** Live observer called after wrapped tool outcomes are recorded. */
onToolOutcome?: ToolOutcomeObserver;
model: Model<Api>;

View File

@@ -855,7 +855,7 @@ describe("deliverSubagentAnnouncement active requester steering", () => {
});
describe("deliverSubagentAnnouncement completion delivery", () => {
it("keeps completion announces session-internal while preserving route context for active requesters", async () => {
it("uses an active requester queue as the completion handoff when message-tool delivery is not required", async () => {
const callGateway = createGatewayMock();
const queueEmbeddedPiMessageWithOutcome = createQueueOutcomeMock(true);
const result = await deliverSlackThreadAnnouncement({
@@ -886,6 +886,29 @@ describe("deliverSubagentAnnouncement completion delivery", () => {
expect(callGateway).not.toHaveBeenCalled();
});
it("does not also direct-run a queued message-tool-only active completion", async () => {
const callGateway = createGatewayMock();
const queueEmbeddedPiMessageWithOutcome = createQueueOutcomeMock(true);
const result = await deliverSlackThreadAnnouncement({
callGateway,
sessionId: "requester-session-1",
isActive: true,
expectsCompletionMessage: true,
directIdempotencyKey: "announce-harness-task",
queueEmbeddedPiMessageWithOutcome,
sourceTool: "agent_harness_task",
});
expectRecordFields(result, {
delivered: true,
path: "steered",
enqueuedAt: 4_100,
deliveredAt: 4_200,
});
expect(queueEmbeddedPiMessageWithOutcome).toHaveBeenCalledTimes(1);
expect(callGateway).not.toHaveBeenCalled();
});
it("keeps direct external delivery for dormant completion requesters", async () => {
const callGateway = createGatewayMock();
const queueEmbeddedPiMessageWithOutcome = createQueueOutcomeMock(false);

View File

@@ -64,6 +64,7 @@ import type { SpawnSubagentMode } from "./subagent-spawn.types.js";
const DEFAULT_SUBAGENT_ANNOUNCE_TIMEOUT_MS = 120_000;
const MAX_TIMER_SAFE_TIMEOUT_MS = 2_147_000_000;
const AGENT_MEDIATED_COMPLETION_TOOLS = new Set([
"agent_harness_task",
"image_generate",
"music_generate",
"subagent_announce",
@@ -954,6 +955,17 @@ async function sendSubagentAnnounceDirectly(params: {
const directAnnounceStillPending = isGatewayAgentRunPending(directAnnounceResponse);
if (directAnnounceStillPending) {
if (
params.expectsCompletionMessage &&
expectedMediaUrls.length === 0 &&
!requiresMessageToolDelivery
) {
return {
delivered: false,
path: "direct",
error: "completion agent handoff is still pending",
};
}
return {
delivered: true,
path: "direct",

View File

@@ -267,6 +267,7 @@ async function writeLiveGatewayConfig(params: {
async function requestAgentTextWithEvents(params: {
client: GatewayClient;
eventPrefix?: string;
includeAllSessions?: boolean;
message: string;
sessionKey: string;
}): Promise<{ text: string; events: CapturedAgentEvent[] }> {
@@ -277,7 +278,7 @@ async function requestAgentTextWithEvents(params: {
const unsubscribe = onAgentEvent((event) => {
if (
!event.stream.startsWith(eventPrefix) ||
(event.sessionKey && event.sessionKey !== params.sessionKey)
(!params.includeAllSessions && event.sessionKey && event.sessionKey !== params.sessionKey)
) {
return;
}
@@ -958,6 +959,63 @@ async function verifyCodexSubagentProbe(params: {
}
}
async function verifyCodexNativeSubagentBridgeProbe(params: {
client: GatewayClient;
sessionKey: string;
}): Promise<void> {
const runId = randomUUID();
const childToken = `CODEX-NATIVE-CHILD-${runId.slice(0, 6).toUpperCase()}`;
const parentToken = `CODEX-NATIVE-PARENT-${runId.slice(0, 6).toUpperCase()}`;
const { listTaskRecords } = await import("../tasks/runtime-internal.js");
const { text, events } = await requestAgentTextWithEvents({
client: params.client,
eventPrefix: "codex_app_server.",
includeAllSessions: true,
sessionKey: params.sessionKey,
message: [
"Bridge probe.",
"You must use the Codex native spawn_agent tool exactly once before replying.",
`Give the subagent this exact instruction: Reply exactly ${childToken} and nothing else.`,
"Wait for the subagent result. Do not answer from your own knowledge.",
`After the subagent result returns, reply exactly ${parentToken} ${childToken} and nothing else.`,
].join("\n"),
});
logCodexLiveStep("native-subagent-bridge-probe:initial-reply", { text });
expect(
events.some((event) => event.stream === "codex_app_server.lifecycle"),
`expected Codex lifecycle events; events=${JSON.stringify(events)}`,
).toBe(true);
let codexNativeTasks = listCodexNativeTasks();
let deliveredTask = findDeliveredCodexNativeTask(codexNativeTasks);
const deadline = Date.now() + CODEX_HARNESS_REQUEST_TIMEOUT_MS;
while (!deliveredTask && Date.now() < deadline) {
await delay(1_000);
codexNativeTasks = listCodexNativeTasks();
deliveredTask = findDeliveredCodexNativeTask(codexNativeTasks);
}
expect(
deliveredTask,
`expected delivered Codex-native subagent task with child result; initialText=${JSON.stringify(
text,
)}; events=${JSON.stringify(events)}; tasks=${JSON.stringify(codexNativeTasks)}`,
).toBeDefined();
function listCodexNativeTasks() {
return listTaskRecords().filter(
(entry) => entry.runtime === "subagent" && entry.taskKind === "codex-native",
);
}
function findDeliveredCodexNativeTask(tasks: ReturnType<typeof listCodexNativeTasks>) {
return tasks.find(
(entry) =>
entry.status === "succeeded" &&
entry.deliveryStatus === "delivered" &&
entry.terminalSummary?.includes(childToken),
);
}
}
describeLive("gateway live (Codex harness)", () => {
it(
"runs gateway agent turns through the plugin-owned Codex app-server harness",
@@ -1037,6 +1095,8 @@ describeLive("gateway live (Codex harness)", () => {
if (CODEX_HARNESS_SUBAGENT_PROBE) {
logCodexLiveStep("subagent-probe:start", { sessionKey });
await verifyCodexSubagentProbe({ client, sessionKey });
logCodexLiveStep("native-subagent-bridge-probe:start", { sessionKey });
await verifyCodexNativeSubagentBridgeProbe({ client, sessionKey });
logCodexLiveStep("subagent-probe:done");
if (CODEX_HARNESS_SUBAGENT_ONLY) {
return;

View File

@@ -0,0 +1,201 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { deliverSubagentAnnouncement } from "../agents/subagent-announce-delivery.js";
import { createAgentHarnessTaskRuntimeScope } from "../tasks/agent-harness-task-runtime-scope.js";
import { createRunningTaskRun, finalizeTaskRunByRunId } from "../tasks/detached-task-runtime.js";
import { listTaskRecords } from "../tasks/runtime-internal.js";
import {
createAgentHarnessTaskRuntime,
deliverAgentHarnessTaskCompletion,
isDurableAgentHarnessCompletionDelivery,
} from "./agent-harness-task-runtime.js";
vi.mock("../agents/subagent-announce-delivery.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../agents/subagent-announce-delivery.js")>();
return {
...actual,
deliverSubagentAnnouncement: vi.fn(async () => ({ delivered: true, path: "steered" })),
isInternalAnnounceRequesterSession: vi.fn(() => true),
};
});
vi.mock("../tasks/detached-task-runtime.js", () => ({
createRunningTaskRun: vi.fn((params) => ({ taskId: "task-1", ...params })),
recordTaskRunProgressByRunId: vi.fn(() => []),
finalizeTaskRunByRunId: vi.fn(() => []),
setDetachedTaskDeliveryStatusByRunId: vi.fn(() => []),
}));
vi.mock("../tasks/runtime-internal.js", () => ({
listTaskRecords: vi.fn(() => []),
}));
describe("agent-harness-task-runtime", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(listTaskRecords).mockReturnValue([]);
});
function createScope(requesterSessionKey = "agent:main:channel:C123") {
return createAgentHarnessTaskRuntimeScope({ requesterSessionKey });
}
it("scopes task lifecycle mutations to the owning requester session", () => {
const runtime = createAgentHarnessTaskRuntime({
runtime: "subagent",
taskKind: "example-harness",
scope: createScope(),
runIdPrefix: "example:",
});
runtime.createRunningTaskRun({
runId: "example:child-1",
sourceId: "example:child-1",
task: "do work",
label: "worker",
});
runtime.finalizeTaskRunByRunId({
runId: "example:child-1",
status: "succeeded",
endedAt: 1,
});
expect(createRunningTaskRun).toHaveBeenCalledWith(
expect.objectContaining({
runtime: "subagent",
taskKind: "example-harness",
requesterSessionKey: "agent:main:channel:C123",
ownerKey: "agent:main:channel:C123",
scopeKind: "session",
runId: "example:child-1",
}),
);
expect(finalizeTaskRunByRunId).toHaveBeenCalledWith(
expect.objectContaining({
runtime: "subagent",
sessionKey: "agent:main:channel:C123",
runId: "example:child-1",
}),
);
});
it("rejects task run ids outside the configured harness scope", () => {
const runtime = createAgentHarnessTaskRuntime({
runtime: "subagent",
scope: createScope(),
runIdPrefix: "example:",
});
expect(() =>
runtime.finalizeTaskRunByRunId({
runId: "other:child-1",
status: "succeeded",
endedAt: 1,
}),
).toThrow(/outside the configured scope/);
});
it("rejects caller-forged task runtime scopes", async () => {
const forgedScope = {
requesterSessionKey: "agent:other:channel:C999",
} as ReturnType<typeof createScope>;
expect(() =>
createAgentHarnessTaskRuntime({
runtime: "subagent",
scope: forgedScope,
}),
).toThrow(/host-issued scope/);
await expect(
deliverAgentHarnessTaskCompletion({
scope: forgedScope,
childSessionKey: "harness-thread:child",
childSessionId: "child",
announceId: "harness:parent:child:succeeded",
status: "succeeded",
result: "child final answer",
}),
).rejects.toThrow(/host-issued scope/);
});
it("lists only task records owned by the scoped requester session", () => {
vi.mocked(listTaskRecords).mockReturnValue([
{
taskId: "task-1",
runtime: "subagent",
taskKind: "example-harness",
requesterSessionKey: "agent:main:channel:C123",
ownerKey: "agent:main:channel:C123",
scopeKind: "session",
runId: "example:child-1",
task: "owned",
status: "running",
deliveryStatus: "not_applicable",
notifyPolicy: "silent",
createdAt: 1,
},
{
taskId: "task-2",
runtime: "subagent",
taskKind: "example-harness",
requesterSessionKey: "agent:other:channel:C999",
ownerKey: "agent:other:channel:C999",
scopeKind: "session",
runId: "example:child-2",
task: "other",
status: "running",
deliveryStatus: "not_applicable",
notifyPolicy: "silent",
createdAt: 1,
},
]);
const runtime = createAgentHarnessTaskRuntime({
runtime: "subagent",
taskKind: "example-harness",
scope: createScope(),
runIdPrefix: "example:",
});
expect(runtime.listTaskRecords().map((task) => task.taskId)).toEqual(["task-1"]);
});
it("delivers a generic harness completion through subagent announcement delivery", async () => {
await deliverAgentHarnessTaskCompletion({
scope: createScope("agent:main:main"),
childSessionKey: "harness-thread:child",
childSessionId: "child",
announceId: "harness:parent:child:succeeded",
announceType: "Example harness worker",
taskLabel: "Example worker",
status: "succeeded",
statusLabel: "task_complete",
result: "child final answer",
});
expect(deliverSubagentAnnouncement).toHaveBeenCalledWith(
expect.objectContaining({
requesterSessionKey: "agent:main:main",
announceId: "harness:parent:child:succeeded",
sourceSessionKey: "harness-thread:child",
sourceTool: "agent_harness_task",
expectsCompletionMessage: true,
directIdempotencyKey: "announce:harness:parent:child:succeeded",
}),
);
});
it("checks durable direct delivery phases", () => {
expect(
isDurableAgentHarnessCompletionDelivery({
delivered: true,
path: "direct",
phases: [{ phase: "direct-primary", delivered: true, path: "direct" }],
}),
).toBe(true);
expect(
isDurableAgentHarnessCompletionDelivery({
delivered: true,
path: "direct",
phases: [{ phase: "steer-fallback", delivered: true, path: "steered" }],
}),
).toBe(false);
});
});

View File

@@ -0,0 +1,265 @@
import { buildAnnounceIdempotencyKey } from "../agents/announce-idempotency.js";
import {
AGENT_INTERNAL_EVENT_TYPE_TASK_COMPLETION,
type AgentInternalEventStatus,
} from "../agents/internal-event-contract.js";
import {
formatAgentInternalEventsForPrompt,
type AgentInternalEvent,
} from "../agents/internal-events.js";
import {
deliverSubagentAnnouncement,
isInternalAnnounceRequesterSession,
loadRequesterSessionEntry,
resolveSubagentCompletionOrigin,
} from "../agents/subagent-announce-delivery.js";
import { resolveAnnounceOrigin } from "../agents/subagent-announce-origin.js";
import {
assertAgentHarnessTaskRuntimeScope,
type AgentHarnessTaskRuntimeScope,
} from "../tasks/agent-harness-task-runtime-scope.js";
import {
createRunningTaskRun,
finalizeTaskRunByRunId,
recordTaskRunProgressByRunId,
setDetachedTaskDeliveryStatusByRunId,
} from "../tasks/detached-task-runtime.js";
import { listTaskRecords, type TaskRecord } from "../tasks/runtime-internal.js";
import { INTERNAL_MESSAGE_CHANNEL } from "../utils/message-channel.js";
export type { TaskRecord as AgentHarnessTaskRecord };
export type { AgentHarnessTaskRuntimeScope };
type AgentHarnessTaskRuntimeId = Parameters<typeof createRunningTaskRun>[0]["runtime"];
type CreateRunningTaskRunParams = Parameters<typeof createRunningTaskRun>[0];
type RecordTaskRunProgressParams = Parameters<typeof recordTaskRunProgressByRunId>[0];
type FinalizeTaskRunParams = Parameters<typeof finalizeTaskRunByRunId>[0];
type SetDeliveryStatusParams = Parameters<typeof setDetachedTaskDeliveryStatusByRunId>[0];
export type AgentHarnessTaskRuntimeScopeParams = {
runtime: AgentHarnessTaskRuntimeId;
scope: AgentHarnessTaskRuntimeScope;
taskKind?: string;
runIdPrefix?: string;
};
export type AgentHarnessScopedCreateRunningTaskRunParams = Omit<
CreateRunningTaskRunParams,
"runtime" | "taskKind" | "requesterSessionKey" | "ownerKey" | "scopeKind"
> & {
runId: string;
};
export type AgentHarnessScopedRecordTaskRunProgressParams = Omit<
RecordTaskRunProgressParams,
"runtime" | "sessionKey"
>;
export type AgentHarnessScopedFinalizeTaskRunParams = Omit<
FinalizeTaskRunParams,
"runtime" | "sessionKey"
>;
export type AgentHarnessScopedSetDeliveryStatusParams = Omit<
SetDeliveryStatusParams,
"runtime" | "sessionKey"
>;
export type AgentHarnessTaskRuntime = {
createRunningTaskRun(params: AgentHarnessScopedCreateRunningTaskRunParams): TaskRecord;
recordTaskRunProgressByRunId(params: AgentHarnessScopedRecordTaskRunProgressParams): TaskRecord[];
finalizeTaskRunByRunId(params: AgentHarnessScopedFinalizeTaskRunParams): TaskRecord[];
setDetachedTaskDeliveryStatusByRunId(
params: AgentHarnessScopedSetDeliveryStatusParams,
): TaskRecord[];
listTaskRecords(): TaskRecord[];
};
export type AgentHarnessCompletionStatus = "succeeded" | "failed" | "cancelled";
export type AgentHarnessCompletionDelivery = Awaited<
ReturnType<typeof deliverSubagentAnnouncement>
>;
const AGENT_HARNESS_COMPLETION_SOURCE_TOOL = "agent_harness_task";
export function createAgentHarnessTaskRuntime(
params: AgentHarnessTaskRuntimeScopeParams,
): AgentHarnessTaskRuntime {
const runtime = params.runtime;
const scope = assertAgentHarnessTaskRuntimeScope(params.scope);
const requesterSessionKey = scope.requesterSessionKey;
const taskKind = normalizeOptionalString(params.taskKind);
const runIdPrefix = normalizeOptionalString(params.runIdPrefix);
const assertRunId = (runId: string) => assertScopedRunId(runId, runIdPrefix);
return {
createRunningTaskRun(taskParams) {
assertRunId(taskParams.runId);
return createRunningTaskRun({
...taskParams,
runtime,
...(taskKind ? { taskKind } : {}),
requesterSessionKey,
ownerKey: requesterSessionKey,
scopeKind: "session",
});
},
recordTaskRunProgressByRunId(taskParams) {
assertRunId(taskParams.runId);
return recordTaskRunProgressByRunId({
...taskParams,
runtime,
sessionKey: requesterSessionKey,
});
},
finalizeTaskRunByRunId(taskParams) {
assertRunId(taskParams.runId);
return finalizeTaskRunByRunId({
...taskParams,
runtime,
sessionKey: requesterSessionKey,
});
},
setDetachedTaskDeliveryStatusByRunId(taskParams) {
assertRunId(taskParams.runId);
return setDetachedTaskDeliveryStatusByRunId({
...taskParams,
runtime,
sessionKey: requesterSessionKey,
});
},
listTaskRecords() {
return listTaskRecords().filter(
(task) =>
task.runtime === runtime &&
(!taskKind || task.taskKind === taskKind) &&
task.scopeKind === "session" &&
task.ownerKey === requesterSessionKey &&
(!runIdPrefix || task.runId?.startsWith(runIdPrefix)),
);
},
};
}
export async function deliverAgentHarnessTaskCompletion(params: {
scope: AgentHarnessTaskRuntimeScope;
childSessionKey: string;
childSessionId: string;
announceId: string;
status: AgentHarnessCompletionStatus;
statusLabel?: string;
result: string;
taskLabel?: string;
announceType?: string;
replyInstruction?: string;
signal?: AbortSignal;
}): Promise<AgentHarnessCompletionDelivery> {
const scope = assertAgentHarnessTaskRuntimeScope(params.scope);
const requesterSessionKey = scope.requesterSessionKey;
const childSessionKey = params.childSessionKey.trim();
const childSessionId = params.childSessionId.trim();
const taskLabel = params.taskLabel?.trim() || "Agent harness task";
const announceType = params.announceType?.trim() || "Agent harness task";
const statusLabel = params.statusLabel?.trim() || params.status;
const eventStatus = mapHarnessCompletionStatus(params.status);
const requesterIsSubagent = isInternalAnnounceRequesterSession(requesterSessionKey);
let directOrigin = scope.requesterOrigin;
if (!requesterIsSubagent) {
const { entry } = loadRequesterSessionEntry(requesterSessionKey);
directOrigin = resolveAnnounceOrigin(entry, scope.requesterOrigin);
}
const completionDirectOrigin =
requesterIsSubagent || !directOrigin
? directOrigin
: await resolveSubagentCompletionOrigin({
childSessionKey,
requesterSessionKey,
requesterOrigin: directOrigin,
childRunId: childSessionKey,
spawnMode: "run",
expectsCompletionMessage: true,
});
const internalEvents: AgentInternalEvent[] = [
{
type: AGENT_INTERNAL_EVENT_TYPE_TASK_COMPLETION,
source: "subagent",
childSessionKey,
childSessionId,
announceType,
taskLabel,
status: eventStatus,
statusLabel,
result: params.result,
replyInstruction:
params.replyInstruction?.trim() ||
"Use the completed harness task result to continue or wrap up the parent task. If this is a channel session, send the visible response with the message tool instead of only writing a transcript final answer.",
},
];
const prompt = formatAgentInternalEventsForPrompt(internalEvents);
return await deliverSubagentAnnouncement({
requesterSessionKey,
announceId: params.announceId,
triggerMessage: prompt,
steerMessage: prompt,
internalEvents,
summaryLine: taskLabel,
requesterSessionOrigin: scope.requesterOrigin,
requesterOrigin: completionDirectOrigin ?? directOrigin,
completionDirectOrigin: completionDirectOrigin ?? directOrigin,
directOrigin,
sourceSessionKey: childSessionKey,
sourceChannel: INTERNAL_MESSAGE_CHANNEL,
sourceTool: AGENT_HARNESS_COMPLETION_SOURCE_TOOL,
targetRequesterSessionKey: requesterSessionKey,
requesterIsSubagent,
expectsCompletionMessage: true,
bestEffortDeliver: true,
directIdempotencyKey: buildAnnounceIdempotencyKey(params.announceId),
signal: params.signal,
});
}
function mapHarnessCompletionStatus(
status: AgentHarnessCompletionStatus,
): AgentInternalEventStatus {
if (status === "succeeded") {
return "ok";
}
return "error";
}
export function isDurableAgentHarnessCompletionDelivery(
delivery: AgentHarnessCompletionDelivery,
): boolean {
if (!delivery.delivered) {
return false;
}
if (delivery.path === "steered") {
return true;
}
if (delivery.path !== "direct") {
return false;
}
const phases = Array.isArray(delivery.phases) ? delivery.phases : undefined;
if (!phases) {
return true;
}
return phases.some(
(phase) => phase.phase === "direct-primary" && phase.delivered && phase.path === "direct",
);
}
function normalizeOptionalString(value: string | undefined): string | undefined {
const normalized = value?.trim();
return normalized || undefined;
}
function assertScopedRunId(runId: string, runIdPrefix: string | undefined): void {
const normalized = runId.trim();
if (!normalized) {
throw new Error("Agent harness task runtime requires runId");
}
if (runIdPrefix && !normalized.startsWith(runIdPrefix)) {
throw new Error("Agent harness task runId is outside the configured scope");
}
}

View File

@@ -35,10 +35,7 @@ export const deprecatedBarrelPluginSdkEntrypoints = pluginSdkSubpaths.filter((en
// Transitional compatibility/helper surfaces owned by their matching bundled plugin.
// Cross-owner extension imports are blocked by the package contract guardrails.
export const reservedBundledPluginSdkEntrypoints = [
"codex-mcp-projection",
"codex-native-task-runtime",
] as const;
export const reservedBundledPluginSdkEntrypoints = ["codex-mcp-projection"] as const;
// Supported SDK facades backed by bundled plugins. These are intentionally public
// until they move to generic, plugin-neutral contracts.

View File

@@ -445,16 +445,16 @@ describe("plugin-sdk root alias", () => {
it("keeps non-QA private local-only plugin-sdk subpaths out of the CJS root alias", () => {
const packageRoot = path.dirname(path.dirname(path.dirname(rootAliasPath)));
const sourceCodexNativeTaskRuntimePath = path.join(
const sourceCodexMcpProjectionPath = path.join(
packageRoot,
"src",
"plugin-sdk",
"codex-native-task-runtime.ts",
"codex-mcp-projection.ts",
);
const sourceQaRuntimePath = path.join(packageRoot, "src", "plugin-sdk", "qa-runtime.ts");
const lazyModule = loadRootAliasWithStubs({
privateLocalOnlySubpaths: ["codex-native-task-runtime", "qa-runtime"],
existingPaths: [sourceCodexNativeTaskRuntimePath, sourceQaRuntimePath],
privateLocalOnlySubpaths: ["codex-mcp-projection", "qa-runtime"],
existingPaths: [sourceCodexMcpProjectionPath, sourceQaRuntimePath],
monolithicExports: {
slowHelper: (): string => "loaded",
},
@@ -462,8 +462,8 @@ describe("plugin-sdk root alias", () => {
expect((lazyModule.moduleExports.slowHelper as () => string)()).toBe("loaded");
const aliasMap = (lazyModule.createJitiOptions.at(-1)?.alias ?? {}) as Record<string, string>;
expect(aliasMap).not.toHaveProperty("openclaw/plugin-sdk/codex-native-task-runtime");
expect(aliasMap).not.toHaveProperty("@openclaw/plugin-sdk/codex-native-task-runtime");
expect(aliasMap).not.toHaveProperty("openclaw/plugin-sdk/codex-mcp-projection");
expect(aliasMap).not.toHaveProperty("@openclaw/plugin-sdk/codex-mcp-projection");
expect(aliasMap).not.toHaveProperty("openclaw/plugin-sdk/qa-runtime");
});

View File

@@ -653,7 +653,7 @@ describe("plugin sdk alias helpers", () => {
expect(subpaths).toEqual(["core", "qa-channel", "qa-channel-protocol", "qa-lab", "qa-runtime"]);
});
it("adds the non-QA private Codex task runtime subpath only for trusted Codex plugins", () => {
it("adds non-QA private Codex helper subpaths only for trusted Codex plugins", () => {
const fixture = createPluginSdkAliasFixture({
packageExports: {
"./plugin-sdk/core": { default: "./dist/plugin-sdk/core.js" },
@@ -664,13 +664,13 @@ describe("plugin sdk alias helpers", () => {
{ force: true },
);
fs.writeFileSync(
path.join(fixture.root, "src", "plugin-sdk", "codex-native-task-runtime.ts"),
"export const codexNativeTaskRuntime = true;\n",
path.join(fixture.root, "src", "plugin-sdk", "codex-mcp-projection.ts"),
"export const codexMcpProjection = true;\n",
"utf-8",
);
fs.writeFileSync(
path.join(fixture.root, "src", "plugin-sdk", "codex-mcp-projection.ts"),
"export const codexMcpProjection = true;\n",
path.join(fixture.root, "src", "plugin-sdk", "codex-native-task-runtime.ts"),
"export const codexNativeTaskRuntime = true;\n",
"utf-8",
);
fs.writeFileSync(
@@ -927,31 +927,31 @@ describe("plugin sdk alias helpers", () => {
},
});
const sourceRootAlias = path.join(fixture.root, "src", "plugin-sdk", "root-alias.cjs");
const sourceCodexNativeTaskRuntimePath = path.join(
fixture.root,
"src",
"plugin-sdk",
"codex-native-task-runtime.ts",
);
const sourceCodexMcpProjectionPath = path.join(
fixture.root,
"src",
"plugin-sdk",
"codex-mcp-projection.ts",
);
const distRootAlias = path.join(fixture.root, "dist", "plugin-sdk", "root-alias.cjs");
const distCodexNativeTaskRuntimePath = path.join(
const sourceCodexNativeTaskRuntimePath = path.join(
fixture.root,
"dist",
"src",
"plugin-sdk",
"codex-native-task-runtime.js",
"codex-native-task-runtime.ts",
);
const distRootAlias = path.join(fixture.root, "dist", "plugin-sdk", "root-alias.cjs");
const distCodexMcpProjectionPath = path.join(
fixture.root,
"dist",
"plugin-sdk",
"codex-mcp-projection.js",
);
const distCodexNativeTaskRuntimePath = path.join(
fixture.root,
"dist",
"plugin-sdk",
"codex-native-task-runtime.js",
);
const sourceQaRuntimePath = path.join(fixture.root, "src", "plugin-sdk", "qa-runtime.ts");
fs.writeFileSync(sourceRootAlias, "module.exports = {};\n", "utf-8");
fs.writeFileSync(distRootAlias, "module.exports = {};\n", "utf-8");
@@ -959,18 +959,13 @@ describe("plugin sdk alias helpers", () => {
path.join(fixture.root, "scripts", "lib", "plugin-sdk-private-local-only-subpaths.json"),
{ force: true },
);
fs.writeFileSync(
sourceCodexNativeTaskRuntimePath,
"export const codexNativeTaskRuntime = true;\n",
"utf-8",
);
fs.writeFileSync(
sourceCodexMcpProjectionPath,
"export const codexMcpProjection = true;\n",
"utf-8",
);
fs.writeFileSync(
distCodexNativeTaskRuntimePath,
sourceCodexNativeTaskRuntimePath,
"export const codexNativeTaskRuntime = true;\n",
"utf-8",
);
@@ -979,6 +974,11 @@ describe("plugin sdk alias helpers", () => {
"export const codexMcpProjection = true;\n",
"utf-8",
);
fs.writeFileSync(
distCodexNativeTaskRuntimePath,
"export const codexNativeTaskRuntime = true;\n",
"utf-8",
);
fs.writeFileSync(sourceQaRuntimePath, "export const qaRuntime = true;\n", "utf-8");
const sourcePluginEntry = writePluginEntry(
fixture.root,
@@ -1050,25 +1050,25 @@ describe("plugin sdk alias helpers", () => {
expect(fs.realpathSync(aliases["openclaw/plugin-sdk"] ?? "")).toBe(
fs.realpathSync(sourceRootAlias),
);
expect(fs.realpathSync(aliases["openclaw/plugin-sdk/codex-native-task-runtime"] ?? "")).toBe(
fs.realpathSync(sourceCodexNativeTaskRuntimePath),
);
expect(fs.realpathSync(aliases["openclaw/plugin-sdk/codex-mcp-projection"] ?? "")).toBe(
fs.realpathSync(sourceCodexMcpProjectionPath),
);
expect(
fs.realpathSync(installedAliases["openclaw/plugin-sdk/codex-native-task-runtime"] ?? ""),
).toBe(fs.realpathSync(distCodexNativeTaskRuntimePath));
expect(fs.realpathSync(aliases["openclaw/plugin-sdk/codex-native-task-runtime"] ?? "")).toBe(
fs.realpathSync(sourceCodexNativeTaskRuntimePath),
);
expect(
fs.realpathSync(installedAliases["openclaw/plugin-sdk/codex-mcp-projection"] ?? ""),
).toBe(fs.realpathSync(distCodexMcpProjectionPath));
expect(
fs.realpathSync(installedAliases["openclaw/plugin-sdk/codex-native-task-runtime"] ?? ""),
).toBe(fs.realpathSync(distCodexNativeTaskRuntimePath));
expect(aliases["openclaw/plugin-sdk/qa-runtime"]).toBeUndefined();
expect(otherAliases["openclaw/plugin-sdk/codex-native-task-runtime"]).toBeUndefined();
expect(otherAliases["openclaw/plugin-sdk/codex-mcp-projection"]).toBeUndefined();
expect(installedOtherAliases["openclaw/plugin-sdk/codex-native-task-runtime"]).toBeUndefined();
expect(otherAliases["openclaw/plugin-sdk/codex-native-task-runtime"]).toBeUndefined();
expect(installedOtherAliases["openclaw/plugin-sdk/codex-mcp-projection"]).toBeUndefined();
expect(shadowCodexAliases["openclaw/plugin-sdk/codex-native-task-runtime"]).toBeUndefined();
expect(installedOtherAliases["openclaw/plugin-sdk/codex-native-task-runtime"]).toBeUndefined();
expect(shadowCodexAliases["openclaw/plugin-sdk/codex-mcp-projection"]).toBeUndefined();
expect(shadowCodexAliases["openclaw/plugin-sdk/codex-native-task-runtime"]).toBeUndefined();
});
it("applies explicit dist resolution to plugin-sdk subpath aliases too", () => {

View File

@@ -0,0 +1,51 @@
import { normalizeDeliveryContext } from "../utils/delivery-context.shared.js";
import type { DeliveryContext } from "../utils/delivery-context.types.js";
const scopeRegistryKey = Symbol.for("openclaw.agentHarnessTaskRuntimeScope.registry");
type ScopeRegistry = {
hostIssuedScopes: WeakSet<object>;
};
type GlobalWithScopeRegistry = typeof globalThis & {
[scopeRegistryKey]?: ScopeRegistry;
};
function getScopeRegistry(): ScopeRegistry {
const globalState = globalThis as GlobalWithScopeRegistry;
globalState[scopeRegistryKey] ??= {
hostIssuedScopes: new WeakSet<object>(),
};
return globalState[scopeRegistryKey];
}
export type AgentHarnessTaskRuntimeScope = {
readonly requesterSessionKey: string;
readonly requesterOrigin?: DeliveryContext;
};
export function createAgentHarnessTaskRuntimeScope(params: {
requesterSessionKey: string;
requesterOrigin?: DeliveryContext;
}): AgentHarnessTaskRuntimeScope {
const requesterSessionKey = params.requesterSessionKey.trim();
if (!requesterSessionKey) {
throw new Error("Agent harness task runtime scope requires requesterSessionKey");
}
const requesterOrigin = normalizeDeliveryContext(params.requesterOrigin);
const scope: AgentHarnessTaskRuntimeScope = {
requesterSessionKey,
...(requesterOrigin ? { requesterOrigin } : {}),
};
getScopeRegistry().hostIssuedScopes.add(scope);
return scope;
}
export function assertAgentHarnessTaskRuntimeScope(
scope: AgentHarnessTaskRuntimeScope,
): AgentHarnessTaskRuntimeScope {
if (!getScopeRegistry().hostIssuedScopes.has(scope)) {
throw new Error("Agent harness task runtime requires a host-issued scope");
}
return scope;
}

View File

@@ -29,3 +29,4 @@ export {
setTaskRunDeliveryStatusByRunId,
updateTaskNotifyPolicyById,
} from "./task-registry.js";
export type { TaskRecord } from "./task-registry.types.js";

View File

@@ -0,0 +1,41 @@
import { importFreshModule } from "openclaw/plugin-sdk/test-fixtures";
import { describe, expect, it } from "vitest";
describe("task registry process state", () => {
it("shares state across duplicate module instances", async () => {
const firstModule = await importFreshModule<typeof import("./task-registry.process-state.js")>(
import.meta.url,
"./task-registry.process-state.js?scope=task-registry-state-a",
);
const secondModule = await importFreshModule<typeof import("./task-registry.process-state.js")>(
import.meta.url,
"./task-registry.process-state.js?scope=task-registry-state-b",
);
const firstState = firstModule.getTaskRegistryProcessState();
const secondState = secondModule.getTaskRegistryProcessState();
firstState.tasks.set("task-duplicate", {
taskId: "task-duplicate",
runtime: "subagent",
taskKind: "agent-harness",
requesterSessionKey: "agent:main:parent",
ownerKey: "agent:main:parent",
scopeKind: "session",
runId: "agent-harness:child-duplicate",
task: "Duplicate module task",
status: "running",
deliveryStatus: "pending",
notifyPolicy: "silent",
createdAt: 1,
});
expect(secondState.tasks.get("task-duplicate")).toEqual(
expect.objectContaining({
runtime: "subagent",
taskKind: "agent-harness",
runId: "agent-harness:child-duplicate",
}),
);
firstState.tasks.clear();
});
});

View File

@@ -0,0 +1,29 @@
import type { TaskDeliveryState, TaskRecord } from "./task-registry.types.js";
export type TaskRegistryProcessState = {
tasks: Map<string, TaskRecord>;
taskDeliveryStates: Map<string, TaskDeliveryState>;
taskIdsByRunId: Map<string, Set<string>>;
taskIdsByOwnerKey: Map<string, Set<string>>;
taskIdsByParentFlowId: Map<string, Set<string>>;
taskIdsByRelatedSessionKey: Map<string, Set<string>>;
tasksWithPendingDelivery: Set<string>;
};
const TASK_REGISTRY_PROCESS_STATE_KEY = Symbol.for("openclaw.taskRegistry.state");
export function getTaskRegistryProcessState(): TaskRegistryProcessState {
const globalState = globalThis as typeof globalThis & {
[TASK_REGISTRY_PROCESS_STATE_KEY]?: TaskRegistryProcessState;
};
globalState[TASK_REGISTRY_PROCESS_STATE_KEY] ??= {
tasks: new Map<string, TaskRecord>(),
taskDeliveryStates: new Map<string, TaskDeliveryState>(),
taskIdsByRunId: new Map<string, Set<string>>(),
taskIdsByOwnerKey: new Map<string, Set<string>>(),
taskIdsByParentFlowId: new Map<string, Set<string>>(),
taskIdsByRelatedSessionKey: new Map<string, Set<string>>(),
tasksWithPendingDelivery: new Set<string>(),
};
return globalState[TASK_REGISTRY_PROCESS_STATE_KEY];
}

View File

@@ -29,6 +29,7 @@ import {
updateFlowRecordByIdExpectedRevision,
} from "./task-flow-runtime-internal.js";
import type { TaskRegistryControlRuntime } from "./task-registry-control.types.js";
import { getTaskRegistryProcessState } from "./task-registry.process-state.js";
import {
getTaskRegistryObservers,
getTaskRegistryStore,
@@ -54,13 +55,14 @@ import type {
const log = createSubsystemLogger("tasks/registry");
const DEFAULT_TASK_RETENTION_MS = 7 * 24 * 60 * 60_000;
const tasks = new Map<string, TaskRecord>();
const taskDeliveryStates = new Map<string, TaskDeliveryState>();
const taskIdsByRunId = new Map<string, Set<string>>();
const taskIdsByOwnerKey = new Map<string, Set<string>>();
const taskIdsByParentFlowId = new Map<string, Set<string>>();
const taskIdsByRelatedSessionKey = new Map<string, Set<string>>();
const tasksWithPendingDelivery = new Set<string>();
const taskRegistryProcessState = getTaskRegistryProcessState();
const tasks = taskRegistryProcessState.tasks;
const taskDeliveryStates = taskRegistryProcessState.taskDeliveryStates;
const taskIdsByRunId = taskRegistryProcessState.taskIdsByRunId;
const taskIdsByOwnerKey = taskRegistryProcessState.taskIdsByOwnerKey;
const taskIdsByParentFlowId = taskRegistryProcessState.taskIdsByParentFlowId;
const taskIdsByRelatedSessionKey = taskRegistryProcessState.taskIdsByRelatedSessionKey;
const tasksWithPendingDelivery = taskRegistryProcessState.tasksWithPendingDelivery;
let listenerStarted = false;
let listenerStop: (() => void) | null = null;
let restoreAttempted = false;

View File

@@ -309,8 +309,6 @@ function buildUnifiedDistEntries(): Record<string, string> {
...dockerE2eHarnessEntries,
// Internal compat artifact for the root-alias.cjs lazy loader.
"plugin-sdk/compat": "src/plugin-sdk/compat.ts",
// Private bundled Codex helper for app-server native subagent task mirroring.
"plugin-sdk/codex-native-task-runtime": "src/plugin-sdk/codex-native-task-runtime.ts",
// Private bundled Codex helper for app-server user MCP config projection.
"plugin-sdk/codex-mcp-projection": "src/plugin-sdk/codex-mcp-projection.ts",
...Object.fromEntries(