Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions readme-dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,24 @@ npm run package:all
2. Regenerate Codex types in `src/app-server/`: `npm run generate-types`
3. Ensure there are no type errors or failed tests: `npm run typecheck` and `npm run test`

### Session notices

The adapter implements [Session Notices](https://agentclientprotocol.com/rfds/session-notices)
for Codex warnings, configuration warnings, deprecation notices, model rerouting, and the legacy
`thread/compacted` advisory when the client advertises `clientCapabilities.session.notices: {}`.
These are live `session/update` notifications with
`sessionUpdate: "notice"`, a severity, a plain-text title, and optional description.
They are not replayed from session history and repeated notices remain independent events.

Without that capability (including absent or null capability objects), the adapter preserves
the existing assistant/thought text or AIR `sessionFailure` advisory records. When notices are
enabled, they take precedence over AIR advisory records. Clients control their presentation;
the adapter does not rely on notices being displayed.

Command replies, review results, and terminal/retrying errors retain their existing response or
failure channels. Clients advertising session compaction support continue to receive the dedicated
compaction lifecycle instead of the legacy completion advisory.

### AIR diff statistics

See the [diff statistics specification](docs/diff-statistics-extension.md) for the
Expand Down
2 changes: 2 additions & 0 deletions src/CodexAcpServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ import {
type TerminalOutputMode,
} from "./TerminalOutputMode";
import {clientSupportsPlanUpdates} from "./PlanCapabilities";
import {clientSupportsNotices} from "./SessionNotice";
import {
createAgentTextMessageChunk,
createAgentTextThoughtChunk,
Expand Down Expand Up @@ -2815,6 +2816,7 @@ export class CodexAcpServer {
(accountUpdated) => this.handleAccountUpdated(accountUpdated),
agentFileChangeReportRequest !== null,
clientSupportsCompaction(this.clientCapabilities),
clientSupportsNotices(this.clientCapabilities),
);
eventHandler = promptEventHandler;
const permissionLifecycle = this.permissionLifecycleContext(sessionState);
Expand Down
35 changes: 28 additions & 7 deletions src/CodexEventHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ import {CodexSubagentEventRouter} from "./subagents/CodexSubagentEventRouter";
import type {SubagentState} from "./subagents/AcpSubagents";
import {mergeRateLimitSnapshot} from "./RateLimitsMap";
import {AGENT_FILE_CHANGE_REPORT_MAX_DIFF_BYTES} from "./AgentFileChangeReport";
import {createSessionNotice} from "./SessionNotice";

export { stripShellPrefix };

Expand Down Expand Up @@ -249,6 +250,7 @@ export class CodexEventHandler {
onAccountUpdated?: (notification: AccountUpdatedNotification) => void,
collectTurnDiffs = false,
private readonly supportsCompaction = false,
private readonly supportsNotices = false,
) {
this.onAccountUpdated = onAccountUpdated;
this.sessionState = sessionState;
Expand Down Expand Up @@ -710,34 +712,46 @@ export class CodexEventHandler {
}

private async createConfigWarningEvent(event: ConfigWarningNotification): Promise<UpdateSessionEvent> {
if (this.supportsNotices) {
return createSessionNotice("warning", event.summary.trim() || "Configuration warning", event.details);
}
if (this.supportsTypedSessionFailures) {
return this.createSessionFailureUpdate(this.recordSessionNotice(...this.sessionNoticeContent(event.summary, event.details)));
}
const text = event.details ? `${event.summary}\n\n${event.details}` : event.summary;
return createAgentTextMessageChunk(`Config warning: ${text}\n\n`);
}

/**
* Unlike `warning` and `configWarning`, this notification was dropped outright, so there is no
* legacy rendering to preserve. It is surfaced only to clients that negotiated typed records;
* every other client keeps seeing exactly what it sees today, which is nothing.
*/
private createDeprecationNoticeEvent(event: DeprecationNoticeNotification): UpdateSessionEvent | null {
if (this.supportsNotices) {
return createSessionNotice("warning", event.summary.trim() || "Deprecated configuration", event.details);
}
// Legacy clients without typed failures have never received deprecation notices.
if (!this.supportsTypedSessionFailures) return null;
return this.createSessionFailureUpdate(
this.recordSessionNotice(...this.sessionNoticeContent(event.summary, event.details)),
);
}

private createWarningEvent(event: WarningNotification): UpdateSessionEvent {
if (this.supportsNotices) {
return createSessionNotice("warning", event.message.trim() || "Codex warning");
}
if (this.supportsTypedSessionFailures) {
return this.createSessionFailureUpdate(this.recordSessionNotice(event.message));
}
return createAgentTextMessageChunk(`Warning: ${event.message}\n\n`);
}

private createModelReroutedEvent(event: ModelReroutedNotification): UpdateSessionEvent {
return createAgentTextThoughtChunk(`Model rerouted from ${event.fromModel} to ${event.toModel} (${event.reason}).\n\n`);
if (!this.supportsNotices) {
return createAgentTextThoughtChunk(`Model rerouted from ${event.fromModel} to ${event.toModel} (${event.reason}).\n\n`);
}
return createSessionNotice(
"info",
"Model rerouted",
`Switched from ${event.fromModel} to ${event.toModel} (${event.reason}).`,
);
}

private createThreadGoalUpdatedEvent(event: ThreadGoalUpdatedNotification): UpdateSessionEvent | null {
Expand Down Expand Up @@ -1018,7 +1032,14 @@ export class CodexEventHandler {
}

private createContextCompactedEvent(): UpdateSessionEvent {
return createAgentTextMessageChunk("*Context compacted to fit the model's context window.*\n\n");
if (!this.supportsNotices) {
return createAgentTextMessageChunk("*Context compacted to fit the model's context window.*\n\n");
}
return createSessionNotice(
"info",
"Context compacted",
"Conversation compacted to fit the model's context window.",
);
}

private createCommandOutputDeltaEvent(event: CommandExecutionOutputDeltaNotification): UpdateSessionEvent {
Expand Down
20 changes: 20 additions & 0 deletions src/SessionNotice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type {ClientCapabilities, Notice, NoticeSeverity} from "@agentclientprotocol/sdk";

export function clientSupportsNotices(capabilities?: ClientCapabilities | null): boolean {
const notices = capabilities?.session?.notices;
return typeof notices === "object" && notices !== null && !Array.isArray(notices);
}

/** Live advisory only: no identity, lifecycle, or replay. Callers must negotiate support. */
export function createSessionNotice(
severity: NoticeSeverity,
title: string,
description?: string | null,
): Notice & {sessionUpdate: "notice"} {
return {
sessionUpdate: "notice",
severity,
title,
...(description == null ? {} : {description}),
};
}
82 changes: 82 additions & 0 deletions src/__tests__/CodexACPAgent/data/session-notices-advisories.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
{
"method": "sessionUpdate",
"args": [
{
"sessionId": "notice-session",
"update": {
"sessionUpdate": "notice",
"severity": "warning",
"title": "Optional integration unavailable"
}
}
]
}
{
"method": "sessionUpdate",
"args": [
{
"sessionId": "notice-session",
"update": {
"sessionUpdate": "notice",
"severity": "warning",
"title": "Optional integration unavailable"
}
}
]
}
{
"method": "sessionUpdate",
"args": [
{
"sessionId": "notice-session",
"update": {
"sessionUpdate": "notice",
"severity": "warning",
"title": "Configuration fallback",
"description": "Using the default configuration.\nCheck the configured path."
}
}
]
}
{
"method": "sessionUpdate",
"args": [
{
"sessionId": "notice-session",
"update": {
"sessionUpdate": "notice",
"severity": "warning",
"title": "Deprecated setting",
"description": "Use the replacement setting."
}
}
]
}
{
"method": "sessionUpdate",
"args": [
{
"sessionId": "notice-session",
"update": {
"sessionUpdate": "notice",
"severity": "info",
"title": "Model rerouted",
"description": "Switched from original-model to fallback-model (highRiskCyberActivity)."
}
}
]
}
{
"method": "sessionUpdate",
"args": [
{
"sessionId": "notice-session",
"update": {
"sessionUpdate": "notice",
"severity": "info",
"title": "Context compacted",
"description": "Conversation compacted to fit the model's context window."
}
}
]
}
134 changes: 134 additions & 0 deletions src/__tests__/CodexACPAgent/data/session-notices-air-fallback.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
{
"method": "sessionUpdate",
"args": [
{
"sessionId": "notice-session",
"update": {
"sessionUpdate": "session_info_update",
"_meta": {
"jetbrains": {
"air": {
"version": 1,
"sessionFailure": {
"id": "id",
"revision": 1,
"category": "unknown",
"severity": "warning",
"title": "Optional integration unavailable",
"actions": []
}
}
}
}
}
}
]
}
{
"method": "sessionUpdate",
"args": [
{
"sessionId": "notice-session",
"update": {
"sessionUpdate": "session_info_update",
"_meta": {
"jetbrains": {
"air": {
"version": 1,
"sessionFailure": {
"id": "id",
"revision": 2,
"category": "unknown",
"severity": "warning",
"title": "Optional integration unavailable",
"actions": []
}
}
}
}
}
}
]
}
{
"method": "sessionUpdate",
"args": [
{
"sessionId": "notice-session",
"update": {
"sessionUpdate": "session_info_update",
"_meta": {
"jetbrains": {
"air": {
"version": 1,
"sessionFailure": {
"id": "id",
"revision": 1,
"category": "unknown",
"severity": "warning",
"title": " Configuration fallback — Using the default configuration.\nCheck the configured path.",
"actions": []
}
}
}
}
}
}
]
}
{
"method": "sessionUpdate",
"args": [
{
"sessionId": "notice-session",
"update": {
"sessionUpdate": "session_info_update",
"_meta": {
"jetbrains": {
"air": {
"version": 1,
"sessionFailure": {
"id": "id",
"revision": 1,
"category": "unknown",
"severity": "warning",
"title": " Deprecated setting — Use the replacement setting.",
"actions": []
}
}
}
}
}
}
]
}
{
"method": "sessionUpdate",
"args": [
{
"sessionId": "notice-session",
"update": {
"sessionUpdate": "agent_thought_chunk",
"content": {
"type": "text",
"text": "Model rerouted from original-model to fallback-model (highRiskCyberActivity).\n\n"
}
}
}
]
}
{
"method": "sessionUpdate",
"args": [
{
"sessionId": "notice-session",
"update": {
"sessionUpdate": "agent_message_chunk",
"content": {
"type": "text",
"text": "*Context compacted to fit the model's context window.*\n\n"
}
}
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"method": "sessionUpdate",
"args": [
{
"sessionId": "notice-session",
"update": {
"sessionUpdate": "compaction_update",
"compactionId": "notice-compaction",
"status": "in_progress"
}
}
]
}
{
"method": "sessionUpdate",
"args": [
{
"sessionId": "notice-session",
"update": {
"sessionUpdate": "compaction_update",
"compactionId": "notice-compaction",
"status": "completed"
}
}
]
}
Loading
Loading