fix: resolve oneshot ACP identities before close

This commit is contained in:
Peter Steinberger
2026-04-25 22:15:46 +01:00
parent cbfc0ddfd1
commit 8f1a214a23
3 changed files with 90 additions and 9 deletions

View File

@@ -87,6 +87,9 @@ Docs: https://docs.openclaw.ai
- ACP/sessions_spawn: apply `runTimeoutSeconds` to ACP child turns and dispatch
those turns on the background subagent lane, so quota-stalled ACP harnesses do
not occupy the main agent lane indefinitely. Fixes #68823.
- ACP/oneshot: reconcile runtime session identity before closing completed
oneshot ACP runs, so finished `sessions.json` entries do not stay stuck with
`acp.identity.state="pending"`.
- ACP/models: document that non-Codex ACP model overrides require adapter
support for ACP `models` plus `session/set_model`, so unsupported harnesses
fail clearly instead of silently falling back to their defaults.

View File

@@ -929,15 +929,8 @@ export class AcpSessionManager {
if (activeTurn && this.activeTurnBySession.get(actorKey) === activeTurn) {
this.activeTurnBySession.delete(actorKey);
}
if (
!retryFreshHandle &&
!skipPostTurnCleanup &&
runtime &&
handle &&
meta &&
meta.mode !== "oneshot"
) {
({ handle } = await this.reconcileRuntimeSessionIdentifiers({
if (!retryFreshHandle && !skipPostTurnCleanup && runtime && handle && meta) {
({ handle, meta } = await this.reconcileRuntimeSessionIdentifiers({
cfg: input.cfg,
sessionKey,
runtime,

View File

@@ -2399,6 +2399,91 @@ describe("AcpSessionManager", () => {
expect(currentMeta.identity?.agentSessionId).toBe("agent-fresh");
});
it("reconciles oneshot ACP identity from runtime status before closing after a turn", async () => {
const runtimeState = createRuntime();
runtimeState.ensureSession.mockResolvedValue({
sessionKey: "agent:codex:acp:session-1",
backend: "acpx",
runtimeSessionName: "runtime-1",
backendSessionId: "acpx-oneshot",
});
runtimeState.getStatus.mockResolvedValue({
summary: "status=done",
backendSessionId: "acpx-oneshot",
agentSessionId: "agent-oneshot",
details: { status: "done" },
});
hoisted.requireAcpRuntimeBackendMock.mockReturnValue({
id: "acpx",
runtime: runtimeState.runtime,
});
let currentMeta: SessionAcpMeta | undefined;
hoisted.readAcpSessionEntryMock.mockImplementation((paramsUnknown: unknown) => {
const sessionKey =
(paramsUnknown as { sessionKey?: string }).sessionKey ?? "agent:codex:acp:session-1";
return {
sessionKey,
storeSessionKey: sessionKey,
acp: currentMeta,
};
});
hoisted.upsertAcpSessionMetaMock.mockImplementation(async (paramsUnknown: unknown) => {
const params = paramsUnknown as {
mutate: (
current: SessionAcpMeta | undefined,
entry: { acp?: SessionAcpMeta } | undefined,
) => SessionAcpMeta | null | undefined;
};
const next = params.mutate(currentMeta, { acp: currentMeta });
if (next) {
currentMeta = next;
}
return {
sessionId: "session-1",
updatedAt: Date.now(),
acp: currentMeta,
};
});
const manager = new AcpSessionManager();
await manager.initializeSession({
cfg: baseCfg,
sessionKey: "agent:codex:acp:session-1",
agent: "codex",
mode: "oneshot",
});
expect(currentMeta?.identity).toMatchObject({
state: "pending",
acpxSessionId: "acpx-oneshot",
source: "ensure",
});
await manager.runTurn({
cfg: baseCfg,
sessionKey: "agent:codex:acp:session-1",
text: "do work",
mode: "prompt",
requestId: "run-1",
});
expect(runtimeState.getStatus).toHaveBeenCalledTimes(2);
expect(runtimeState.close).toHaveBeenCalledWith({
handle: expect.objectContaining({
backendSessionId: "acpx-oneshot",
agentSessionId: "agent-oneshot",
}),
reason: "oneshot-complete",
});
expect(currentMeta?.identity).toMatchObject({
state: "resolved",
acpxSessionId: "acpx-oneshot",
agentSessionId: "agent-oneshot",
source: "status",
});
});
it("reconciles pending ACP identities during startup scan", async () => {
const runtimeState = createRuntime();
runtimeState.getStatus.mockResolvedValue({