mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 20:05:46 -06:00
fix(acp): capture caller lifecycle before setup
This commit is contained in:
@@ -788,6 +788,73 @@ describe("tryDispatchAcpReply", () => {
|
||||
clearAgentRunContext(runId, replacementGeneration);
|
||||
});
|
||||
|
||||
it("captures caller-owned ACP lifecycle before lazy dispatch setup yields", async () => {
|
||||
setReadyAcpResolution();
|
||||
const runId = "caller-reused-during-setup";
|
||||
const admittedGeneration = getAgentEventLifecycleGeneration();
|
||||
claimAgentRunContext(runId, {
|
||||
agentId: "caller",
|
||||
lifecycleGeneration: admittedGeneration,
|
||||
sessionKey,
|
||||
});
|
||||
const admittedToken = getAgentRunContextLifecycleToken(runId, admittedGeneration);
|
||||
let startGeneration: string | undefined;
|
||||
let startToken: object | undefined;
|
||||
let callbackGeneration: string | undefined;
|
||||
let callbackToken: object | undefined;
|
||||
let terminalGeneration: string | undefined;
|
||||
let terminalToken: object | undefined;
|
||||
|
||||
auditMocks.emitAcpLifecycleStart.mockImplementationOnce(
|
||||
(params: { lifecycleGeneration?: string }) => {
|
||||
startGeneration = params.lifecycleGeneration;
|
||||
startToken = getAgentRunExecutionContextLifecycleToken(runId);
|
||||
},
|
||||
);
|
||||
auditMocks.emitAcpRuntimeEvent.mockImplementationOnce(() => {
|
||||
callbackGeneration = getAgentRunExecutionLifecycleGeneration();
|
||||
callbackToken = getAgentRunExecutionContextLifecycleToken(runId);
|
||||
});
|
||||
auditMocks.emitAcpLifecycleEnd.mockImplementationOnce(() => {
|
||||
terminalGeneration = getAgentRunExecutionLifecycleGeneration();
|
||||
terminalToken = getAgentRunExecutionContextLifecycleToken(runId);
|
||||
});
|
||||
managerMocks.runTurn.mockImplementationOnce(
|
||||
async ({ onEvent }: { onEvent?: (event: unknown) => Promise<void> }) => {
|
||||
if (onEvent) {
|
||||
await emitToolLifecycleEvents(onEvent, "tool-after-setup-reuse");
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const dispatch = runDispatch({ bodyForAgent: "audit setup race", runId });
|
||||
const replacementGeneration = rotateAgentEventLifecycleGeneration();
|
||||
claimAgentRunContext(runId, {
|
||||
agentId: "replacement",
|
||||
lifecycleGeneration: replacementGeneration,
|
||||
sessionKey: "agent:replacement:main",
|
||||
});
|
||||
const replacementToken = getAgentRunContextLifecycleToken(runId, replacementGeneration);
|
||||
|
||||
await dispatch;
|
||||
|
||||
expect(admittedToken).toBeTypeOf("object");
|
||||
expect(replacementToken).toBeTypeOf("object");
|
||||
expect(replacementToken).not.toBe(admittedToken);
|
||||
expect(startGeneration).toBe(admittedGeneration);
|
||||
expect(startToken).toBe(admittedToken);
|
||||
expect(callbackGeneration).toBe(admittedGeneration);
|
||||
expect(callbackToken).toBe(admittedToken);
|
||||
expect(terminalGeneration).toBe(admittedGeneration);
|
||||
expect(terminalToken).toBe(admittedToken);
|
||||
expect(getAgentRunContext(runId)).toMatchObject({
|
||||
agentId: "replacement",
|
||||
lifecycleGeneration: replacementGeneration,
|
||||
sessionKey: "agent:replacement:main",
|
||||
});
|
||||
clearAgentRunContext(runId, replacementGeneration);
|
||||
});
|
||||
|
||||
it("keeps audit run ids unique when channel message ids repeat", async () => {
|
||||
setReadyAcpResolution();
|
||||
|
||||
|
||||
@@ -22,8 +22,10 @@ import {
|
||||
captureAgentRunLifecycleGeneration,
|
||||
withAgentRunLifecycleGeneration,
|
||||
} from "../../infra/agent-events.js";
|
||||
import { captureAgentRunExecutionContextLifecycleToken } from "../../infra/agent-run-execution-context.js";
|
||||
import {
|
||||
claimAgentRunContext,
|
||||
getAgentRunContextLifecycleToken,
|
||||
releaseAgentRunContext,
|
||||
retainActiveAgentRunContext,
|
||||
} from "../../infra/agent-run-registry.js";
|
||||
@@ -455,6 +457,17 @@ export async function tryDispatchAcpReply(params: {
|
||||
return null;
|
||||
}
|
||||
|
||||
const existingRunId = normalizeOptionalString(params.runId);
|
||||
// Lazy ACP setup can yield while a caller-owned run id is rotated and reused.
|
||||
// Snapshot both ownership identities now so later audit callbacks cannot rebound.
|
||||
const admittedAuditLifecycleGeneration = existingRunId
|
||||
? captureAgentRunLifecycleGeneration(existingRunId)
|
||||
: undefined;
|
||||
const admittedAuditContextLifecycleToken =
|
||||
existingRunId && admittedAuditLifecycleGeneration
|
||||
? getAgentRunContextLifecycleToken(existingRunId, admittedAuditLifecycleGeneration)
|
||||
: undefined;
|
||||
|
||||
const { getAcpSessionManager } = await loadDispatchAcpManagerRuntime();
|
||||
const acpManager = getAcpSessionManager();
|
||||
const acpResolution = acpManager.resolveSession({
|
||||
@@ -581,14 +594,13 @@ export async function tryDispatchAcpReply(params: {
|
||||
markIdle: params.markIdle,
|
||||
});
|
||||
const requestId = resolveAcpRequestId(params.ctx);
|
||||
const existingRunId = normalizeOptionalString(params.runId);
|
||||
const auditOnly = existingRunId === undefined;
|
||||
const auditRunId = existingRunId ?? generateSecureUuid();
|
||||
const auditRuntime = await loadDispatchAcpAuditRuntime();
|
||||
const auditToolTracker = auditRuntime.createAcpToolLifecycleTracker();
|
||||
let auditStarted = false;
|
||||
let auditFinished = false;
|
||||
let auditLifecycleGeneration: string | undefined;
|
||||
let auditLifecycleGeneration = admittedAuditLifecycleGeneration;
|
||||
let auditContextOwnerToken: string | undefined;
|
||||
let releaseAuditContextLease: (() => void) | undefined;
|
||||
let auditTerminalOutcome: "blocked" | undefined;
|
||||
@@ -639,7 +651,16 @@ export async function tryDispatchAcpReply(params: {
|
||||
const runWithAuditLifecycle = <T>(run: () => T): T => {
|
||||
claimAuditContext();
|
||||
return auditLifecycleGeneration
|
||||
? withAgentRunLifecycleGeneration(auditLifecycleGeneration, run)
|
||||
? withAgentRunLifecycleGeneration(auditLifecycleGeneration, () => {
|
||||
if (admittedAuditContextLifecycleToken) {
|
||||
captureAgentRunExecutionContextLifecycleToken(
|
||||
auditRunId,
|
||||
auditLifecycleGeneration,
|
||||
admittedAuditContextLifecycleToken,
|
||||
);
|
||||
}
|
||||
return run();
|
||||
})
|
||||
: run();
|
||||
};
|
||||
const releaseAuditContext = () => {
|
||||
|
||||
Reference in New Issue
Block a user