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 @@...@@ -1,8 +1,10 @@
1import * as DateTime from "effect/DateTime";
1import * as Effect from "effect/Effect";2import * as Effect from "effect/Effect";
2import * as FileSystem from "effect/FileSystem";3import * as FileSystem from "effect/FileSystem";
3import * as Path from "effect/Path";4import * as Path from "effect/Path";
4import {5import {
5 type ClientOrchestrationCommand,6 type ClientOrchestrationCommand,
7 type IsoDateTime,
6 type OrchestrationCommand,8 type OrchestrationCommand,
7 OrchestrationDispatchCommandError,9 OrchestrationDispatchCommandError,
8 PROVIDER_SEND_TURN_MAX_IMAGE_BYTES,10 PROVIDER_SEND_TURN_MAX_IMAGE_BYTES,
...@@ -13,8 +15,38 @@ import { ServerConfig } from "../config.ts";...@@ -13,8 +15,38 @@ import { ServerConfig } from "../config.ts";
13import { parseBase64DataUrl } from "../imageMime.ts";15import { parseBase64DataUrl } from "../imageMime.ts";
14import * as WorkspacePaths from "../workspace/WorkspacePaths.ts";16import * 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
16export const normalizeDispatchCommand = (command: ClientOrchestrationCommand) =>46export const normalizeDispatchCommand = (command: ClientOrchestrationCommand) =>
17 Effect.gen(function* () {47 Effect.gen(function* () {
48 const receivedAt = DateTime.formatIso(yield* DateTime.now);
49 const canonicalCommand = canonicalizeClientCommandTimestamps(command, receivedAt);
18 const fileSystem = yield* FileSystem.FileSystem;50 const fileSystem = yield* FileSystem.FileSystem;
19 const path = yield* Path.Path;51 const path = yield* Path.Path;
20 const serverConfig = yield* ServerConfig;52 const serverConfig = yield* ServerConfig;
...@@ -47,30 +79,33 @@ export const normalizeDispatchCommand = (command: ClientOrchestrationCommand) =>...@@ -47,30 +79,33 @@ export const normalizeDispatchCommand = (command: ClientOrchestrationCommand) =>
47 ),79 ),
48 );80 );
4981
50 if (command.type === "project.create") {82 if (canonicalCommand.type === "project.create") {
51 return {83 return {
52 ...command,84 ...canonicalCommand,
53 workspaceRoot: yield* normalizeProjectWorkspaceRootForCreate(85 workspaceRoot: yield* normalizeProjectWorkspaceRootForCreate(
54 command.workspaceRoot,86 canonicalCommand.workspaceRoot,
55 command.createWorkspaceRootIfMissing,87 canonicalCommand.createWorkspaceRootIfMissing,
56 ),88 ),
57 createWorkspaceRootIfMissing: command.createWorkspaceRootIfMissing === true,89 createWorkspaceRootIfMissing: canonicalCommand.createWorkspaceRootIfMissing === true,
58 } satisfies OrchestrationCommand;90 } satisfies OrchestrationCommand;
59 }91 }
6092
61 if (command.type === "project.meta.update" && command.workspaceRoot !== undefined) {93 if (
94 canonicalCommand.type === "project.meta.update" &&
95 canonicalCommand.workspaceRoot !== undefined
96 ) {
62 return {97 return {
63 ...command,98 ...canonicalCommand,
64 workspaceRoot: yield* normalizeProjectWorkspaceRoot(command.workspaceRoot),99 workspaceRoot: yield* normalizeProjectWorkspaceRoot(canonicalCommand.workspaceRoot),
65 } satisfies OrchestrationCommand;100 } satisfies OrchestrationCommand;
66 }101 }
67102
68 if (command.type !== "thread.turn.start") {103 if (canonicalCommand.type !== "thread.turn.start") {
69 return command as OrchestrationCommand;104 return canonicalCommand as OrchestrationCommand;
70 }105 }
71106
72 const normalizedAttachments = yield* Effect.forEach(107 const normalizedAttachments = yield* Effect.forEach(
73 command.message.attachments,108 canonicalCommand.message.attachments,
74 (attachment) =>109 (attachment) =>
75 Effect.gen(function* () {110 Effect.gen(function* () {
76 const parsed = parseBase64DataUrl(attachment.dataUrl);111 const parsed = parseBase64DataUrl(attachment.dataUrl);
...@@ -87,7 +122,7 @@ export const normalizeDispatchCommand = (command: ClientOrchestrationCommand) =>...@@ -87,7 +122,7 @@ export const normalizeDispatchCommand = (command: ClientOrchestrationCommand) =>
87 });122 });
88 }123 }
89124
90 const attachmentId = createAttachmentId(command.threadId);125 const attachmentId = createAttachmentId(canonicalCommand.threadId);
91 if (!attachmentId) {126 if (!attachmentId) {
92 return yield* new OrchestrationDispatchCommandError({127 return yield* new OrchestrationDispatchCommandError({
93 message: "Failed to create a safe attachment id.",128 message: "Failed to create a safe attachment id.",
...@@ -135,9 +170,9 @@ export const normalizeDispatchCommand = (command: ClientOrchestrationCommand) =>...@@ -135,9 +170,9 @@ export const normalizeDispatchCommand = (command: ClientOrchestrationCommand) =>
135 );170 );
136171
137 return {172 return {
138 ...command,173 ...canonicalCommand,
139 message: {174 message: {
140 ...command.message,175 ...canonicalCommand.message,
141 attachments: normalizedAttachments,176 attachments: normalizedAttachments,
142 },177 },
143 } satisfies OrchestrationCommand;178 } satisfies OrchestrationCommand;