fix: code/cli acpx reliability 20260304 (#34020)

* agents: switch claude-cli defaults to bypassPermissions

* agents: add claude-cli default args coverage

* agents: emit watchdog stall system event for cli runs

* agents: test cli watchdog stall system event

* acpx: fallback to sessions new when ensure returns no ids

* acpx tests: mock sessions new fallback path

* acpx tests: cover ensure-empty fallback flow

* skills: clarify claude print mode without pty

* docs: update cli-backends claude default args

* docs: refresh cli live test default args

* gateway tests: align live claude args defaults

* changelog: credit claude/acpx reliability fixes

* Agents: normalize legacy Claude permission flag overrides

* Tests: cover legacy Claude permission override normalization

* Changelog: note legacy Claude permission flag auto-normalization

* ACPX: fail fast when ensure/new return no session IDs

* ACPX tests: support empty sessions new fixture output

* ACPX tests: assert ensureSession failure when IDs missing

* CLI runner: scope watchdog heartbeat wake to session

* CLI runner tests: assert session-scoped watchdog wake

* Update CHANGELOG.md
This commit is contained in:
Vincent Koc
2026-03-03 22:15:28 -08:00
committed by GitHub
parent dfb4cb87f9
commit 4d183af0cf
12 changed files with 362 additions and 30 deletions

View File

@@ -75,14 +75,35 @@ const setValue = command === "set" ? String(args[commandIndex + 2] || "") : "";
if (command === "sessions" && args[commandIndex + 1] === "ensure") {
writeLog({ kind: "ensure", agent, args, sessionName: ensureName });
emitJson({
action: "session_ensured",
acpxRecordId: "rec-" + ensureName,
acpxSessionId: "sid-" + ensureName,
agentSessionId: "inner-" + ensureName,
name: ensureName,
created: true,
});
if (process.env.MOCK_ACPX_ENSURE_EMPTY === "1") {
emitJson({ action: "session_ensured", name: ensureName });
} else {
emitJson({
action: "session_ensured",
acpxRecordId: "rec-" + ensureName,
acpxSessionId: "sid-" + ensureName,
agentSessionId: "inner-" + ensureName,
name: ensureName,
created: true,
});
}
process.exit(0);
}
if (command === "sessions" && args[commandIndex + 1] === "new") {
writeLog({ kind: "new", agent, args, sessionName: ensureName });
if (process.env.MOCK_ACPX_NEW_EMPTY === "1") {
emitJson({ action: "session_created", name: ensureName });
} else {
emitJson({
action: "session_created",
acpxRecordId: "rec-" + ensureName,
acpxSessionId: "sid-" + ensureName,
agentSessionId: "inner-" + ensureName,
name: ensureName,
created: true,
});
}
process.exit(0);
}

View File

@@ -377,4 +377,51 @@ describe("AcpxRuntime", () => {
expect(report.code).toBe("ACP_BACKEND_UNAVAILABLE");
expect(report.installCommand).toContain("acpx");
});
it("falls back to 'sessions new' when 'sessions ensure' returns no session IDs", async () => {
process.env.MOCK_ACPX_ENSURE_EMPTY = "1";
try {
const { runtime, logPath } = await createMockRuntimeFixture();
const handle = await runtime.ensureSession({
sessionKey: "agent:claude:acp:fallback-test",
agent: "claude",
mode: "persistent",
});
expect(handle.backend).toBe("acpx");
expect(handle.acpxRecordId).toBe("rec-agent:claude:acp:fallback-test");
expect(handle.agentSessionId).toBe("inner-agent:claude:acp:fallback-test");
const logs = await readMockRuntimeLogEntries(logPath);
expect(logs.some((entry) => entry.kind === "ensure")).toBe(true);
expect(logs.some((entry) => entry.kind === "new")).toBe(true);
} finally {
delete process.env.MOCK_ACPX_ENSURE_EMPTY;
}
});
it("fails with ACP_SESSION_INIT_FAILED when both ensure and new omit session IDs", async () => {
process.env.MOCK_ACPX_ENSURE_EMPTY = "1";
process.env.MOCK_ACPX_NEW_EMPTY = "1";
try {
const { runtime, logPath } = await createMockRuntimeFixture();
await expect(
runtime.ensureSession({
sessionKey: "agent:claude:acp:fallback-fail",
agent: "claude",
mode: "persistent",
}),
).rejects.toMatchObject({
code: "ACP_SESSION_INIT_FAILED",
message: expect.stringContaining("neither 'sessions ensure' nor 'sessions new'"),
});
const logs = await readMockRuntimeLogEntries(logPath);
expect(logs.some((entry) => entry.kind === "ensure")).toBe(true);
expect(logs.some((entry) => entry.kind === "new")).toBe(true);
} finally {
delete process.env.MOCK_ACPX_ENSURE_EMPTY;
delete process.env.MOCK_ACPX_NEW_EMPTY;
}
});
});

View File

@@ -179,7 +179,7 @@ export class AcpxRuntime implements AcpRuntime {
const cwd = asTrimmedString(input.cwd) || this.config.cwd;
const mode = input.mode;
const events = await this.runControlCommand({
let events = await this.runControlCommand({
args: this.buildControlArgs({
cwd,
command: [agent, "sessions", "ensure", "--name", sessionName],
@@ -187,12 +187,36 @@ export class AcpxRuntime implements AcpRuntime {
cwd,
fallbackCode: "ACP_SESSION_INIT_FAILED",
});
const ensuredEvent = events.find(
let ensuredEvent = events.find(
(event) =>
asOptionalString(event.agentSessionId) ||
asOptionalString(event.acpxSessionId) ||
asOptionalString(event.acpxRecordId),
);
if (!ensuredEvent) {
events = await this.runControlCommand({
args: this.buildControlArgs({
cwd,
command: [agent, "sessions", "new", "--name", sessionName],
}),
cwd,
fallbackCode: "ACP_SESSION_INIT_FAILED",
});
ensuredEvent = events.find(
(event) =>
asOptionalString(event.agentSessionId) ||
asOptionalString(event.acpxSessionId) ||
asOptionalString(event.acpxRecordId),
);
if (!ensuredEvent) {
throw new AcpRuntimeError(
"ACP_SESSION_INIT_FAILED",
`ACP session init failed: neither 'sessions ensure' nor 'sessions new' returned valid session identifiers for ${sessionName}.`,
);
}
}
const acpxRecordId = ensuredEvent ? asOptionalString(ensuredEvent.acpxRecordId) : undefined;
const agentSessionId = ensuredEvent ? asOptionalString(ensuredEvent.agentSessionId) : undefined;
const backendSessionId = ensuredEvent