authorgravatar for maxtheyoung@gmail.comMaxwell Young <maxtheyoung@gmail.com> 2026-07-18 23:40:34+12:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-18 13:40:34+02:00
logda3c2030f710c4e1243035613eda94f6526afa1f
treea74dcf5fd6dcce583fcf95915d7a6249101729e1
parent0506508e9b0072a5ffdd5d403cdef3a704f76540
signature Signed by PGP key B5690EEEBB952194

[codex] canonicalize client timestamps (#4112)


2 files changed, 122 insertions(+), 14 deletions(-)

apps/server/src/orchestration/Normalizer.test.ts created+73
......@@ -0,0 +1,73 @@
1import { describe, expect, it } from "vite-plus/test";
2import {
3 CommandId,
4 type ClientOrchestrationCommand,
5 MessageId,
6 ProjectId,
7 ProviderInstanceId,
8 ThreadId,
9} from "@t3tools/contracts";
10
11import { canonicalizeClientCommandTimestamps } from "./Normalizer.ts";
12
13const clientCreatedAt = "2031-01-01T00:00:00.000Z";
14const serverReceivedAt = "2026-07-18T00:00:00.000Z";
15
16describe("canonicalizeClientCommandTimestamps", () => {
17 it("replaces a client command timestamp with the server receipt timestamp", () => {
18 const command: ClientOrchestrationCommand = {
19 type: "project.create",
20 commandId: CommandId.make("command-1"),
21 projectId: ProjectId.make("project-1"),
22 title: "Clock-safe project",
23 workspaceRoot: "/tmp/clock-safe-project",
24 createdAt: clientCreatedAt,
25 };
26
27 expect(canonicalizeClientCommandTimestamps(command, serverReceivedAt)).toEqual({
28 ...command,
29 createdAt: serverReceivedAt,
30 });
31 });
32
33 it("replaces both timestamps when the first turn bootstraps a thread", () => {
34 const command: ClientOrchestrationCommand = {
35 type: "thread.turn.start",
36 commandId: CommandId.make("command-2"),
37 threadId: ThreadId.make("thread-1"),
38 message: {
39 messageId: MessageId.make("message-1"),
40 role: "user",
41 text: "Start a thread",
42 attachments: [],
43 },
44 runtimeMode: "full-access",
45 interactionMode: "default",
46 bootstrap: {
47 createThread: {
48 projectId: ProjectId.make("project-1"),
49 title: "Clock-safe thread",
50 modelSelection: {
51 instanceId: ProviderInstanceId.make("codex"),
52 model: "gpt-5.4",
53 },
54 runtimeMode: "full-access",
55 interactionMode: "default",
56 branch: null,
57 worktreePath: null,
58 createdAt: clientCreatedAt,
59 },
60 },
61 createdAt: clientCreatedAt,
62 };
63
64 const result = canonicalizeClientCommandTimestamps(command, serverReceivedAt);
65
66 expect(result.type).toBe("thread.turn.start");
67 if (result.type !== "thread.turn.start") {
68 throw new Error("Expected a thread.turn.start command");
69 }
70 expect(result.createdAt).toBe(serverReceivedAt);
71 expect(result.bootstrap?.createThread?.createdAt).toBe(serverReceivedAt);
72 });
73});
apps/server/src/orchestration/Normalizer.ts+49-14
......@@ -1,8 +1,10 @@
1import * as DateTime from "effect/DateTime";
12import * as Effect from "effect/Effect";
23import * as FileSystem from "effect/FileSystem";
34import * as Path from "effect/Path";
45import {
56 type ClientOrchestrationCommand,
7 type IsoDateTime,
68 type OrchestrationCommand,
79 OrchestrationDispatchCommandError,
810 PROVIDER_SEND_TURN_MAX_IMAGE_BYTES,
......@@ -13,8 +15,38 @@ import { ServerConfig } from "../config.ts";
1315import { parseBase64DataUrl } from "../imageMime.ts";
1416import * as WorkspacePaths from "../workspace/WorkspacePaths.ts";
1517
18export const canonicalizeClientCommandTimestamps = (
19 command: ClientOrchestrationCommand,
20 receivedAt: IsoDateTime,
21): ClientOrchestrationCommand => {
22 const canonicalCommand =
23 "createdAt" in command
24 ? {
25 ...command,
26 createdAt: receivedAt,
27 }
28 : command;
29
30 if (canonicalCommand.type !== "thread.turn.start" || !canonicalCommand.bootstrap?.createThread) {
31 return canonicalCommand;
32 }
33
34 return {
35 ...canonicalCommand,
36 bootstrap: {
37 ...canonicalCommand.bootstrap,
38 createThread: {
39 ...canonicalCommand.bootstrap.createThread,
40 createdAt: receivedAt,
41 },
42 },
43 };
44};
45
1646export const normalizeDispatchCommand = (command: ClientOrchestrationCommand) =>
1747 Effect.gen(function* () {
48 const receivedAt = DateTime.formatIso(yield* DateTime.now);
49 const canonicalCommand = canonicalizeClientCommandTimestamps(command, receivedAt);
1850 const fileSystem = yield* FileSystem.FileSystem;
1951 const path = yield* Path.Path;
2052 const serverConfig = yield* ServerConfig;
......@@ -47,30 +79,33 @@ export const normalizeDispatchCommand = (command: ClientOrchestrationCommand) =>
4779 ),
4880 );
4981
50 if (command.type === "project.create") {
82 if (canonicalCommand.type === "project.create") {
5183 return {
52 ...command,
84 ...canonicalCommand,
5385 workspaceRoot: yield* normalizeProjectWorkspaceRootForCreate(
54 command.workspaceRoot,
55 command.createWorkspaceRootIfMissing,
86 canonicalCommand.workspaceRoot,
87 canonicalCommand.createWorkspaceRootIfMissing,
5688 ),
57 createWorkspaceRootIfMissing: command.createWorkspaceRootIfMissing === true,
89 createWorkspaceRootIfMissing: canonicalCommand.createWorkspaceRootIfMissing === true,
5890 } satisfies OrchestrationCommand;
5991 }
6092
61 if (command.type === "project.meta.update" && command.workspaceRoot !== undefined) {
93 if (
94 canonicalCommand.type === "project.meta.update" &&
95 canonicalCommand.workspaceRoot !== undefined
96 ) {
6297 return {
63 ...command,
64 workspaceRoot: yield* normalizeProjectWorkspaceRoot(command.workspaceRoot),
98 ...canonicalCommand,
99 workspaceRoot: yield* normalizeProjectWorkspaceRoot(canonicalCommand.workspaceRoot),
65100 } satisfies OrchestrationCommand;
66101 }
67102
68 if (command.type !== "thread.turn.start") {
69 return command as OrchestrationCommand;
103 if (canonicalCommand.type !== "thread.turn.start") {
104 return canonicalCommand as OrchestrationCommand;
70105 }
71106
72107 const normalizedAttachments = yield* Effect.forEach(
73 command.message.attachments,
108 canonicalCommand.message.attachments,
74109 (attachment) =>
75110 Effect.gen(function* () {
76111 const parsed = parseBase64DataUrl(attachment.dataUrl);
......@@ -87,7 +122,7 @@ export const normalizeDispatchCommand = (command: ClientOrchestrationCommand) =>
87122 });
88123 }
89124
90 const attachmentId = createAttachmentId(command.threadId);
125 const attachmentId = createAttachmentId(canonicalCommand.threadId);
91126 if (!attachmentId) {
92127 return yield* new OrchestrationDispatchCommandError({
93128 message: "Failed to create a safe attachment id.",
......@@ -135,9 +170,9 @@ export const normalizeDispatchCommand = (command: ClientOrchestrationCommand) =>
135170 );
136171
137172 return {
138 ...command,
173 ...canonicalCommand,
139174 message: {
140 ...command.message,
175 ...canonicalCommand.message,
141176 attachments: normalizedAttachments,
142177 },
143178 } satisfies OrchestrationCommand;