@howaboua/pi-codex-conversion 1.5.5 → 1.5.6-dev.28.300a94c

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,8 +5,9 @@ import type { AdapterState } from "./state.ts";
5
5
  import { rewriteNativeImageGenerationTool } from "../tools/image-generation-tool.ts";
6
6
  import { rewriteNativeWebSearchTool } from "../tools/web-search-tool.ts";
7
7
  import { shouldUseCodexAdapter } from "./activation.ts";
8
+ import { rewriteCodexCompactedProviderRequest } from "./compaction.ts";
8
9
 
9
- export function rewriteCodexProviderRequest(payload: unknown, ctx: ExtensionContext, state: AdapterState): unknown | undefined {
10
+ export async function rewriteCodexProviderRequest(payload: unknown, ctx: ExtensionContext, state: AdapterState): Promise<unknown | undefined> {
10
11
  if (!shouldUseCodexAdapter(ctx, state.config) || (!isOpenAICodexContext(ctx) && !isResponsesContext(ctx))) {
11
12
  return undefined;
12
13
  }
@@ -16,8 +17,9 @@ export function rewriteCodexProviderRequest(payload: unknown, ctx: ExtensionCont
16
17
  const imageGenerationPayload = isOpenAICodex && state.config.imageGeneration
17
18
  ? rewriteNativeImageGenerationTool(webSearchPayload, ctx.model)
18
19
  : webSearchPayload;
19
- return applyCodexRequestParams(imageGenerationPayload, state.config, {
20
+ const configuredPayload = applyCodexRequestParams(imageGenerationPayload, state.config, {
20
21
  serviceTier: isOpenAICodex,
21
22
  verbosity: true,
22
23
  });
24
+ return (await rewriteCodexCompactedProviderRequest(configuredPayload, ctx, state)) ?? configuredPayload;
23
25
  }
@@ -0,0 +1,288 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import type { AgentMessage } from "@earendil-works/pi-agent-core";
4
+ import { convertToLlm, getAgentDir } from "@earendil-works/pi-coding-agent";
5
+ import type { Api, ImageContent, Message, Model, TextContent, ToolResultMessage, UserMessage } from "@earendil-works/pi-ai";
6
+ import type { ResponsesCompatibleRequestPayload } from "./compaction-runtime.ts";
7
+ import { CODEX_TOOL_CALL_PROVIDERS, convertResponsesMessages } from "../providers/openai-responses-shared.ts";
8
+
9
+ /**
10
+ * Decision for native compaction: reuse the provider's Responses serializer.
11
+ *
12
+ * Replay parity must match the actual OpenAI Codex provider payload, including
13
+ * tool-call id normalization and cross-model/provider history handling.
14
+ */
15
+ export const COMPACTION_SERIALIZER_STRATEGY = "provider-responses-serializer" as const;
16
+
17
+ export type CompactionSerializerStrategy = typeof COMPACTION_SERIALIZER_STRATEGY;
18
+ export type AssistantPhase = "commentary" | "final_answer";
19
+
20
+ type ResponsesTextInputItem = {
21
+ type: "input_text";
22
+ text: string;
23
+ };
24
+
25
+ type ResponsesImageInputItem = {
26
+ type: "input_image";
27
+ detail: "auto";
28
+ image_url: string;
29
+ };
30
+
31
+ export type ResponsesInputContentItem = ResponsesTextInputItem | ResponsesImageInputItem;
32
+
33
+ export type ResponsesInputMessageItem = {
34
+ role: "user" | "developer" | "system";
35
+ content: ResponsesInputContentItem[] | string;
36
+ };
37
+
38
+ export type ResponsesAssistantOutputItem = {
39
+ type: "message";
40
+ role: "assistant";
41
+ content: Array<{
42
+ type: "output_text";
43
+ text: string;
44
+ annotations: [];
45
+ }>;
46
+ status: "completed";
47
+ id: string;
48
+ phase?: AssistantPhase;
49
+ };
50
+
51
+ export type ResponsesFunctionCallItem = {
52
+ type: "function_call";
53
+ id?: string;
54
+ call_id: string;
55
+ name: string;
56
+ arguments: string;
57
+ };
58
+
59
+ export type ResponsesFunctionCallOutputItem = {
60
+ type: "function_call_output";
61
+ call_id: string;
62
+ output: ResponsesInputContentItem[] | string;
63
+ };
64
+
65
+ export type ResponsesReasoningItem = Record<string, unknown>;
66
+
67
+ export type ResponsesInputItem =
68
+ | ResponsesInputMessageItem
69
+ | ResponsesAssistantOutputItem
70
+ | ResponsesFunctionCallItem
71
+ | ResponsesFunctionCallOutputItem
72
+ | ResponsesReasoningItem;
73
+
74
+ export type NativeCompactionRequestBody = {
75
+ model: string;
76
+ input: ResponsesInputItem[];
77
+ instructions: string;
78
+ parallel_tool_calls?: boolean;
79
+ prompt_cache_key?: string;
80
+ service_tier?: string;
81
+ text?: { verbosity: string };
82
+ tools?: unknown[];
83
+ reasoning?: unknown;
84
+ };
85
+
86
+ export type NativeCompactionRequestOptions = Pick<
87
+ NativeCompactionRequestBody,
88
+ "parallel_tool_calls" | "prompt_cache_key" | "service_tier" | "text" | "tools" | "reasoning"
89
+ >;
90
+
91
+ export type SerializeResponsesMessagesOptions = {
92
+ instructions?: string;
93
+ includeInstructionsInInput?: boolean;
94
+ blockImages?: boolean;
95
+ };
96
+
97
+ export type ResponsesParityReport = {
98
+ ok: boolean;
99
+ actual: string[];
100
+ expected: string[];
101
+ mismatches: string[];
102
+ };
103
+
104
+
105
+ function sanitizeSurrogates(text: string): string {
106
+ return text.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, "");
107
+ }
108
+
109
+ function isRecord(value: unknown): value is Record<string, unknown> {
110
+ return !!value && typeof value === "object" && !Array.isArray(value);
111
+ }
112
+
113
+ let cachedBlockImagesSetting: boolean | undefined;
114
+
115
+ function readBlockImagesSetting(): boolean {
116
+ if (cachedBlockImagesSetting !== undefined) return cachedBlockImagesSetting;
117
+ try {
118
+ const parsed = JSON.parse(readFileSync(join(getAgentDir(), "settings.json"), "utf-8")) as unknown;
119
+ cachedBlockImagesSetting = isRecord(parsed) && isRecord(parsed.images) && parsed.images.blockImages === true;
120
+ } catch {
121
+ cachedBlockImagesSetting = false;
122
+ }
123
+ return cachedBlockImagesSetting;
124
+ }
125
+
126
+ function replaceImagesWithDisabledPlaceholder<TMessage extends UserMessage | ToolResultMessage>(message: TMessage): TMessage {
127
+ if (!Array.isArray(message.content) || !message.content.some((item) => item.type === "image")) return message;
128
+ const content = message.content
129
+ .map((item): TextContent | ImageContent => item.type === "image" ? { type: "text", text: "Image reading is disabled." } : item)
130
+ .filter((item, index, items) => {
131
+ const previous = items[index - 1];
132
+ return !(item.type === "text" && item.text === "Image reading is disabled." && previous?.type === "text" && previous.text === "Image reading is disabled.");
133
+ });
134
+ return { ...message, content };
135
+ }
136
+
137
+ function applyBlockImages(messages: Message[], blockImages: boolean): Message[] {
138
+ if (!blockImages) return messages;
139
+ return messages.map((message) => {
140
+ if (message.role === "user" || message.role === "toolResult") return replaceImagesWithDisabledPlaceholder(message);
141
+ return message;
142
+ });
143
+ }
144
+
145
+ type CompactionPreparationLike = { messagesToSummarize: AgentMessage[]; turnPrefixMessages: AgentMessage[]; previousSummary?: string };
146
+
147
+ export function collectCompactionWindowMessages(preparation: CompactionPreparationLike): AgentMessage[] {
148
+ const previousSummary = preparation.previousSummary?.trim();
149
+ const previousSummaryMessages: AgentMessage[] = previousSummary
150
+ ? [
151
+ {
152
+ role: "user",
153
+ content: `Previous compaction summary:\n${previousSummary}`,
154
+ timestamp: Date.now(),
155
+ } as AgentMessage,
156
+ ]
157
+ : [];
158
+ return [...previousSummaryMessages, ...preparation.messagesToSummarize, ...preparation.turnPrefixMessages];
159
+ }
160
+
161
+ export function serializeCompactionPreparationToRequest<TApi extends Api>(args: {
162
+ model: Model<TApi>;
163
+ preparation: CompactionPreparationLike;
164
+ instructions: string;
165
+ requestOptions?: NativeCompactionRequestOptions;
166
+ }): NativeCompactionRequestBody {
167
+ return serializeMessagesToCompactRequest({
168
+ model: args.model,
169
+ messages: collectCompactionWindowMessages(args.preparation),
170
+ instructions: args.instructions,
171
+ requestOptions: args.requestOptions,
172
+ });
173
+ }
174
+
175
+ export function serializeMessagesToCompactRequest<TApi extends Api>(args: {
176
+ model: Model<TApi>;
177
+ messages: AgentMessage[];
178
+ instructions: string;
179
+ requestOptions?: NativeCompactionRequestOptions;
180
+ }): NativeCompactionRequestBody {
181
+ return {
182
+ model: args.model.id,
183
+ input: serializeMessagesToResponsesInput(args.model, args.messages),
184
+ instructions: sanitizeSurrogates(args.instructions),
185
+ ...args.requestOptions,
186
+ };
187
+ }
188
+
189
+ export function serializeMessagesToResponsesInput<TApi extends Api>(
190
+ model: Model<TApi>,
191
+ messages: AgentMessage[],
192
+ options: SerializeResponsesMessagesOptions = {},
193
+ ): ResponsesInputItem[] {
194
+ const llmMessages = applyBlockImages(convertToLlm(messages), options.blockImages ?? readBlockImagesSetting());
195
+ return convertResponsesMessages(
196
+ model,
197
+ {
198
+ messages: llmMessages,
199
+ ...(options.includeInstructionsInInput && options.instructions ? { systemPrompt: options.instructions } : {}),
200
+ },
201
+ CODEX_TOOL_CALL_PROVIDERS,
202
+ { includeSystemPrompt: options.includeInstructionsInInput ?? false },
203
+ ) as ResponsesInputItem[];
204
+ }
205
+
206
+ export function createResponsesInputParitySignature(input: readonly unknown[]): string[] {
207
+ return input.map(describeResponsesInputItem);
208
+ }
209
+
210
+ export function compareResponsesInputParity(actual: readonly unknown[], expected: readonly unknown[]): ResponsesParityReport {
211
+ const actualSignature = createResponsesInputParitySignature(actual);
212
+ const expectedSignature = createResponsesInputParitySignature(expected);
213
+ const maxLength = Math.max(actualSignature.length, expectedSignature.length);
214
+ const mismatches: string[] = [];
215
+
216
+ for (let index = 0; index < maxLength; index++) {
217
+ const actualValue = actualSignature[index];
218
+ const expectedValue = expectedSignature[index];
219
+ if (actualValue !== expectedValue) {
220
+ mismatches.push(`index ${index}: expected ${expectedValue ?? "<missing>"}, got ${actualValue ?? "<missing>"}`);
221
+ }
222
+ }
223
+
224
+ return {
225
+ ok: mismatches.length === 0,
226
+ actual: actualSignature,
227
+ expected: expectedSignature,
228
+ mismatches,
229
+ };
230
+ }
231
+
232
+ export function compareCompactRequestToPayload(
233
+ request: NativeCompactionRequestBody,
234
+ payload: Pick<ResponsesCompatibleRequestPayload, "model" | "input" | "instructions">,
235
+ ): ResponsesParityReport {
236
+ const parity = compareResponsesInputParity(request.input, payload.input);
237
+ const mismatches = [...parity.mismatches];
238
+
239
+ if (payload.model !== request.model) {
240
+ mismatches.unshift(`model: expected ${payload.model}, got ${request.model}`);
241
+ }
242
+
243
+ if ((payload.instructions ?? "") !== request.instructions) {
244
+ mismatches.unshift("instructions: expected serialized instructions to match payload instructions");
245
+ }
246
+
247
+ return {
248
+ ok: mismatches.length === 0,
249
+ actual: parity.actual,
250
+ expected: parity.expected,
251
+ mismatches,
252
+ };
253
+ }
254
+
255
+ function describeResponsesInputItem(item: unknown): string {
256
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
257
+ return typeof item;
258
+ }
259
+
260
+ const record = item as Record<string, unknown>;
261
+ const type = typeof record.type === "string" ? record.type : undefined;
262
+ if (type === "message") {
263
+ const phase =
264
+ record.phase === "commentary" || record.phase === "final_answer"
265
+ ? `:${record.phase}`
266
+ : "";
267
+ return `message:${typeof record.role === "string" ? record.role : "unknown"}${phase}`;
268
+ }
269
+
270
+ if (type === "function_call") {
271
+ return `function_call:${typeof record.name === "string" ? record.name : "unknown"}`;
272
+ }
273
+
274
+ if (type === "function_call_output") {
275
+ return "function_call_output";
276
+ }
277
+
278
+ if (type === "reasoning") {
279
+ return "reasoning";
280
+ }
281
+
282
+ if (typeof record.role === "string") {
283
+ const content = Array.isArray(record.content) ? `[${record.content.length}]` : "";
284
+ return `input:${record.role}${content}`;
285
+ }
286
+
287
+ return type ? `item:${type}` : "object";
288
+ }
@@ -15,3 +15,11 @@ export function getCodexSkillPaths(cwd: string, home: string = homedir()): strin
15
15
  }
16
16
  return skillPaths.filter((path) => existsSync(path));
17
17
  }
18
+
19
+ export function hasNoSkillsFlag(argv: readonly string[] = process.argv): boolean {
20
+ for (const arg of argv) {
21
+ if (arg === "--") return false;
22
+ if (arg === "--no-skills" || arg === "-ns") return true;
23
+ }
24
+ return false;
25
+ }
@@ -1,11 +1,13 @@
1
1
  export const STATUS_KEY = "codex-adapter";
2
2
  export const STATUS_TEXT = "\u001b[38;2;0;76;255mCodex adapter\u001b[0m";
3
+ export const APPLY_PATCH_ONLY_STATUS_TEXT = `${STATUS_TEXT} • apply patch only`;
3
4
 
4
- export function buildStatusText(options: { verbosity?: string; webSearch: boolean; imageGeneration: boolean; fast: boolean; useOnAllModels: boolean }): string {
5
+ export function buildStatusText(options: { verbosity?: string; webSearch: boolean; imageGeneration: boolean; fast: boolean; useOnAllModels: boolean; compaction?: { enabled: boolean; model: string; reasoning: string } }): string {
5
6
  const extras = [
6
7
  options.useOnAllModels ? "all models" : undefined,
7
8
  options.webSearch ? "web search" : undefined,
8
9
  options.imageGeneration ? "image gen" : undefined,
10
+ options.compaction?.enabled ? `compact ${options.compaction.model}/${options.compaction.reasoning}` : undefined,
9
11
  options.fast ? "fast" : undefined,
10
12
  ]
11
13
  .filter(Boolean)
@@ -16,7 +18,9 @@ export function buildStatusText(options: { verbosity?: string; webSearch: boolea
16
18
 
17
19
  export const DEFAULT_TOOL_NAMES = ["read", "bash", "edit", "write"];
18
20
 
19
- export const CORE_ADAPTER_TOOL_NAMES = ["exec_command", "write_stdin", "apply_patch"];
21
+ export const SHELL_ADAPTER_TOOL_NAMES = ["exec_command", "write_stdin"];
22
+ export const APPLY_PATCH_TOOL_NAME = "apply_patch";
23
+ export const CORE_ADAPTER_TOOL_NAMES = [...SHELL_ADAPTER_TOOL_NAMES, APPLY_PATCH_TOOL_NAME];
20
24
  export const IMAGE_GENERATION_TOOL_NAME = "image_generation";
21
25
  export const VIEW_IMAGE_TOOL_NAME = "view_image";
22
- export const WEB_SEARCH_TOOL_NAME = "web_search";
26
+ export const WEB_SEARCH_TOOL_NAME = "web.run";
@@ -0,0 +1,220 @@
1
+ import type { CompactionEntry, CompactionResult } from "@earendil-works/pi-coding-agent";
2
+
3
+ export const EXTENSION_ID = "openai-native-compaction";
4
+ export const NATIVE_COMPACTION_STRATEGY = "openai-native-compact-v1";
5
+ export const NATIVE_COMPACTION_SHIM_SUMMARY = "[OpenAI native compaction checkpoint]";
6
+ export const NATIVE_COMPACTION_DISPLAY_MESSAGE_TYPE = "codex-native-compaction-display";
7
+ export const NATIVE_COMPACTION_DISPLAY_TEXT = [
8
+ "Codex native compaction was used for this checkpoint.",
9
+ "",
10
+ "The compaction result is encrypted by OpenAI and is not human-readable in Pi.",
11
+ "",
12
+ "Warning: do not turn Responses compaction off mid-session; old context may be much less reliable.",
13
+ ].join("\n");
14
+
15
+ export type NativeCompactionStrategy = typeof NATIVE_COMPACTION_STRATEGY;
16
+ export type NativeCompactionShimSummary = typeof NATIVE_COMPACTION_SHIM_SUMMARY;
17
+
18
+ export type NativeCompactionRequestMeta = {
19
+ tokensBefore?: number;
20
+ previousSummaryPresent?: boolean;
21
+ compactedKeptWindow?: boolean;
22
+ };
23
+
24
+ export type NativeCompactionIdentity = {
25
+ provider: string;
26
+ api: string;
27
+ model: string;
28
+ baseUrl: string;
29
+ };
30
+
31
+ export type NativeCompactionDetails = NativeCompactionIdentity & {
32
+ strategy: NativeCompactionStrategy;
33
+ compactedWindow: unknown[];
34
+ compactResponseId?: string;
35
+ createdAt: string;
36
+ requestMeta?: NativeCompactionRequestMeta;
37
+ };
38
+
39
+ export type NativeCompactionEntry = CompactionEntry<NativeCompactionDetails>;
40
+
41
+ export type CreateNativeCompactionDetailsInput = NativeCompactionIdentity & {
42
+ compactedWindow: unknown[];
43
+ compactResponseId?: string;
44
+ createdAt?: string;
45
+ requestMeta?: NativeCompactionRequestMeta;
46
+ };
47
+
48
+ export type CreateNativeCompactionShimResultInput = {
49
+ summary: string;
50
+ firstKeptEntryId: string;
51
+ tokensBefore: number;
52
+ details: NativeCompactionDetails;
53
+ };
54
+
55
+ function isRecord(value: unknown): value is Record<string, unknown> {
56
+ return !!value && typeof value === "object" && !Array.isArray(value);
57
+ }
58
+
59
+ function isNonEmptyString(value: unknown): value is string {
60
+ return typeof value === "string" && value.trim().length > 0;
61
+ }
62
+
63
+ function isFiniteNonNegativeNumber(value: unknown): value is number {
64
+ return typeof value === "number" && Number.isFinite(value) && value >= 0;
65
+ }
66
+
67
+ function normalizeString(value: string): string {
68
+ return value.trim();
69
+ }
70
+
71
+ function isStructuredValue(value: unknown): boolean {
72
+ if (
73
+ value === null ||
74
+ typeof value === "string" ||
75
+ typeof value === "number" ||
76
+ typeof value === "boolean"
77
+ ) {
78
+ return true;
79
+ }
80
+
81
+ if (Array.isArray(value)) {
82
+ return value.every(isStructuredValue);
83
+ }
84
+
85
+ if (isRecord(value)) {
86
+ return Object.values(value).every(isStructuredValue);
87
+ }
88
+
89
+ return false;
90
+ }
91
+
92
+ function cloneStructuredValue(value: unknown): unknown {
93
+ if (
94
+ value === null ||
95
+ typeof value === "string" ||
96
+ typeof value === "number" ||
97
+ typeof value === "boolean"
98
+ ) {
99
+ return value;
100
+ }
101
+
102
+ if (Array.isArray(value)) {
103
+ return value.map(cloneStructuredValue);
104
+ }
105
+
106
+ if (isRecord(value)) {
107
+ const clone: Record<string, unknown> = {};
108
+ for (const [key, nested] of Object.entries(value)) {
109
+ clone[key] = cloneStructuredValue(nested);
110
+ }
111
+ return clone;
112
+ }
113
+
114
+ throw new Error(`Unsupported structured value: ${typeof value}`);
115
+ }
116
+
117
+ function isCompactedWindowItem(value: unknown): value is Record<string, unknown> {
118
+ return isRecord(value) && Object.values(value).every(isStructuredValue);
119
+ }
120
+
121
+ export function isNativeCompactionRequestMeta(value: unknown): value is NativeCompactionRequestMeta {
122
+ if (!isRecord(value)) {
123
+ return false;
124
+ }
125
+
126
+ const { tokensBefore, previousSummaryPresent, compactedKeptWindow } = value;
127
+ if (tokensBefore !== undefined && !isFiniteNonNegativeNumber(tokensBefore)) {
128
+ return false;
129
+ }
130
+
131
+ if (previousSummaryPresent !== undefined && typeof previousSummaryPresent !== "boolean") {
132
+ return false;
133
+ }
134
+
135
+ if (compactedKeptWindow !== undefined && typeof compactedKeptWindow !== "boolean") {
136
+ return false;
137
+ }
138
+
139
+ return true;
140
+ }
141
+
142
+ export function isNativeCompactionIdentity(value: unknown): value is NativeCompactionIdentity {
143
+ if (!isRecord(value)) {
144
+ return false;
145
+ }
146
+
147
+ return (
148
+ isNonEmptyString(value.provider) &&
149
+ isNonEmptyString(value.api) &&
150
+ isNonEmptyString(value.model) &&
151
+ isNonEmptyString(value.baseUrl)
152
+ );
153
+ }
154
+
155
+ export function isNativeCompactionDetails(value: unknown): value is NativeCompactionDetails {
156
+ if (!isRecord(value)) {
157
+ return false;
158
+ }
159
+ const candidate = value as Record<string, unknown>;
160
+
161
+ return (
162
+ candidate.strategy === NATIVE_COMPACTION_STRATEGY &&
163
+ isNonEmptyString(candidate.provider) &&
164
+ isNonEmptyString(candidate.api) &&
165
+ isNonEmptyString(candidate.model) &&
166
+ isNonEmptyString(candidate.baseUrl) &&
167
+ Array.isArray(candidate.compactedWindow) &&
168
+ candidate.compactedWindow.every(isCompactedWindowItem) &&
169
+ isNonEmptyString(candidate.createdAt) &&
170
+ (candidate.compactResponseId === undefined || isNonEmptyString(candidate.compactResponseId)) &&
171
+ (candidate.requestMeta === undefined || isNativeCompactionRequestMeta(candidate.requestMeta))
172
+ );
173
+ }
174
+
175
+ export function isNativeCompactionEntry(value: unknown): value is NativeCompactionEntry {
176
+ return isRecord(value) && value.type === "compaction" && isNativeCompactionDetails(value.details);
177
+ }
178
+
179
+ export function isNativeCompactionShimSummary(value: unknown): value is NativeCompactionShimSummary {
180
+ return value === NATIVE_COMPACTION_SHIM_SUMMARY;
181
+ }
182
+
183
+ export function createNativeCompactionDetails(input: CreateNativeCompactionDetailsInput): NativeCompactionDetails {
184
+ return {
185
+ strategy: NATIVE_COMPACTION_STRATEGY,
186
+ provider: normalizeString(input.provider),
187
+ api: normalizeString(input.api),
188
+ model: normalizeString(input.model),
189
+ baseUrl: normalizeString(input.baseUrl),
190
+ compactedWindow: input.compactedWindow.map((item) => cloneStructuredValue(item)),
191
+ compactResponseId: isNonEmptyString(input.compactResponseId) ? normalizeString(input.compactResponseId) : undefined,
192
+ createdAt: isNonEmptyString(input.createdAt) ? normalizeString(input.createdAt) : new Date().toISOString(),
193
+ requestMeta: input.requestMeta
194
+ ? {
195
+ ...(input.requestMeta.tokensBefore !== undefined ? { tokensBefore: input.requestMeta.tokensBefore } : {}),
196
+ ...(input.requestMeta.previousSummaryPresent !== undefined
197
+ ? { previousSummaryPresent: input.requestMeta.previousSummaryPresent }
198
+ : {}),
199
+ ...(input.requestMeta.compactedKeptWindow !== undefined
200
+ ? { compactedKeptWindow: input.requestMeta.compactedKeptWindow }
201
+ : {}),
202
+ }
203
+ : undefined,
204
+ };
205
+ }
206
+
207
+ export function createNativeCompactionShimSummary(): NativeCompactionShimSummary {
208
+ return NATIVE_COMPACTION_SHIM_SUMMARY;
209
+ }
210
+
211
+ export function createNativeCompactionShimResult(
212
+ input: CreateNativeCompactionShimResultInput,
213
+ ): CompactionResult<NativeCompactionDetails> {
214
+ return {
215
+ summary: input.summary,
216
+ firstKeptEntryId: input.firstKeptEntryId,
217
+ tokensBefore: input.tokensBefore,
218
+ details: input.details,
219
+ };
220
+ }
@@ -9,7 +9,8 @@ import { syncAdapter } from "../adapter/activation.ts";
9
9
  import type { AdapterState } from "../adapter/state.ts";
10
10
  import { openCodexSettingsScreen } from "./ui.ts";
11
11
 
12
- const CODEX_COMMAND_COMPLETIONS = ["all", "status", "fast", "search", "image", "low", "medium", "high"] as const;
12
+ const CODEX_COMMAND_COMPLETIONS = ["all", "status", "fast", "search", "image", "compact", "low", "medium", "high"] as const;
13
+ const CODEX_USAGE = "Usage: /codex, /codex all, /codex status, /codex fast, /codex search, /codex image, /codex compact, /codex low|medium|high";
13
14
 
14
15
  export function registerCodexCommand(pi: ExtensionAPI, state: AdapterState, onConfigApplied?: (config: CodexConversionConfig) => void): void {
15
16
  function saveAndApply(ctx: ExtensionContext, nextConfig: CodexConversionConfig): boolean {
@@ -31,6 +32,18 @@ export function registerCodexCommand(pi: ExtensionAPI, state: AdapterState, onCo
31
32
  handler: async (args, ctx) => {
32
33
  state.config = readCodexConversionConfig();
33
34
  const arg = args.trim().toLowerCase();
35
+ if (arg === "compact") {
36
+ if (!ctx.hasUI) {
37
+ ctx.ui.notify(formatCodexSettings(state.config), "info");
38
+ return;
39
+ }
40
+ await openCodexSettingsScreen(ctx, {
41
+ initialConfig: state.config,
42
+ initialTab: "compaction",
43
+ onChange: (config) => saveAndApply(ctx, config),
44
+ });
45
+ return;
46
+ }
34
47
  const nextConfig = getCommandConfigUpdate(arg, state.config);
35
48
  if (nextConfig) {
36
49
  saveAndApply(ctx, nextConfig);
@@ -38,7 +51,7 @@ export function registerCodexCommand(pi: ExtensionAPI, state: AdapterState, onCo
38
51
  }
39
52
 
40
53
  if (arg) {
41
- ctx.ui.notify("Usage: /codex, /codex all, /codex status, /codex fast, /codex search, /codex image, /codex low|medium|high", "warning");
54
+ ctx.ui.notify(CODEX_USAGE, "warning");
42
55
  return;
43
56
  }
44
57
 
@@ -66,5 +79,5 @@ function getCommandConfigUpdate(arg: string, config: CodexConversionConfig): Cod
66
79
  }
67
80
 
68
81
  function formatCodexSettings(config: CodexConversionConfig): string {
69
- return `Codex settings: all models ${config.useOnAllModels ? "on" : "off"}, statusline ${config.statusLine ? "on" : "off"}, fast ${config.fast ? "on" : "off"}, web search ${config.webSearch ? "on" : "off"}, image generation ${config.imageGeneration ? "on" : "off"}, verbosity ${config.verbosity}`;
82
+ return `Codex settings: all models ${config.useOnAllModels ? "on" : "off"}, statusline ${config.statusLine ? "on" : "off"}, fast ${config.fast ? "on" : "off"}, web search ${config.webSearch ? "on" : "off"}, image generation ${config.imageGeneration ? "on" : "off"}, responses compaction ${(config.responsesCompaction ?? false) ? "on" : "off"} (${config.compactionModel}/${config.compactionReasoning}), verbosity ${config.verbosity}`;
70
83
  }