Skip to content

Goal extension is unavailable to capable non-AIR clients (gated on client identity, not capability) #582

Open

Description

@mrmans0n

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 in clientCapabilities (mirroring how Alas and other clients already read _meta.goal symmetrically 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions