Revert "refactor(agents): centralize exact execution attribution"

This reverts commit e628b42a49.
This commit is contained in:
joshavant
2026-08-07 13:53:44 -05:00
committed by Josh Avant
parent c6a03a5853
commit 2d80e78886
22 changed files with 80 additions and 665 deletions
+3 -12
View File
@@ -19,7 +19,7 @@ function systemIngress(boundary: string): AgentCommandAdmissionIngress {
}
function recordAgentCommandExecutionIdentity(params: {
attribution?: AgentCommandOpts["executionAttribution"];
admission?: AgentCommandOpts["executionIdentityAdmission"];
agentId: string;
cfg: OpenClawConfig;
ingress: AgentCommandAdmissionIngress;
@@ -37,17 +37,8 @@ function recordAgentCommandExecutionIdentity(params: {
},
{
enabled: isExecutionIdentityCollectionEnabled(params.cfg),
...(params.attribution
? params.attribution.executionIdentityAdmission
? {
token: params.attribution.executionIdentityAdmission.token,
retryOnly: params.attribution.executionIdentityAdmission.retryOnly,
}
: {
contextId: params.attribution.contextId,
executionId: params.attribution.executionId,
now: params.attribution.createdAt,
}
...(params.admission
? { token: params.admission.token, retryOnly: params.admission.retryOnly }
: {}),
},
);
+3 -3
View File
@@ -231,7 +231,7 @@ async function agentCommandInternal(
});
return await sessionWorkAdmission.run(async () => {
executionIdentity.record({
attribution: opts.executionAttribution,
admission: opts.executionIdentityAdmission,
agentId: sessionAgentId,
cfg,
ingress: admissionIngress,
@@ -674,10 +674,10 @@ export async function agentCommandFromIngress(
runtime: RuntimeEnv = defaultRuntime,
deps?: CliDeps,
) {
// Plugin SDK callers may be plain JavaScript. Enforce the private execution
// Plugin SDK callers may be plain JavaScript. Enforce the private recovery
// boundary at runtime so extra or inherited properties cannot author audit identity.
return await agentCommandFromIngressInternal(
{ ...opts, executionAttribution: undefined },
{ ...opts, executionIdentityAdmission: undefined },
runtime,
deps,
);
@@ -1,125 +0,0 @@
import { describe, expect, it } from "vitest";
import { createExecutionIdentityAdmissionToken } from "../audit/execution-identity-admission.js";
import {
createAgentExecutionAttribution,
rebindAgentExecutionAttribution,
} from "./agent-execution-attribution.js";
describe("createAgentExecutionAttribution", () => {
it("preserves required identities, normalizes optional correlation, and freezes the record", () => {
const token = createExecutionIdentityAdmissionToken(" run-1 ", {
contextId: "context-1",
executionId: "execution-1",
now: 123,
});
const attribution = createAgentExecutionAttribution({
runId: " run-1 ",
lifecycleGeneration: " generation-1 ",
sessionKey: " agent:main:main ",
sessionId: " session-1 ",
agentId: " main ",
executionIdentityAdmission: { token, retryOnly: true },
});
expect(attribution).toEqual({
runId: " run-1 ",
contextId: "context-1",
executionId: "execution-1",
createdAt: 123,
lifecycleGeneration: " generation-1 ",
executionIdentityAdmission: { token, retryOnly: true },
sessionKey: "agent:main:main",
sessionId: "session-1",
agentId: "main",
});
expect(Object.isFrozen(attribution)).toBe(true);
expect(Reflect.set(attribution, "sessionId", "replacement")).toBe(false);
});
it("leaves unknown optional correlation absent", () => {
const attribution = createAgentExecutionAttribution({
runId: "run-1",
lifecycleGeneration: "generation-1",
sessionKey: " ",
sessionId: "",
});
expect(attribution).toMatchObject({
runId: "run-1",
lifecycleGeneration: "generation-1",
});
expect(attribution).not.toHaveProperty("sessionKey");
expect(attribution).not.toHaveProperty("sessionId");
expect(attribution).not.toHaveProperty("executionIdentityAdmission");
expect(attribution.contextId).toBeTruthy();
expect(attribution.executionId).toBeTruthy();
expect(attribution.createdAt).toBeGreaterThan(0);
});
it("does not apply the opt-in audit token bounds to default runtime attribution", () => {
const runId = "r".repeat(1_024);
const attribution = createAgentExecutionAttribution({
runId,
lifecycleGeneration: "generation-1",
});
expect(attribution.runId).toBe(runId);
expect(attribution).not.toHaveProperty("executionIdentityAdmission");
});
it.each([false, true])(
"changes only lifecycle ownership when rebound (token=%s)",
(withToken) => {
const runId = "run-rebound";
const attribution = createAgentExecutionAttribution({
runId,
lifecycleGeneration: "generation-1",
...(withToken
? {
executionIdentityAdmission: {
token: createExecutionIdentityAdmissionToken(runId, {
contextId: "context-1",
executionId: "execution-1",
now: 123,
}),
retryOnly: true,
},
}
: {}),
});
const rebound = rebindAgentExecutionAttribution(attribution, "generation-2");
expect(rebound).toEqual({
...attribution,
lifecycleGeneration: "generation-2",
});
expect(rebound.contextId).toBe(attribution.contextId);
expect(rebound.executionId).toBe(attribution.executionId);
expect(rebound.createdAt).toBe(attribution.createdAt);
expect(rebound.executionIdentityAdmission).toBe(attribution.executionIdentityAdmission);
expect(Object.isFrozen(rebound)).toBe(true);
},
);
it.each([
["runId", { runId: " ", lifecycleGeneration: "generation-1" }],
["lifecycleGeneration", { runId: "run-1", lifecycleGeneration: "" }],
])("rejects a missing required %s", (field, params) => {
expect(() => createAgentExecutionAttribution(params)).toThrow(
`Agent execution attribution requires ${field}`,
);
});
it("rejects an admission token owned by another run", () => {
expect(() =>
createAgentExecutionAttribution({
runId: "run-1",
lifecycleGeneration: "generation-1",
executionIdentityAdmission: {
token: createExecutionIdentityAdmissionToken("run-2"),
retryOnly: false,
},
}),
).toThrow("Agent execution attribution token disagrees with runId");
});
});
-79
View File
@@ -1,79 +0,0 @@
import { randomUUID } from "node:crypto";
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import {
parseExecutionIdentityAdmissionToken,
type ExecutionIdentityAdmissionToken,
} from "../audit/execution-identity-admission.js";
export type AgentExecutionIdentityAdmission = Readonly<{
token: ExecutionIdentityAdmissionToken;
retryOnly: boolean;
}>;
/** Host-owned correlation captured once for an admitted agent execution. */
export type AgentExecutionAttribution = Readonly<{
runId: string;
contextId: string;
executionId: string;
createdAt: number;
lifecycleGeneration: string;
executionIdentityAdmission?: AgentExecutionIdentityAdmission;
sessionKey?: string;
sessionId?: string;
agentId?: string;
}>;
function requireAttributionField(value: string, field: "runId" | "lifecycleGeneration"): string {
if (!value.trim()) {
throw new TypeError(`Agent execution attribution requires ${field}`);
}
return value;
}
export function createAgentExecutionAttribution(params: {
runId: string;
lifecycleGeneration: string;
sessionKey?: string;
sessionId?: string;
agentId?: string;
executionIdentityAdmission?: AgentExecutionIdentityAdmission;
}): AgentExecutionAttribution {
const runId = requireAttributionField(params.runId, "runId");
const token = params.executionIdentityAdmission
? parseExecutionIdentityAdmissionToken(params.executionIdentityAdmission.token)
: undefined;
if (token && token.runId !== runId) {
throw new TypeError("Agent execution attribution token disagrees with runId");
}
const sessionKey = normalizeOptionalString(params.sessionKey);
const sessionId = normalizeOptionalString(params.sessionId);
const agentId = normalizeOptionalString(params.agentId);
return Object.freeze({
runId,
contextId: token?.contextId ?? randomUUID(),
executionId: token?.executionId ?? randomUUID(),
createdAt: token?.createdAt ?? Date.now(),
lifecycleGeneration: requireAttributionField(params.lifecycleGeneration, "lifecycleGeneration"),
...(token
? {
executionIdentityAdmission: Object.freeze({
token,
retryOnly: params.executionIdentityAdmission?.retryOnly === true,
}),
}
: {}),
...(sessionKey ? { sessionKey } : {}),
...(sessionId ? { sessionId } : {}),
...(agentId ? { agentId } : {}),
});
}
export function rebindAgentExecutionAttribution(
attribution: AgentExecutionAttribution,
lifecycleGeneration: string,
): AgentExecutionAttribution {
return Object.freeze({
...attribution,
lifecycleGeneration: requireAttributionField(lifecycleGeneration, "lifecycleGeneration"),
});
}
+9 -6
View File
@@ -5,6 +5,7 @@ import type { FastMode } from "@openclaw/normalization-core/string-coerce";
import type { AgentInternalEvent } from "../../agents/internal-events.js";
import type { SpawnedRunMetadata } from "../../agents/spawned-context.js";
import type { PromptMode } from "../../agents/system-prompt.types.js";
import type { ExecutionIdentityAdmissionToken } from "../../audit/execution-identity-admission.js";
import type { SourceReplyDeliveryMode } from "../../auto-reply/get-reply-options.types.js";
import type { ChannelOutboundTargetMode } from "../../channels/plugins/types.public.js";
import type { MediaFact } from "../../media/media-facts.js";
@@ -16,7 +17,6 @@ import type {
UserTurnInput,
UserTurnTranscriptRecorder,
} from "../../sessions/user-turn-transcript.types.js";
import type { AgentExecutionAttribution } from "../agent-execution-attribution.js";
import type { ExecApprovalContinuationPromptRange } from "../bash-tools.exec-approval-output.js";
import type { ExecElevatedDefaults } from "../bash-tools.exec-types.js";
import type { BootstrapContextRunKind } from "../bootstrap-mode.js";
@@ -193,8 +193,11 @@ export type AgentCommandOpts = {
mainRestartRecoveryOwnerLease?: MainSessionRecoveryOwnerLease;
/** Gateway already consumed this automatic recovery run's durable reservation. */
mainRestartRecoveryAdmitted?: boolean;
/** Private host-owned execution identity; public ingress callers cannot author it. */
executionAttribution?: AgentExecutionAttribution;
/** Private recovery correlation; public ingress callers cannot author identity evidence. */
executionIdentityAdmission?: {
token: ExecutionIdentityAdmissionToken;
retryOnly: boolean;
};
/** Called when the actual run model is selected, including fallback retries. */
onActiveModelSelected?: (ctx: { provider: string; model: string }) => void | Promise<void>;
/** Called when every candidate in the run's model fallback chain failed. */
@@ -218,7 +221,7 @@ export type AgentCommandOpts = {
/** Restricted option surface for external ingress callsites. */
export type AgentCommandIngressOpts = Omit<
AgentCommandOpts,
"senderIsOwner" | "allowModelOverride" | "executionAttribution"
"senderIsOwner" | "allowModelOverride" | "executionIdentityAdmission"
> & {
/** Trusted sender identity bit for command/channel-action auth; defaults false for ingress. */
senderIsOwner?: boolean;
@@ -226,6 +229,6 @@ export type AgentCommandIngressOpts = Omit<
allowModelOverride: boolean;
};
/** Gateway-only ingress extends the public Plugin SDK surface with private execution correlation. */
/** Gateway-only ingress extends the public Plugin SDK surface with private recovery correlation. */
export type AgentCommandGatewayIngressOpts = AgentCommandIngressOpts &
Pick<AgentCommandOpts, "executionAttribution">;
Pick<AgentCommandOpts, "executionIdentityAdmission">;
@@ -6,7 +6,6 @@ import {
} from "../../../infra/agent-events.js";
import { claimAgentRunContext, getAgentRunContext } from "../../../infra/agent-run-registry.js";
import type { CommandQueueEnqueueOptions } from "../../../process/command-queue.types.js";
import { createAgentExecutionAttribution } from "../../agent-execution-attribution.js";
import type { EmbeddedAgentRunResult } from "../types.js";
import { createEmbeddedRunLaneController } from "./lane-controller.js";
import type { RunEmbeddedAgentParams } from "./params.js";
@@ -130,41 +129,6 @@ describe("createEmbeddedRunLaneController lifecycle admission", () => {
});
});
it("rebinds admitted attribution with foreground work across lifecycle rotation", async () => {
const queue = deferredTaskQueue();
const generation = getAgentEventLifecycleGeneration();
const attribution = createAgentExecutionAttribution({
runId: "attributed-across-restart",
lifecycleGeneration: generation,
sessionKey: "agent:main:session-1",
sessionId: "session-1",
agentId: "main",
});
claimAgentRunContext(attribution.runId, {
attribution,
...(attribution.sessionKey ? { sessionKey: attribution.sessionKey } : {}),
...(attribution.sessionId ? { sessionId: attribution.sessionId } : {}),
...(attribution.agentId ? { agentId: attribution.agentId } : {}),
lifecycleGeneration: generation,
});
const state = createController({
lifecycleGeneration: generation,
enqueue: queue.enqueue as LaneParams["enqueue"],
trigger: "user",
runId: attribution.runId,
});
const run = state.controller.enqueueGlobal(async () => completedResult);
const currentGeneration = rotateAgentEventLifecycleGeneration();
queue.release();
await run;
expect(getAgentRunContext(attribution.runId)?.attribution).toEqual({
...attribution,
lifecycleGeneration: currentGeneration,
});
});
it("rejects background work queued across lifecycle rotation", async () => {
const queue = deferredTaskQueue();
const generation = getAgentEventLifecycleGeneration();
@@ -10,7 +10,6 @@ import {
} from "../../../infra/agent-run-registry.js";
import { enqueueCommandInLane, getCommandLaneSnapshot } from "../../../process/command-queue.js";
import type { CommandQueueEnqueueOptions } from "../../../process/command-queue.types.js";
import { rebindAgentExecutionAttribution } from "../../agent-execution-attribution.js";
import { withSessionPlacementTurnAdmission } from "../../session-placement-admission.js";
import type { EmbeddedAgentRunResult } from "../types.js";
import {
@@ -169,12 +168,8 @@ export function createEmbeddedRunLaneController<TParams extends LaneParams>(opti
assertAgentRunLifecycleGenerationCurrent(lifecycleGeneration);
releaseQueuedContext("admitted");
// Queue-stage rotation may rebind, but placement admitted into a retired runtime must fail.
const attribution = existingContext?.attribution
? rebindAgentExecutionAttribution(existingContext.attribution, lifecycleGeneration)
: undefined;
claimAgentRunContext(params.runId, {
...existingContext,
...(attribution ? { attribution } : {}),
sessionKey: params.sessionKey ?? existingContext?.sessionKey,
sessionId: params.sessionId ?? existingContext?.sessionId,
lifecycleGeneration,
+1 -1
View File
@@ -498,7 +498,7 @@ describe("agentCommand", () => {
);
expect(record).toHaveBeenCalledWith(
expect.objectContaining({ attribution: undefined, runId: "public-ingress-run" }),
expect.objectContaining({ admission: undefined, runId: "public-ingress-run" }),
);
} finally {
record.mockRestore();
@@ -1,5 +1,4 @@
import { randomUUID } from "node:crypto";
import { ErrorCodes, errorShape } from "../../../packages/gateway-protocol/src/index.js";
import { AGENT_RUN_RESTART_ABORT_STOP_REASON } from "../../agents/run-termination.js";
import { resolveAgentTimeoutMs } from "../../agents/timeout.js";
import { getAgentEventLifecycleGeneration } from "../../infra/agent-events.js";
@@ -104,27 +103,6 @@ export function createAgentDedupeLifecycle(params: {
reserved = false;
};
const failBeforeDispatch = (summary: string) => {
const error = errorShape(ErrorCodes.UNAVAILABLE, summary);
const payload = {
runId: params.runId,
status: "error" as const,
summary,
};
// A reserved request owns its idempotency keys even when admission fails.
// Persist the terminal failure so retries replay it instead of re-entering admission.
accepted = true;
setGatewayDedupeEntries({
dedupe: params.context.dedupe,
keys: params.agentDedupeKeys,
entry: { ts: Date.now(), ok: false, payload, error },
});
params.respond(false, payload, error, {
runId: params.runId,
error: summary,
});
};
const abortForLifecycleRotation = (target?: { sessionKey?: string; agentId?: string }) => {
if (params.lifecycleGeneration === getAgentEventLifecycleGeneration()) {
return false;
@@ -186,7 +164,6 @@ export function createAgentDedupeLifecycle(params: {
reservationId,
reserve,
clearUnaccepted,
failBeforeDispatch,
abortForLifecycleRotation,
isReserved: () => reserved,
isAccepted: () => accepted,
@@ -1,29 +0,0 @@
import { describe, expect, it } from "vitest";
import { isPreRegistrationAbortedAgentDedupeEntryForSession } from "./agent-dedupe.js";
describe("agent dedupe", () => {
it("compares admitted run identities without normalizing them", () => {
const entry = {
ts: 1,
ok: true,
payload: {
runId: " padded-agent-run ",
status: "timeout",
stopReason: "rpc",
},
} as const;
expect(
isPreRegistrationAbortedAgentDedupeEntryForSession({
entry,
runId: " padded-agent-run ",
}),
).toBe(true);
expect(
isPreRegistrationAbortedAgentDedupeEntryForSession({
entry,
runId: "padded-agent-run",
}),
).toBe(false);
});
});
+1 -2
View File
@@ -72,8 +72,7 @@ export function isPreRegistrationAbortedAgentDedupeEntryForSession(params: {
return false;
}
const payload = params.entry.payload;
const payloadRunId =
typeof payload.runId === "string" && payload.runId.trim() ? payload.runId : "";
const payloadRunId = typeof payload.runId === "string" ? payload.runId.trim() : "";
if (payloadRunId && payloadRunId !== params.runId) {
return false;
}
@@ -73,14 +73,6 @@ export function prepareAgentRequestPreflight(
return undefined;
}
const request = params.params as AgentRunRequest;
if (!request.idempotencyKey.trim()) {
params.respond(
false,
undefined,
errorShape(ErrorCodes.INVALID_REQUEST, "idempotencyKey must not be blank"),
);
return undefined;
}
const cfg = params.context.getRuntimeConfig();
const canUseInternalRuntimeHandoff = resolveCanUseInternalRuntimeHandoff(params.client);
const requestSessionKey = request.sessionKey?.trim();
@@ -278,7 +270,7 @@ export function prepareAgentRequestPreflight(
if (cached.ok && isAcceptedAgentDedupePayload(cached.payload)) {
const cachedRunId =
typeof cached.payload.runId === "string" && cached.payload.runId.trim()
? cached.payload.runId
? cached.payload.runId.trim()
: runId;
const cachedSessionKey =
typeof cached.payload.sessionKey === "string" && cached.payload.sessionKey.trim()
@@ -1,10 +1,5 @@
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import { ErrorCodes, errorShape } from "../../../packages/gateway-protocol/src/index.js";
import {
createAgentExecutionAttribution,
type AgentExecutionIdentityAdmission,
type AgentExecutionAttribution,
} from "../../agents/agent-execution-attribution.js";
import {
clearEmbeddedAgentRunAbortabilityForRunId,
isEmbeddedAgentRunAbortableForRunId,
@@ -50,7 +45,6 @@ import type { GatewayRequestHandlerOptions } from "./types.js";
export type PreparedAgentRunDispatch = {
activeGatewayWorkAdmission: SessionWorkAdmissionLease;
activeRunAbort: ReturnType<typeof registerChatAbortController>;
attribution: AgentExecutionAttribution;
effectiveProviderOverride?: string;
effectiveModelOverride?: string;
effectiveThinking?: string;
@@ -92,7 +86,6 @@ export async function prepareAgentRunDispatch(params: {
inputProvenance?: InputProvenance;
isOneShotModelRun: boolean;
isRestartRecoveryResumeRun: boolean;
executionIdentityAdmission?: AgentExecutionIdentityAdmission;
runId: string;
agentDedupeKeys: readonly string[];
context: GatewayRequestHandlerOptions["context"];
@@ -259,16 +252,6 @@ export async function prepareAgentRunDispatch(params: {
});
return undefined;
}
const attribution = createAgentExecutionAttribution({
runId: params.runId,
lifecycleGeneration: params.lifecycleGeneration,
sessionKey: params.resolvedSessionKey,
sessionId: params.getAdmittedSessionId(),
agentId: params.activeSessionAgentId,
...(params.executionIdentityAdmission
? { executionIdentityAdmission: params.executionIdentityAdmission }
: {}),
});
if (!activeRunAbort.registered) {
activeGatewayWorkAdmission.release();
} else {
@@ -280,14 +263,15 @@ export async function prepareAgentRunDispatch(params: {
});
}
if (params.resolvedSessionKey) {
claimAgentRunContext(params.runId, {
attribution,
...(attribution.sessionKey ? { sessionKey: attribution.sessionKey } : {}),
...(attribution.sessionId ? { sessionId: attribution.sessionId } : {}),
...(attribution.agentId ? { agentId: attribution.agentId } : {}),
...(params.suppressVisibleSessionEffects ? { isControlUiVisible: false } : {}),
lifecycleGeneration: attribution.lifecycleGeneration,
});
claimAgentRunContext(
params.runId,
params.suppressVisibleSessionEffects
? { isControlUiVisible: false, lifecycleGeneration: params.lifecycleGeneration }
: {
sessionKey: params.resolvedSessionKey,
lifecycleGeneration: params.lifecycleGeneration,
},
);
}
}
@@ -450,7 +434,6 @@ export async function prepareAgentRunDispatch(params: {
return {
activeGatewayWorkAdmission,
activeRunAbort,
attribution,
effectiveProviderOverride,
effectiveModelOverride,
effectiveThinking,
@@ -21,6 +21,7 @@ import {
} from "../../agents/main-session-recovery-store.js";
import { resolveScheduledToolPolicyContext } from "../../agents/scheduled-tool-policy.js";
import { resolveIngressWorkspaceOverrideForSessionRun } from "../../agents/spawned-context.js";
import { isExecutionIdentityCollectionEnabled } from "../../audit/audit-config.js";
import {
setChannelSourceTurnId,
setChannelSourceTurnSameThreadRequired,
@@ -51,7 +52,10 @@ import {
type RestoredCronContinuation,
} from "./agent-handler-helpers.js";
import type { AgentRunRequest } from "./agent-request-types.js";
import { resolveAgentRestartRecoveryChannelContext } from "./agent-restart-recovery-context.js";
import {
resolveAgentRestartRecoveryChannelContext,
resolveAgentRestartRecoveryExecutionIdentityAdmission,
} from "./agent-restart-recovery-context.js";
import type { PreparedAgentRunDispatch } from "./agent-run-admission-phase.js";
import {
resolveAbortedAgentStopReason,
@@ -325,6 +329,13 @@ export function startAgentRunExecution(params: {
params.client.internal.runtimePluginToolGrant?.pluginId
? params.client.internal.runtimePluginToolGrant
: undefined;
const executionIdentityAdmission = resolveAgentRestartRecoveryExecutionIdentityAdmission({
collectionEnabled: isExecutionIdentityCollectionEnabled(params.cfg),
isRestartRecoveryResumeRun: params.isRestartRecoveryResumeRun,
retryOnly: params.request.internalExecutionIdentityRetry,
runId: params.runId,
sessionEntry: params.sessionEntry,
});
const restartRecoveryChannelContext = resolveAgentRestartRecoveryChannelContext({
canUseInternalRuntimeHandoff: params.canUseInternalRuntimeHandoff,
expectedExistingSessionId: params.request.expectedExistingSessionId,
@@ -421,7 +432,7 @@ export function startAgentRunExecution(params: {
swarmOutputSchema: params.request.swarmOutputSchema,
forceRestartSafeTools: params.request.forceRestartSafeTools,
forceCodeModeTools: params.request.forceCodeModeTools,
executionAttribution: prepared.attribution,
...(executionIdentityAdmission ? { executionIdentityAdmission } : {}),
internalDeliveryMediaUrls: params.client?.internal?.internalDeliveryMediaUrls,
internalDeliverySuppressText: params.client?.internal?.internalDeliverySuppressText,
suppressPromptPersistence:
@@ -5,7 +5,6 @@ import {
releaseMainSessionRecoveryOwner,
type MainSessionRecoveryOwnerLease,
} from "../../agents/main-session-recovery-store.js";
import { isExecutionIdentityCollectionEnabled } from "../../audit/audit-config.js";
import { mergeSessionEntry, type SessionEntry } from "../../config/sessions.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { normalizeDeliveryContext } from "../../utils/delivery-context.shared.js";
@@ -20,7 +19,6 @@ import type { RestoredCronContinuation } from "./agent-handler-helpers.js";
import { prepareAgentRequestPreflight } from "./agent-request-preflight.js";
import { prepareAgentRequestRouting } from "./agent-request-routing.js";
import { runAgentResetPhase } from "./agent-reset-phase.js";
import { resolveAgentRestartRecoveryExecutionIdentityAdmission } from "./agent-restart-recovery-context.js";
import { prepareAgentRunDispatch } from "./agent-run-admission-phase.js";
import { startAgentRunExecution } from "./agent-run-execution-phase.js";
import { buildAgentSessionPatch } from "./agent-session-patch.js";
@@ -434,19 +432,6 @@ export const agentRunHandler: GatewayRequestHandlers["agent"] = async ({
return;
}
const { activeSessionAgentId } = delivery;
let executionIdentityAdmission;
try {
executionIdentityAdmission = resolveAgentRestartRecoveryExecutionIdentityAdmission({
collectionEnabled: isExecutionIdentityCollectionEnabled(cfg),
isRestartRecoveryResumeRun,
retryOnly: request.internalExecutionIdentityRetry,
runId,
sessionEntry,
});
} catch (error) {
dedupeLifecycle.failBeforeDispatch(formatForLog(error));
return;
}
const preparedDispatch = await prepareAgentRunDispatch({
request,
@@ -472,7 +457,6 @@ export const agentRunHandler: GatewayRequestHandlers["agent"] = async ({
inputProvenance,
isOneShotModelRun,
isRestartRecoveryResumeRun,
executionIdentityAdmission,
runId,
agentDedupeKeys,
context,
@@ -2309,99 +2309,6 @@ describe("gateway agent handler chat.abort integration", () => {
).toBe(true);
});
it("replays restart recovery identity admission failures for the same idempotency key", async () => {
const sessionKey = "agent:main:main";
const sessionId = "recovery-session";
const runId = "recovery-identity-token-unavailable";
const storePath = "/tmp/sessions.json";
const store: Record<string, SessionEntry> = {
[sessionKey]: {
sessionId,
updatedAt: Date.now() - 10_000,
status: "running",
abortedLastRun: true,
mainRestartRecovery: {
cycleId: "cycle-1",
revision: 1,
chargedAttempts: 1,
reservation: {
runId,
attempt: 1,
lifecycleGeneration: "test-generation",
},
},
},
};
mocks.loadConfigReturn = {
logging: {
audit: {
enabled: true,
executionIdentity: true,
},
},
};
mocks.loadSessionEntry.mockImplementation(() => ({
cfg: mocks.loadConfigReturn,
storePath,
entry: structuredClone(store[sessionKey]),
canonicalKey: sessionKey,
}));
mocks.updateSessionStore.mockImplementation(async (_path, updater) => await updater(store));
const context = makeContext();
const request = {
message: "resume after restart",
agentId: "main",
sessionKey,
sessionId,
expectedExistingSessionId: sessionId,
idempotencyKey: runId,
internalExecutionIdentityRetry: false,
inputProvenance: {
kind: "internal_system" as const,
sourceSessionKey: sessionKey,
sourceTool: "main_session_restart_recovery",
},
};
const firstRespond = vi.fn();
await invokeAgent(request, {
client: backendGatewayClient(),
context,
reqId: runId,
respond: firstRespond,
});
const summary = "Error: restart recovery execution identity token is unavailable";
expect(firstRespond).toHaveBeenCalledWith(
false,
{ runId, status: "error", summary },
expect.objectContaining({ code: "UNAVAILABLE", message: summary }),
{ runId, error: summary },
);
expect(context.dedupe.get(`agent:${runId}`)).toMatchObject({
ok: false,
payload: { runId, status: "error", summary },
error: { code: "UNAVAILABLE", message: summary },
});
expect(mocks.agentCommand).not.toHaveBeenCalled();
const secondRespond = vi.fn();
await invokeAgent(request, {
client: backendGatewayClient(),
context,
reqId: `${runId}-retry`,
respond: secondRespond,
});
expect(secondRespond).toHaveBeenCalledWith(
false,
{ runId, status: "error", summary },
expect.objectContaining({ code: "UNAVAILABLE", message: summary }),
{ cached: true },
);
expect(mocks.agentCommand).not.toHaveBeenCalled();
});
it("releases a foreground recovery owner if pre-dispatch reactivation fails", async () => {
const sessionKey = "agent:main:main";
const sessionId = "interrupted-session";
@@ -301,71 +301,10 @@ describe("gateway agent handler", () => {
expect(callArgs.suppressPromptPersistence).toBe(true);
expect(mocks.updateSessionStore).not.toHaveBeenCalled();
expect(context.addChatRun).not.toHaveBeenCalled();
const runContext = mockCallArg(mocks.registerAgentRunContext, 0, 1) as {
attribution: Record<string, unknown>;
};
expect(runContext).toEqual({
attribution: expect.objectContaining({
runId: "test-backend-internal-effects",
contextId: expect.any(String),
executionId: expect.any(String),
createdAt: expect.any(Number),
lifecycleGeneration: "test-generation",
sessionKey: "agent:main:main",
sessionId: "existing-session-id",
agentId: "main",
}),
sessionKey: "agent:main:main",
sessionId: "existing-session-id",
agentId: "main",
expect(mocks.registerAgentRunContext).toHaveBeenCalledWith("test-backend-internal-effects", {
isControlUiVisible: false,
lifecycleGeneration: "test-generation",
});
expect(Object.isFrozen(runContext.attribution)).toBe(true);
});
it("preserves the admitted idempotency key exactly in execution attribution", async () => {
primeMainAgentRun({ cfg: mocks.loadConfigReturn });
mocks.registerAgentRunContext.mockClear();
const runId = " padded-agent-run ";
await invokeAgent({
message: "preserve exact run identity",
agentId: "main",
sessionKey: "agent:main:main",
idempotencyKey: runId,
});
await waitForAgentCommandCall();
expect(mockCallArg(mocks.registerAgentRunContext, 0, 0)).toBe(runId);
expect(mockCallArg(mocks.registerAgentRunContext, 0, 1)).toMatchObject({
attribution: { runId },
});
});
it("rejects blank idempotency keys before registering run state", async () => {
const context = makeContext();
const respond = vi.fn();
mocks.registerAgentRunContext.mockClear();
mocks.agentCommand.mockClear();
await invokeAgent(
{
message: "reject blank run identity",
agentId: "main",
sessionKey: "agent:main:main",
idempotencyKey: " \t ",
},
{ context, respond },
);
expectRespondError(respond, {
code: ErrorCodes.INVALID_REQUEST,
message: "idempotencyKey must not be blank",
});
expect(context.chatAbortControllers.size).toBe(0);
expect(mocks.registerAgentRunContext).not.toHaveBeenCalled();
expect(mocks.agentCommand).not.toHaveBeenCalled();
});
it("allows backend internal runs without a persisted session row", async () => {
@@ -663,27 +602,10 @@ describe("gateway agent handler", () => {
expect(context.broadcastToConnIds).not.toHaveBeenCalled();
expect(mocks.getLatestSubagentRunByChildSessionKey).not.toHaveBeenCalled();
expect(mocks.replaceSubagentRunAfterSteer).not.toHaveBeenCalled();
const runContext = mockCallArg(mocks.registerAgentRunContext, 0, 1) as {
attribution: Record<string, unknown>;
};
expect(runContext).toEqual({
attribution: expect.objectContaining({
runId: "test-stateless-model-run",
contextId: expect.any(String),
executionId: expect.any(String),
createdAt: expect.any(Number),
lifecycleGeneration: "test-generation",
sessionKey: "agent:main:explicit:model-run-123e4567-e89b-12d3-a456-426614174000",
sessionId: "model-run-123e4567-e89b-12d3-a456-426614174000",
agentId: "main",
}),
sessionKey: "agent:main:explicit:model-run-123e4567-e89b-12d3-a456-426614174000",
sessionId: "model-run-123e4567-e89b-12d3-a456-426614174000",
agentId: "main",
expect(mocks.registerAgentRunContext).toHaveBeenCalledWith("test-stateless-model-run", {
isControlUiVisible: false,
lifecycleGeneration: "test-generation",
});
expect(Object.isFrozen(runContext.attribution)).toBe(true);
});
it("respects explicit bestEffortDeliver=false for main session runs", async () => {
@@ -136,6 +136,7 @@ describe("gateway agent handler", () => {
payloads: [{ text: "ok" }],
meta: { durationMs: 100 },
});
await invokeAgent(
{
message: "forged exec followup",
@@ -1265,6 +1266,7 @@ describe("gateway agent handler", () => {
payloads: [{ text: "ok" }],
meta: { durationMs: 100 },
});
await invokeAgent(
{
message: "resume channel session",
@@ -2069,7 +2071,6 @@ describe("gateway agent handler", () => {
});
it("infers selected-global agent id from agent-prefixed session aliases", async () => {
mocks.registerAgentRunContext.mockClear();
mocks.listAgentIds.mockReturnValue(["main", "work"]);
mocks.loadConfigReturn = {
agents: { list: [{ id: "main", default: true }, { id: "work" }] },
@@ -2114,24 +2115,6 @@ describe("gateway agent handler", () => {
agentId: "work",
clone: false,
});
expect(mockCallArg(mocks.registerAgentRunContext, 0, 1)).toEqual(
expect.objectContaining({
attribution: expect.objectContaining({
runId: "alias-global-session-agent-id",
contextId: expect.any(String),
executionId: expect.any(String),
createdAt: expect.any(Number),
lifecycleGeneration: "test-generation",
sessionKey: "global",
sessionId: "global-work-session-id",
agentId: "work",
}),
sessionKey: "global",
sessionId: "global-work-session-id",
agentId: "work",
lifecycleGeneration: "test-generation",
}),
);
});
it("registers tool event recipients for active selected-global alias runs", async () => {
-22
View File
@@ -1,22 +0,0 @@
import { expect, test } from "vitest";
import { emitAgentEvent, onAgentEvent } from "./agent-events.js";
import { clearAgentRunContext, registerAgentRunContext } from "./agent-run-registry.js";
test("clearAgentRunContext also cleans up seqByRun to prevent memory leak (#63643)", () => {
registerAgentRunContext("run-leak", { sessionKey: "main" });
emitAgentEvent({ runId: "run-leak", stream: "lifecycle", data: {} });
emitAgentEvent({ runId: "run-leak", stream: "lifecycle", data: {} });
clearAgentRunContext("run-leak");
const seqs: number[] = [];
const stop = onAgentEvent((evt) => {
if (evt.runId === "run-leak") {
seqs.push(evt.seq);
}
});
emitAgentEvent({ runId: "run-leak", stream: "lifecycle", data: {} });
stop();
expect(seqs).toEqual([1]);
});
+24 -40
View File
@@ -1,6 +1,5 @@
// Covers agent event sequencing and run context cleanup.
import { beforeEach, describe, expect, test, vi } from "vitest";
import { createAgentExecutionAttribution } from "../agents/agent-execution-attribution.js";
import {
type AgentEventPayload,
captureAgentRunLifecycleGeneration,
@@ -898,45 +897,6 @@ describe("agent-events sequencing", () => {
expect(context?.lastActiveAt).toBe(12_345);
});
test("keeps the first same-generation attribution private and immutable", () => {
const attribution = createAgentExecutionAttribution({
runId: "run-ctx",
lifecycleGeneration: getAgentEventLifecycleGeneration(),
sessionKey: "agent:main:main",
sessionId: "session-1",
agentId: "main",
});
const replacement = createAgentExecutionAttribution({
runId: "run-ctx",
lifecycleGeneration: attribution.lifecycleGeneration,
sessionKey: "agent:main:other",
sessionId: "session-2",
agentId: "main",
});
registerAgentRunContext("run-ctx", {
attribution,
lifecycleGeneration: attribution.lifecycleGeneration,
});
registerAgentRunContext("run-ctx", {
attribution: replacement,
lifecycleGeneration: attribution.lifecycleGeneration,
verboseLevel: "full",
});
expect(getAgentRunContext("run-ctx")?.attribution).toBe(attribution);
expect(getAgentRunContext("run-ctx")?.verboseLevel).toBe("full");
expect(Reflect.set(getAgentRunContext("run-ctx")!, "attribution", replacement)).toBe(false);
let received: AgentEventPayload | undefined;
const stop = onAgentEvent((event) => {
received = event;
});
emitAgentEvent({ runId: "run-ctx", stream: "lifecycle", data: { phase: "end" } });
stop();
expect(JSON.stringify(received)).not.toContain("attribution");
});
test("falls back to registered sessionKey when event sessionKey is blank", () => {
registerAgentRunContext("run-ctx", { sessionKey: "session-main" });
@@ -1157,3 +1117,27 @@ describe("agent-events sequencing", () => {
clock.mockRestore();
});
});
test("clearAgentRunContext also cleans up seqByRun to prevent memory leak (#63643)", () => {
// Regression test: seqByRun entries were never deleted when a run ended,
// causing unbounded growth over time.
registerAgentRunContext("run-leak", { sessionKey: "main" });
emitAgentEvent({ runId: "run-leak", stream: "lifecycle", data: {} });
emitAgentEvent({ runId: "run-leak", stream: "lifecycle", data: {} });
// After clearing run context, the sequence counter should also be removed.
clearAgentRunContext("run-leak");
// Emitting a new event on the same runId should start seq from 1 again,
// proving the old entry was deleted.
const seqs: number[] = [];
const stop = onAgentEvent((evt) => {
if (evt.runId === "run-leak") {
seqs.push(evt.seq);
}
});
emitAgentEvent({ runId: "run-leak", stream: "lifecycle", data: {} });
stop();
expect(seqs).toEqual([1]);
});
+10 -35
View File
@@ -1,14 +1,11 @@
// Owns process-local agent run context, ownership, and projection state.
import { randomUUID } from "node:crypto";
import type { AgentExecutionAttribution } from "../agents/agent-execution-attribution.js";
import type { VerboseLevel } from "../auto-reply/thinking.js";
import { resolveGlobalSingleton } from "../shared/global-singleton.js";
import { clearAgentRunUsage, resetAgentRunUsageForTest } from "./agent-run-usage.js";
/** Per-run metadata used to stamp events and gate Control UI visibility. */
type AgentRunContext = {
/** Immutable admission-owned correlation; later context merges cannot replace it. */
readonly attribution?: AgentExecutionAttribution;
sessionKey?: string;
/** Resolved agent owner, including for unscoped session keys. */
agentId?: string;
@@ -66,35 +63,6 @@ function bumpAgentRunIndexVersion(): void {
getAgentRunRegistryState().version += 1;
}
function attachAgentExecutionAttribution(
context: AgentRunContext,
attribution: AgentExecutionAttribution | undefined,
): void {
if (!attribution || context.attribution) {
return;
}
Object.defineProperty(context, "attribution", {
value: attribution,
enumerable: true,
configurable: false,
writable: false,
});
}
function createAgentRunContext(
context: AgentRunContext,
lifecycleGeneration: string,
): AgentRunContext {
const { attribution, ...fields } = context;
const stored: AgentRunContext = {
...fields,
lifecycleGeneration,
registeredAt: context.registeredAt ?? Date.now(),
};
attachAgentExecutionAttribution(stored, attribution);
return stored;
}
/** Reads the process-local version of the active-run projection inputs. */
export function readAgentRunIndexVersion(): number {
return getAgentRunRegistryState().version;
@@ -137,7 +105,11 @@ export function registerAgentRunContext(
}
const existing = state.contexts.get(runId);
if (!existing) {
state.contexts.set(runId, createAgentRunContext(context, lifecycleGeneration));
state.contexts.set(runId, {
...context,
lifecycleGeneration,
registeredAt: context.registeredAt ?? Date.now(),
});
bumpAgentRunIndexVersion();
return;
}
@@ -148,7 +120,6 @@ export function registerAgentRunContext(
) {
return;
}
attachAgentExecutionAttribution(existing, context.attribution);
let runIndexChanged = false;
if (context.sessionKey && existing.sessionKey !== context.sessionKey) {
existing.sessionKey = context.sessionKey;
@@ -270,7 +241,11 @@ export function claimAgentRunContext(
}
return claimId;
}
state.contexts.set(runId, createAgentRunContext(context, lifecycleGeneration));
state.contexts.set(runId, {
...context,
lifecycleGeneration,
registeredAt: context.registeredAt ?? Date.now(),
});
state.sequenceResetHandler?.(runId);
clearAgentRunUsage(runId);
bumpAgentRunIndexVersion();
@@ -7,13 +7,13 @@ const optionalRunIdCaller: PublicIngressOptions = {
sessionKey: "agent:main:plugin-session",
allowModelOverride: false,
};
const privateExecutionCorrelationIsHidden: "executionAttribution" extends keyof PublicIngressOptions
const privateRecoveryCorrelationIsHidden: "executionIdentityAdmission" extends keyof PublicIngressOptions
? false
: true = true;
describe("public agent ingress correlation contract", () => {
it("keeps runId optional and private execution recovery state unavailable", () => {
expect(optionalRunIdCaller).not.toHaveProperty("runId");
expect(privateExecutionCorrelationIsHidden).toBe(true);
expect(privateRecoveryCorrelationIsHidden).toBe(true);
});
});