Skip to content

Commit 807f7c4

Browse files
authored
Merge branch 'main' into codex-update/0.114.0
2 parents 088e4df + 99c150f commit 807f7c4

10 files changed

Lines changed: 149 additions & 14 deletions

.github/workflows/codex-update.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,9 @@ jobs:
6969
with:
7070
openai-api-key: ${{ secrets.OPENAI_API_KEY }}
7171
codex-version: ${{ env.VERSION }}
72-
codex-args: '-c shell_environment_policy.inherit=none'
7372
output-file: pr-body.md
73+
codex-args: >-
74+
-c sandbox_workspace_write.writable_roots=["${{ github.workspace }}/.git"]
7475
prompt: >
7576
Finalize the update using codex-update-compat skill.
7677
Commit the changes, the message should mention that types or/and tests after the update were fixed.

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "codex-acp",
3-
"version": "0.0.29",
3+
"version": "0.0.30",
44
"description": "",
55
"main": "dist/index.js",
66
"scripts": {

src/CodexCommands.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,8 +245,9 @@ export class CodexCommands {
245245
}
246246
const total = this.formatTokenCount(usage.totalTokens);
247247
const input = this.formatTokenCount(usage.inputTokens);
248+
const cachedInput = this.formatTokenCount(usage.cachedInputTokens);
248249
const output = this.formatTokenCount(usage.outputTokens);
249-
return `${total} total (${input} input + ${output} output)`;
250+
return `${total} total (${input} input + ${cachedInput} cached input, ${output} output)`;
250251
}
251252

252253
private formatContextWindow(usage: TokenCount | null, contextWindow: number | null): string {

src/CodexToolCallMapper.ts

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { ToolCallContent } from "@agentclientprotocol/sdk";
2-
import { applyPatch } from "diff";
2+
import { applyPatch, parsePatch } from "diff";
33
import { readFile } from "node:fs/promises";
44
import path from "node:path";
55
import type { UpdateSessionEvent } from "./ACPSessionConnection";
@@ -86,6 +86,7 @@ export async function createMcpToolCallUpdate(
8686
): Promise<UpdateSessionEvent> {
8787
return createExecuteToolCallUpdate(item, `mcp.${item.server}.${item.tool}`);
8888
}
89+
8990
export async function createDynamicToolCallUpdate(
9091
item: ThreadItem & { type: "dynamicToolCall" }
9192
): Promise<UpdateSessionEvent> {
@@ -235,7 +236,28 @@ async function createPatchContent(change: FileUpdateChange): Promise<ToolCallCon
235236
};
236237
}
237238

238-
const oldContent = change.kind.type === "add" ? "" : await readFile(change.path, { encoding: "utf8" });
239+
if (change.kind.type === "delete") {
240+
// If the patch deletes a file, the old content may be only available from the diff.
241+
const oldContent = await readFile(change.path, { encoding: "utf8"} ).catch(() =>
242+
isUnifiedDiff(change.diff) ? patchToDeletedContent(change.diff) : change.diff
243+
);
244+
245+
return {
246+
type: "diff",
247+
oldText: oldContent,
248+
newText: "",
249+
path: change.path,
250+
_meta: {
251+
kind: "delete",
252+
}
253+
}
254+
}
255+
256+
const oldContent = change.kind.type === "add" ? "" : await readFile(change.path, { encoding: "utf8" }).catch(() => null);
257+
if (oldContent === null) {
258+
return null;
259+
}
260+
239261
const newContent = applyPatch(oldContent, change.diff);
240262
if (newContent === false) {
241263
return null;
@@ -254,3 +276,40 @@ async function createPatchContent(change: FileUpdateChange): Promise<ToolCallCon
254276
function isUnifiedDiff(content: string): boolean {
255277
return content.startsWith("--- ") || content.includes("\n--- ");
256278
}
279+
280+
/**
281+
* Recreates the content of a deleted file from the unified diff.
282+
* @param unifiedDiff The unified diff of the file deletion patch
283+
*/
284+
function patchToDeletedContent(unifiedDiff: string): string | null {
285+
try {
286+
const [patch] = parsePatch(unifiedDiff);
287+
if (!patch || patch.hunks.length === 0) {
288+
return null;
289+
}
290+
291+
const oldLines: string[] = [];
292+
let hasNoTrailingNewlineMarker = false;
293+
294+
for (const hunk of patch.hunks) {
295+
for (const line of hunk.lines) {
296+
if (line === "\\ No newline at end of file") {
297+
hasNoTrailingNewlineMarker = true;
298+
continue;
299+
}
300+
if (line.startsWith("-") || line.startsWith(" ")) {
301+
oldLines.push(line.slice(1));
302+
}
303+
}
304+
}
305+
306+
if (oldLines.length === 0) {
307+
return "";
308+
}
309+
310+
const oldText = oldLines.join("\n");
311+
return hasNoTrailingNewlineMarker || !unifiedDiff.endsWith("\n") ? oldText : `${oldText}\n`;
312+
} catch {
313+
return null;
314+
}
315+
}

src/TokenCount.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@ import type {TokenUsageBreakdown} from "./app-server/v2";
33
/**
44
* Token usage information for a turn.
55
* This interface decouples our API from Codex's internal types.
6+
*
7+
* [totalTokens]: total number of tokens used (the sum of all other fields)
8+
* [inputTokens]: number of non-cached input tokens
9+
* [cachedInputTokens]: number of cached input tokens
10+
* [outputTokens]: number of output tokens (including reasoning output tokens)
11+
* [reasoningOutputTokens]: number of reasoning output tokens
612
*/
713
export interface TokenCount {
814
totalTokens: number;
@@ -15,11 +21,13 @@ export interface TokenCount {
1521
/**
1622
* Maps Codex's TokenUsageBreakdown to our TokenCount interface.
1723
* This explicit mapping ensures compile-time errors if Codex changes their types.
24+
* Note: Codex includes cached input tokens in the input token count, so they are subtracted here.
1825
*/
1926
export function toTokenCount(usage: TokenUsageBreakdown): TokenCount {
27+
2028
return {
2129
totalTokens: usage.totalTokens,
22-
inputTokens: usage.inputTokens,
30+
inputTokens: usage.inputTokens - usage.cachedInputTokens,
2331
cachedInputTokens: usage.cachedInputTokens,
2432
outputTokens: usage.outputTokens,
2533
reasoningOutputTokens: usage.reasoningOutputTokens,

src/__tests__/CodexACPAgent/data/file-change-delete-raw-content.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
{
1414
"type": "diff",
1515
"oldText": "fun main() {\n println(\"Hello, World!\")\n}\n",
16-
"newText": "fun main() {\n println(\"Hello, World!\")\n}\n",
16+
"newText": "",
1717
"path": "/test/project/RawDeleteFile.kt",
1818
"_meta": {
1919
"kind": "delete"

src/__tests__/CodexACPAgent/data/token-usage-end-turn.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"quota": {
55
"token_count": {
66
"totalTokens": 2500,
7-
"inputTokens": 2000,
7+
"inputTokens": 1500,
88
"cachedInputTokens": 500,
99
"outputTokens": 450,
1010
"reasoningOutputTokens": 50
@@ -14,7 +14,7 @@
1414
"model": "model-id",
1515
"token_count": {
1616
"totalTokens": 2500,
17-
"inputTokens": 2000,
17+
"inputTokens": 1500,
1818
"cachedInputTokens": 500,
1919
"outputTokens": 450,
2020
"reasoningOutputTokens": 50

src/__tests__/CodexACPAgent/data/token-usage-multiple-updates.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"quota": {
55
"token_count": {
66
"totalTokens": 1500,
7-
"inputTokens": 1200,
7+
"inputTokens": 700,
88
"cachedInputTokens": 500,
99
"outputTokens": 200,
1010
"reasoningOutputTokens": 100
@@ -14,7 +14,7 @@
1414
"model": "model-id",
1515
"token_count": {
1616
"totalTokens": 1500,
17-
"inputTokens": 1200,
17+
"inputTokens": 700,
1818
"cachedInputTokens": 500,
1919
"outputTokens": 200,
2020
"reasoningOutputTokens": 100

src/__tests__/CodexACPAgent/file-change-events.test.ts

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ import type { ServerNotification } from '../../app-server';
44
import { createCodexMockTestFixture, createTestSessionState, setupPromptAndSendNotifications, type CodexMockTestFixture } from '../acp-test-utils';
55
import {AgentMode} from "../../AgentMode";
66

7-
const { mockFiles, mockFileContent, clearMockFiles } = vi.hoisted(() => {
7+
const { mockFiles, mockFileContent, removeMockFile, clearMockFiles } = vi.hoisted(() => {
88
const files = new Map<string, string>();
99
return {
1010
mockFiles: files,
1111
mockFileContent: (path: string, content: string) => files.set(path, content),
12+
removeMockFile: (path: string) => files.delete(path),
1213
clearMockFiles: () => files.clear(),
1314
};
1415
});
@@ -205,4 +206,69 @@ describe('CodexEventHandler - file change events', () => {
205206
'data/file-change-delete-raw-content.json'
206207
);
207208
});
209+
210+
it('should handle file deletion when old file is already missing', async () => {
211+
removeMockFile('/test/project/OldFile.kt');
212+
213+
const deleteFileNotification: ServerNotification = {
214+
method: 'item/started',
215+
params: {
216+
threadId: 'thread-1',
217+
turnId: 'turn-1',
218+
item: {
219+
type: 'fileChange',
220+
id: 'file-change-3',
221+
changes: [
222+
{
223+
path: '/test/project/OldFile.kt',
224+
kind: { type: 'delete' },
225+
diff: `--- /test/project/OldFile.kt
226+
+++ /dev/null
227+
@@ -1,3 +0,0 @@
228+
-package test.project
229+
-
230+
-class OldFile {}`,
231+
},
232+
],
233+
status: 'completed',
234+
},
235+
},
236+
};
237+
238+
await setupPromptAndSendNotifications(mockFixture, sessionId, sessionState, [deleteFileNotification]);
239+
240+
await expect(mockFixture.getAcpConnectionDump(['id'])).toMatchFileSnapshot(
241+
'data/file-change-delete-file.json'
242+
);
243+
});
244+
245+
it('should handle file deletion with raw content when old file is already missing', async () => {
246+
removeMockFile('/test/project/RawDeleteFile.kt');
247+
248+
const deletedFileNotification: ServerNotification = {
249+
method: 'item/started',
250+
params: {
251+
threadId: 'thread-1',
252+
turnId: 'turn-1',
253+
item: {
254+
type: 'fileChange',
255+
id: 'file-delete-raw',
256+
changes: [
257+
{
258+
path: '/test/project/RawDeleteFile.kt',
259+
kind: { type: 'delete' },
260+
diff: 'fun main() {\n println("Hello, World!")\n}\n',
261+
},
262+
],
263+
status: 'completed',
264+
},
265+
},
266+
};
267+
268+
await setupPromptAndSendNotifications(mockFixture, sessionId, sessionState, [deletedFileNotification]);
269+
270+
await expect(mockFixture.getAcpConnectionDump(['id'])).toMatchFileSnapshot(
271+
'data/file-change-delete-raw-content.json'
272+
);
273+
});
208274
});

0 commit comments

Comments
 (0)