Skip to content
Open
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
65 changes: 65 additions & 0 deletions src/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
formatSkills,
formatThreadButtonLabel,
formatThreadPickerIntro,
formatContextUsageAlert,
formatTurnCompletion,
} from "./format.js";
import {
Expand All @@ -58,6 +59,7 @@ import { formatCommandUsage, renderCommandHelpText } from "./help.js";
import type {
AccountSummary,
CollaborationMode,
ContextAlertLevel,
ConversationPreferences,
InteractiveMessageRef,
PermissionsMode,
Expand Down Expand Up @@ -89,6 +91,8 @@ import {
paginateItems,
} from "./thread-picker.js";
import {
CONTEXT_ALERT_CRITICAL_PERCENT,
CONTEXT_ALERT_WARNING_PERCENT,
INTERACTIVE_NAMESPACE,
PLUGIN_ID,
type CallbackAction,
Expand Down Expand Up @@ -2643,6 +2647,7 @@ export class CodexPluginController {
await this.store.upsertBinding({
...binding,
contextUsage: result.usage,
lastContextAlertLevel: null,
updatedAt: Date.now(),
});
}
Expand Down Expand Up @@ -3150,6 +3155,12 @@ export class CodexPluginController {
? await this.describeEmptyTurnCompletion()
: formatTurnCompletion(result);
await this.sendText(params.conversation, completionText);
const updatedBinding = this.store.getBinding(params.conversation);
if (updatedBinding) {
await this.checkContextUsageAlert(params.conversation, updatedBinding).catch((alertError) => {
this.api.logger.debug?.(`codex context usage alert failed: ${String(alertError)}`);
});
}
})
.catch(async (error) => {
const message = error instanceof Error ? error.message : String(error);
Expand Down Expand Up @@ -6590,4 +6601,58 @@ export class CodexPluginController {
});
}
}

private async checkContextUsageAlert(
conversation: ConversationTarget,
binding: StoredBinding,
): Promise<void> {
const usage = binding.contextUsage;
if (!usage || typeof usage.remainingPercent !== "number") {
return;
}
const remaining = usage.remainingPercent;
let level: ContextAlertLevel | null = null;
if (remaining <= CONTEXT_ALERT_CRITICAL_PERCENT) {
level = "critical";
} else if (remaining <= CONTEXT_ALERT_WARNING_PERCENT) {
level = "warning";
}
if (!level) {
if (binding.lastContextAlertLevel) {
await this.store.upsertBinding({
...binding,
lastContextAlertLevel: null,
updatedAt: Date.now(),
});
}
return;
}
const previous = binding.lastContextAlertLevel;
if (previous === level) {
return;
}
if (previous === "critical" && level === "warning") {
return;
}
const alertText = formatContextUsageAlert({ level, usage });
const compactCallback = await this.store.putCallback({
kind: "run-prompt",
conversation,
prompt: "/cas_compact",
});
const buttons: PluginInteractiveButtons = [
[
{
text: "Compact Now",
callback_data: `${INTERACTIVE_NAMESPACE}:${compactCallback.token}`,
},
],
];
await this.sendText(conversation, alertText, { buttons });
await this.store.upsertBinding({
...binding,
lastContextAlertLevel: level,
updatedAt: Date.now(),
});
}
}
31 changes: 31 additions & 0 deletions src/format.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
formatCodexPlanSteps,
formatCodexReviewFindingMessage,
formatCodexStatusText,
formatContextUsageAlert,
getCodexStatusTimeZoneLabel,
formatMcpServers,
formatModels,
Expand Down Expand Up @@ -566,3 +567,33 @@ describe("formatCodexPlanInlineText", () => {
expect(formatCodexPlanInlineText(plan)).toContain("# Plan");
});
});

describe("formatContextUsageAlert", () => {
it("returns a warning message when level is warning", () => {
const result = formatContextUsageAlert({
level: "warning",
usage: { totalTokens: 150_000, contextWindow: 200_000, remainingPercent: 25 },
});
expect(result).toContain("Context notice:");
expect(result).toContain("Consider compacting");
expect(result).toContain("150k / 200k tokens used");
});

it("returns a critical message when level is critical", () => {
const result = formatContextUsageAlert({
level: "critical",
usage: { totalTokens: 186_000, contextWindow: 200_000, remainingPercent: 7 },
});
expect(result).toContain("Context alert:");
expect(result).toContain("Compact soon");
expect(result).toContain("186k / 200k tokens used");
});

it("falls back to unknown usage when snapshot is empty", () => {
const result = formatContextUsageAlert({
level: "warning",
usage: {},
});
expect(result).toContain("unknown usage");
});
});
12 changes: 12 additions & 0 deletions src/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import os from "node:os";
import { formatModelCapabilitySuffix } from "./model-capabilities.js";
import type {
AccountSummary,
ContextAlertLevel,
ContextUsageSnapshot,
ExperimentalFeatureSummary,
McpServerSummary,
Expand Down Expand Up @@ -979,3 +980,14 @@ export function formatCodexPlanAttachmentFallback(
}
return lines.join("\n").trim();
}

export function formatContextUsageAlert(params: {
level: ContextAlertLevel;
usage: ContextUsageSnapshot;
}): string {
const usageText = formatCodexContextUsageSnapshot(params.usage) ?? "unknown usage";
if (params.level === "critical") {
return `Context alert: ${usageText}. Compact soon to avoid degraded output.`;
}
return `Context notice: ${usageText}. Consider compacting to free up space.`;
}
5 changes: 5 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export const CALLBACK_TOKEN_BYTES = 9;
export const CALLBACK_TTL_MS = 30 * 60_000;
export const PENDING_INPUT_TTL_MS = 7 * 24 * 60 * 60_000;
export const DEFAULT_REQUEST_TIMEOUT_MS = 60_000;
export const CONTEXT_ALERT_WARNING_PERCENT = 25;
export const CONTEXT_ALERT_CRITICAL_PERCENT = 10;

export type CodexTransport = "stdio" | "websocket";
export type PermissionsMode = "default" | "full-access";
Expand Down Expand Up @@ -266,10 +268,13 @@ export type StoredBinding = {
threadTitle?: string;
pinnedBindingMessage?: InteractiveMessageRef;
contextUsage?: ContextUsageSnapshot;
lastContextAlertLevel?: ContextAlertLevel | null;
preferences?: ConversationPreferences;
updatedAt: number;
};

export type ContextAlertLevel = "warning" | "critical";

export type InteractiveMessageRef =
| {
provider: "telegram";
Expand Down