Compare commits

..

1 Commits

Author SHA1 Message Date
mark.tkachenko
3dba368bc4 refactor: move the v1 steering and async task stop requests out of acp-agent.ts 2026-11-10 18:39:40 +01:00
104 changed files with 468 additions and 3128 deletions

View File

@@ -1,3 +1,3 @@
{
".": "0.106.0"
".": "0.104.1"
}

View File

@@ -1,36 +1,5 @@
# Changelog
## [0.106.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.105.0...v0.106.0) (2026-11-10)
### Features
* report the auth detail as the account's description ([#1591](https://github.com/agentclientprotocol/claude-agent-acp/issues/1591)) ([f6d6429](https://github.com/agentclientprotocol/claude-agent-acp/commit/f6d64291876177f9f593415abfc7e50eb7e7ff56))
* serve session/options and initial config options ([#1592](https://github.com/agentclientprotocol/claude-agent-acp/issues/1592)) ([b1763c7](https://github.com/agentclientprotocol/claude-agent-acp/commit/b1763c750aef83fd05ce16901ff1a0d9b4b90234))
### Bug Fixes
* honour the client's settings option in idle compaction ([#1555](https://github.com/agentclientprotocol/claude-agent-acp/issues/1555)) ([b73ee49](https://github.com/agentclientprotocol/claude-agent-acp/commit/b73ee491c8f60a1584a95a7f03557f356e1a991c))
* replay the messages before each compaction on ACP v1 and v2 ([#1430](https://github.com/agentclientprotocol/claude-agent-acp/issues/1430)) ([626015b](https://github.com/agentclientprotocol/claude-agent-acp/commit/626015bf81fd34f952406ae77bf60feb9ca39a16))
* stop the release executable from loading the project's .env and bunfig.toml ([#1589](https://github.com/agentclientprotocol/claude-agent-acp/issues/1589)) ([4a3ff6a](https://github.com/agentclientprotocol/claude-agent-acp/commit/4a3ff6a1eb16075b96676616ab2bbb98694a4d24))
* update to @agentclientprotocol/sdk 2.6.0 ([#1590](https://github.com/agentclientprotocol/claude-agent-acp/issues/1590)) ([9fca22a](https://github.com/agentclientprotocol/claude-agent-acp/commit/9fca22a03bdec5dc265bfdc42df316fb286a8142))
## [0.105.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.104.1...v0.105.0) (2026-11-10)
### Features
* serve session/set_title on ACP v1 and v2 ([#1585](https://github.com/agentclientprotocol/claude-agent-acp/issues/1585)) ([a4a8534](https://github.com/agentclientprotocol/claude-agent-acp/commit/a4a8534c23b08cdfd7f656d25f9ab16068fd8ffd))
* serve session/status on v1 and v2 ([#1584](https://github.com/agentclientprotocol/claude-agent-acp/issues/1584)) ([6bc2e05](https://github.com/agentclientprotocol/claude-agent-acp/commit/6bc2e058cb27624b4307462ac46167535af400d1))
### Bug Fixes
* include inline model settings in the model picker ([#1488](https://github.com/agentclientprotocol/claude-agent-acp/issues/1488)) ([36cb674](https://github.com/agentclientprotocol/claude-agent-acp/commit/36cb6745897bf9dabcb967b54233c42b860436b3))
* keep a v2 session idle when the model changes between turns ([#1586](https://github.com/agentclientprotocol/claude-agent-acp/issues/1586)) ([f5d7916](https://github.com/agentclientprotocol/claude-agent-acp/commit/f5d79167a649737bdeb1ce3b55ad449a34bb9d47))
* report Claude Code's API error message as a notice in v2 ([#1582](https://github.com/agentclientprotocol/claude-agent-acp/issues/1582)) ([99cc1f3](https://github.com/agentclientprotocol/claude-agent-acp/commit/99cc1f324586fad07f1da14bb9764cd16c64e920))
## [0.104.1](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.104.0...v0.104.1) (2026-11-10)

View File

@@ -140,8 +140,7 @@ for v2 is negotiated down to v1. With `v2`, the adapter rejects the `initialize`
On v2 the adapter serves the session baseline (`session/new`, `session/list`, `session/resume`
with replay, `session/close`, `session/prompt`, `session/cancel`), `session/delete`,
`session/fork`, `session/status`, `session/options` (with initial `configOptions` on `session/new` and
`session/resume`), `session/set_title`, additional directories, config options (the mode is one), plans, notices,
`session/fork`, additional directories, config options (the mode is one), plans, notices,
compaction, display terminals for commands, permission requests, elicitation, providers, auth, and
[subagents](#subagent-sessions). Steering is v1 only for now.

View File

@@ -228,7 +228,7 @@ The adapter keeps them where they are.
## Auth source display
The connection-scoped `_auth/status_update` continues to report agent-owned authentication, not client-owned `providers/set` routing. The adapter also reports the same identity through the ACP `auth/status` method and `auth/status_update` notification (advertised as `auth.status`), with `kind`, `detail` and `plan` under the account's `_meta.claudeCode`. The account's `description` carries the `detail` too, e.g. `label: "Anthropic API key"` with `description: "ANTHROPIC_API_KEY"`. Each notification is sent when its own payload changed. `auth/status` answers with the identity last reported and starts a new read, whose change arrives as a notification. When the active key source is `apiKeyHelper`, the adapter reads the effective helper command through SDK `resolveSettings` / the session's `SettingsManager`, including programmatic settings overrides.
The connection-scoped `_auth/status_update` continues to report agent-owned authentication, not client-owned `providers/set` routing. The adapter also reports the same identity through the ACP `auth/status` method and `auth/status_update` notification (advertised as `auth.status`), with `kind`, `detail` and `plan` under the account's `_meta.claudeCode`. The account's `label` also carries a `detail` in parentheses, e.g. `Anthropic API key (ANTHROPIC_API_KEY)`. Each notification is sent when its own payload changed. `auth/status` answers with the identity last reported and starts a new read, whose change arrives as a notification. When the active key source is `apiKeyHelper`, the adapter reads the effective helper command through SDK `resolveSettings` / the session's `SettingsManager`, including programmatic settings overrides.
A command containing the whole word `central` and the word sequence `proxy start` is displayed as:
@@ -1408,8 +1408,6 @@ Each row carries `_meta.jetbrains.air` with flat row fields of the RFDs. `archiv
`_session/rename { "sessionId": "…", "title": "…" }` returns `{}`.
ACP's `session/set_title` renames the same way for every client, on v1 and v2, without `sessionIndex`, and returns `{ "title": "…" }`: the title as `session/list` shows it to the client. On v1 an AIR client gets it without the archive prefix, and every other client, and every v2 client, with it.
- A blank title is `-32602`. The adapter collapses whitespace and keeps the first 200 characters, trimmed, as the CLI does. An `[archived] ` prefix of the requested title is dropped: a rename never changes the archive state.
- The title is stored as the title records of [Archive](#archive): a `custom-title` and an `agent-name` record. An archived session keeps its `[archived] ` prefix (and the title is cut to 200 characters with it), so a rename does not unarchive it.
- A session that runs here on a CLI without the `rename_session` control request is refused with `-32600`: its CLI holds the title and would write it back.

12
package-lock.json generated
View File

@@ -1,15 +1,15 @@
{
"name": "@agentclientprotocol/claude-agent-acp",
"version": "0.106.0",
"version": "0.104.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@agentclientprotocol/claude-agent-acp",
"version": "0.106.0",
"version": "0.104.1",
"license": "Apache-2.0",
"dependencies": {
"@agentclientprotocol/sdk": "2.6.0",
"@agentclientprotocol/sdk": "2.5.0",
"@anthropic-ai/claude-agent-sdk": "0.3.322",
"diff": "9.0.0",
"zod": "4.6.7"
@@ -38,9 +38,9 @@
}
},
"node_modules/@agentclientprotocol/sdk": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-2.6.0.tgz",
"integrity": "sha512-3FtL6MkAV5F9qxg0FQV8wmBszIm2xuVPoDWOILIFSDQ+Vq3Twdh7Cw6Hk2GOPUbevgrTUpUAqZXQy7Geaj79mw==",
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-2.5.0.tgz",
"integrity": "sha512-MFlXeIuwN6nZSK45SAsSnVjbLgQ2r4fnthoF1lQoGJP3PdaPRpOZeCF2Dds8S4cN8yZP5QbxB1bkJccGskmcYw==",
"license": "Apache-2.0",
"peerDependencies": {
"@modelcontextprotocol/server": "^2.4.0",

View File

@@ -3,7 +3,7 @@
"publishConfig": {
"access": "public"
},
"version": "0.106.0",
"version": "0.104.1",
"description": "An ACP-compatible coding agent powered by the Claude Agent SDK (TypeScript)",
"main": "dist/lib.js",
"types": "dist/lib.d.ts",
@@ -68,7 +68,7 @@
"node": ">=22"
},
"dependencies": {
"@agentclientprotocol/sdk": "2.6.0",
"@agentclientprotocol/sdk": "2.5.0",
"@anthropic-ai/claude-agent-sdk": "0.3.322",
"diff": "9.0.0",
"zod": "4.6.7"

View File

@@ -74,24 +74,9 @@ try {
process.exit(1);
}
// By default a compiled executable loads `.env`, `.env.local` and `bunfig.toml`
// from its working directory at startup. That directory is the user's project:
// its dotenv values would reach Claude and every Bash command as real
// environment variables (overriding the project's own `.env.test`), and a
// `bunfig.toml` that fails to parse would stop the agent from starting.
const bun = spawnSync(
"bun",
[
"build",
"src/binary-entry.ts",
"--compile",
"--no-compile-autoload-dotenv",
"--no-compile-autoload-bunfig",
"--target",
target,
"--outfile",
outfile,
],
["build", "src/binary-entry.ts", "--compile", "--target", target, "--outfile", outfile],
{ cwd: repoRoot, stdio: "inherit", shell: process.platform === "win32" },
);
if (bun.error) {

View File

@@ -58,9 +58,8 @@ function check(ok, label, detail) {
// ---------------------------------------------------------------- basic
function runSync(args, [cmd, ...cmdArgs] = [command, ...commandArgs], cwd) {
function runSync(args, [cmd, ...cmdArgs] = [command, ...commandArgs]) {
const result = spawnSync(cmd, [...cmdArgs, ...args], {
cwd,
encoding: "utf8",
timeout: 60_000,
});
@@ -102,26 +101,6 @@ if (positionals.length === 0) {
"without claude next to it, says the CLI is missing",
missing.stdout || missing.error,
);
// Started in a project directory, the executable must not read the
// project's dotenv files or bunfig.toml. Node stands in for `claude`, to
// print the environment the CLI is spawned with.
copyFileSync(process.execPath, join(scratch, `claude${exe}`));
const project = join(scratch, "project");
mkdirSync(project);
writeFileSync(join(project, ".env"), "ACP_SMOKE_DOTENV=leaked\n");
writeFileSync(join(project, ".env.local"), "ACP_SMOKE_DOTENV_LOCAL=leaked\n");
writeFileSync(join(project, "bunfig.toml"), "= not toml\n");
const probe = runSync(
["--cli", "-p", "[process.env.ACP_SMOKE_DOTENV, process.env.ACP_SMOKE_DOTENV_LOCAL].join()"],
[alone],
project,
);
check(
probe.stdout === ",",
"ignores .env, .env.local and bunfig.toml in the working directory",
probe.stdout || probe.error,
);
} finally {
rmSync(scratch, { recursive: true, force: true });
}

View File

@@ -104,10 +104,10 @@ export type AcpAuthStatus = Pick<AuthStatusResponse, "state" | "accounts">;
* - No identity read yet (`undefined`): `unknown`, no accounts.
* - `kind: "none"`: `unauthenticated`, no accounts.
* - Any other kind: `authenticated`, with the identity as the one account,
* marked `active`. `detail` is the account's `description`, e.g. label
* "Anthropic API key" with description "ANTHROPIC_API_KEY".
* `_meta.claudeCode` carries the `kind` and `plan`, and the `detail` again
* for clients that read it there.
* marked `active`. The account has no field for `detail`, so its label
* carries it in parentheses, e.g. "Anthropic API key (ANTHROPIC_API_KEY)",
* for clients that show only the standard fields. `_meta.claudeCode`
* carries the `kind`, `detail` and `plan` on their own.
*
* `--hide-claude-auth` does not change the payload: it reports the login the
* CLI holds, also when the subscription guard refuses to use it.
@@ -119,8 +119,8 @@ export function toAcpAuthStatus(status: AuthStatus | undefined): AcpAuthStatus {
if (status.kind === "none") {
return { state: "unauthenticated" };
}
const account: AuthAccount = { label: status.label, active: true };
if (status.detail) account.description = status.detail;
const label = status.detail ? `${status.label} (${status.detail})` : status.label;
const account: AuthAccount = { label, active: true };
if (status.account?.email) account.email = status.account.email;
if (status.account?.organization) account.organization = status.account.organization;
account._meta = {

View File

@@ -6,10 +6,11 @@ import {
import {
forkSession as forkClaudeSession,
getSessionMessages,
importSessionToStore,
type SessionStore,
type SessionStoreEntry,
} from "@anthropic-ai/claude-agent-sdk";
import { createHash } from "node:crypto";
import { loadFullSessionHistory } from "./resumed-session.js";
import { assistantMessageText } from "./session-failure-extension.js";
import { SessionTiming } from "./session-timing.js";
@@ -68,6 +69,19 @@ function forkPointMessageIdCandidates(messageId: string): string[] {
return protocolMessageId === messageId ? [messageId] : [messageId, protocolMessageId];
}
async function loadFullSessionHistory(sessionId: string): Promise<SessionStoreEntry[]> {
// getSessionMessages returns only the active parentUuid chain. The import keeps inactive branches that AIR can still reference.
const entries: SessionStoreEntry[] = [];
const store: SessionStore = {
append: async (_key, batch) => {
entries.push(...batch);
},
load: async () => null,
};
await importSessionToStore(sessionId, store, { includeSubagents: false });
return entries;
}
function assistantGroups(
entries: SessionStoreEntry[],
messageIdForGrouping: ForkSessionDependencies["messageIdForGrouping"],
@@ -221,7 +235,6 @@ async function forkTranscript(
.find((message) => dependencies.messageIdForGrouping(message) === candidateId)?.uuid,
)
.find(Boolean);
// getSessionMessages returns only the active parentUuid chain. The full history keeps inactive branches that AIR can still reference.
const fullHistoryUuid = messageUuid
? undefined
: resolveFromFullHistory(

View File

@@ -5,8 +5,7 @@ export type IdleCompactionHost = {
isBusy(): boolean;
/** Context size after the last main-thread request, or undefined if unknown. */
contextTokens(): number | undefined;
/** Current Claude Code settings, the client's `settings` option included
* (`SettingsManager.getCompactionSettings`). */
/** Current merged Claude Code settings. */
settings(): {
idleCompaction?: boolean;
autoCompactEnabled?: boolean;

View File

@@ -1,63 +0,0 @@
/**
* The unstable "Session options" RFD, as both cores serve it: `session/options`
* answers with what a session would start with, and a setup request's
* `configOptions` set the initial values of the session's config options.
*/
import { RequestError } from "@agentclientprotocol/sdk/unstable";
import type { Logger } from "./logger.js";
/**
* What a core advertises as `options` in its session capabilities:
* `session/options`, and `configOptions` on its setup requests.
*/
export const SESSION_OPTIONS_CAPABILITY = {};
const INVALID_PARAMS = RequestError.invalidParams().code;
/**
* The entries of a setup request's `configOptions`. Omitted and `null` are no
* entries. Two entries with the same `configId` fail with `-32602`. Call it
* before the session starts, so a refused request starts none.
*/
export function initialConfigOptions<E extends { configId: string }>(
entries: readonly E[] | null | undefined,
): readonly E[] {
const seen = new Set<string>();
for (const { configId } of entries ?? []) {
if (seen.has(configId)) {
throw RequestError.invalidParams(
{ configId },
`\`configOptions\` sets ${configId} more than once. Send one entry for each config option.`,
);
}
seen.add(configId);
}
return entries ?? [];
}
/**
* Sets each entry with `set`, in list order. `set` checks an entry against the
* options as the entries before it left them, so a model entry decides which
* effort levels a later entry can pick.
*
* An entry that `set` refuses is skipped, and the entries after it still
* apply. The option keeps its value, which the setup response reports:
*
* - `-32602`: an unknown `configId`, a value of another type than the
* option's, or a value that the option does not offer.
* - Any other failure, such as a model that Claude Code refuses. It is logged.
*/
export async function applyInitialConfigOptions<E extends { configId: string }>(
entries: readonly E[],
set: (entry: E) => Promise<unknown>,
logger: Logger,
): Promise<void> {
for (const entry of entries) {
try {
await set(entry);
} catch (error) {
if (error instanceof RequestError && error.code === INVALID_PARAMS) continue;
logger.error(`Could not set the initial value of config option ${entry.configId}:`, error);
}
}
}

View File

@@ -87,22 +87,6 @@ function hasMarkerTag(text: string, tag: string): boolean {
return markerTagText(text, tag) !== undefined;
}
/**
* Whether the content of a user message is only the output of a local
* command, such as the `<local-command-stdout>Set model to …</local-command-stdout>`
* that Claude Code echoes when the model changes.
*/
export function isLocalCommandOutput(content: unknown): boolean {
if (typeof content !== "string") return false;
if (
!hasMarkerTag(content, "local-command-stdout") &&
!hasMarkerTag(content, "local-command-stderr")
) {
return false;
}
return stripMarkerTags(content).trim() === "";
}
function commandInvocationFromMarkerOnlyText(text: string): string | null {
if (hasMarkerTag(text, "local-command-stdout") || hasMarkerTag(text, "local-command-stderr")) {
return null;

View File

@@ -5,16 +5,3 @@ import * as path from "node:path";
export function claudeConfigDir(): string {
return process.env.CLAUDE_CONFIG_DIR ?? path.join(os.homedir(), ".claude");
}
/** `<config>/projects`, normalized like the SDK does. */
export function projectsRoot(): string {
return path.join(claudeConfigDir(), "projects").normalize("NFC");
}
const SESSION_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
/** Whether `value` is a Claude session id (a UUID). Only such ids name a
* transcript, and only such ids are safe to use as a file name. */
export function isSessionId(value: unknown): value is string {
return typeof value === "string" && SESSION_ID_PATTERN.test(value);
}

View File

@@ -1,13 +1,6 @@
import {
getSessionMessages,
importSessionToStore,
type SessionMessage,
type SessionStore,
type SessionStoreEntry,
} from "@anthropic-ai/claude-agent-sdk";
import { getSessionMessages, type SessionMessage } from "@anthropic-ai/claude-agent-sdk";
import { access, open, readdir } from "node:fs/promises";
import path from "node:path";
import { isCompactSummaryMessage } from "./context-compaction.js";
import { claudeConfigDir } from "./paths.js";
import { SessionTiming } from "./session-timing.js";
@@ -75,97 +68,6 @@ export async function readResumedSession(
}
}
/**
* Read the messages of a session from its first prompt on, given the
* `getSessionMessages` chain of the session.
*
* `getSessionMessages` follows `parentUuid` links. Claude Code writes each
* `compact_boundary` with `parentUuid: null`, so after a compaction the chain
* starts at its summary. The boundary keeps the last message before it in
* `logicalParentUuid`. For a chain that starts at a summary, this links each
* boundary to that message and reads the transcript through
* `getSessionMessages` again. For a session compacted twice, the result has
* both summaries and the messages before, between and after them. The result
* ends at the last message of `messages`. Any other chain is returned as it
* is, and so is the chain when the read fails or when its result does not
* reach that last message (e.g. the transcript changed between the reads).
*
* `dir` is the project directory of the first read: both reads then resolve
* the same transcript file. Without it, all project directories are searched.
*
* The preserved-message metadata of each boundary is dropped. With it,
* `getSessionMessages` moves the messages that a compaction kept to after the
* summary, and the relinked boundary then leads from them back to the summary:
* the chain loops and ends there. Without it, the kept messages stay where
* they were said, before the boundary.
*/
export async function readHistoryAcrossCompactions(
sessionId: string,
messages: SessionMessage[],
logger?: ResumeLogger,
dir?: string,
): Promise<SessionMessage[]> {
if (!isCompactSummaryMessage(messages[0])) return messages;
try {
const entries = (await loadFullSessionHistory(sessionId, dir)).map(linkCompactBoundary);
const history = await getSessionMessages(sessionId, {
dir,
sessionStore: { append: async () => {}, load: async () => entries },
});
const leaf = messages.at(-1)?.uuid;
const end = history.findIndex((message) => message.uuid === leaf);
if (end < 0) {
logger?.error(
`The history before the last compaction of session ${sessionId} does not reach message ${leaf}: replaying the messages after it`,
);
return messages;
}
return history.slice(0, end + 1);
} catch (error) {
logger?.error(
`Failed to read the history before the last compaction of session ${sessionId}:`,
error,
);
return messages;
}
}
/** Every record of a session's transcript, including the inactive branches
* and the history before a compaction that `getSessionMessages` leaves out.
* Without `dir`, all project directories are searched. */
export async function loadFullSessionHistory(
sessionId: string,
dir?: string,
): Promise<SessionStoreEntry[]> {
const entries: SessionStoreEntry[] = [];
const store: SessionStore = {
append: async (_key, batch) => {
entries.push(...batch);
},
load: async () => null,
};
await importSessionToStore(sessionId, store, { dir, includeSubagents: false });
return entries;
}
/** The record unchanged, or a compaction boundary linked to the message before it. */
function linkCompactBoundary(entry: SessionStoreEntry): SessionStoreEntry {
if (
entry.type !== "system" ||
entry.subtype !== "compact_boundary" ||
typeof entry.logicalParentUuid !== "string"
) {
return entry;
}
const compactMetadata: Record<string, unknown> =
typeof entry.compactMetadata === "object" && entry.compactMetadata !== null
? { ...entry.compactMetadata }
: {};
delete compactMetadata.preservedSegment;
delete compactMetadata.preservedMessages;
return { ...entry, parentUuid: entry.logicalParentUuid, compactMetadata };
}
/** What a resume restores from the end of the local transcript. */
export type ResumedTail = {
model?: string;

View File

@@ -1,115 +0,0 @@
/**
* Client renames of a session: `session/set_title` (v1 and v2) and AIR's
* `_session/rename` (v1). Both cores call {@link renameSession}, which stores
* the title in every transcript of the session (see
* {@link SessionIndexService.retitle}) and, for a session loaded on the
* connection, makes it the client title that no generated title replaces.
*/
import { RequestError } from "@agentclientprotocol/sdk/unstable";
import type { Query } from "@anthropic-ai/claude-agent-sdk";
import type { SessionTitles, TitledSession } from "../session-titles.js";
import { storedTitle } from "./archive-title.js";
import {
renameTo,
type RenameSessionRequest,
type RetitleOptions,
type SessionIndexService,
type TitleChange,
} from "./service.js";
/** A session loaded on the connection, and its title state. */
export interface RetitledSession {
readonly session: TitledSession;
readonly titles: Pick<
SessionTitles,
"setExplicitTitle" | "storedTitle" | "rememberStoredTitle" | "shownTitle"
>;
}
type RenamingQuery = Query & {
renameSession?: (title: string, sessionId?: string) => Promise<void>;
};
/**
* Names a session; no generated title replaces it. An archived session stays
* archived. `loaded` is the session loaded on this connection, if any;
* `clientTitle` turns a stored title into the one the client shows (an AIR
* client's drops the archive prefix). Returns that title, which the loaded
* session also publishes as a `session_info_update`.
*
* A session that is not loaded and has no transcript is unknown (`-32002`).
*/
export async function renameSession(
index: SessionIndexService,
request: RenameSessionRequest,
loaded: RetitledSession | undefined,
clientTitle: (stored: string) => string = (stored) => stored,
): Promise<string> {
const { sessionId } = request;
// As the CLI keeps it, and never with the archive prefix: a rename does
// not change the archive state.
const title = storedTitle(request.title, false, sessionId);
// The title as stored: an archived one is cut with its prefix.
const shown = (stored: string | undefined) => clientTitle(stored ?? title);
const stored = await retitleSession(index, sessionId, renameTo(title), shown, loaded, {
mayBeUnwritten: true,
sidecar: "always",
});
return shown(stored);
}
/**
* Retitles a session (see {@link SessionIndexService.retitle}) and returns
* the title stored. A session that runs here is titled through its CLI
* (`rename_session`). A loaded session's title state takes the change as a
* client title, so no generated title replaces it; `publish` is the title it
* then publishes.
*/
export async function retitleSession(
index: SessionIndexService,
sessionId: string,
change: TitleChange,
publish: Parameters<SessionTitles["setExplicitTitle"]>[0],
loaded: RetitledSession | undefined,
options: Pick<RetitleOptions, "mayBeUnwritten" | "sidecar">,
): Promise<string | undefined> {
let stored: string | undefined;
// Decided once a title generation in flight has ended: the query may
// have closed meanwhile.
const persist = async () => {
const session = loaded?.session;
const query = session?.query as RenamingQuery | undefined;
// A closed session's CLI is gone here: only a running query titles it.
const running = session !== undefined && !session.queryClosed;
if (running && typeof query?.renameSession !== "function") {
// Its CLI holds the title and would write it back over ours.
throw RequestError.invalidRequest(
{ sessionId },
"The Claude Code CLI of this session cannot change its title while it runs.",
);
}
const live = running
? {
cwd: session.cwd,
rename: (title: string) => query!.renameSession!(title, sessionId),
stored: () => loaded!.titles.storedTitle,
remember: (title: string) => loaded!.titles.rememberStoredTitle(title),
shown: loaded!.titles.shownTitle,
}
: undefined;
stored = await index.retitle(sessionId, change, {
...options,
// Without its CLI, a session needs its transcript.
mayBeUnwritten: live !== undefined && options.mayBeUnwritten,
live,
});
return stored;
};
if (loaded) {
await loaded.titles.setExplicitTitle(publish, persist);
} else {
await persist();
}
return stored;
}

View File

@@ -285,16 +285,7 @@ export class SessionModeManager<S extends SessionMode> {
return true;
}
/**
* Sets the mode on the query. `auto` on a model without Auto runs as its
* fallback, with the warning. With `deferWarning`, the warning waits for
* the next prompt (see {@link deferFallbackWarning}).
*/
async selectMode(
sessionId: string,
modeId: string,
deferWarning = false,
): Promise<PermissionMode> {
async selectMode(sessionId: string, modeId: string): Promise<PermissionMode> {
const session = this.requireOpenSession(sessionId);
const requestedMode = this.parseMode(modeId);
if (!this.isAvailable(session.modes.availableModes, requestedMode)) {
@@ -317,8 +308,7 @@ export class SessionModeManager<S extends SessionMode> {
}
if (effectiveMode !== requestedMode) {
if (deferWarning) this.deferFallbackWarning(session);
else await this.publishFallbackWarning(sessionId, session);
await this.publishFallbackWarning(sessionId, session);
}
return effectiveMode;
}
@@ -362,16 +352,6 @@ export class SessionModeManager<S extends SessionMode> {
}
}
/**
* Leaves the Auto fallback's warning for the next prompt
* (`autoModeFallbackWarningPending`), as a session's start does. For a
* fallback during session setup: the client cannot place an update of a
* session before the setup response names it.
*/
deferFallbackWarning(session: SessionMode): void {
if (!session.autoModeFallbackWarningShown) session.autoModeFallbackWarningPending = true;
}
async publishFallbackState(sessionId: string, session: SessionMode): Promise<void> {
await this.publishCurrent(sessionId, AUTO_MODE_FALLBACK);
await this.publishFallbackWarning(sessionId, session);

View File

@@ -1,68 +0,0 @@
/**
* `session/status` (the unstable "Session status" RFD) of a session that the
* connection does not hold: `saved` or unknown. Each core answers `open`
* itself for the sessions it holds.
*/
import * as fs from "node:fs/promises";
import * as path from "node:path";
import { RequestError } from "@agentclientprotocol/sdk/unstable";
import { isSessionId, projectsRoot } from "./paths.js";
/** What a core advertises as `status` in its session capabilities. */
export const SESSION_STATUS_CAPABILITY = {};
/**
* `saved` when a project directory holds a transcript file of `sessionId`
* that is not empty. Its content is not read, so a transcript without a
* conversation is `saved` too, and its resume fails with `-32002`. Fails with
* `-32002` and `data.sessionId` otherwise:
*
* - an id that is not a UUID, such as a subagent's session id;
* - an id without a transcript: one never used, a session never prompted
* (Claude Code writes the transcript at the first prompt), and a session
* whose transcripts were removed.
*
* The request names no `cwd`, so a transcript of any project counts: a
* resume with another `cwd` can still fail with `-32002`.
*
* It lists the project directories and stats the one file name in each. No
* query starts and no session is loaded. A directory or file that cannot be
* read for another reason than being absent fails the request: it does not
* make the session unknown.
*/
export async function savedSessionStatus(sessionId: string): Promise<{ status: "saved" }> {
if (!isSessionId(sessionId) || !(await hasTranscript(sessionId))) {
throw RequestError.sessionNotFound(sessionId);
}
return { status: "saved" };
}
async function hasTranscript(sessionId: string): Promise<boolean> {
const root = projectsRoot();
let projects: string[];
try {
projects = await fs.readdir(root);
} catch (error) {
// No session was ever stored.
if (isAbsent(error)) return false;
throw error;
}
const found = await Promise.all(
projects.map((project) =>
fs.stat(path.join(root, project, `${sessionId}.jsonl`)).then(
(stats) => stats.isFile() && stats.size > 0,
(error: unknown) => {
// No such transcript, or `project` is a file.
if (isAbsent(error)) return false;
throw error;
},
),
),
);
return found.includes(true);
}
function isAbsent(error: unknown): boolean {
const code = (error as { code?: unknown }).code;
return code === "ENOENT" || code === "ENOTDIR";
}

View File

@@ -37,15 +37,6 @@ export interface SettingsManagerOptions {
logger?: { log: (...args: any[]) => void; error: (...args: any[]) => void };
}
type ModelSelectionSettings = Pick<Settings, "availableModels" | "modelOverrides">;
/** The settings that decide whether Claude Code compacts (see `idle-compaction.ts`). */
export type CompactionSettings = {
idleCompaction?: boolean;
autoCompactEnabled?: boolean;
env?: Record<string, string>;
};
/**
* Manages Claude Code settings using the SDK's `resolveSettings` merge engine
* so the values we see match what `query()` would observe.
@@ -59,9 +50,7 @@ export class SettingsManager {
private cwd: string;
private effective: Settings = {};
private managedDeniedModels: string[] = [];
private managedModelSettings: ModelSelectionSettings = {};
private managedApiKeyHelper?: string;
private managedCompactionSettings: CompactionSettings = {};
private watchers: fs.FSWatcher[] = [];
private onChange?: () => void;
private logger: { log: (...args: any[]) => void; error: (...args: any[]) => void };
@@ -123,36 +112,15 @@ export class SettingsManager {
this.managedDeniedModels = resolved.sources
.filter((entry) => entry.source === "managed")
.flatMap((entry) => entry.settings.deniedModels ?? []);
this.managedModelSettings = Object.assign(
{},
...resolved.sources
.filter((entry) => entry.source === "managed")
.map((entry) => ({
availableModels: entry.settings.availableModels,
modelOverrides: entry.settings.modelOverrides,
})),
);
this.managedApiKeyHelper =
resolved.provenance.apiKeyHelper?.source === "managed"
? resolved.effective.apiKeyHelper
: undefined;
const managed = resolved.sources
.filter((entry) => entry.source === "managed")
.map((entry) => entry.settings);
this.managedCompactionSettings = {
idleCompaction: managed.findLast((tier) => tier.idleCompaction !== undefined)
?.idleCompaction,
autoCompactEnabled: managed.findLast((tier) => tier.autoCompactEnabled !== undefined)
?.autoCompactEnabled,
env: Object.assign({}, ...managed.map((tier) => tier.env)),
};
} catch (error) {
this.logger.error("Failed to resolve settings:", error);
this.effective = {};
this.managedDeniedModels = [];
this.managedModelSettings = {};
this.managedApiKeyHelper = undefined;
this.managedCompactionSettings = {};
}
}
@@ -225,51 +193,6 @@ export class SettingsManager {
);
}
/** Model selection settings including the inline SDK settings tier. */
getModelSelectionSettings(override?: Settings): ModelSelectionSettings {
// The SDK unions non-managed allowlists, but a managed list owns the
// whole allowlist. Keep that policy when adding
// models the SDK did not include in its initialization result.
const lists = [this.effective.availableModels, override?.availableModels].filter(
(list): list is string[] => Array.isArray(list),
);
const managed = this.managedModelSettings;
return {
availableModels:
managed.availableModels ?? (lists.length > 0 ? [...new Set(lists.flat())] : undefined),
modelOverrides: {
...this.effective.modelOverrides,
...override?.modelOverrides,
...managed.modelOverrides,
},
};
}
/**
* `idleCompaction`, `autoCompactEnabled` and `env` as the CLI resolves them:
* managed settings outrank the `--settings` override, which outranks the
* user/project/local files. `env` merges entry by entry in the same order. A
* key whose value is `undefined` in the override does not hide the files' value.
*/
getCompactionSettings(settingsOverride?: Settings): CompactionSettings {
const managed = this.managedCompactionSettings;
const env: Record<string, string> = {};
for (const tier of [this.effective.env, settingsOverride?.env, managed.env]) {
for (const [name, value] of Object.entries(tier ?? {})) {
if (value !== undefined) env[name] = value;
}
}
return {
idleCompaction:
managed.idleCompaction ?? settingsOverride?.idleCompaction ?? this.effective.idleCompaction,
autoCompactEnabled:
managed.autoCompactEnabled ??
settingsOverride?.autoCompactEnabled ??
this.effective.autoCompactEnabled,
env,
};
}
/**
* `deniedModels` entries from the managed tier, the only tier the CLI
* honors them from.

View File

@@ -1,7 +1,7 @@
/**
* The `<task-notification>` inputs that Claude Code writes to a transcript
* when a background task stops. The replay of a session (`v1/acp-agent.ts`) and
* the session index (`session-index/background-tasks.ts`) read them here, so
* the session index (`v1/session-index/background-tasks.ts`) read them here, so
* both read the same fields.
*/

View File

@@ -1,4 +1,4 @@
{"kind":"initialize","payload":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","agentFileChangeReport","nativeSubagentSessions","asyncTasks","customInstructions","recommendedValue","diffPatch","planFile","backgroundSubagents"],"goal":{"actions":["set","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"logout":{},"status":{}},"loadSession":true,"mcpCapabilities":{"http":true,"sse":true},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"options":{},"resume":{},"status":{},"subagents":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"agentInfo":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"authMethods":[],"protocolVersion":1}}
{"kind":"initialize","payload":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","agentFileChangeReport","nativeSubagentSessions","asyncTasks","customInstructions","recommendedValue","diffPatch","planFile","backgroundSubagents"],"goal":{"actions":["set","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"logout":{},"status":{}},"loadSession":true,"mcpCapabilities":{"http":true,"sse":true},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{},"systemPrompt":{"mode":{"override":{}}}}},"agentInfo":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"authMethods":[],"protocolVersion":1}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"content":{"text":"Fix the bug","type":"text"},"messageId":"00000000-0000-4000-8000-00000000c001","sessionUpdate":"user_message_chunk"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"content":{"text":"Check the file.","type":"text"},"messageId":"msg_r1","sessionUpdate":"agent_thought_chunk"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"content":{"text":"I will edit it.","type":"text"},"messageId":"msg_r1","sessionUpdate":"agent_message_chunk"}}}

View File

@@ -1,3 +1,3 @@
{"kind":"initialize","payload":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","agentFileChangeReport","nativeSubagentSessions","asyncTasks","customInstructions","recommendedValue","diffPatch","planFile","backgroundSubagents"],"goal":{"actions":["set","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"logout":{},"status":{}},"loadSession":true,"mcpCapabilities":{"http":true,"sse":true},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"options":{},"resume":{},"status":{},"subagents":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"agentInfo":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"authMethods":[],"protocolVersion":1}}
{"kind":"initialize","payload":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","agentFileChangeReport","nativeSubagentSessions","asyncTasks","customInstructions","recommendedValue","diffPatch","planFile","backgroundSubagents"],"goal":{"actions":["set","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"logout":{},"status":{}},"loadSession":true,"mcpCapabilities":{"http":true,"sse":true},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{},"systemPrompt":{"mode":{"override":{}}}}},"agentInfo":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"authMethods":[],"protocolVersion":1}}
{"kind":"newSession","payload":{"configOptions":[{"category":"mode","currentValue":"auto","description":"Session permission mode","id":"mode","name":"Mode","options":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Always ask before making changes","name":"Manual","value":"default"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"_meta":{"jetbrains":{"air":{"kind":"plan","version":1}}},"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","id":"model","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"modes":{"availableModes":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Always ask before making changes","id":"default","name":"Manual"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Automatically accept all file edits","id":"acceptEdits","name":"Accept edits"},{"_meta":{"jetbrains":{"air":{"kind":"plan","version":1}}},"description":"Create a plan before making changes","id":"plan","name":"Plan"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Claude handles permission decisions","id":"auto","name":"Auto"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Accepts all permissions","id":"bypassPermissions","name":"Bypass permissions"}],"currentModeId":"auto"},"sessionId":"<session>"}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]"},"name":"mcp"}],"sessionUpdate":"available_commands_update"}}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"/goal Ship the feature","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[],"messageId":"00000000-0000-4000-8000-00000000c001","sessionUpdate":"user_message"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":{"text":"Fix the bug","type":"text"},"messageId":"00000000-0000-4000-8000-00000000c001","sessionUpdate":"user_message_chunk"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[],"messageId":"msg_r1","sessionUpdate":"agent_thought"}}}

View File

@@ -1,2 +1,2 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -28,16 +28,6 @@
* - The `initialize` response also advertises `agentCapabilities.auth.status`,
* which origin/main did not: the agent now answers `auth/status` and sends
* `auth/status_update`.
* - The `initialize` response also advertises
* `agentCapabilities.sessionCapabilities.status`, which origin/main did
* not: the agent now serves `session/status`.
* - The `initialize` response also advertises
* `agentCapabilities.sessionCapabilities.title`, which origin/main did not:
* the agent now serves `session/set_title`.
* - The `initialize` response also advertises
* `agentCapabilities.sessionCapabilities.options`, which origin/main did
* not: the agent now serves `session/options` and takes `configOptions` on
* session setup requests.
* - A SandboxNetworkAccess tool call is settled by a `tool_call_update` with
* only its final `status` once its permission request is answered. The
* callback runs nothing, so origin/main never settled the call (see
@@ -239,17 +229,19 @@ function isMeta(key: string): boolean {
return key.startsWith("_meta.");
}
/** `record` without the session capabilities `keys`, when it is an `initialize` response. */
function withoutSessionCapabilities(record: Recorded, keys: readonly string[]): Recorded {
/** `record` without the session capability `key`, when it is an `initialize` response. */
function withoutSessionCapability(record: Recorded, key: string): Recorded {
if (record.kind !== "initialize") return record;
const payload = record.payload as Json;
const agent = payload.agentCapabilities as Json | undefined;
const session = agent?.sessionCapabilities as Json | undefined;
if (!session || !keys.some((key) => key in session)) return record;
const remaining = keys.reduce((rest, key) => without(rest, key), session);
if (!session || !(key in session)) return record;
return {
...record,
payload: { ...payload, agentCapabilities: { ...agent, sessionCapabilities: remaining } },
payload: {
...payload,
agentCapabilities: { ...agent, sessionCapabilities: without(session, key) },
},
} as Recorded;
}
@@ -272,18 +264,14 @@ function withoutAuthStatusCapability(record: Recorded): Recorded {
*/
export function compareWithBaseline(baseline: Recorded[], traffic: Recorded[]): string[] {
const expected = baseline
.map((record) =>
withoutSessionCapabilities(withoutAirOnlyKeys(record) as Recorded, ["subagents"]),
)
.map((record) => withoutSessionCapability(withoutAirOnlyKeys(record) as Recorded, "subagents"))
.filter((record) => {
const update = updateOf(record);
return !(update?.sessionUpdate === "session_info_update" && Object.keys(update).length === 1);
});
const current = withoutSandboxNetworkSettlements(
traffic.map((record) =>
withoutAuthStatusCapability(
withoutSessionCapabilities(record, ["systemPrompt", "status", "title", "options"]),
),
withoutAuthStatusCapability(withoutSessionCapability(record, "systemPrompt")),
),
);
const violations: string[] = [];

View File

@@ -9,8 +9,6 @@ import {
type ClientCapabilities,
type InitializeRequest,
} from "@agentclientprotocol/sdk/unstable";
import { mkdir, writeFile } from "node:fs/promises";
import path from "node:path";
/** The context-usage report the base mock query returns. `rawMaxTokens`
* matches the agent's DEFAULT_CONTEXT_WINDOW so window-related assertions
@@ -84,73 +82,3 @@ export function heldRequest<T>(_params?: unknown, signal?: AbortSignal): Promise
else signal?.addEventListener("abort", cancel, { once: true });
});
}
/** A transcript record of a user message. */
export function transcriptUser(uuid: string, content: string, extra: object = {}) {
return { type: "user", uuid, message: { role: "user", content }, ...extra };
}
/** A transcript record of an assistant text response. */
export function transcriptAssistant(uuid: string, text: string) {
return {
type: "assistant",
uuid,
message: {
id: `msg_${uuid}`,
role: "assistant",
model: "claude-opus-5",
content: [{ type: "text", text }],
},
};
}
/** A `compact_boundary` as Claude Code writes it: no parent, and the last
* message before it in `logicalParentUuid`. */
export function transcriptCompactBoundary(
uuid: string,
logicalParentUuid: string,
compactMetadata: object = {},
) {
return {
type: "system",
subtype: "compact_boundary",
uuid,
parentUuid: null,
logicalParentUuid,
compactMetadata: { trigger: "manual", preTokens: 1000, ...compactMetadata },
};
}
/** The summary message that follows a `compact_boundary`. */
export function transcriptCompactSummary(
uuid: string,
content = "This session is being continued from a previous conversation that ran out of context. The summary below covers the earlier portion of the conversation.\n\n" +
"Summary:\n1. Primary Request and Intent:\n Say hi.\n\n" +
"Continue the conversation from where it left off without asking the user any further questions. Resume directly — do not acknowledge the summary.",
) {
return transcriptUser(uuid, content, { isCompactSummary: true });
}
/** Writes `<directory>/<sessionId>.jsonl` as Claude Code does: each record
* links to the record before it, except a record that sets its own
* `parentUuid`. */
export async function writeTranscript(
directory: string,
sessionId: string,
records: Record<string, unknown>[],
): Promise<void> {
await mkdir(directory, { recursive: true });
let parentUuid: unknown = null;
const lines = records.map((record) => {
const line = {
parentUuid,
sessionId,
isSidechain: false,
timestamp: "2026-10-11T00:00:00.000Z",
...record,
};
parentUuid = record.uuid;
return JSON.stringify(line);
});
await writeFile(path.join(directory, `${sessionId}.jsonl`), lines.join("\n") + "\n");
}

View File

@@ -1,92 +0,0 @@
import { describe, it, expect, afterEach, vi } from "vitest";
import { resolveSettings } from "@anthropic-ai/claude-agent-sdk";
import { SettingsManager } from "../settings.js";
vi.mock("@anthropic-ai/claude-agent-sdk", async (importOriginal) => ({
...(await importOriginal<typeof import("@anthropic-ai/claude-agent-sdk")>()),
resolveSettings: vi.fn(),
}));
describe("model selection settings policy", () => {
let manager: SettingsManager;
afterEach(() => manager?.dispose());
it.each([{ availableModels: ["claude-haiku-4-5"] }, { availableModels: [] }])(
"keeps the managed allowlist %j and mappings above inline settings",
async ({ availableModels }) => {
const managed = {
availableModels,
modelOverrides: { "claude-haiku-4-5": "managed-haiku" },
deniedModels: ["claude-opus-5"],
};
vi.mocked(resolveSettings).mockResolvedValue({
effective: managed,
provenance: {},
sources: [{ source: "managed", settings: managed }],
});
manager = new SettingsManager(process.cwd());
await manager.initialize();
expect(
manager.getModelSelectionSettings({
availableModels: ["claude-opus-5"],
modelOverrides: { "claude-haiku-4-5": "inline-haiku" },
}),
).toEqual({
availableModels: managed.availableModels,
modelOverrides: managed.modelOverrides,
});
expect(manager.getManagedDeniedModels()).toEqual(managed.deniedModels);
},
);
it("keeps managed override keys above inline keys without discarding other inline mappings", async () => {
const managed = { modelOverrides: { "claude-haiku-4-5": "managed-haiku" } };
vi.mocked(resolveSettings).mockResolvedValue({
effective: managed,
provenance: {},
sources: [{ source: "managed", settings: managed }],
});
manager = new SettingsManager(process.cwd());
await manager.initialize();
expect(
manager.getModelSelectionSettings({
availableModels: ["claude-haiku-4-5", "claude-sonnet-4-6"],
modelOverrides: {
"claude-haiku-4-5": "inline-haiku",
"claude-sonnet-4-6": "inline-sonnet",
},
}),
).toEqual({
availableModels: ["claude-haiku-4-5", "claude-sonnet-4-6"],
modelOverrides: { "claude-haiku-4-5": "managed-haiku", "claude-sonnet-4-6": "inline-sonnet" },
});
});
it("preserves file and inline mappings when the managed allowlist supplies no mappings", async () => {
vi.mocked(resolveSettings).mockResolvedValue({
effective: {
availableModels: ["claude-haiku-4-5"],
modelOverrides: { "claude-haiku-4-5": "file-haiku" },
},
provenance: {},
sources: [
{ source: "user", settings: { modelOverrides: { "claude-haiku-4-5": "file-haiku" } } },
{ source: "managed", settings: { availableModels: ["claude-haiku-4-5"] } },
],
});
manager = new SettingsManager(process.cwd());
await manager.initialize();
expect(manager.getModelSelectionSettings()).toEqual({
availableModels: ["claude-haiku-4-5"],
modelOverrides: { "claude-haiku-4-5": "file-haiku" },
});
expect(
manager.getModelSelectionSettings({
modelOverrides: { "claude-haiku-4-5": "inline-haiku" },
}),
).toEqual({
availableModels: ["claude-haiku-4-5"],
modelOverrides: { "claude-haiku-4-5": "inline-haiku" },
});
});
});

View File

@@ -1,20 +1,9 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { getSessionMessages, type SessionMessage } from "@anthropic-ai/claude-agent-sdk";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import type { SessionMessage } from "@anthropic-ai/claude-agent-sdk";
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import {
transcriptAssistant,
transcriptCompactBoundary,
transcriptCompactSummary,
transcriptUser,
writeTranscript,
} from "./helpers.js";
import os from "node:os";
import path from "node:path";
import {
readHistoryAcrossCompactions,
readResumedTail,
resumedModelFromTranscript,
} from "../resumed-session.js";
import { readResumedTail, resumedModelFromTranscript } from "../resumed-session.js";
function assistant(
model: unknown,
@@ -214,187 +203,3 @@ describe("readResumedTail", () => {
expect(await readResumedTail("no-mode")).toEqual({ model: "claude-opus-5" });
});
});
describe("readHistoryAcrossCompactions", () => {
const sessionId = "33333333-3333-4333-8333-333333333333";
let configDir: string;
let originalConfigDir: string | undefined;
beforeEach(async () => {
configDir = await mkdtemp(path.join(os.tmpdir(), "claude-acp-compacted-"));
originalConfigDir = process.env.CLAUDE_CONFIG_DIR;
process.env.CLAUDE_CONFIG_DIR = configDir;
});
afterEach(async () => {
if (originalConfigDir === undefined) delete process.env.CLAUDE_CONFIG_DIR;
else process.env.CLAUDE_CONFIG_DIR = originalConfigDir;
await rm(configDir, { recursive: true, force: true });
});
const transcript = (records: Record<string, unknown>[], project = "-workspace") =>
writeTranscript(path.join(configDir, "projects", project), sessionId, records);
const user = transcriptUser;
const assistant = transcriptAssistant;
const boundary = transcriptCompactBoundary;
const summary = (uuid: string) => transcriptCompactSummary(uuid, `Summary ${uuid}`);
const texts = (messages: SessionMessage[]) =>
messages.map((message) => {
const content = (message.message as { content: unknown }).content;
return typeof content === "string" ? content : (content as { text: string }[])[0]!.text;
});
it("reads the messages before each compaction", async () => {
await transcript([
user("u1", "hi"),
assistant("a1", "Hello"),
boundary("b1", "a1"),
summary("s1"),
user("u2", "next"),
assistant("a2", "Next"),
boundary("b2", "a2"),
summary("s2"),
user("u3", "identify yourself"),
assistant("a3", "Claude"),
]);
// getSessionMessages stops at the last boundary.
const chain = await getSessionMessages(sessionId);
expect(texts(chain)).toEqual(["Summary s2", "identify yourself", "Claude"]);
expect(texts(await readHistoryAcrossCompactions(sessionId, chain))).toEqual([
"hi",
"Hello",
"Summary s1",
"next",
"Next",
"Summary s2",
"identify yourself",
"Claude",
]);
});
it("keeps the messages that a compaction preserved where they were said", async () => {
// Claude Code 2.1.293 keeps u2 and a2 in the context after the summary,
// and records them in both forms of preserved-message metadata.
await transcript([
user("u1", "hi"),
assistant("a1", "Hello"),
user("u2", "keep this"),
assistant("a2", "Kept"),
boundary("b1", "a2", {
preservedSegment: { headUuid: "u2", anchorUuid: "s1", tailUuid: "a2" },
preservedMessages: { anchorUuid: "s1", uuids: ["u2", "a2"] },
}),
summary("s1"),
user("u3", "after"),
assistant("a3", "After"),
]);
// getSessionMessages moves the preserved messages after the summary.
const chain = await getSessionMessages(sessionId);
expect(texts(chain)).toEqual(["Summary s1", "keep this", "Kept", "after", "After"]);
expect(texts(await readHistoryAcrossCompactions(sessionId, chain))).toEqual([
"hi",
"Hello",
"keep this",
"Kept",
"Summary s1",
"after",
"After",
]);
});
it("returns the chain of a session without a compaction as it is", async () => {
await transcript([user("u1", "hi"), assistant("a1", "Hello")]);
const chain = await getSessionMessages(sessionId);
expect(await readHistoryAcrossCompactions(sessionId, chain)).toBe(chain);
});
it("ends at the last message of the chain it was given", async () => {
await transcript([
user("u1", "hi"),
assistant("a1", "Hello"),
boundary("b1", "a1"),
summary("s1"),
user("u2", "identify yourself"),
]);
const chain = await getSessionMessages(sessionId);
// Claude Code appends a response after the first read.
await transcript([
user("u1", "hi"),
assistant("a1", "Hello"),
boundary("b1", "a1"),
summary("s1"),
user("u2", "identify yourself"),
assistant("a2", "Claude"),
]);
expect(texts(await readHistoryAcrossCompactions(sessionId, chain))).toEqual([
"hi",
"Hello",
"Summary s1",
"identify yourself",
]);
});
it.each([
["the transcript is gone", async () => rm(configDir, { recursive: true, force: true })],
[
"the transcript no longer has the last message",
() =>
transcript([
user("u1", "hi"),
assistant("a1", "Hello"),
boundary("b1", "a1"),
summary("s1"),
user("u9", "other"),
]),
],
])("returns the chain and logs when %s", async (_case, change) => {
await transcript([
user("u1", "hi"),
assistant("a1", "Hello"),
boundary("b1", "a1"),
summary("s1"),
user("u2", "identify yourself"),
]);
const chain = await getSessionMessages(sessionId);
await change();
const error = vi.fn();
expect(await readHistoryAcrossCompactions(sessionId, chain, { log: () => {}, error })).toBe(
chain,
);
expect(error).toHaveBeenCalledOnce();
});
it("reads the transcript in the project directory it is given", async () => {
await transcript(
[
user("u1", "elsewhere"),
assistant("a1", "Elsewhere"),
boundary("b1", "a1"),
summary("s1"),
user("u2", "identify yourself"),
],
"-a-other",
);
await transcript(
[
user("u1", "hi"),
assistant("a1", "Hello"),
boundary("b1", "a1"),
summary("s1"),
user("u2", "identify yourself"),
],
"-z-workspace",
);
const chain = await getSessionMessages(sessionId, { dir: "/z/workspace" });
expect(
texts(await readHistoryAcrossCompactions(sessionId, chain, undefined, "/z/workspace")),
).toEqual(["hi", "Hello", "Summary s1", "identify yourself"]);
});
});

View File

@@ -38,62 +38,3 @@ describe("SettingsManager.getApiKeyHelper", () => {
manager.dispose();
});
});
describe("SettingsManager.getCompactionSettings", () => {
it("ranks managed settings over the --settings override over the files, env entry by entry", async () => {
const manager = await managerResolving({
effective: {
idleCompaction: true,
autoCompactEnabled: false,
env: {
DISABLE_COMPACT: "1",
DISABLE_AUTO_COMPACT: "1",
CLAUDE_CODE_IDLE_COMPACT_MIN_TOKENS: "1",
},
},
provenance: {},
sources: [
{ source: "userSettings", settings: { autoCompactEnabled: false } },
{
source: "managed",
settings: { idleCompaction: true, env: { DISABLE_AUTO_COMPACT: "1" } },
},
],
});
expect(
manager.getCompactionSettings({
idleCompaction: false,
autoCompactEnabled: true,
env: { DISABLE_COMPACT: "0", DISABLE_AUTO_COMPACT: "0" },
}),
).toEqual({
idleCompaction: true,
autoCompactEnabled: true,
env: {
DISABLE_COMPACT: "0",
DISABLE_AUTO_COMPACT: "1",
CLAUDE_CODE_IDLE_COMPACT_MIN_TOKENS: "1",
},
});
manager.dispose();
});
it("keeps the files' value for a key the override sets to undefined", async () => {
const manager = await managerResolving({
effective: { idleCompaction: false, env: { DISABLE_COMPACT: "1" } },
provenance: {},
sources: [],
});
expect(
manager.getCompactionSettings({
idleCompaction: undefined,
env: { DISABLE_COMPACT: undefined as unknown as string },
}),
).toEqual({
idleCompaction: false,
autoCompactEnabled: undefined,
env: { DISABLE_COMPACT: "1" },
});
manager.dispose();
});
});

View File

@@ -921,136 +921,6 @@ describe("ClaudeAcpAgent settings", () => {
return { setModelSpy };
}
it.each(["environment", "caller object", "caller file"])(
"includes configured models omitted by the SDK from %s settings",
async (source) => {
const configured = {
availableModels: ["claude-haiku-4-5", "claude-haiku-5-5"],
modelOverrides: {
"claude-haiku-4-5": "global.anthropic.claude-haiku-4-5-20251001-v1:0",
"claude-haiku-5-5": "global.anthropic.claude-haiku-5-5",
},
};
let settings: object | string | undefined;
if (source === "environment") {
vi.stubEnv("CLAUDE_MODEL_CONFIG", JSON.stringify(configured));
} else if (source === "caller file") {
settings = "caller-settings.json";
await fs.promises.writeFile(path.join(tempDir, settings), JSON.stringify(configured));
} else {
settings = configured;
}
const { setModelSpy } = mockQueryWithModels([
{ value: "default", displayName: "Default", description: "" },
{ value: "sonnet", displayName: "Sonnet", description: "" },
]);
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
const agent = new ClaudeAcpAgent(createMockClient());
try {
const response = await (agent as any).createSession({
cwd: tempDir,
mcpServers: [],
_meta: { claudeCode: { options: { settings } } },
});
expect(querySpy.mock.calls[0][0].options.settings).toEqual(configured);
const ids = ["default", ...Object.values(configured.modelOverrides)];
expect(
agent.sessions[response.sessionId].models.availableModels.map(
(model: any) => model.modelId,
),
).toEqual(ids);
const modelOption = response.configOptions.find((option: any) => option.id === "model");
expect(modelOption.options.map((option: any) => option.value)).toEqual(ids);
await agent.setSessionConfigOption({
sessionId: response.sessionId,
configId: "model",
value: ids[2],
});
expect(setModelSpy).toHaveBeenLastCalledWith(ids[2]);
expect(agent.sessions[response.sessionId].modelInfos.map((model) => model.value)).toEqual(
ids,
);
} finally {
await agent.dispose();
vi.unstubAllEnvs();
}
},
);
it("merges file settings with caller settings after caller keys replace the env config", async () => {
await fs.promises.writeFile(
path.join(tempDir, "settings.json"),
JSON.stringify({
availableModels: ["claude-sonnet-4-6", "claude-haiku-4-5"],
modelOverrides: {
"claude-sonnet-4-6": "file-sonnet",
"claude-haiku-4-5": "file-haiku",
},
}),
);
vi.stubEnv(
"CLAUDE_MODEL_CONFIG",
JSON.stringify({
availableModels: ["claude-opus-4-6"],
modelOverrides: { "claude-sonnet-4-6": "env-sonnet", "claude-haiku-4-5": "env-haiku" },
}),
);
mockQueryWithModels([{ value: "default", displayName: "Default", description: "" }]);
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
const agent = new ClaudeAcpAgent(createMockClient());
const settings = {
availableModels: ["claude-haiku-4-5", "claude-haiku-5-5"],
modelOverrides: { "claude-haiku-4-5": "caller-haiku" },
};
try {
const response = await (agent as any).createSession({
cwd: tempDir,
mcpServers: [],
_meta: { claudeCode: { options: { settings } } },
});
expect(querySpy.mock.calls[0][0].options.settings).toEqual(settings);
const modelOption = response.configOptions.find((option: any) => option.id === "model");
expect(modelOption.options.map((option: any) => option.value)).toEqual([
"default",
"file-sonnet",
"caller-haiku",
"claude-haiku-5-5",
]);
} finally {
await agent.dispose();
vi.unstubAllEnvs();
}
});
it.each([[], undefined])(
"preserves empty and absent configured allowlists: %j",
async (availableModels) => {
vi.stubEnv(
"CLAUDE_MODEL_CONFIG",
JSON.stringify({
availableModels,
modelOverrides: { "claude-haiku-4-5": "provider-haiku" },
}),
);
mockQueryWithModels([
{ value: "default", displayName: "Default", description: "" },
{ value: "sonnet", displayName: "Sonnet", description: "" },
]);
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
const agent = new ClaudeAcpAgent(createMockClient());
try {
const response = await (agent as any).createSession({ cwd: tempDir, mcpServers: [] });
const modelOption = response.configOptions.find((option: any) => option.id === "model");
expect(modelOption.options.map((option: any) => option.value)).toEqual(
availableModels ? ["default"] : ["default", "sonnet"],
);
} finally {
await agent.dispose();
vi.unstubAllEnvs();
}
},
);
it("restricts configOptions to the user's allowlist using their exact IDs", async () => {
// Reproduces the scenario from
// https://github.com/agentclientprotocol/claude-agent-acp/issues/620:

View File

@@ -32,8 +32,8 @@ import {
ClaudeAcpAgent,
computeSessionFingerprint,
runPromptWithCancellation,
type SteerRequest,
} from "../../v1/acp-agent.js";
import type { SteerRequest } from "../../v1/steering.js";
import { buildConfigOptions, discoverCustomAgents } from "../../session-config-options.js";
import { createFastModeConfigOption } from "../../session-fast-mode.js";
import {
@@ -58,16 +58,7 @@ import {
parseUsageResponse,
} from "../../usage-markdown.js";
import { Pushable } from "../../utils.js";
import {
heldRequest,
initializeClient,
makeMockQuery,
transcriptAssistant,
transcriptCompactBoundary,
transcriptCompactSummary,
transcriptUser,
writeTranscript,
} from "../helpers.js";
import { heldRequest, initializeClient, makeMockQuery } from "../helpers.js";
import { acpProtocolRouter } from "../../serve.js";
import * as agentSdk from "@anthropic-ai/claude-agent-sdk";
import {
@@ -86,7 +77,7 @@ import {
type SDKControlGetUsageResponse,
} from "@anthropic-ai/claude-agent-sdk";
import { createHash, randomUUID } from "crypto";
import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import {
@@ -3638,63 +3629,6 @@ describe("usage-limit failure replay", () => {
});
});
describe("compacted session replay", () => {
it("replays the messages before the compaction, then the compaction", async () => {
const configDir = await mkdtemp(path.join(os.tmpdir(), "claude-acp-compacted-replay-"));
const originalConfigDir = process.env.CLAUDE_CONFIG_DIR;
process.env.CLAUDE_CONFIG_DIR = configDir;
try {
const sessionId = "44444444-4444-4444-8444-444444444444";
// The records of `/compact` between two prompts, as Claude Code writes
// them: the boundary has no parent and links "Hello" logically.
await writeTranscript(path.join(configDir, "projects", "-tmp-proj"), sessionId, [
transcriptUser("u1", "hi"),
transcriptAssistant("a1", "Hello"),
transcriptCompactBoundary("b1", "a1"),
transcriptCompactSummary("s1"),
transcriptUser("u2", "identify yourself"),
transcriptAssistant("a2", "Claude"),
]);
const updates: SessionNotification[] = [];
const client = {
sessionUpdate: async (update: SessionNotification) => {
updates.push(update);
},
} as unknown as AcpClient;
const agent = new ClaudeAcpAgent(client, { log: () => {}, error: () => {} });
await initializeClient(agent, { session: { compaction: {} } } as ClientCapabilities);
await (
agent as unknown as { replaySessionHistory(sessionId: string): Promise<void> }
).replaySessionHistory(sessionId);
expect(
updates.flatMap(({ update }) => {
if (update.sessionUpdate === "compaction_update") return [["compaction", update.status]];
if (
(update.sessionUpdate === "user_message_chunk" ||
update.sessionUpdate === "agent_message_chunk") &&
update.content.type === "text"
) {
return [[update.sessionUpdate, update.content.text]];
}
return [];
}),
).toEqual([
["user_message_chunk", "hi"],
["agent_message_chunk", "Hello"],
["compaction", "completed"],
["user_message_chunk", "identify yourself"],
["agent_message_chunk", "Claude"],
]);
} finally {
if (originalConfigDir === undefined) delete process.env.CLAUDE_CONFIG_DIR;
else process.env.CLAUDE_CONFIG_DIR = originalConfigDir;
await rm(configDir, { recursive: true, force: true });
}
});
});
describe("session/list", () => {
const sdkSession = (index: number) => ({
sessionId: `session-${index}`,
@@ -15019,79 +14953,6 @@ describe("session/fork", () => {
});
});
describe("session/status", () => {
it("answers open for the sessions it holds, and the others from their stored transcripts", async () => {
const configDir = await mkdtemp(path.join(os.tmpdir(), "session-status-"));
vi.stubEnv("CLAUDE_CONFIG_DIR", configDir);
try {
const agent = new ClaudeAcpAgent({ sessionUpdate: async () => {} } as unknown as AcpClient, {
log: () => {},
error: () => {},
});
const { agentCapabilities } = await agent.initialize({
protocolVersion: 1,
clientCapabilities: {},
});
expect(agentCapabilities?.sessionCapabilities?.status).toEqual({});
const status = (sessionId: string) => agent.sessionStatus({ sessionId });
const unknown = (sessionId: string) => ({ code: -32002, data: { sessionId } });
const sessionId = randomUUID();
agent.sessions[sessionId] = {} as ClaudeAcpAgent["sessions"][string];
await expect(status(sessionId)).resolves.toEqual({ status: "open" });
// A held session whose query ended stays open: a load returns it as it is.
agent.sessions[sessionId]!.queryClosed = true;
await expect(status(sessionId)).resolves.toEqual({ status: "open" });
// A subagent's session id, and a name that every object inherits.
await expect(status("agent-1")).rejects.toMatchObject(unknown("agent-1"));
await expect(status("constructor")).rejects.toMatchObject(unknown("constructor"));
// Closed, and never prompted: no projects directory exists yet.
delete agent.sessions[sessionId];
await expect(status(sessionId)).rejects.toMatchObject(unknown(sessionId));
// A transcript of any project counts, next to a project without one.
await mkdir(path.join(configDir, "projects", "-other"), { recursive: true });
const project = path.join(configDir, "projects", "-project");
// A directory of that name is no transcript.
await mkdir(path.join(project, `${sessionId}.jsonl`), { recursive: true });
await expect(status(sessionId)).rejects.toMatchObject(unknown(sessionId));
await rm(path.join(project, `${sessionId}.jsonl`), { recursive: true });
await writeFile(path.join(project, `${sessionId}.jsonl`), "{}\n");
await expect(status(sessionId)).resolves.toEqual({ status: "saved" });
// The request loaded nothing.
expect(agent.sessions[sessionId]).toBeUndefined();
} finally {
vi.unstubAllEnvs();
await rm(configDir, { recursive: true, force: true });
}
});
// Not on Windows, where `chmod` does not take the read permission away.
it.skipIf(process.platform === "win32" || process.getuid?.() === 0)(
"fails, not -32002, when a project directory cannot be read",
async () => {
const configDir = await mkdtemp(path.join(os.tmpdir(), "session-status-"));
vi.stubEnv("CLAUDE_CONFIG_DIR", configDir);
const project = path.join(configDir, "projects", "-project");
await mkdir(project, { recursive: true });
await chmod(project, 0o000);
try {
const agent = new ClaudeAcpAgent(
{ sessionUpdate: async () => {} } as unknown as AcpClient,
{ log: () => {}, error: () => {} },
);
await expect(agent.sessionStatus({ sessionId: randomUUID() })).rejects.toMatchObject({
code: "EACCES",
});
} finally {
await chmod(project, 0o700);
vi.unstubAllEnvs();
await rm(configDir, { recursive: true, force: true });
}
},
);
});
describe("session/close", () => {
function createMockAgent() {
const mockClient = {

View File

@@ -220,8 +220,7 @@ describe("auth status mappers", () => {
state: "authenticated",
accounts: [
{
label: "Anthropic API key",
description: "env",
label: "Anthropic API key (env)",
active: true,
_meta: { claudeCode: { kind: "api_key", detail: "env" } },
},
@@ -843,8 +842,7 @@ describe("auth status over ACP", () => {
state: "authenticated",
accounts: [
{
label: "Custom model gateway",
description: "gw.example.com",
label: "Custom model gateway (gw.example.com)",
active: true,
_meta: { claudeCode: { kind: "gateway", detail: "gw.example.com" } },
},
@@ -862,7 +860,7 @@ describe("auth status over ACP", () => {
agent.setAuthStatus({ kind: "api_key", label: "Anthropic API key", detail: "env" });
expect(await agent.authStatusResponse()).toMatchObject({
state: "authenticated",
accounts: [{ label: "Anthropic API key", description: "env" }],
accounts: [{ label: "Anthropic API key (env)" }],
});
});

View File

@@ -11,11 +11,11 @@ import * as fsSync from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import type { SessionInfo } from "@agentclientprotocol/sdk/unstable";
import type { ActivityState } from "../../session-index/activity.js";
import { endedTaskId, searchBackgroundTasks } from "../../session-index/background-tasks.js";
import { encodeProjectPath } from "../../session-index/project-dirs.js";
import { SessionIndex } from "../../session-index/session-index.js";
import { SessionIndexService } from "../../session-index/service.js";
import type { ActivityState } from "../../v1/session-index/activity.js";
import { endedTaskId, searchBackgroundTasks } from "../../v1/session-index/background-tasks.js";
import { encodeProjectPath } from "../../v1/session-index/project-dirs.js";
import { SessionIndex } from "../../v1/session-index/session-index.js";
import { SessionIndexService } from "../../v1/session-index/service.js";
let configDir: string;
let workspace: string;

View File

@@ -1365,229 +1365,6 @@ describe("createSession options merging", () => {
});
});
describe("session options", () => {
const cwd = process.cwd();
const TWO_MODELS = [
{
value: "claude-sonnet-4-6",
displayName: "Claude Sonnet",
description: "Fast",
supportsAutoMode: true,
supportsEffort: true,
supportedEffortLevels: ["low", "medium", "high"],
},
{
value: "claude-haiku-4-5",
displayName: "Claude Haiku",
description: "Faster",
supportsEffort: true,
supportedEffortLevels: ["low", "medium"],
},
];
let updates: SessionNotification[];
const values = (response: { configOptions?: { id: string; currentValue: unknown }[] | null }) =>
Object.fromEntries((response.configOptions ?? []).map((o) => [o.id, o.currentValue]));
beforeEach(() => {
initModels = TWO_MODELS;
updates = [];
vi.spyOn(agent.client, "sessionUpdate").mockImplementation(async (notification) => {
updates.push(notification as SessionNotification);
});
});
it("advertises session/options", async () => {
const response = await agent.initialize({ protocolVersion: 1, clientCapabilities: {} });
expect(response.agentCapabilities?.sessionCapabilities?.options).toEqual({});
});
it("answers session/options with what session/new returns, from a query that it closes", async () => {
const options = await agent.sessionOptions({ cwd, additionalDirectories: ["/extra"] });
expect(capturedOptions).toMatchObject({
cwd,
additionalDirectories: ["/extra"],
mcpServers: {},
persistSession: false,
});
expect(capturedOptions!.resume).toBeUndefined();
expect(closeSpy).toHaveBeenCalledTimes(1);
expect(agent.sessions).toEqual({});
expect(updates).toEqual([]);
const { sessionId, ...created } = await agent.newSession({ cwd, mcpServers: [] });
expect(sessionId).toBeDefined();
expect(capturedOptions!.persistSession).toBeUndefined();
expect(options).toEqual(created);
});
it("answers session/options of a held session with its current options, without a query", async () => {
const { sessionId } = await agent.newSession({ cwd, mcpServers: [] });
const { configOptions } = await agent.setSessionConfigOption({
sessionId,
configId: "model",
value: "claude-haiku-4-5",
});
capturedOptions = undefined;
const options = await agent.sessionOptions({ cwd, sessionId });
expect(capturedOptions).toBeUndefined();
expect(options).toEqual({ modes: agent.sessions[sessionId]!.modes, configOptions });
expect(values(options).model).toBe("claude-haiku-4-5");
});
it("answers session/options of a stored session from a query that resumes it", async () => {
const sessionId = "7f1c5b0e-3c53-4c0c-9d0a-0d6c1f0a2b11";
await agent.sessionOptions({ cwd, sessionId });
expect(capturedOptions).toMatchObject({ resume: sessionId, persistSession: false });
expect(capturedOptions!.sessionId).toBeUndefined();
expect(closeSpy).toHaveBeenCalledTimes(1);
expect(agent.sessions).toEqual({});
});
it("fails session/options of an unknown session with -32002", async () => {
const sessionId = "7f1c5b0e-3c53-4c0c-9d0a-0d6c1f0a2b11";
initError = new Error(`No conversation found with session ID: ${sessionId}`);
await expect(agent.sessionOptions({ cwd, sessionId })).rejects.toMatchObject({
code: RequestError.sessionNotFound(sessionId).code,
data: { sessionId },
});
expect(closeSpy).toHaveBeenCalledTimes(1);
});
it("starts a new session with the request's configOptions, in list order, without an update", async () => {
const setModels: string[] = [];
setModelImpl = async (model) => void setModels.push(model);
const response = await agent.newSession({
cwd,
mcpServers: [],
configOptions: [
{ configId: "model", value: "claude-haiku-4-5" },
// Haiku offers no "high": the entry is checked against the options the model left.
{ configId: "effort", value: "high" },
{ configId: "mode", value: "plan" },
],
});
expect(setModels).toEqual(["claude-haiku-4-5"]);
expect(values(response)).toMatchObject({ model: "claude-haiku-4-5", mode: "plan" });
expect(values(response).effort).not.toBe("high");
expect(response.modes?.currentModeId).toBe("plan");
expect(response.configOptions).toEqual(agent.sessions[response.sessionId]!.configOptions);
expect(updates.filter((u) => u.sessionId === response.sessionId)).toEqual([]);
// A restart of the query restores the session's choices, not the request's.
expect(agent.sessions[response.sessionId]!.creationParams).not.toHaveProperty(
"configOptions",
);
});
it.each([
["an unknown configId", { configId: "nope", value: "x" }],
["a boolean for a select", { configId: "model", type: "boolean" as const, value: true }],
["a value the option does not offer", { configId: "model", value: "gpt-4" }],
])("skips an entry with %s and sets the entries after it", async (_name, entry) => {
const plain = await agent.newSession({ cwd, mcpServers: [] });
const response = await agent.newSession({
cwd,
mcpServers: [],
configOptions: [entry, { configId: "mode", value: "plan" }],
});
expect(values(response)).toEqual({ ...values(plain), mode: "plan" });
});
it("skips an entry that Claude Code refuses", async () => {
setModelImpl = async () => {
throw new Error("model refused");
};
const errors = vi.spyOn(console, "error").mockImplementation(() => {});
const response = await agent.newSession({
cwd,
mcpServers: [],
configOptions: [
{ configId: "model", value: "claude-haiku-4-5" },
{ configId: "mode", value: "plan" },
],
});
expect(values(response)).toMatchObject({ model: "claude-sonnet-4-6", mode: "plan" });
expect(errors).toHaveBeenCalledWith(
"Could not set the initial value of config option model:",
expect.any(Error),
);
errors.mockRestore();
});
it.each(["newSession", "loadSession", "resumeSession"] as const)(
"%s rejects two entries of one configId before it starts a query",
async (method) => {
await expect(
agent[method]({
sessionId: "7f1c5b0e-3c53-4c0c-9d0a-0d6c1f0a2b11",
cwd,
mcpServers: [],
configOptions: [
{ configId: "mode", value: "plan" },
{ configId: "mode", value: "default" },
],
}),
).rejects.toMatchObject({
code: RequestError.invalidParams().code,
data: { configId: "mode" },
});
expect(capturedOptions).toBeUndefined();
},
);
it.each(["loadSession", "resumeSession"] as const)(
"%s sets the request's configOptions on the session, a held one too",
async (method) => {
const sessionId = "7f1c5b0e-3c53-4c0c-9d0a-0d6c1f0a2b11";
const started = await agent[method]({
sessionId,
cwd,
mcpServers: [],
configOptions: [{ configId: "mode", value: "plan" }],
});
expect(values(started).mode).toBe("plan");
expect(started.modes?.currentModeId).toBe("plan");
const held = await agent[method]({
sessionId,
cwd,
mcpServers: [],
configOptions: [{ configId: "model", value: "claude-haiku-4-5" }],
});
expect(values(held)).toMatchObject({ mode: "plan", model: "claude-haiku-4-5" });
expect(updates.map((u) => u.update.sessionUpdate)).not.toContain("current_mode_update");
expect(updates.map((u) => u.update.sessionUpdate)).not.toContain("config_option_update");
},
);
it("leaves the warning of an Auto mode fallback for the first prompt", async () => {
const response = await agent.newSession({
cwd,
mcpServers: [],
configOptions: [
{ configId: "mode", value: "auto" },
// Haiku cannot serve Auto.
{ configId: "model", value: "claude-haiku-4-5" },
],
});
expect(values(response)).toMatchObject({ mode: "acceptEdits", model: "claude-haiku-4-5" });
expect(updates.filter((u) => u.sessionId === response.sessionId)).toEqual([]);
expect(agent.sessions[response.sessionId]!.autoModeFallbackWarningPending).toBe(true);
});
});
describe("cwd validation", () => {
it("rejects a relative cwd with invalidParams", async () => {
await expect(

View File

@@ -6,7 +6,7 @@ import * as path from "node:path";
import * as os from "node:os";
import { messageIdForGrouping } from "../../sdk-to-acp.js";
import { forkSession } from "../../fork-session.js";
import { encodeProjectPath } from "../../session-index/project-dirs.js";
import { encodeProjectPath } from "../../v1/session-index/project-dirs.js";
// Use the real SDK against temporary transcripts, without starting Claude Code.
let root: string;

View File

@@ -226,10 +226,7 @@ function scriptClaudeCode({
};
}
async function connect(
clientCapabilities: Record<string, unknown> = {},
sessionMeta?: Record<string, unknown>,
) {
async function connect(clientCapabilities: Record<string, unknown> = {}) {
vi.resetModules();
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
const updates: Array<{ update: Record<string, any> }> = [];
@@ -244,7 +241,7 @@ async function connect(
error: () => {},
});
await initializeClient(agent, clientCapabilities);
const session = await agent.newSession({ cwd, mcpServers: [], _meta: sessionMeta });
const session = await agent.newSession({ cwd, mcpServers: [] });
return { agent, sessionId: session.sessionId, updates, logs };
}
@@ -325,22 +322,6 @@ describe("idle compaction in the agent", () => {
await agent.dispose();
});
/** The client's `settings` option reaches the CLI as `--settings`, and idle compaction reads it too. */
it("does not compact a session whose client turned idle compaction off in the settings option", async () => {
const claude = scriptClaudeCode();
const { agent, sessionId } = await connect(
{},
{ claudeCode: { options: { settings: { idleCompaction: false } } } },
);
await promptHi(agent, sessionId);
await claude.untilIdle();
await vi.advanceTimersByTimeAsync(2 * 60 * MINUTE_MS);
expect(claude.received).toEqual(["hi"]);
await agent.dispose();
});
/** Closing the session during the wait drops the timer. */
it("does not compact once the session is closed", async () => {
const claude = scriptClaudeCode();

View File

@@ -2,28 +2,28 @@ import { describe, expect, it } from "vitest";
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { deriveActivity, selectCost, type ActivityState } from "../session-index/activity.js";
import { LiveSessionRegistry, type LiveRecord } from "../session-index/live-registry.js";
import { isSessionId } from "../paths.js";
import { deriveActivity, selectCost, type ActivityState } from "../../v1/session-index/activity.js";
import { LiveSessionRegistry, type LiveRecord } from "../../v1/session-index/live-registry.js";
import {
encodeProjectPath,
isSessionId,
pathAndAncestors,
projectDirMatches,
} from "../session-index/project-dirs.js";
} from "../../v1/session-index/project-dirs.js";
import {
decodeListCursor,
encodeListCursor,
parseListOptions,
parseRenameSessionRequest,
type ListScope,
} from "../session-index/service.js";
} from "../../v1/session-index/service.js";
import {
effectiveTitle,
isArchivedTitle,
storedTitle,
titleRecords,
visibleTitle,
} from "../session-index/archive-title.js";
} from "../../v1/session-index/archive-title.js";
import {
agentWorktreeOf,
continuedInSessionId,
@@ -33,11 +33,11 @@ import {
sdkTitles,
transcriptAgentName,
type TranscriptFacts,
} from "../session-index/transcript-scan.js";
import { firstPrompt } from "../session-index/first-prompt.js";
import { DirListings, statFiles } from "../session-index/dir-listing.js";
import { SessionIndex, type IndexRow } from "../session-index/session-index.js";
import { changeSignature, sessionInfoOf } from "../session-index/session-info.js";
} from "../../v1/session-index/transcript-scan.js";
import { firstPrompt } from "../../v1/session-index/first-prompt.js";
import { DirListings, statFiles } from "../../v1/session-index/dir-listing.js";
import { SessionIndex, type IndexRow } from "../../v1/session-index/session-index.js";
import { changeSignature, sessionInfoOf } from "../../v1/session-index/session-info.js";
const SESSION = "11111111-1111-4111-8111-111111111111";
const lines = (...entries: object[]) => entries.map((entry) => JSON.stringify(entry)).join("\n");

View File

@@ -23,7 +23,7 @@ import {
type InitializeResponse,
type SessionInfo,
} from "@agentclientprotocol/sdk/unstable";
import { encodeProjectPath } from "../../session-index/project-dirs.js";
import { encodeProjectPath } from "../../v1/session-index/project-dirs.js";
import {
archiveInsteadOfDelete,
archiveTo,
@@ -31,16 +31,16 @@ import {
parseBatchGetSessionsRequest,
SessionIndexService,
writeCustomTitleSidecar,
} from "../../session-index/service.js";
import { scanTranscriptFile } from "../../session-index/transcript-scan.js";
import { repositoryWorktrees } from "../../session-index/worktrees.js";
} from "../../v1/session-index/service.js";
import { scanTranscriptFile } from "../../v1/session-index/transcript-scan.js";
import { repositoryWorktrees } from "../../v1/session-index/worktrees.js";
import {
ListSubscriptions,
MAX_SUBSCRIPTIONS,
parseListSubscribeRequest,
parseListUnsubscribeRequest,
type ListChanges,
} from "../../session-index/list-subscriptions.js";
} from "../../v1/session-index/list-subscriptions.js";
import { SessionTitles } from "../../session-titles.js";
import { initializeClient } from "../helpers.js";
import {
@@ -1350,48 +1350,6 @@ describe("_session/rename", () => {
});
});
describe("session/set_title", () => {
it("titles a stored session for a client without the session index", async () => {
const session = await writeTranscript({});
const { agent } = createAgent();
await initializeClient(agent, {});
expect(
await agent.setSessionTitle({ sessionId: session.id, title: " Investigate\n OAuth " }),
).toEqual({ title: "Investigate OAuth" });
expect(await lastRecords(session.file)).toEqual(titleRecords(session.id, "Investigate OAuth"));
const page = await agent.listSessions({ cwd: workspace });
expect(page.sessions[0]!.title).toBe("Investigate OAuth");
});
it.each([
["an AIR client with sessionIndex", air("sessionIndex"), "Renamed"],
["an AIR client without sessionIndex", air(), "Renamed"],
["a client that is not AIR", {}, "[archived] Renamed"],
])(
"keeps an archived session archived and answers the title %s shows",
async (_client, capabilities, shown) => {
const session = await writeTranscript({ trailer: titleRecords("", "[archived] Fix it") });
const { agent } = createAgent();
await initializeClient(agent, capabilities);
expect(await agent.setSessionTitle({ sessionId: session.id, title: "Renamed" })).toEqual({
title: shown,
});
expect(await lastRecords(session.file)).toEqual(
titleRecords(session.id, "[archived] Renamed"),
);
},
);
it.each([
["an unknown session", () => ({ sessionId: randomUUID(), title: "x" }), -32002],
["an empty title", () => ({ sessionId: randomUUID(), title: " \n " }), -32602],
])("refuses %s", async (_case, params, code) => {
const { agent } = createAgent();
await initializeClient(agent, {});
await expect(agent.setSessionTitle(params())).rejects.toMatchObject({ code });
});
});
describe("_session/archive and _session/unarchive", () => {
it("append AIR's archive title records, are idempotent and keep updatedAt", async () => {
const session = await writeTranscript({ prompt: "Fix the\nparser" });
@@ -1896,7 +1854,7 @@ describe("_session/list/subscribe", () => {
});
it("clears the failed turn of a session that runs here when a new turn starts", async () => {
const { noteSessionState } = await import("../../v1/session-index.js");
const { noteSessionState } = await import("../../v1/session-index/connection.js");
const session = { lastTurnFailed: true } as any;
noteSessionState(session, "idle", "running");
expect(session.lastTurnFailed).toBe(false);

View File

@@ -5,7 +5,7 @@
* schema, so a malformed v2 message fails the test.
*/
import { randomUUID } from "node:crypto";
import { appendFile, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { appendFile, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
@@ -35,7 +35,6 @@ import {
v1SetSessionConfigOptionRequest,
v2ConfigOptions,
} from "../../v2/session-requests.js";
import { encodeProjectPath } from "../../session-index/project-dirs.js";
import { startupFailure } from "../helpers.js";
import packageJson from "../../../package.json" with { type: "json" };
@@ -109,8 +108,6 @@ const sdk = vi.hoisted(() => ({
accountInfo: async (): Promise<Record<string, unknown>> => ({}),
/** The query switches its model (`setModel`). */
setModel: async (_model?: string): Promise<void> => {},
/** Claude Code stores a session title (`rename_session`). */
renameSession: async (_title: string, _sessionId?: string): Promise<void> => {},
/** Set to serve Remote Control, as a query of a CLI that supports it does. */
enableRemoteControl: undefined as
((enabled: boolean, name?: string) => Promise<{ session_url?: string }>) | undefined,
@@ -145,7 +142,6 @@ vi.mock("@anthropic-ai/claude-agent-sdk", async () => {
accountInfo: () => sdk.accountInfo(),
setMcpServers: (servers: Record<string, unknown>) => sdk.setMcpServers(servers),
setModel: (model?: string) => sdk.setModel(model),
renameSession: (title: string, sessionId?: string) => sdk.renameSession(title, sessionId),
...(sdk.enableRemoteControl ? { enableRemoteControl: sdk.enableRemoteControl } : {}),
});
return Object.assign(messages, controls);
@@ -398,9 +394,6 @@ describe("ACP protocol routing", () => {
fork: {},
additionalDirectories: {},
systemPrompt: { mode: { override: {} } },
status: {},
options: {},
title: {},
});
expect(response.capabilities?.providers).toEqual({});
// Not `_session/steering`, which v1 advertises in the top-level `_meta`.
@@ -754,236 +747,6 @@ describe("ACP v2 sessions", () => {
});
});
describe("session/status", () => {
/** Stores a transcript of `sessionId`, as Claude Code does at the first prompt. */
async function storeTranscript(sessionId: string, content = "{}\n") {
const project = path.join(tempDir, "claude", "projects", "-project");
await mkdir(project, { recursive: true });
await writeFile(path.join(project, `${sessionId}.jsonl`), content);
}
const status = (agent: v2.ClientContext, sessionId: string) =>
agent.request(v2.methods.agent.session.status, { sessionId });
const unknown = (sessionId: string) => ({ code: -32002, data: { sessionId } });
it("answers from memory and the stored transcript, and starts or sends nothing", async () => {
const client = v2Client();
await client.app.connectWith(connectRouter(), async (agent) => {
await initializeV2(agent);
const neverUsed = randomUUID();
await expect(status(agent, neverUsed)).rejects.toMatchObject(unknown(neverUsed));
// Not a UUID: no file is looked up under such a name.
await expect(status(agent, "../settings")).rejects.toMatchObject(unknown("../settings"));
const { sessionId } = await agent.request(v2.methods.agent.session.new, { cwd });
await client.authUpdate(1);
// The agent's post-setup work runs on a timer.
await new Promise((resolve) => setTimeout(resolve, 20));
const sent = client.sessionUpdates.length;
// The state as a `state_update` snapshot holds it, with the session's list.
expect(await status(agent, sessionId)).toEqual({
status: "open",
state: { state: "idle", background: [] },
});
await agent.request(v2.methods.agent.session.close, { sessionId });
// Claude Code writes the transcript at the first prompt: none is stored.
await expect(status(agent, sessionId)).rejects.toMatchObject(unknown(sessionId));
// An empty transcript holds no conversation to resume.
await storeTranscript(sessionId, "");
await expect(status(agent, sessionId)).rejects.toMatchObject(unknown(sessionId));
await storeTranscript(sessionId);
expect(await status(agent, sessionId)).toEqual({ status: "saved" });
expect(sdk.queryOptions).toHaveLength(1);
expect(client.sessionUpdates).toHaveLength(sent);
});
});
it("answers a session whose query ended without session/close from its transcript", async () => {
// Claude Code exits right after it started.
sdk.run = async function* () {};
const client = v2Client();
await client.app.connectWith(connectRouter(), async (agent) => {
await initializeV2(agent);
const { sessionId } = await agent.request(v2.methods.agent.session.new, { cwd });
await vi.waitFor(() => expect(v2Session(sessionId)).toBeUndefined());
await expect(status(agent, sessionId)).rejects.toMatchObject(unknown(sessionId));
await storeTranscript(sessionId);
expect(await status(agent, sessionId)).toEqual({ status: "saved" });
await client.authUpdate(1);
});
});
it("answers saved while a resume still starts the session, without waiting for it", async () => {
const sessionId = randomUUID();
await storeTranscript(sessionId);
let started!: () => void;
const starting = new Promise<void>((resolve) => {
started = resolve;
});
const initialization = sdk.initializationResult;
sdk.initializationResult = async () => {
await starting;
return initialization();
};
const client = v2Client();
await client.app.connectWith(connectRouter(), async (agent) => {
await initializeV2(agent);
const resumed = agent.request(v2.methods.agent.session.resume, { sessionId, cwd });
await vi.waitFor(() => expect(sdk.queryOptions).toHaveLength(1));
expect(await status(agent, sessionId)).toEqual({ status: "saved" });
started();
await resumed;
expect(await status(agent, sessionId)).toMatchObject({ status: "open" });
await client.authUpdate(1);
});
});
it("reports a session that waits for a permission as requires_action", async () => {
let answer!: (response: v2.RequestPermissionResponse) => void;
scriptTurns(async function* (options) {
await options.canUseTool!("Bash", { command: "ls" }, {
signal: new AbortController().signal,
suggestions: [],
toolUseID: "toolu_ls",
requestId: randomUUID(),
} as Parameters<NonNullable<Options["canUseTool"]>>[2]);
yield result(options);
yield idleState(options);
});
const client = v2Client({
answerPermission: (() =>
new Promise<v2.RequestPermissionResponse>((resolve) => {
answer = resolve;
})) as unknown as () => v2.RequestPermissionResponse,
});
await client.app.connectWith(connectRouter(), async (agent) => {
await initializeV2(agent);
const { sessionId } = await agent.request(v2.methods.agent.session.new, { cwd });
await agent.request(v2.methods.agent.session.prompt, { sessionId, prompt: text("go") });
await vi.waitFor(() => expect(client.permissionRequests).toHaveLength(1));
await vi.waitFor(() =>
expect(turnTrace(client.sessionUpdates).at(-1)).toBe("requires_action"),
);
expect(await agent.request(v2.methods.agent.session.status, { sessionId })).toEqual({
status: "open",
state: { state: "requires_action", background: [] },
});
answer({ outcome: { outcome: "selected", optionId: "allow-once" } });
await vi.waitFor(() => expect(turnTrace(client.sessionUpdates).at(-1)).toMatch(/^idle/));
await client.authUpdate(1);
});
// The status request added no state to the turn's.
expect(turnTrace(client.sessionUpdates)).toEqual([
"user_message",
"running",
"requires_action",
"running",
"idle end_turn",
]);
});
});
describe("session/set_title", () => {
it("titles an active session through Claude Code and publishes the stored title", async () => {
const renameSession = vi.fn(sdkDefaults.renameSession);
sdk.renameSession = renameSession;
const client = v2Client();
await client.app.connectWith(connectRouter(), async (agent) => {
await initializeV2(agent);
const { sessionId } = await agent.request(v2.methods.agent.session.new, { cwd });
client.sessionUpdates.length = 0;
const response = await agent.request(v2.methods.agent.session.setTitle, {
sessionId,
title: " Investigate\n OAuth timeout ",
});
expect(response).toEqual({ title: "Investigate OAuth timeout" });
expect(renameSession).toHaveBeenCalledWith("Investigate OAuth timeout", sessionId);
expect(client.sessionUpdates).toEqual([
{
sessionId,
update: { sessionUpdate: "session_info_update", title: "Investigate OAuth timeout" },
},
]);
await client.authUpdate(1);
});
});
it("titles the query that a resume started while the rename ran", async () => {
let release!: () => void;
const renameSession = vi.fn(async (_title: string, _sessionId?: string) => {
if (renameSession.mock.calls.length === 1) {
await new Promise<void>((resolve) => (release = resolve));
}
});
sdk.renameSession = renameSession;
const client = v2Client();
await client.app.connectWith(connectRouter(), async (agent) => {
await initializeV2(agent);
const { sessionId } = await agent.request(v2.methods.agent.session.new, { cwd });
const renamed = agent.request(v2.methods.agent.session.setTitle, {
sessionId,
title: "Mine",
});
await vi.waitFor(() => expect(renameSession).toHaveBeenCalledTimes(1));
await agent.request(v2.methods.agent.session.close, { sessionId });
await agent.request(v2.methods.agent.session.resume, { sessionId, cwd });
release();
expect(await renamed).toEqual({ title: "Mine" });
expect(renameSession.mock.calls).toEqual([
["Mine", sessionId],
["Mine", sessionId],
]);
await client.authUpdate(1);
});
});
it("titles a stored session that is not active in its transcript", async () => {
const sessionId = randomUUID();
const dir = path.join(tempDir, "claude", "projects", encodeProjectPath(cwd));
await mkdir(dir, { recursive: true });
const file = path.join(dir, `${sessionId}.jsonl`);
const prompt = { type: "user", sessionId, cwd, message: { role: "user", content: "hi" } };
await writeFile(file, JSON.stringify(prompt) + "\n");
const { app, authUpdate } = v2Client();
await app.connectWith(connectRouter(), async (agent) => {
await initializeV2(agent);
expect(
await agent.request(v2.methods.agent.session.setTitle, { sessionId, title: "Stored" }),
).toEqual({ title: "Stored" });
await authUpdate(1);
});
const records = (await readFile(file, "utf8"))
.trim()
.split("\n")
.slice(-2)
.map((line) => JSON.parse(line));
expect(records).toEqual([
{ type: "custom-title", customTitle: "Stored", sessionId },
{ type: "agent-name", agentName: "Stored", sessionId },
]);
});
it.each([
["an unknown session", { sessionId: randomUUID(), title: "Title" }, -32002],
["a title that is empty once normalized", { sessionId: randomUUID(), title: " \n " }, -32602],
])("refuses %s", async (_case, params, code) => {
const { app, authUpdate } = v2Client();
await app.connectWith(connectRouter(), async (agent) => {
await initializeV2(agent);
await expect(
agent.request(v2.methods.agent.session.setTitle, params),
).rejects.toMatchObject({ code });
await authUpdate(1);
});
});
});
it("offers a resumed Fable 5.2 session's model as `fable`, as a new session lists it", async () => {
// On resume, the CLI lists the transcript's Fable row under its id, with no `fable` row.
const row = (value: string, resolvedModel: string, displayName: string) => ({
@@ -4212,60 +3975,47 @@ describe("ACP v2 idle compaction", () => {
},
});
/**
* Prompts "hi" in a session created with `meta`, answers it with a large
* one-hour request, and waits 54 minutes. Returns the prompts Claude Code
* received and the client.
*/
async function promptAndIdle(meta?: Record<string, unknown>) {
const received: string[] = [];
sdk.run = async function* (prompts, options) {
const input = prompts[Symbol.asyncIterator]();
for (;;) {
const { value, done } = await input.next();
if (done) return;
received.push((value.message.content as Array<{ text: string }>)[0].text);
if (received.length === 1) {
yield {
type: "user",
message: value.message,
parent_tool_use_id: null,
uuid: value.uuid,
session_id: options.sessionId,
isReplay: true,
};
yield largeOneHourRequest(options);
} else {
yield { type: "system", subtype: "status", status: "compacting", uuid: randomUUID() };
}
yield result(options);
yield idleState(options);
}
};
const client = v2Client();
await client.app.connectWith(connectRouter(), async (agent) => {
await initializeV2(agent);
const { sessionId } = await agent.request(v2.methods.agent.session.new, {
cwd,
_meta: meta,
});
await agent.request(v2.methods.agent.session.prompt, { sessionId, prompt: text("hi") });
await vi.advanceTimersByTimeAsync(0);
await vi.advanceTimersByTimeAsync(54 * 60_000);
for (let i = 0; i < 20 && received.length < 2; i++) await vi.advanceTimersByTimeAsync(0);
for (let i = 0; i < 20 && turnTrace(client.sessionUpdates).length < 6; i++) {
await vi.advanceTimersByTimeAsync(0);
}
await client.authUpdate(1);
});
return { received, client };
}
/** The session is idle for 54 minutes after a large one-hour request, and the adapter's `/compact` runs. */
it("reports the compaction that the adapter starts as running, then idle, with no user_message", async () => {
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout", "Date"] });
try {
const { received, client } = await promptAndIdle();
const received: string[] = [];
sdk.run = async function* (prompts, options) {
const input = prompts[Symbol.asyncIterator]();
for (;;) {
const { value, done } = await input.next();
if (done) return;
received.push((value.message.content as Array<{ text: string }>)[0].text);
if (received.length === 1) {
yield {
type: "user",
message: value.message,
parent_tool_use_id: null,
uuid: value.uuid,
session_id: options.sessionId,
isReplay: true,
};
yield largeOneHourRequest(options);
} else {
yield { type: "system", subtype: "status", status: "compacting", uuid: randomUUID() };
}
yield result(options);
yield idleState(options);
}
};
const client = v2Client();
await client.app.connectWith(connectRouter(), async (agent) => {
await initializeV2(agent);
const { sessionId } = await agent.request(v2.methods.agent.session.new, { cwd });
await agent.request(v2.methods.agent.session.prompt, { sessionId, prompt: text("hi") });
await vi.advanceTimersByTimeAsync(0);
await vi.advanceTimersByTimeAsync(54 * 60_000);
for (let i = 0; i < 20 && received.length < 2; i++) await vi.advanceTimersByTimeAsync(0);
for (let i = 0; i < 20 && turnTrace(client.sessionUpdates).length < 6; i++) {
await vi.advanceTimersByTimeAsync(0);
}
await client.authUpdate(1);
});
expect(received).toEqual(["hi", "/compact"]);
expect(turnTrace(client.sessionUpdates)).toEqual([
@@ -4280,20 +4030,6 @@ describe("ACP v2 idle compaction", () => {
vi.useRealTimers();
}
});
/** The client's `settings` option reaches the CLI as `--settings`, and idle compaction reads it too. */
it("does not compact a session whose client turned idle compaction off in the settings option", async () => {
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout", "Date"] });
try {
const { received } = await promptAndIdle({
claudeCode: { options: { settings: { idleCompaction: false } } },
});
expect(received).toEqual(["hi"]);
} finally {
vi.useRealTimers();
}
});
});
// Claude Code runs model cycles that no prompt started, for example its
@@ -5941,57 +5677,6 @@ describe("ACP v2 requests that subagents make", () => {
]);
});
it("refuses session/status of a subagent's session, which does not advertise it", async () => {
let finish!: () => void;
const finished = new Promise<void>((resolve) => {
finish = resolve;
});
scriptTurns(async function* (options) {
yield toolUseMessage(options, "toolu_agent-1", "Agent", {
description: "Explore the project",
prompt: "Look around",
subagent_type: "Explore",
});
yield taskStarted(options, "agent-1", { prompt: "Look around" });
await finished;
yield taskNotification(options, "agent-1", {});
yield result(options);
yield idleState(options);
});
const client = v2Client();
await client.app.connectWith(connectRouter(), async (agent) => {
await initializeV2(agent);
const { sessionId } = await agent.request(v2.methods.agent.session.new, { cwd });
await agent.request(v2.methods.agent.session.prompt, { sessionId, prompt: text("hi") });
await vi.waitFor(() =>
expect(
client.sessionUpdates.some(
(notification) =>
notification.sessionId === "agent-1" &&
notification.update.sessionUpdate === "state_update",
),
).toBe(true),
);
await expect(
agent.request(v2.methods.agent.session.status, { sessionId: "agent-1" }),
).rejects.toMatchObject({
code: -32602,
message: expect.stringContaining("agent-1 is a subagent session"),
});
finish();
await vi.waitFor(() =>
expect(
client
.updates("state_update")
.some((update) => (update as { state: string }).state === "idle"),
).toBe(true),
);
await client.authUpdate(1);
});
});
it("sends a live child's configuration again after a replay's response, when the replay sent another", async () => {
scriptTurns(async function* (options) {
yield toolUseMessage(options, "toolu_agent-1", "Agent", {

View File

@@ -9,7 +9,7 @@
* `session_state_changed`.
*/
import { randomUUID } from "node:crypto";
import { mkdtemp, realpath, rm, writeFile } from "node:fs/promises";
import { mkdtemp, realpath, rm } from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
@@ -24,13 +24,6 @@ import { acpProtocolRouter } from "../../serve.js";
import { SettingsManager } from "../../settings.js";
import type { AgentHandle } from "../../v2/agent.js";
import { StoppedWork } from "../../v2/stopped-work.js";
import {
transcriptAssistant,
transcriptCompactBoundary,
transcriptCompactSummary,
transcriptUser,
writeTranscript,
} from "../helpers.js";
/** One Claude Code process: the prompts it reads and the messages it sends. */
type FakeClaudeCode = {
@@ -496,58 +489,12 @@ describe("ACP v2 core", () => {
expect(response.protocolVersion).toBe(2);
expect(response.capabilities?.session?.fork).toEqual({});
expect(response.capabilities?.session?.options).toEqual({});
expect(response.capabilities?.session?.prompt).toEqual({ image: {}, embeddedContext: {} });
expect((response.authMethods ?? []).map((method) => method.methodId)).toContain(
"console-login",
);
});
it.each(["environment", "caller object", "caller file"])(
"includes configured models omitted by the SDK from %s settings",
async (source) => {
const configured = {
availableModels: ["claude-haiku-4-5", "claude-haiku-5-5"],
modelOverrides: {
"claude-haiku-4-5": "global.anthropic.claude-haiku-4-5-20251001-v1:0",
"claude-haiku-5-5": "global.anthropic.claude-haiku-5-5",
},
};
let settings: typeof configured | string | undefined;
if (source === "environment") {
vi.stubEnv("CLAUDE_MODEL_CONFIG", JSON.stringify(configured));
} else if (source === "caller file") {
settings = "caller-settings.json";
await writeFile(path.join(cwd, settings), JSON.stringify(configured));
} else {
settings = configured;
}
const client = v2Client();
await client.connect(async (agent) => {
await agent.request(v2.methods.agent.initialize, {
protocolVersion: v2.PROTOCOL_VERSION,
info: { name: "v2-test-client", version: "1" },
});
const response = await agent.request(v2.methods.agent.session.new, {
cwd,
_meta: { claudeCode: { options: { settings } } },
});
const ids = ["default", ...Object.values(configured.modelOverrides)];
const model = response.configOptions?.find((option) => option.configId === "model");
expect(model).toMatchObject({
options: ids.map((value) => expect.objectContaining({ value })),
});
await agent.request(v2.methods.agent.session.setConfigOption, {
sessionId: response.sessionId,
configId: "model",
type: "id",
value: ids[2],
} as v2.SetSessionConfigOptionRequest);
expect(sdk.processes[0].setModel).toHaveBeenLastCalledWith(ids[2]);
});
},
);
it("routes the sessions started after a gateway login through the gateway", async () => {
const client = v2Client();
const baseUrl = "https://gateway.example.com";
@@ -1065,30 +1012,6 @@ describe("ACP v2 core", () => {
});
});
it("stays idle at the echo of a model change's local command output", async () => {
const client = v2Client();
await client.connect(async (agent) => {
const { claude } = await openSession(agent);
// As the bundled CLI sends it after `setModel`, with no turn: no
// `command_lifecycle` frame, result or `session_state_changed` follows.
claude.send({
type: "user",
message: {
role: "user",
content:
"<local-command-stdout>Set model to `haiku (claude-haiku-5-5)`</local-command-stdout>",
},
parent_tool_use_id: null,
uuid: randomUUID(),
session_id: "session",
timestamp: new Date().toISOString(),
isReplay: true,
});
await new Promise((resolve) => setTimeout(resolve, 20));
expect(client.states()).toEqual([]);
});
});
it("forwards the result of a local command, which streamed no text", async () => {
const client = v2Client();
await client.connect(async (agent) => {
@@ -1813,234 +1736,6 @@ describe("ACP v2 core", () => {
expect(error).toMatchObject({ code: -32602 });
});
describe("session options", () => {
const STORED = "7f1c5b0e-3c53-4c0c-9d0a-0d6c1f0a2b11";
const values = (options: v2.SessionConfigOption[] | undefined) =>
Object.fromEntries((options ?? []).map((o) => [o.configId, o.currentValue]));
const initialize = (agent: v2.ClientContext) =>
agent.request(v2.methods.agent.initialize, {
protocolVersion: v2.PROTOCOL_VERSION,
info: { name: "v2-test-client", version: "1.0.0" },
capabilities: {},
});
beforeEach(() => {
sdk.models = [
{
value: "default",
displayName: "Default",
description: "Recommended",
supportsAutoMode: true,
supportsEffort: true,
supportedEffortLevels: ["low", "medium", "high"],
},
{
value: "haiku",
displayName: "Haiku",
description: "Fast",
supportsAutoMode: false,
supportsEffort: true,
supportedEffortLevels: ["low", "medium"],
},
];
});
it("answers session/options with what session/new returns, from a query that it closes", async () => {
const client = v2Client();
const { options, created } = await client.connect(async (agent) => {
await initialize(agent);
const options = await agent.request(v2.methods.agent.session.options, {
cwd,
additionalDirectories: ["/extra"],
});
const created = await agent.request(v2.methods.agent.session.new, { cwd });
return { options, created };
});
const [probe, session] = sdk.processes;
expect(probe.options).toMatchObject({
cwd,
additionalDirectories: ["/extra"],
mcpServers: {},
persistSession: false,
});
expect(probe.close).toHaveBeenCalledTimes(1);
expect(session.options.persistSession).toBeUndefined();
const { sessionId, ...fields } = created;
expect(sessionId).not.toBe(probe.options.sessionId);
expect(options).toEqual(fields);
expect(options.availableCommands?.map((command) => command.name)).toContain("review");
expect(client.notifications).toEqual([]);
});
it("answers session/options of an active session with its current options, without a query", async () => {
const client = v2Client();
const { options, configOptions, wrongCwd } = await client.connect(async (agent) => {
const { response } = await openSession(agent);
const { sessionId } = response;
const { configOptions } = await agent.request(v2.methods.agent.session.setConfigOption, {
sessionId,
configId: "model",
type: "id",
value: "haiku",
});
const options = await agent.request(v2.methods.agent.session.options, { cwd, sessionId });
const wrongCwd = await agent
.request(v2.methods.agent.session.options, { cwd: tempDir, sessionId })
.catch((error: unknown) => error);
return { options, configOptions, wrongCwd };
});
expect(sdk.processes).toHaveLength(1);
expect(options.configOptions).toEqual(configOptions);
expect(values(options.configOptions).model).toBe("haiku");
expect(wrongCwd).toMatchObject({ code: -32602 });
});
it("answers session/options of a stored session from a query that resumes it", async () => {
const client = v2Client();
const status = await client.connect(async (agent) => {
await initialize(agent);
await agent.request(v2.methods.agent.session.options, { cwd, sessionId: STORED });
return agent
.request(v2.methods.agent.session.status, { sessionId: STORED })
.catch((error: unknown) => error);
});
const [probe] = sdk.processes;
expect(probe.options).toMatchObject({ resume: STORED, persistSession: false });
expect(probe.options.sessionId).toBeUndefined();
expect(probe.close).toHaveBeenCalledTimes(1);
// The session is not active: it has no transcript in this test.
expect(status).toMatchObject({ code: -32002 });
});
it("fails session/options of an unknown session with -32002", async () => {
sdk.startFailure = new Error(`No conversation found with session ID: ${STORED}`);
const client = v2Client();
const error = await client.connect(async (agent) => {
await initialize(agent);
return agent
.request(v2.methods.agent.session.options, { cwd, sessionId: STORED })
.catch((error: unknown) => error);
});
expect(error).toMatchObject({ code: -32002, data: { sessionId: STORED } });
expect(sdk.processes[0].close).toHaveBeenCalled();
});
it("starts a new session with the request's configOptions, in list order, without an update", async () => {
const client = v2Client();
const response = await client.connect(async (agent) => {
await initialize(agent);
return agent.request(v2.methods.agent.session.new, {
cwd,
configOptions: [
// Skipped: an unknown option, a boolean for a select, and a value not offered.
{ configId: "nope", type: "id", value: "x" },
{ configId: "ultracode", type: "boolean", value: true },
{ configId: "model", type: "id", value: "haiku" },
// Haiku offers no "high": the entry is checked against the options the model left.
{ configId: "effort", type: "id", value: "high" },
{ configId: "mode", type: "id", value: "plan" },
],
});
});
const [claude] = sdk.processes;
expect(claude.setModel).toHaveBeenCalledWith("haiku");
expect(claude.setPermissionMode).toHaveBeenCalledWith("plan");
expect(values(response.configOptions)).toMatchObject({ model: "haiku", mode: "plan" });
expect(values(response.configOptions).effort).not.toBe("high");
expect(client.notifications).toEqual([]);
});
it.each<[string, v2.InitialSessionConfigOption[]]>([
[
"two entries of one configId",
[
{ configId: "mode", type: "id", value: "plan" },
{ configId: "mode", type: "id", value: "default" },
],
],
["a value type it does not take", [{ configId: "mode", type: "_custom", value: 1 }]],
])("rejects %s before it starts a query", async (_name, configOptions) => {
const client = v2Client();
const errors = await client.connect(async (agent) => {
await initialize(agent);
return Promise.all(
[
agent.request(v2.methods.agent.session.new, { cwd, configOptions }),
agent.request(v2.methods.agent.session.resume, {
sessionId: STORED,
cwd,
configOptions,
}),
].map((request) => request.catch((error: unknown) => error)),
);
});
expect(errors).toEqual([
expect.objectContaining({ code: -32602 }),
expect.objectContaining({ code: -32602 }),
]);
expect(sdk.processes).toHaveLength(0);
});
it("sets the configOptions of a session/resume on the session, an active one too", async () => {
const client = v2Client();
const { started, active } = await client.connect(async (agent) => {
await initialize(agent);
const resume = (configOptions: v2.InitialSessionConfigOption[]) =>
agent.request(v2.methods.agent.session.resume, { sessionId: STORED, cwd, configOptions });
const started = await resume([{ configId: "mode", type: "id", value: "plan" }]);
const active = await resume([{ configId: "model", type: "id", value: "haiku" }]);
return { started, active };
});
expect(sdk.processes).toHaveLength(1);
expect(values(started.configOptions).mode).toBe("plan");
expect(values(active.configOptions)).toMatchObject({ mode: "plan", model: "haiku" });
expect(client.sessionUpdates.map((update) => update.sessionUpdate)).not.toContain(
"config_option_update",
);
});
it("leaves the notice of an Auto mode fallback for the first prompt", async () => {
const client = v2Client();
const { response, before } = await client.connect(async (agent) => {
await initialize(agent);
const response = await agent.request(v2.methods.agent.session.new, {
cwd,
configOptions: [
{ configId: "mode", type: "id", value: "auto" },
// Haiku cannot serve Auto.
{ configId: "model", type: "id", value: "haiku" },
],
});
const before = [...client.sessionUpdates];
const claude = sdk.processes[0];
const prompted = agent.request(v2.methods.agent.session.prompt, {
sessionId: response.sessionId,
prompt: PROMPT,
});
const prompt = await claude.prompt(0);
claude.send(lifecycle(prompt, "started"));
claude.send(echo(prompt));
claude.send(result());
claude.send(sessionState("idle"));
await prompted;
return { response, before };
});
expect(values(response.configOptions)).toMatchObject({ mode: "acceptEdits", model: "haiku" });
expect(before).toEqual([]);
expect(client.sessionUpdates.filter((update) => update.sessionUpdate === "notice")).toEqual([
expect.objectContaining({ title: "Auto mode unavailable" }),
]);
});
});
it("rejects additional directories that are not absolute", async () => {
const client = v2Client();
const error = await client.connect(async (agent) => {
@@ -2200,49 +1895,6 @@ describe("ACP v2 core", () => {
]);
});
it("replays the messages before a compaction from the transcript", async () => {
const sessionId = randomUUID();
// The records of `/compact` between two prompts, as Claude Code writes
// them: the boundary has no parent and links "Hello" logically. The SDK
// looks the project up by the real path of `cwd` (`/private/var` on macOS).
const project = (await realpath(cwd)).replace(/[^a-zA-Z0-9]/g, "-");
await writeTranscript(path.join(tempDir, "claude", "projects", project), sessionId, [
transcriptUser("u1", "hi"),
transcriptAssistant("a1", "Hello"),
transcriptCompactBoundary("b1", "a1"),
transcriptCompactSummary("s1"),
transcriptUser("u2", "identify yourself"),
]);
// The transcript read and the read across the compaction both go to disk.
const actual = await vi.importActual<typeof import("@anthropic-ai/claude-agent-sdk")>(
"@anthropic-ai/claude-agent-sdk",
);
vi.mocked(getSessionMessages)
.mockImplementationOnce(actual.getSessionMessages)
.mockImplementationOnce(actual.getSessionMessages);
const client = v2Client();
await client.connect(async (agent) => {
await agent.request(v2.methods.agent.initialize, {
protocolVersion: v2.PROTOCOL_VERSION,
info: { name: "v2-test-client", version: "1.0.0" },
capabilities: {},
});
await agent.request(v2.methods.agent.session.resume, {
sessionId,
cwd,
replayFrom: { type: "start" },
});
});
expect(
client.sessionUpdates.flatMap((update) =>
update.sessionUpdate === "user_message" || update.sessionUpdate === "compaction_update"
? [update.sessionUpdate]
: [],
),
).toEqual(["user_message", "compaction_update", "user_message"]);
});
it.each([
[
"a prompt that Claude Code started after the cancel answered it",
@@ -2467,44 +2119,6 @@ describe("ACP v2 core", () => {
});
});
it.each([
["a notice when the turn goes on to end", { result: "" }, "end_turn", 1],
["a notice once when the result repeats its text", {}, "end_turn", 1],
["the idle's error when the turn fails", { is_error: true }, "error", 0],
])(
"reports Claude Code's message for a failed API request as %s, not as Claude's text",
async (_, fields, stopReason, notices) => {
const text = "API Error: Connection lost mid-response. The response above may be incomplete.";
const client = v2Client();
await client.connect(async (agent) => {
const { claude } = await openSession(agent);
claude.send(cycleStart());
claude.send({
...assistant("msg_api_error", [{ type: "text", text }]),
message: {
id: "msg_api_error",
type: "message",
role: "assistant",
model: "<synthetic>",
content: [{ type: "text", text }],
usage: {},
},
error: "server_error",
});
claude.send(result({ result: text, ...fields }));
await vi.waitFor(() => expect(client.states()).toEqual(["running", "idle"]));
});
expect(
client.sessionUpdates.filter((update) => update.sessionUpdate === "agent_message_chunk"),
).toEqual([]);
expect(client.sessionUpdates.filter((update) => update.sessionUpdate === "notice")).toEqual(
Array(notices).fill({ sessionUpdate: "notice", severity: "error", title: text }),
);
expect(client.sessionUpdates.at(-1)).toMatchObject({ state: "idle", stopReason });
},
);
it.each([
[
"an error at the turn limit",

View File

@@ -15,7 +15,6 @@ import {
ForkSessionResponse,
InitializeRequest,
InitializeResponse,
InitialSessionConfigOption,
ListProvidersRequest,
ListProvidersResponse,
McpServer,
@@ -44,15 +43,9 @@ import {
SetSessionConfigOptionResponse,
SetSessionModeRequest,
SetSessionModeResponse,
SessionOptionsRequest,
SessionOptionsResponse,
SessionStatusRequest,
SessionStatusResponse,
CloseSessionRequest,
CloseSessionResponse,
DeleteSessionRequest,
SetSessionTitleRequest,
SetSessionTitleResponse,
DeleteSessionResponse,
WriteTextFileRequest,
WriteTextFileResponse,
@@ -98,12 +91,6 @@ import {
} from "./goal-extension.js";
import { checklistPlanUpdate, resetPlanUpdates, type SubagentChecklists } from "../plans.js";
import { PromptSuggestions } from "../prompt-suggestions.js";
import { savedSessionStatus, SESSION_STATUS_CAPABILITY } from "../session-status.js";
import {
applyInitialConfigOptions,
initialConfigOptions,
SESSION_OPTIONS_CAPABILITY,
} from "../initial-config-options.js";
import { sanitizeTitle, SessionTitles } from "../session-titles.js";
import {
AIR_NATIVE_SUBAGENT_SESSIONS_CAPABILITY,
@@ -143,16 +130,16 @@ import {
clientSupportsAirCapability,
withAirMeta,
} from "../air-extension.js";
import * as sessionIndex from "./session-index.js";
import * as sessionIndex from "./session-index/connection.js";
import type {
BatchGetSessionsRequest,
RenameSessionRequest,
SessionIdRequest,
} from "../session-index/service.js";
} from "./session-index/service.js";
import type {
ListSubscribeRequest,
ListUnsubscribeRequest,
} from "../session-index/list-subscriptions.js";
} from "./session-index/list-subscriptions.js";
import {
AsyncTaskRuntime,
backgroundBashTaskFromToolResult,
@@ -160,6 +147,20 @@ import {
clientSupportsAsyncTasks,
} from "../async-tasks.js";
import { SessionBackgroundWork } from "./background-work.js";
import {
ASYNC_TASK_STOP_METHOD,
parseAsyncTaskStopRequest,
type AsyncTaskStopRequest,
type AsyncTaskStopResponse,
} from "./async-task-stop.js";
import {
STEER_METHOD,
STEER_PRIORITY_LATER,
STEER_PRIORITY_NOW,
parseSteerRequest,
type SteerRequest,
type SteerResponse,
} from "./steering.js";
import {
AUTH_STATUS_UPDATE_METHOD,
type AuthStatus,
@@ -211,7 +212,6 @@ import {
replaySubagentTerminalState,
} from "../subagent-replay.js";
import {
readHistoryAcrossCompactions,
readResumedSession,
readResumedTail,
type ResumedSessionSnapshot,
@@ -576,112 +576,6 @@ const TURN_NO_RESULT_MESSAGE =
"The turn ended without a result: the agent went idle while this prompt was still in flight " +
"(e.g. the model stream dropped mid-turn). Any partial output may be incomplete; please retry.";
/** Custom (extension) request method a client uses to steer the turn that is
* currently running: the message is injected into the in-flight turn rather
* than queued as a separate `session/prompt`. Named `_session/steering` per the
* agreed ACP steering wire protocol; advertised to clients via the top-level
* `InitializeResponse._meta.steering.supported`. */
const STEER_METHOD = "_session/steering";
/** Stops one Claude background task without cancelling the parent prompt turn. */
const ASYNC_TASK_STOP_METHOD = "_session/async_task/stop";
type AsyncTaskStopRequest = {
sessionId: string;
asyncTaskId: string;
};
type AsyncTaskStopResponse = {
stopped: boolean;
};
function parseAsyncTaskStopRequest(value: unknown): AsyncTaskStopRequest {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw RequestError.invalidParams(undefined, "async task stop params must be an object");
}
const params = value as Record<string, unknown>;
const sessionId = typeof params.sessionId === "string" ? params.sessionId.trim() : "";
const asyncTaskId = typeof params.asyncTaskId === "string" ? params.asyncTaskId.trim() : "";
if (!sessionId) {
throw RequestError.invalidParams(
undefined,
"async task stop params require a non-empty sessionId",
);
}
if (!asyncTaskId) {
throw RequestError.invalidParams(
undefined,
"async task stop params require a non-empty asyncTaskId",
);
}
return { sessionId, asyncTaskId };
}
/** How urgently the SDK delivers a steered message relative to the running
* turn — an internal Claude implementation detail, not part of the wire
* contract. `now` pre-empts the current generation, while `later` waits for a
* pending permission/elicitation callback to settle instead of cancelling its
* ACP request and hiding the client's user-input card (IJAI-1191). */
const STEER_PRIORITY_NOW = "now" as const;
const STEER_PRIORITY_LATER = "later" as const;
/** Request-level steering options. `promptRequired` is opt-in so existing Hosts
* keep the established idle fallback behavior. */
type SteerMeta = {
[key: string]: unknown;
steering?: {
idleBehavior?: "promptRequired";
};
};
/** Params of a {@link STEER_METHOD} request. Shaped like the relevant subset of
* a `PromptRequest` so the same `promptToClaude` conversion applies. Delivery
* priority is deliberately NOT exposed here — it's an internal detail the agent
* chooses (see {@link STEER_PRIORITY}). */
export type SteerRequest = {
sessionId: string;
prompt: PromptRequest["prompt"];
_meta?: SteerMeta | null;
};
/** Result of a {@link STEER_METHOD} request. The legacy `startedNewTurn` result
* remains the default idle behavior; `promptRequired` is returned only when the
* Host explicitly opts into the host-owned fallback in request `_meta`. */
export type SteerResponse =
| { outcome: "injected" }
| { outcome: "startedNewTurn" }
| { outcome: "promptRequired"; reason: "noRunningTurn" };
/** Validate raw JSON-RPC params into a {@link SteerRequest}. Kept minimal — the
* content blocks are handed to `promptToClaude`, which tolerates unknown block
* types — but `sessionId` and a non-empty `prompt` array are required. */
function parseSteerRequest(params: unknown): SteerRequest {
if (!params || typeof params !== "object") {
throw RequestError.invalidParams(undefined, "steer params must be an object");
}
const { sessionId, prompt, _meta } = params as Record<string, unknown>;
if (typeof sessionId !== "string" || sessionId.length === 0) {
throw RequestError.invalidParams(undefined, "steer params require a non-empty sessionId");
}
if (!Array.isArray(prompt) || prompt.length === 0) {
throw RequestError.invalidParams(undefined, "steer params require a non-empty prompt array");
}
const steering =
_meta && typeof _meta === "object" ? (_meta as Record<string, unknown>).steering : undefined;
const idleBehavior =
steering && typeof steering === "object"
? (steering as Record<string, unknown>).idleBehavior
: undefined;
if (idleBehavior !== undefined && idleBehavior !== "promptRequired") {
throw RequestError.invalidParams(undefined, "unsupported steering idleBehavior");
}
return {
sessionId,
prompt: prompt as PromptRequest["prompt"],
_meta: _meta as SteerMeta | null | undefined,
};
}
/** Who started a turn. `idleCompaction` is the adapter's own `/compact` (see
* `IdleCompaction`): it is not a user prompt, so it neither disarms idle
* compaction, asks for a title from its prompt, nor clears the published prompt suggestion. */
@@ -2033,13 +1927,8 @@ export class ClaudeAcpAgent {
delete: {},
fork: {},
list: {},
// `session/options`, and `configOptions` on `session/new`, `load` and `resume`.
options: SESSION_OPTIONS_CAPABILITY,
resume: {},
status: SESSION_STATUS_CAPABILITY,
systemPrompt: SYSTEM_PROMPT_CAPABILITY,
// `session/set_title`.
title: {},
...(this.toolCallCapabilities.air.client ? { subagents: {} } : {}),
};
@@ -2120,77 +2009,15 @@ export class ClaudeAcpAgent {
}
async newSession(params: NewSessionRequest): Promise<NewSessionResponse> {
// Without `configOptions`: a restart of the query restores the session's choices itself.
const { configOptions, ...request } = params;
const initial = initialConfigOptions(configOptions);
while (this.providerUpdate) await this.providerUpdate;
const created = await this.createSession(request, {
const response = await this.createSession(params, {
// Revisit these meta values once we support resume
resume: (params._meta as NewSessionMeta | undefined)?.claudeCode?.options?.resume,
});
const response = await this.withInitialConfigOptions(created, initial);
this.afterSetupResponse(response.sessionId, params.mcpServers);
return response;
}
/**
* `session/options`: the modes and config options that `session/new` with
* the same `cwd`, additional directories and `_meta` would answer with, or
* `session/load` and `session/resume` of `sessionId`.
*
* A session that this connection holds answers with what it has now.
* Otherwise a query starts as the session's would, without MCP servers, and
* closes once its options are read (`optionsOnly` in `createSession`). The
* query does not persist: it stores no session, and it does not write to
* the transcript of `sessionId`. Claude Code runs the user's `SessionStart`
* hooks in it, as in every query.
*
* It sends no update, and it creates, loads and changes no session. A
* `sessionId` without a stored conversation fails with `-32002`.
*/
async sessionOptions(params: SessionOptionsRequest): Promise<SessionOptionsResponse> {
while (this.providerUpdate) await this.providerUpdate;
const held = params.sessionId != null ? this.sessions[params.sessionId] : undefined;
if (held) {
await this.validateCwd(params.cwd, params.additionalDirectories);
return { modes: held.modes, configOptions: held.configOptions };
}
const { modes, configOptions } = await this.createSession(
{
cwd: params.cwd,
mcpServers: [],
additionalDirectories: params.additionalDirectories,
_meta: params._meta,
},
{ resume: params.sessionId ?? undefined, optionsOnly: true },
);
return { modes, configOptions };
}
/**
* The setup response once the request's `configOptions` are set on the
* session, in list order, as `session/set_config_option` sets each one. An
* entry that the session refuses is skipped (`applyInitialConfigOptions`).
* No update goes out for them: the response carries the modes and options.
*/
private async withInitialConfigOptions<R extends NewSessionResponse>(
response: R,
entries: readonly InitialSessionConfigOption[],
): Promise<R> {
if (entries.length === 0) return response;
const { sessionId } = response;
await this.serializeConfigurationChange(sessionId, () =>
applyInitialConfigOptions(
entries,
(entry) => this.selectSessionConfigOption({ ...entry, sessionId }, true),
this.logger,
),
);
const session = this.sessions[sessionId];
if (!session) return response;
return { ...response, modes: session.modes, configOptions: session.configOptions };
}
/**
* Start the CLI of a loaded session, and replay the history while the CLI
* starts. The CLI start takes most of the load time, and the replay needs
@@ -2296,12 +2123,8 @@ export class ClaudeAcpAgent {
}
async resumeSession(params: ResumeSessionRequest): Promise<ResumeSessionResponse> {
const initial = initialConfigOptions(params.configOptions);
while (this.providerUpdate) await this.providerUpdate;
const result = await this.withInitialConfigOptions(
await this.getOrCreateSession(params),
initial,
);
const result = await this.getOrCreateSession(params);
this.afterSetupResponse(params.sessionId, params.mcpServers ?? []);
return result;
@@ -2309,7 +2132,6 @@ export class ClaudeAcpAgent {
async loadSession(params: LoadSessionRequest): Promise<LoadSessionResponse> {
const timing = new SessionTiming(this.logger, "load", params.sessionId);
const initial = initialConfigOptions(params.configOptions);
while (this.providerUpdate) await this.providerUpdate;
let result: NewSessionResponse;
if (this.sessions[params.sessionId]) {
@@ -2321,7 +2143,6 @@ export class ClaudeAcpAgent {
result = await this.createSessionWhileReplaying(params, timing);
}
timing.phase("replay");
result = await this.withInitialConfigOptions(result, initial);
// After the replay, so the commands do not interleave with the history.
this.afterSetupResponse(params.sessionId, params.mcpServers ?? []);
@@ -7946,24 +7767,6 @@ export class ClaudeAcpAgent {
return {};
}
/**
* `session/status`: `open` for a session that this connection holds. A held
* session whose query ended (`queryClosed`) is `open` too: a `session/load`
* with the same params returns it as it is, and its prompts fail with what
* happened. Every other session is `saved` or unknown by its stored
* transcript (see `session-status.ts`). A subagent's session is unknown,
* as for the other session requests.
*/
async sessionStatus({ sessionId }: SessionStatusRequest): Promise<SessionStatusResponse> {
if (Object.hasOwn(this.sessions, sessionId)) return { status: "open" };
return savedSessionStatus(sessionId);
}
/** See {@link sessionIndex.SessionIndexConnection.setTitle}. */
setSessionTitle(params: SetSessionTitleRequest): Promise<SetSessionTitleResponse> {
return this.sessionIndex.setTitle(params);
}
/** Per client: see {@link sessionIndex.SessionIndexConnection.deleteSession}. */
async deleteSession(params: DeleteSessionRequest): Promise<DeleteSessionResponse> {
return this.sessionIndex.deleteSession(params);
@@ -7983,14 +7786,8 @@ export class ClaudeAcpAgent {
);
}
/**
* `duringSetup`: the value is one of a setup request's `configOptions`. No
* mode update goes out, and the warning of an Auto mode fallback waits for
* the next prompt.
*/
private async selectSessionConfigOption(
params: SetSessionConfigOptionRequest,
duringSetup = false,
): Promise<SetSessionConfigOptionResponse> {
const session = this.sessions[params.sessionId];
if (!session) {
@@ -8021,12 +7818,8 @@ export class ClaudeAcpAgent {
let effectiveValue = resolvedValue;
if (params.configId === MODE_CONFIG_ID) {
effectiveValue = await this.sessionModes.selectMode(
params.sessionId,
resolvedValue,
duringSetup,
);
if (!duringSetup) await this.sessionModes.publishCurrent(params.sessionId, effectiveValue);
effectiveValue = await this.sessionModes.selectMode(params.sessionId, resolvedValue);
await this.sessionModes.publishCurrent(params.sessionId, effectiveValue);
} else if (params.configId === MODEL_CONFIG_ID) {
await this.sessions[params.sessionId].query.setModel(resolvedValue);
}
@@ -8034,13 +7827,7 @@ export class ClaudeAcpAgent {
// effort changes and effort changes induced by a model switch go through
// the same path.
await this.applyConfigOptionValue(
params.sessionId,
session,
params.configId,
effectiveValue,
duringSetup,
);
await this.applyConfigOptionValue(params.sessionId, session, params.configId, effectiveValue);
return { configOptions: session.configOptions };
}
@@ -8052,12 +7839,7 @@ export class ClaudeAcpAgent {
): Promise<void> {
const replayStartedAt = performance.now();
const toolUseCache: ToolUseCache = {};
// A load replays the messages before each compaction too, unless reading them fails.
const messages = await readHistoryAcrossCompactions(
sessionId,
resumedMessages ?? (await getSessionMessages(sessionId)),
this.logger,
);
const messages = resumedMessages ?? (await getSessionMessages(sessionId));
const historyLoadedAt = performance.now();
this.logger.log(
`[session/replay] sessionId=${sessionId} phase=read durationMs=${Math.round(historyLoadedAt - replayStartedAt)} messages=${messages.length}`,
@@ -9556,8 +9338,6 @@ export class ClaudeAcpAgent {
session: Session,
configId: string,
value: string,
/** See {@link selectSessionConfigOption}. */
duringSetup = false,
): Promise<void> {
if (configId === MODE_CONFIG_ID) {
this.sessionModes.syncConfig(session, value);
@@ -9667,8 +9447,7 @@ export class ClaudeAcpAgent {
// still precedes the caller's config_option_update so order-sensitive
// clients update currentModeId before re-rendering the option list.
if (modeDowngraded) {
if (duringSetup) this.sessionModes.deferFallbackWarning(session);
else await this.sessionModes.publishFallbackState(sessionId, session);
await this.sessionModes.publishFallbackState(sessionId, session);
}
// Last, so the switch's own control requests don't queue behind it.
if (modelChanged) this.refreshContextWindowInBackground(sessionId, session);
@@ -9967,12 +9746,6 @@ export class ClaudeAcpAgent {
replayState?: ReplayState;
/** A close of this session during startup cancels registration of its replacement. */
replacingSession?: Session;
/**
* `session/options`: the query answers with the modes and config options
* and closes. No session is registered, and Claude Code stores nothing
* of the query (`persistSession: false`).
*/
optionsOnly?: boolean;
} = {},
): Promise<NewSessionResponse> {
const createStartedAt = performance.now();
@@ -10365,7 +10138,6 @@ export class ClaudeAcpAgent {
},
...(creationOpts.resume !== undefined && { resume: creationOpts.resume }),
...(creationOpts.forkSession !== undefined && { forkSession: creationOpts.forkSession }),
...(creationOpts.optionsOnly ? { persistSession: false } : {}),
abortController,
};
@@ -10449,15 +10221,15 @@ export class ClaudeAcpAgent {
}
}
// Apply the effective `availableModels` allowlist before any
// Apply user's `availableModels` allowlist from settings.json before any
// downstream model handling. `initializationResult.models` is already
// policy-filtered by the CLI; we rebuild the picker from the allowlist
// so the user's exact spellings (and `modelOverrides` targets) are the
// values passed to `setModel`, keeping configOptions, the current-model
// resolver, and the stored modelInfos consistent with what the user
// configured. Managed `deniedModels` drops entries the CLI would refuse.
const { availableModels: settingsAvailableModels, modelOverrides: settingsModelOverrides } =
settingsManager.getModelSelectionSettings(configuredSettingsObject);
const settingsAvailableModels = settingsManager.getSettings().availableModels;
const settingsModelOverrides = settingsManager.getSettings().modelOverrides;
const resumedModel = await resumedModelHint;
// A resumed Fable 5.2 session lists Fable 5.2 as `fable`, as a new
// session does (see `aliasResumedModelRow`).
@@ -10585,10 +10357,6 @@ export class ClaudeAcpAgent {
},
ultracode,
);
if (creationOpts.optionsOnly) {
this.discardUnregisteredQuery(q, input, settingsManager);
return { sessionId, modes, configOptions };
}
const initialEffort = configOptions.find((option) => option.id === EFFORT_CONFIG_ID);
if (useRecommendedValue && typeof initialEffort?.currentValue === "string") {
await q.applyFlagSettings(effortFlagSettings(initialEffort.currentValue, ultracodeEnabled));
@@ -10707,8 +10475,7 @@ export class ClaudeAcpAgent {
this.claudeIsWorking(sessionId, registered) ||
(registered.openUserInputs?.size ?? 0) > 0,
contextTokens: () => registered.contextUsedTokens,
settings: () =>
registered.settingsManager.getCompactionSettings(configuredSettingsObject),
settings: () => registered.settingsManager.getSettings(),
compact: () =>
this.prompt(
{ sessionId, prompt: [{ type: "text", text: "/compact" }] },
@@ -11273,11 +11040,8 @@ export function v1AgentApp(
)
.onRequest(methods.agent.session.list, (ctx) => agent.listSessions(ctx.params))
.onRequest(methods.agent.session.delete, (ctx) => agent.deleteSession(ctx.params))
.onRequest(methods.agent.session.setTitle, (ctx) => agent.setSessionTitle(ctx.params))
.onRequest(methods.agent.session.resume, (ctx) => agent.resumeSession(ctx.params))
.onRequest(methods.agent.session.close, (ctx) => agent.closeSession(ctx.params))
.onRequest(methods.agent.session.status, (ctx) => agent.sessionStatus(ctx.params))
.onRequest(methods.agent.session.options, (ctx) => agent.sessionOptions(ctx.params))
.onRequest(methods.agent.session.setMode, (ctx) => agent.setSessionMode(ctx.params))
.onRequest(methods.agent.session.setConfigOption, (ctx) =>
agent.setSessionConfigOption(ctx.params),

35
src/v1/async-task-stop.ts Normal file
View File

@@ -0,0 +1,35 @@
import { RequestError } from "@agentclientprotocol/sdk/unstable";
/** Stops one Claude background task without cancelling the parent prompt turn. */
export const ASYNC_TASK_STOP_METHOD = "_session/async_task/stop";
export type AsyncTaskStopRequest = {
sessionId: string;
asyncTaskId: string;
};
export type AsyncTaskStopResponse = {
stopped: boolean;
};
export function parseAsyncTaskStopRequest(value: unknown): AsyncTaskStopRequest {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw RequestError.invalidParams(undefined, "async task stop params must be an object");
}
const params = value as Record<string, unknown>;
const sessionId = typeof params.sessionId === "string" ? params.sessionId.trim() : "";
const asyncTaskId = typeof params.asyncTaskId === "string" ? params.asyncTaskId.trim() : "";
if (!sessionId) {
throw RequestError.invalidParams(
undefined,
"async task stop params require a non-empty sessionId",
);
}
if (!asyncTaskId) {
throw RequestError.invalidParams(
undefined,
"async task stop params require a non-empty asyncTaskId",
);
}
return { sessionId, asyncTaskId };
}

View File

@@ -40,7 +40,7 @@
import { createReadStream } from "node:fs";
import { createInterface } from "node:readline";
import { TASK_NOTIFICATION_TAG, taskNotificationsOf } from "../task-notifications.js";
import { TASK_NOTIFICATION_TAG, taskNotificationsOf } from "../../task-notifications.js";
export type BackgroundTasks = {
/** The ids of the tasks that started and did not end, in start order. */

View File

@@ -15,12 +15,11 @@ import {
type InitializeRequest,
type ListSessionsRequest,
type ListSessionsResponse,
type SetSessionTitleRequest,
type SetSessionTitleResponse,
} from "@agentclientprotocol/sdk/unstable";
import type { Session } from "./acp-agent.js";
import type { AcpClient } from "../acp-client.js";
import type { Logger } from "../logger.js";
import type { Query } from "@anthropic-ai/claude-agent-sdk";
import type { Session } from "../acp-agent.js";
import type { AcpClient } from "../../acp-client.js";
import type { Logger } from "../../logger.js";
import {
AIR_SESSION_ARCHIVE_CAPABILITY,
AIR_SESSION_BATCH_GET_CAPABILITY,
@@ -30,10 +29,9 @@ import {
AIR_SESSION_WORKTREES_CAPABILITY,
clientSupportsAirCapability,
withAirMeta,
} from "../air-extension.js";
import type { OwnSessionState } from "../session-index/activity.js";
import { isArchivedTitle, visibleTitle } from "../session-index/archive-title.js";
import { renameSession, retitleSession, type RetitledSession } from "../session-index/rename.js";
} from "../../air-extension.js";
import type { OwnSessionState } from "./activity.js";
import { isArchivedTitle, storedTitle, visibleTitle } from "./archive-title.js";
import {
LIST_CHANGES_METHOD,
LIST_SUBSCRIBE_METHOD,
@@ -43,11 +41,12 @@ import {
type ListSubscribeRequest,
type ListSubscribeResponse,
type ListUnsubscribeRequest,
} from "../session-index/list-subscriptions.js";
} from "./list-subscriptions.js";
import {
archiveInsteadOfDelete,
deleteSessionIfStored,
archiveTo,
renameTo,
parseRenameSessionRequest,
parseSessionIdRequest,
parseBatchGetSessionsRequest,
@@ -56,12 +55,13 @@ import {
SESSION_RENAME_METHOD,
SESSION_UNARCHIVE_METHOD,
SessionIndexService,
transcriptWriter,
type RenameSessionRequest,
type RetitleOptions,
type SessionIdRequest,
type TitleChange,
type BatchGetSessionsRequest,
type BatchGetSessionsResponse,
} from "../session-index/service.js";
} from "./service.js";
/** What the session index keeps on each `Session`. */
export type SessionIndexFields = {
@@ -115,9 +115,9 @@ export class SessionIndexConnection {
* exactly as before.
*/
service?: SessionIndexService;
/** The transcript writes of a client without `sessionIndex` (see
* {@link transcriptWriter}). */
private transcripts?: SessionIndexService;
/** The archive behind `session/delete` of an AIR client without
* `sessionIndex`: the same writes, without a list or a watcher. */
private archiver?: SessionIndexService;
/** When the query of a session failed here, by session id. */
private readonly failedQueries = new Map<string, number>();
@@ -250,34 +250,24 @@ export class SessionIndexConnection {
return sessions.filter((session) => !isArchivedTitle(session.customTitle));
}
/** `_session/rename` of a `sessionIndex` client (see {@link renameSession}). */
/** `_session/rename`: names a session; no generated title replaces it.
* An archived session stays archived. */
async rename(request: RenameSessionRequest): Promise<EmptyResponse> {
await this.renameWith(this.requireService(SESSION_RENAME_METHOD), request);
const index = this.requireService(SESSION_RENAME_METHOD);
const { sessionId } = request;
// As the CLI keeps it, and never with the archive prefix: a rename does
// not change the archive state.
const title = storedTitle(request.title, false, sessionId);
// The title as stored: an archived one is cut with its prefix.
const shown = (stored: string | undefined) =>
stored === undefined ? title : visibleTitle(stored);
await this.retitle(index, sessionId, renameTo(title), shown, {
mayBeUnwritten: true,
sidecar: "always",
});
return {};
}
/** `session/set_title`, for every client: the rename of `_session/rename`,
* without the session index of a client that did not ask for it. */
async setTitle(request: SetSessionTitleRequest): Promise<SetSessionTitleResponse> {
const title = await this.renameWith(
this.service ?? this.writer(),
parseRenameSessionRequest(request),
);
return { title };
}
private renameWith(index: SessionIndexService, request: RenameSessionRequest): Promise<string> {
return renameSession(index, request, this.loaded(request.sessionId), (stored) =>
this.clientTitle(stored),
);
}
/** The session loaded here, as a rename reaches it. */
private loaded(sessionId: string): RetitledSession | undefined {
const session = this.host.agent.sessions[sessionId];
return session && { session, titles: session.titles };
}
/** `_session/archive`: hides a session from the default list and stops
* it. Idempotent; the session need not be loaded. A session loaded here
* is interrupted, retitled through its CLI while that still runs, then
@@ -289,7 +279,7 @@ export class SessionIndexConnection {
// No turn goes on after the archive: the interrupt goes to the CLI ahead
// of the rename.
if (loaded) await this.host.interruptSession(sessionId);
await retitleSession(index, sessionId, archiveTo(true), undefined, this.loaded(sessionId), {
await this.retitle(index, sessionId, archiveTo(true), undefined, {
mayBeUnwritten: this.isUnwrittenSession(sessionId),
sidecar: "existing",
});
@@ -318,7 +308,7 @@ export class SessionIndexConnection {
async unarchive(params: SessionIdRequest): Promise<EmptyResponse> {
const index = this.requireService(SESSION_UNARCHIVE_METHOD);
const { sessionId } = params;
await retitleSession(index, sessionId, archiveTo(false), undefined, this.loaded(sessionId), {
await this.retitle(index, sessionId, archiveTo(false), undefined, {
mayBeUnwritten: this.isUnwrittenSession(sessionId),
sidecar: "existing",
});
@@ -326,6 +316,58 @@ export class SessionIndexConnection {
return {};
}
/**
* Retitles a session (see {@link SessionIndexService.retitle}). A session
* this connection runs is titled through its CLI (`rename_session`). A
* loaded session's title state takes the change as a client title, so no
* generated title replaces it; `publish` is the title it then publishes.
*/
private async retitle(
index: SessionIndexService,
sessionId: string,
change: TitleChange,
publish: Parameters<Session["titles"]["setExplicitTitle"]>[0],
options: Pick<RetitleOptions, "mayBeUnwritten" | "sidecar">,
): Promise<void> {
const session = this.host.agent.sessions[sessionId];
// Decided once a title generation in flight has ended: the query may
// have closed meanwhile.
const persist = () => {
const query = session?.query as
| (Query & { renameSession?: (title: string, sessionId?: string) => Promise<void> })
| undefined;
// A closed session's CLI is gone here: only a running query titles it.
const running = session !== undefined && !session.queryClosed;
if (running && typeof query?.renameSession !== "function") {
// Its CLI holds the title and would write it back over ours.
throw RequestError.invalidRequest(
{ sessionId },
"The Claude Code CLI of this session cannot change its title while it runs.",
);
}
const live = running
? {
cwd: session.cwd,
rename: (title: string) => query!.renameSession!(title, sessionId),
stored: () => session.titles.storedTitle,
remember: (title: string) => session.titles.rememberStoredTitle(title),
shown: session.titles.shownTitle,
}
: undefined;
return index.retitle(sessionId, change, {
...options,
// Without its CLI, a session needs its transcript.
mayBeUnwritten: live !== undefined && options.mayBeUnwritten,
live,
});
};
if (session) {
await session.titles.setExplicitTitle(publish, persist);
} else {
await persist();
}
}
/**
* `session/delete`:
* - A `sessionIndex` client deletes for real: every transcript, whoever
@@ -346,19 +388,19 @@ export class SessionIndexConnection {
if (this.service) {
await this.service.delete(params.sessionId, loaded);
} else if (this.host.isAirClient()) {
await archiveInsteadOfDelete(params.sessionId, this.writer());
await archiveInsteadOfDelete(params.sessionId, this.airArchiver());
} else {
// After a `session/set_title` of the session in flight.
await this.writer().exclusive(params.sessionId, () =>
deleteSessionIfStored(params.sessionId),
);
await deleteSessionIfStored(params.sessionId);
}
return {};
}
private writer(): SessionIndexService {
this.transcripts ??= transcriptWriter(this.host.agent.logger);
return this.transcripts;
private airArchiver(): SessionIndexService {
this.archiver ??= new SessionIndexService({
logError: (message, error) =>
this.host.agent.logger.error(`[session-index] ${message}:`, error),
});
return this.archiver;
}
private requireService(method: string): SessionIndexService {

View File

@@ -49,8 +49,7 @@ import {
type LiveSessionRegistry,
type LiveSnapshot,
} from "./live-registry.js";
import { isSessionId, projectsRoot } from "../paths.js";
import { isExactProjectDir, projectDirMatches } from "./project-dirs.js";
import { isExactProjectDir, isSessionId, projectDirMatches, projectsRoot } from "./project-dirs.js";
import { type IndexRow, type SessionIndex, type TranscriptCandidate } from "./session-index.js";
import { changeSignature, sessionInfoOf } from "./session-info.js";

View File

@@ -23,7 +23,7 @@ import { execFile } from "node:child_process";
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { claudeConfigDir } from "../paths.js";
import { claudeConfigDir } from "../../paths.js";
import { errorCode } from "./project-dirs.js";
const SNAPSHOT_TTL_MS = 5_000;

View File

@@ -14,10 +14,24 @@
import * as fs from "node:fs/promises";
import * as path from "node:path";
import { claudeConfigDir } from "../../paths.js";
/** The SDK cuts an encoded path at this length and appends a hash. */
const MAX_SANITIZED_LENGTH = 200;
const SESSION_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
/** Whether `value` is a Claude session id (a UUID). Only such ids name a
* transcript, and only such ids are safe to use as a file name. */
export function isSessionId(value: unknown): value is string {
return typeof value === "string" && SESSION_ID_PATTERN.test(value);
}
/** `<config>/projects`, normalized like the SDK does. */
export function projectsRoot(): string {
return path.join(claudeConfigDir(), "projects").normalize("NFC");
}
function sanitize(value: string): string {
return value.replace(/[^a-zA-Z0-9]/g, "-");
}

View File

@@ -18,8 +18,8 @@ import {
type SessionInfo,
} from "@agentclientprotocol/sdk/unstable";
import { deleteSession as sdkDeleteSession } from "@anthropic-ai/claude-agent-sdk";
import { airExtensionMeta } from "../air-extension.js";
import { sanitizeTitle } from "../session-titles.js";
import { airExtensionMeta } from "../../air-extension.js";
import { sanitizeTitle } from "../../session-titles.js";
import type { OwnSessionState } from "./activity.js";
import {
effectiveTitle,
@@ -36,11 +36,11 @@ import {
type ListSubscriptionDeps,
} from "./list-subscriptions.js";
import { LiveSessionRegistry } from "./live-registry.js";
import { isSessionId } from "../paths.js";
import {
canonicalPath,
exists,
isExactProjectDir,
isSessionId,
projectDirMatches,
sameProjectPath,
} from "./project-dirs.js";
@@ -466,18 +466,6 @@ export type SessionIndexDeps = {
logError: (message: string, error: unknown) => void;
};
/** The transcript reads and writes of the session index, without its list
* or watcher: what a client without `sessionIndex` uses for
* `session/set_title`, AIR's archive on delete, and delete's ordering
* after them (see {@link SessionIndexService.exclusive}). */
export function transcriptWriter(logger: {
error: (message: string, error: unknown) => void;
}): SessionIndexService {
return new SessionIndexService({
logError: (message, error) => logger.error(`[session-index] ${message}:`, error),
});
}
export class SessionIndexService {
readonly index: SessionIndex;
readonly registry: LiveSessionRegistry;
@@ -553,10 +541,8 @@ export class SessionIndexService {
};
}
/** Runs `mutation` after the previous mutation of the session ended, such
* as a delete that does not go through {@link delete}, so that a retitle
* cannot append to a transcript the delete is removing. */
exclusive<T>(sessionId: string, mutation: () => Promise<T>): Promise<T> {
/** Runs `mutation` after the previous mutation of the session ended. */
private exclusive<T>(sessionId: string, mutation: () => Promise<T>): Promise<T> {
const previous = this.mutations.get(sessionId) ?? Promise.resolve();
const next = previous.catch(() => undefined).then(mutation);
const settled = next.catch(() => undefined);

View File

@@ -21,19 +21,20 @@
import * as fs from "node:fs/promises";
import * as path from "node:path";
import { sanitizeTitle } from "../session-titles.js";
import { sanitizeTitle } from "../../session-titles.js";
import { effectiveTitle, visibleTitle } from "./archive-title.js";
import { isSessionId, projectsRoot } from "../paths.js";
import {
canonicalPath,
encodeProjectPath,
errorCode,
exists,
isExactProjectDir,
isSessionId,
normalizePath,
pathAndAncestors,
projectDirMatches,
projectDirsOf,
projectsRoot,
sameProjectPath,
} from "./project-dirs.js";
import {

View File

@@ -5,7 +5,7 @@
*/
import type { SessionInfo } from "@agentclientprotocol/sdk/unstable";
import { withAirMeta } from "../air-extension.js";
import { withAirMeta } from "../../air-extension.js";
import { deriveActivity, selectCost, type OwnSessionState } from "./activity.js";
import type { LiveRecord } from "./live-registry.js";
import type { IndexRow } from "./session-index.js";

73
src/v1/steering.ts Normal file
View File

@@ -0,0 +1,73 @@
import { PromptRequest, RequestError } from "@agentclientprotocol/sdk/unstable";
/** Custom (extension) request method a client uses to steer the turn that is
* currently running: the message is injected into the in-flight turn rather
* than queued as a separate `session/prompt`. Named `_session/steering` per the
* agreed ACP steering wire protocol; advertised to clients via the top-level
* `InitializeResponse._meta.steering.supported`. */
export const STEER_METHOD = "_session/steering";
/** How urgently the SDK delivers a steered message relative to the running
* turn — an internal Claude implementation detail, not part of the wire
* contract. `now` pre-empts the current generation, while `later` waits for a
* pending permission/elicitation callback to settle instead of cancelling its
* ACP request and hiding the client's user-input card (IJAI-1191). */
export const STEER_PRIORITY_NOW = "now" as const;
export const STEER_PRIORITY_LATER = "later" as const;
/** Request-level steering options. `promptRequired` is opt-in so existing Hosts
* keep the established idle fallback behavior. */
type SteerMeta = {
[key: string]: unknown;
steering?: {
idleBehavior?: "promptRequired";
};
};
/** Params of a {@link STEER_METHOD} request. Shaped like the relevant subset of
* a `PromptRequest` so the same `promptToClaude` conversion applies. Delivery
* priority is deliberately NOT exposed here — it's an internal detail the agent
* chooses (see {@link STEER_PRIORITY}). */
export type SteerRequest = {
sessionId: string;
prompt: PromptRequest["prompt"];
_meta?: SteerMeta | null;
};
/** Result of a {@link STEER_METHOD} request. The legacy `startedNewTurn` result
* remains the default idle behavior; `promptRequired` is returned only when the
* Host explicitly opts into the host-owned fallback in request `_meta`. */
export type SteerResponse =
| { outcome: "injected" }
| { outcome: "startedNewTurn" }
| { outcome: "promptRequired"; reason: "noRunningTurn" };
/** Validate raw JSON-RPC params into a {@link SteerRequest}. Kept minimal — the
* content blocks are handed to `promptToClaude`, which tolerates unknown block
* types — but `sessionId` and a non-empty `prompt` array are required. */
export function parseSteerRequest(params: unknown): SteerRequest {
if (!params || typeof params !== "object") {
throw RequestError.invalidParams(undefined, "steer params must be an object");
}
const { sessionId, prompt, _meta } = params as Record<string, unknown>;
if (typeof sessionId !== "string" || sessionId.length === 0) {
throw RequestError.invalidParams(undefined, "steer params require a non-empty sessionId");
}
if (!Array.isArray(prompt) || prompt.length === 0) {
throw RequestError.invalidParams(undefined, "steer params require a non-empty prompt array");
}
const steering =
_meta && typeof _meta === "object" ? (_meta as Record<string, unknown>).steering : undefined;
const idleBehavior =
steering && typeof steering === "object"
? (steering as Record<string, unknown>).idleBehavior
: undefined;
if (idleBehavior !== undefined && idleBehavior !== "promptRequired") {
throw RequestError.invalidParams(undefined, "unsupported steering idleBehavior");
}
return {
sessionId,
prompt: prompt as PromptRequest["prompt"],
_meta: _meta as SteerMeta | null | undefined,
};
}

View File

@@ -39,32 +39,17 @@ import { forkSession } from "../fork-session.js";
import { supportsMcpOAuth } from "../mcp-oauth.js";
import { sdkMcpServers } from "../mcp-servers.js";
import { ALLOW_BYPASS, autoModeDisabled } from "../permissions/modes.js";
import {
readHistoryAcrossCompactions,
readResumedTail,
resumedModelFromTranscript,
} from "../resumed-session.js";
import { readResumedTail, resumedModelFromTranscript } from "../resumed-session.js";
import { mergeEffortSettings } from "../session-effort.js";
import { configuredSettings, resolveThinkingConfig } from "../session-env.js";
import { resolveInitialPermissionMode } from "../session-mode.js";
import { sanitizeTitle } from "../session-titles.js";
import { renameSession } from "../session-index/rename.js";
import {
parseRenameSessionRequest,
transcriptWriter,
type SessionIndexService,
} from "../session-index/service.js";
import { SettingsManager } from "../settings.js";
import { STARTUP_FAILURE_RESULTS_ENV, startupFailureError } from "../startup-failure.js";
import { createPostToolUseHook } from "../tools.js";
import { Pushable } from "../utils.js";
import { V2_SESSION_CAPABILITIES } from "./initialize.js";
import {
initialConfigRequests,
v1ForkSessionRequest,
v1NewSessionRequest,
} from "./session-requests.js";
import { SessionCommands } from "./commands.js";
import { v1ForkSessionRequest, v1NewSessionRequest } from "./session-requests.js";
import { V2UpdateTranslator } from "./update-translator.js";
import { airInitializeMeta, airSupport, NO_AIR, type AirSupport } from "./air.js";
import { V2Auth } from "./auth.js";
@@ -73,7 +58,6 @@ import { modelSwitchHook, SessionConfig } from "./config.js";
import { fileChangePreviewHook } from "./file-preview.js";
import { canUseTool, denyingStoppedWork } from "./permissions.js";
import { reportHooks } from "./reports.js";
import { savedSessionStatus } from "../session-status.js";
import { SessionRestarts, type SetupParams } from "./restarts.js";
import { V2Session, type SessionConnection } from "./session.js";
import {
@@ -122,10 +106,7 @@ export function v2AgentApp(
.onRequest(v2.methods.agent.session.fork, ({ params }) => agent.forkSession(params))
.onRequest(v2.methods.agent.session.list, ({ params }) => agent.listSessions(params))
.onRequest(v2.methods.agent.session.close, ({ params }) => agent.closeSession(params))
.onRequest(v2.methods.agent.session.status, ({ params }) => agent.sessionStatus(params))
.onRequest(v2.methods.agent.session.options, ({ params }) => agent.sessionOptions(params))
.onRequest(v2.methods.agent.session.delete, ({ params }) => agent.deleteSession(params))
.onRequest(v2.methods.agent.session.setTitle, ({ params }) => agent.setTitle(params))
.onRequest(v2.methods.agent.session.setConfigOption, ({ params }) =>
agent.setConfigOption(params),
)
@@ -171,8 +152,6 @@ class V2Agent implements AgentHandle, SessionConnection {
private readonly guard: SubscriptionGuard;
/** The restarts of the sessions' queries, for a new route or account (see `restarts.ts`). */
private readonly restarts: SessionRestarts;
/** The transcript writes of `session/set_title`, made at its first call (see `transcriptWriter`). */
private transcripts: SessionIndexService | undefined;
/** What the client declared in `initialize`. */
private clientCapabilities: v2.ClientCapabilities | null | undefined;
/** The elicitation modes that the client advertised in `initialize`. */
@@ -327,9 +306,7 @@ class V2Agent implements AgentHandle, SessionConnection {
async newSession(params: v2.NewSessionRequest): Promise<v2.NewSessionResponse> {
const sessionId = randomUUID();
const initial = initialConfigRequests(sessionId, params.configOptions);
const session = await this.startSession(sessionId, params, undefined);
await session.setInitialConfigOptions(initial);
const response = { sessionId, ...(await this.setupFields(session)) };
this.afterSetup(session, params);
return response;
@@ -356,9 +333,7 @@ class V2Agent implements AgentHandle, SessionConnection {
/**
* Resumes a stored session, or attaches to a live one. With `replayFrom:
* { type: "start" }` the stored history replays first. Either way, the
* state snapshot goes out after any replay and before the response. The
* request's `configOptions` are set before the replay, on an active session
* too.
* state snapshot goes out after any replay and before the response.
*/
resumeSession(params: v2.ResumeSessionRequest): Promise<v2.ResumeSessionResponse> {
const { sessionId } = params;
@@ -384,21 +359,23 @@ class V2Agent implements AgentHandle, SessionConnection {
);
}
if (this.ownerOfChild(sessionId)) throw restrictedChild(sessionId, "resume");
const initial = initialConfigRequests(sessionId, params.configOptions);
await this.restarts.settled(sessionId);
// A concurrent resume of this session started its query: this one then
// attaches to the session that it started.
await this.startSettled(sessionId);
let session = this.sessions.get(sessionId);
let history: Promise<SessionMessage[]> | undefined;
// The replay: the messages before each compaction too, unless reading them fails.
let replayed: Promise<SessionMessage[]> | undefined;
if (session) {
// Resuming the active session synchronizes it again. Its work goes on,
// and the request's MCP servers and additional directories replace the
// session's.
const request = await validatedSetup(params);
assertCwdOf(session, request.cwd, "resume it");
if (request.cwd !== session.cwd) {
throw RequestError.invalidParams(
{ cwd: request.cwd },
`Session ${sessionId} works in ${session.cwd}: resume it with that \`cwd\``,
);
}
await session.setMcpServers({
...clientOptions(params._meta)?.mcpServers,
...sdkMcpServers(request.mcpServers),
@@ -414,23 +391,13 @@ class V2Agent implements AgentHandle, SessionConnection {
} else {
// One read of the transcript serves the replay and the resumed model.
history = replayFrom != null ? getSessionMessages(sessionId, { dir: params.cwd }) : undefined;
// The read across compactions overlaps the start of the query.
replayed = history?.then((messages) =>
readHistoryAcrossCompactions(sessionId, messages, this.logger, params.cwd),
);
// A failed start leaves them unawaited.
// A failed start leaves it unawaited.
history?.catch(() => {});
replayed?.catch(() => {});
session = await this.startSession(sessionId, params, sessionId, history);
}
await session.setInitialConfigOptions(initial);
let subagents: SubagentReplay | undefined;
if (replayFrom != null) {
const cwd = session.cwd;
const messages = await (replayed ??
getSessionMessages(sessionId, { dir: cwd }).then((chain) =>
readHistoryAcrossCompactions(sessionId, chain, this.logger, cwd),
));
const messages = await (history ?? getSessionMessages(sessionId, { dir: session.cwd }));
subagents = await session.tasks.loadReplay(messages);
session.output.replay(
messages,
@@ -485,85 +452,6 @@ class V2Agent implements AgentHandle, SessionConnection {
: { sessions };
}
/**
* `session/status`. `open`, with its foreground state, for a session whose
* query runs here. Every other session is `saved` or unknown by its stored
* transcript (see `session-status.ts`): one that a resume is still
* starting, and one whose query ended without `session/close`, which a
* resume starts again.
*
* A subagent's session does not advertise `status`, and is refused like
* its other requests. Its parent's `subagent_update` reports its state.
*
* It waits for no resume, restart or close, and changes nothing.
*/
async sessionStatus({ sessionId }: v2.SessionStatusRequest): Promise<v2.SessionStatusResponse> {
const session = this.sessions.get(sessionId);
if (session) return { status: "open", state: session.statusState() };
if (this.ownerOfChild(sessionId)) throw restrictedChild(sessionId, "ask the status of");
return savedSessionStatus(sessionId);
}
/**
* `session/options`: the config options and commands that `session/new`
* with the same `cwd`, additional directories and `_meta` would answer
* with, or `session/resume` of `sessionId`.
*
* An active session answers with what it has now. Otherwise a query starts
* as the session's would, without MCP servers, and closes once its options
* are read. So the commands lack the MCP prompts, and a failed read of the
* commands leaves them out. The query does not persist
* (`persistSession: false`): it stores no session, and it does not write to
* the transcript of `sessionId`. Claude Code runs the user's `SessionStart`
* hooks in it, as in every query.
*
* It sends no update, and it creates, resumes and changes no session. A
* `sessionId` without a stored conversation fails with `-32002`.
*/
async sessionOptions(params: v2.SessionOptionsRequest): Promise<v2.SessionOptionsResponse> {
const { sessionId } = params;
const setup: v2.NewSessionRequest = {
cwd: params.cwd,
additionalDirectories: params.additionalDirectories,
_meta: params._meta,
};
let session: V2Session | undefined;
if (sessionId != null) {
if (this.ownerOfChild(sessionId)) throw restrictedChild(sessionId, "read the options of");
// A restart or a resume that runs makes the session active.
await this.restarts.settled(sessionId);
await this.startSettled(sessionId);
session = this.sessions.get(sessionId);
}
const fields = (config: SessionConfig, commands: v2.AvailableCommand[]) => ({
configOptions: config.options(),
// The draft leaves out an empty list.
...(commands.length > 0 ? { availableCommands: commands } : {}),
});
const listed = (commands: SessionCommands) =>
commands.list().catch((error: unknown) => {
this.logger.error("session/options: failed to read the commands", error);
return [];
});
if (session) {
assertCwdOf(session, (await validatedSetup(setup)).cwd, "ask for its options");
return fields(session.config, await listed(session.commands));
}
// The probe's own id: its records never meet those of the session `sessionId`.
const probeId = randomUUID();
const probe = await this.openQuery(probeId, setup, sessionId ?? undefined, undefined, {
persist: false,
});
try {
return fields(probe.config, await listed(new SessionCommands(probe.query)));
} finally {
probe.query.close();
probe.settings.dispose();
this.auth.sessionEnded(probeId);
this.guard.sessionEnded(probeId);
}
}
/** Closing is idempotent: a session that is not active closes at once. */
async closeSession({ sessionId }: v2.CloseSessionRequest): Promise<v2.CloseSessionResponse> {
if (this.ownerOfChild(sessionId)) throw restrictedChild(sessionId, "close");
@@ -593,9 +481,7 @@ class V2Agent implements AgentHandle, SessionConnection {
const live = this.sessions.get(sessionId);
if (live || this.stopped.has(sessionId)) await this.closeSession({ sessionId });
try {
// After a `session/set_title` of the session in flight.
const remove = () => deleteSession(sessionId, live ? { dir: live.cwd } : undefined);
await (this.transcripts ? this.transcripts.exclusive(sessionId, remove) : remove());
await deleteSession(sessionId, live ? { dir: live.cwd } : undefined);
} catch (error) {
// A session without a transcript, such as one never prompted, has nothing to delete.
if (error instanceof Error && /not found/i.test(error.message)) return {};
@@ -604,30 +490,6 @@ class V2Agent implements AgentHandle, SessionConnection {
return {};
}
/**
* `session/set_title`, as the v1 core does it (see `session-index/rename.ts`).
* The session need not be active. An active one publishes the title, and no
* generated title replaces it.
*/
async setTitle(params: v2.SetSessionTitleRequest): Promise<v2.SetSessionTitleResponse> {
const request = parseRenameSessionRequest(params);
const { sessionId } = request;
if (this.ownerOfChild(sessionId)) throw restrictedChild(sessionId, "retitle");
// Renamed through the query that runs once a restart or resume settled.
await this.restarts.settled(sessionId);
await this.setupSettled(sessionId);
const writer = (this.transcripts ??= transcriptWriter(this.logger));
for (;;) {
const session = this.sessions.get(sessionId);
const title = await renameSession(writer, request, session?.reports.retitled());
// A restart or resume started another query of the session while the
// rename ran: its CLI may hold the old title and write it back, so it
// is renamed too. A session that only closed took the title already.
const now = this.sessions.get(sessionId);
if (now === undefined || now === session) return { title };
}
}
async setConfigOption(
params: v2.SetSessionConfigOptionRequest,
): Promise<v2.SetSessionConfigOptionResponse> {
@@ -800,7 +662,10 @@ class V2Agent implements AgentHandle, SessionConnection {
return start;
}
/** The query start of {@link startSession}: {@link openQuery}, then the session is active. */
/**
* The query start of {@link startSession}. Its options are the client's
* `_meta.claudeCode.options` under the ones this core controls.
*/
private async launchSession(
sessionId: string,
params: v2.NewSessionRequest | v2.ResumeSessionRequest | v2.ForkSessionRequest,
@@ -808,50 +673,6 @@ class V2Agent implements AgentHandle, SessionConnection {
/** The resumed transcript, when the caller reads it anyway (a replay). */
history?: Promise<SessionMessage[]>,
): Promise<V2Session> {
const {
session,
query: q,
config,
models,
...opened
} = await this.openQuery(sessionId, params, resume, history);
session.reports.configure({
settings: opened.settings,
configuredSettings: opened.configured,
env: opened.env,
mcpServers: opened.request.mcpServers.map((server) => server.name),
mcpOAuth: this.elicitation.url && supportsMcpOAuth(q),
query: q,
model: { id: models.models.currentModelId, info: models.allowlistedModelInfo },
});
session.start(q, config, opened.mcpServers, resume !== undefined);
this.sessions.set(sessionId, session);
this.stopped.delete(sessionId);
// A route change during the start did not find this session live.
this.restarts.track(sessionId, params, {
staleRoute: this.auth.routeGeneration !== opened.routeGeneration,
});
return session;
}
/**
* Starts the query of a session and builds its config. Its options are the
* client's `_meta.claudeCode.options` under the ones this core controls.
* The session is not active yet, and nothing of it goes out. The caller
* closes the query and disposes the settings when it does not keep them.
*
* `resume`: the stored session that the query resumes, `sessionId` itself
* but for a `session/options` probe. Without `persist`, Claude Code stores
* nothing of the query (see {@link sessionOptions}).
*/
private async openQuery(
sessionId: string,
params: v2.NewSessionRequest | v2.ResumeSessionRequest | v2.ForkSessionRequest,
resume: string | undefined,
/** The resumed transcript, when the caller reads it anyway (a replay). */
history?: Promise<SessionMessage[]>,
{ persist = true } = {},
) {
const request = await validatedSetup(params);
const client = clientOptions(params._meta);
// After a provider change that is still listing its models.
@@ -1000,7 +821,6 @@ class V2Agent implements AgentHandle, SessionConnection {
...STARTUP_FAILURE_RESULTS_ENV,
},
...(resume !== undefined ? { resume } : { sessionId }),
...(persist ? {} : { persistSession: false }),
};
const q = query({ prompt: input, options });
let initialization;
@@ -1014,7 +834,7 @@ class V2Agent implements AgentHandle, SessionConnection {
error instanceof Error &&
error.message.includes("No conversation found with session ID")
) {
throw RequestError.sessionNotFound(resume);
throw RequestError.sessionNotFound(sessionId);
}
throw startupFailureError(error) ?? error;
}
@@ -1039,7 +859,6 @@ class V2Agent implements AgentHandle, SessionConnection {
resumed,
sessionId,
this.logger,
configured,
);
} catch (error) {
// Claude Code refused a fresh session's model pin.
@@ -1076,18 +895,22 @@ class V2Agent implements AgentHandle, SessionConnection {
settings.dispose();
throw error;
}
return {
session,
query: q,
config,
models,
session.reports.configure({
settings,
configured,
env: options.env ?? {},
request,
mcpServers,
routeGeneration,
};
mcpServers: request.mcpServers.map((server) => server.name),
mcpOAuth: this.elicitation.url && supportsMcpOAuth(q),
query: q,
model: { id: models.models.currentModelId, info: models.allowlistedModelInfo },
});
session.start(q, config, mcpServers, resume !== undefined);
this.sessions.set(sessionId, session);
this.stopped.delete(sessionId);
// A route change during the start did not find this session live.
this.restarts.track(sessionId, params, {
staleRoute: this.auth.routeGeneration !== routeGeneration,
});
return session;
}
}
@@ -1122,15 +945,6 @@ async function validatedSetup(
return request;
}
/** A request of the active `session` names its `cwd`, as `session/resume` must. */
function assertCwdOf(session: V2Session, cwd: string, action: string): void {
if (cwd === session.cwd) return;
throw RequestError.invalidParams(
{ cwd },
`Session ${session.sessionId} works in ${session.cwd}: ${action} with that \`cwd\``,
);
}
function sameDirectories(left: readonly string[], right: readonly string[]): boolean {
const normalize = (directories: readonly string[]) =>
[...new Set(directories.map((directory) => path.resolve(directory)))].sort().join("\n");

View File

@@ -103,17 +103,6 @@ export class V2Background {
return notification;
}
/**
* `state` as the current work state of the session `sessionId`, with its
* list when it reports one: what {@link stamp} makes of the session's
* `state_update`. Unlike `stamp`, it does not count as a state that carried
* the list, so a later change of the list is reported as before.
*/
snapshot(sessionId: string, state: WorkStateName): v2.StateUpdate {
const session = this.sessions.get(sessionId);
return session ? withBackground(currentState(state), session.items) : currentState(state);
}
/**
* Records the new list of a session, and returns the updates that report it:
*

View File

@@ -43,7 +43,6 @@ import type {
Query,
Settings,
} from "@anthropic-ai/claude-agent-sdk";
import { applyInitialConfigOptions } from "../initial-config-options.js";
import type { Logger } from "../logger.js";
import type { SubagentModels } from "../native-subagents.js";
import { DEFAULT_AGENT_ID } from "../session-config-ids.js";
@@ -347,19 +346,6 @@ export class SessionConfig {
return this.serialized(() => this.apply(params));
}
/**
* Sets the initial values of a setup request's `configOptions`, in list
* order, as {@link set} sets each one. An entry that the session refuses is
* skipped (`applyInitialConfigOptions`). Nothing goes out: the setup
* response carries the options, and the notice of an Auto mode fallback
* waits for the next prompt.
*/
setInitial(entries: readonly v2.SetSessionConfigOptionRequest[]): Promise<void> {
return this.serialized(() =>
applyInitialConfigOptions(entries, (entry) => this.apply(entry, true), this.logger),
);
}
/**
* Sets the choices of `previous`, the config of the query that this one
* replaced (see `restarts.ts`), as the v1 core's `restoreSessionChoices`
@@ -425,11 +411,7 @@ export class SessionConfig {
return result;
}
/** `duringSetup`: see {@link setInitial}. */
private async apply(
params: v2.SetSessionConfigOptionRequest,
duringSetup = false,
): Promise<void> {
private async apply(params: v2.SetSessionConfigOptionRequest): Promise<void> {
const request = v1SetSessionConfigOptionRequest(params);
const option = this.session.configOptions.find((entry) => entry.id === request.configId);
if (!option) {
@@ -447,11 +429,11 @@ export class SessionConfig {
const value = selected.value;
switch (request.configId) {
case MODE_CONFIG_ID:
await this.applyMode(value as PermissionMode, duringSetup);
await this.applyMode(value as PermissionMode);
return;
case MODEL_CONFIG_ID:
await this.session.query.setModel(value);
await this.recordModel(value, duringSetup);
await this.recordModel(value);
return;
case EFFORT_CONFIG_ID:
// Applied first, so a rejected control request leaves the option as it was.
@@ -490,17 +472,11 @@ export class SessionConfig {
return this.rules.effectiveMode(this.session, requested);
}
private async applyMode(requested: PermissionMode, duringSetup = false): Promise<void> {
private async applyMode(requested: PermissionMode): Promise<void> {
const mode = this.rules.effectiveMode(this.session, requested);
await this.session.query.setPermissionMode(mode);
this.rules.syncConfig(this.session, mode);
if (mode !== requested) await this.warnOfFallback(duringSetup);
}
/** The notice of an Auto mode fallback: now, or with the next prompt during setup. */
private async warnOfFallback(duringSetup: boolean): Promise<void> {
if (duringSetup) this.rules.deferFallbackWarning(this.session);
else await this.rules.publishFallbackWarning(this.sessionId, this.session);
if (mode !== requested) await this.rules.publishFallbackWarning(this.sessionId, this.session);
}
/** Claude Code switched the mode itself. Returns whether the option changed. */
@@ -630,7 +606,7 @@ export class SessionConfig {
* show only on a model that supports them, with the user's retained
* choice.
*/
private async recordModel(value: string, duringSetup = false): Promise<void> {
private async recordModel(value: string): Promise<void> {
const info = this.modelInfos.find((model) => model.value === value);
this.session.models = { ...this.session.models, currentModelId: value };
const modeDowngraded = await this.rules.reconcileForModel(this.session, info);
@@ -688,7 +664,7 @@ export class SessionConfig {
effort.error,
);
}
if (modeDowngraded) await this.warnOfFallback(duringSetup);
if (modeDowngraded) await this.rules.publishFallbackWarning(this.sessionId, this.session);
}
}

View File

@@ -6,8 +6,6 @@
import type { AuthMethod } from "@agentclientprotocol/sdk/unstable";
import type * as v2 from "@agentclientprotocol/sdk/experimental/v2/unstable";
import { SYSTEM_PROMPT_CAPABILITY } from "../client-system-prompt.js";
import { SESSION_OPTIONS_CAPABILITY } from "../initial-config-options.js";
import { SESSION_STATUS_CAPABILITY } from "../session-status.js";
/**
* The session capabilities that the v2 core serves.
@@ -19,9 +17,6 @@ import { SESSION_STATUS_CAPABILITY } from "../session-status.js";
* - The prompt content and MCP transports that the v1 `initialize` lists too:
* images, embedded context, and HTTP servers. v2 has no SSE transport.
* - `systemPrompt`, in `append` and `override` mode.
* - `session/status` (unstable), with the foreground state of an open session.
* - `session/options` (unstable), and `configOptions` on `session/new` and
* `session/resume`.
*/
export const V2_SESSION_CAPABILITIES: v2.SessionCapabilities = {
prompt: { image: {}, embeddedContext: {} },
@@ -30,10 +25,6 @@ export const V2_SESSION_CAPABILITIES: v2.SessionCapabilities = {
fork: {},
additionalDirectories: {},
systemPrompt: SYSTEM_PROMPT_CAPABILITY,
status: SESSION_STATUS_CAPABILITY,
options: SESSION_OPTIONS_CAPABILITY,
// `session/set_title`.
title: {},
};
/** v2 names the id of a method `methodId`, requires `type`, and lists `env` as name and value pairs. */

View File

@@ -17,7 +17,7 @@ import type {
import { AUTONOMOUS_RESULT_ORIGINS } from "../result-end.js";
import { isSyntheticLoginMessage } from "../message-filters.js";
import { resultEnd } from "../result-end.js";
import { assistantMessageText, providerFailureCategory } from "../session-failure-extension.js";
import { providerFailureCategory } from "../session-failure-extension.js";
import {
getMatchingModelUsage,
modelUsageIncrement,
@@ -109,8 +109,8 @@ function failed(error: RequestError): IdleOutcome {
/**
* What the assistant messages of the open turn reported about its end: the
* error kind and text of a failed API request, Claude Code's sign-in message,
* and a refusal's explanation. Read from the main conversation's messages.
* error kind of a failed API request, Claude Code's sign-in message, and a
* refusal's explanation. Read from the main conversation's messages.
*/
export class TurnReport {
errorKind: SDKAssistantMessageError | undefined;
@@ -118,22 +118,12 @@ export class TurnReport {
loginRequired = false;
/** The explanation of the turn's refusal. */
refusalExplanation: string | undefined;
/**
* The text of the message that Claude Code wrote for a failed API request,
* e.g. "API Error: Connection lost mid-response. The response above may be
* incomplete.", until {@link takeApiError} reads it.
*/
private apiError: string | undefined;
/**
* An assistant message of the main conversation. Returns whether the
* client is not shown it as Claude's text:
*
* - Claude Code's sign-in message. Its `/login` instruction is for Claude
* Code's terminal, and the turn's idle fails with `authRequired` instead.
* - The message that Claude Code wrote for a failed API request (`error` on
* a `<synthetic>` message). The turn's idle carries it when the turn
* fails, and a notice otherwise (see {@link takeApiError}).
* An assistant message of the main conversation. Returns whether it is
* Claude Code's sign-in message, which the client is not shown: its
* `/login` instruction is for Claude Code's terminal, and the turn's idle
* fails with `authRequired` instead.
*/
assistant(message: SDKAssistantMessage): boolean {
if (message.error) this.errorKind = message.error;
@@ -141,24 +131,11 @@ export class TurnReport {
this.refusalExplanation =
message.message.stop_details?.explanation ?? this.refusalExplanation;
}
if (isSyntheticLoginMessage(message.message)) {
this.loginRequired = true;
return true;
}
if (!message.error || message.message.model !== "<synthetic>") return false;
const text = assistantMessageText(message.message);
if (!text) return false;
this.apiError = text;
if (!isSyntheticLoginMessage(message.message)) return false;
this.loginRequired = true;
return true;
}
/** The text of the turn's failed API request that no notice or idle reported yet. */
takeApiError(): string | undefined {
const text = this.apiError;
this.apiError = undefined;
return text;
}
/**
* Claude Code's `model_refusal_no_fallback`: the explanation when the
* refused message carried none (older CLIs, gateways that drop
@@ -178,7 +155,6 @@ export class TurnReport {
this.errorKind = undefined;
this.loginRequired = false;
this.refusalExplanation = undefined;
this.apiError = undefined;
}
}

Some files were not shown because too many files have changed in this diff Show More