Repository navigation
Goal extension is unavailable to capable non-AIR clients (gated on client identity, not capability) #582
Description
Summary
Third-party ACP clients cannot receive structured goal state at all, because the goal extension is gated behind the client declaring the JetBrains AIR namespace (clientCapabilities._meta.jetbrains.air). The only non-AIR fallback is the /goal slash command, which works but its resulting goal state is invisible: goal snapshots are emitted as session_info_update._meta.jetbrains.air.goal only when isAirClient() is true, and are dropped (null) otherwise.
This makes it impossible for a non-AIR client to (a) discover goal support, (b) display goal progress, or (c) send _session/goal control requests.
Details
initialize response capability construction (codex-acp):
// Only AIR gets the AIR extension, see `docs/air-extensions.md`.
...this.capabilities.airClient ? {
[JETBRAINS_META_KEY]: {
[AIR_META_KEY]: {
[AIR_EXTENSION_VERSION_KEY]: AIR_EXTENSION_VERSION,
[AIR_GOAL_KEY]: goalCapability, // ← only here
...
}
}
} : {}Goal snapshots (codex-acp src/ThreadGoalSnapshot.ts):
function goalSessionInfoUpdate(goal, airClient) {
if (!airClient) return null; // ← dropped entirely for non-AIR
return {
sessionUpdate: "session_info_update",
_meta: withAirMeta(void 0, AIR_GOAL_KEY, goal)
};
}docs/air-extensions.md documents this ("Only AIR gets the goal capability and the goal snapshots"), but the goal extension is explicitly shaped "like a possible future first-class ACP API" and — unlike diffPatch, rawInputRendering, planContentDelta, etc. — has no client capability of its own. It is gated on client identity (declaring the AIR namespace), not on client capability. There is no way for a capable non-JetBrains client to opt in.
Issue #260 asked for structured goal state and was closed by the AIR-only implementation, so from a third-party client's perspective the underlying request remains unaddressed.
Why this is awkward
The negotiated contract in docs/air-extensions.md says goal support is advertised with {version, controlMethod, actions} and consumed by sending _session/goal — a clean, provider-neutral contract any client could implement. The gating on AIR identity is the only thing preventing that. Gating on an explicit capability list (capabilities: ["goal"] inside the client's declaration, or even a top-level _meta.goal: {} opt-in) would keep the "no accidental readers" property while letting implementing clients declare intent.
The same gating exists in claude-agent-acp (publishGoal returns early: "The goal is an AIR extension: only AIR gets it"), so a fix would apply to both adapters.
Suggested fix
Make the goal extension (capability in the initialize response + goal snapshots in session_info_update) available to any client that opts in, e.g.:
- Option A: keep the AIR namespace but key delivery off a client capability declaration (
capabilities: ["goal"]) rather than AIR identity alone. - Option B: also honor a top-level
_meta.goal: {}declaration inclientCapabilities(mirroring how Alas and other clients already read_meta.goalsymmetrically in the response).
Non-AIR behavior of everything else (tool-call contract, permission presentation, etc.) is unaffected; this is only about the self-contained goal contract, which already defines its own error surface (unknown actions, blank objective) and needs no other AIR metadata to function.
Activity