Skip to content
Merged
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
73 changes: 73 additions & 0 deletions apps/server/src/orchestration/Normalizer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { describe, expect, it } from "vite-plus/test";
import {
CommandId,
type ClientOrchestrationCommand,
MessageId,
ProjectId,
ProviderInstanceId,
ThreadId,
} from "@t3tools/contracts";

import { canonicalizeClientCommandTimestamps } from "./Normalizer.ts";

const clientCreatedAt = "2031-01-01T00:00:00.000Z";
const serverReceivedAt = "2026-07-18T00:00:00.000Z";

describe("canonicalizeClientCommandTimestamps", () => {
it("replaces a client command timestamp with the server receipt timestamp", () => {
const command: ClientOrchestrationCommand = {
type: "project.create",
commandId: CommandId.make("command-1"),
projectId: ProjectId.make("project-1"),
title: "Clock-safe project",
workspaceRoot: "/tmp/clock-safe-project",
createdAt: clientCreatedAt,
};

expect(canonicalizeClientCommandTimestamps(command, serverReceivedAt)).toEqual({
...command,
createdAt: serverReceivedAt,
});
});

it("replaces both timestamps when the first turn bootstraps a thread", () => {
const command: ClientOrchestrationCommand = {
type: "thread.turn.start",
commandId: CommandId.make("command-2"),
threadId: ThreadId.make("thread-1"),
message: {
messageId: MessageId.make("message-1"),
role: "user",
text: "Start a thread",
attachments: [],
},
runtimeMode: "full-access",
interactionMode: "default",
bootstrap: {
createThread: {
projectId: ProjectId.make("project-1"),
title: "Clock-safe thread",
modelSelection: {
instanceId: ProviderInstanceId.make("codex"),
model: "gpt-5.4",
},
runtimeMode: "full-access",
interactionMode: "default",
branch: null,
worktreePath: null,
createdAt: clientCreatedAt,
},
},
createdAt: clientCreatedAt,
};

const result = canonicalizeClientCommandTimestamps(command, serverReceivedAt);

expect(result.type).toBe("thread.turn.start");
if (result.type !== "thread.turn.start") {
throw new Error("Expected a thread.turn.start command");
}
expect(result.createdAt).toBe(serverReceivedAt);
expect(result.bootstrap?.createThread?.createdAt).toBe(serverReceivedAt);
});
});
63 changes: 49 additions & 14 deletions apps/server/src/orchestration/Normalizer.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import * as DateTime from "effect/DateTime";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Path from "effect/Path";
import {
type ClientOrchestrationCommand,
type IsoDateTime,
type OrchestrationCommand,
OrchestrationDispatchCommandError,
PROVIDER_SEND_TURN_MAX_IMAGE_BYTES,
Expand All @@ -13,8 +15,38 @@ import { ServerConfig } from "../config.ts";
import { parseBase64DataUrl } from "../imageMime.ts";
import * as WorkspacePaths from "../workspace/WorkspacePaths.ts";

export const canonicalizeClientCommandTimestamps = (
command: ClientOrchestrationCommand,
receivedAt: IsoDateTime,
): ClientOrchestrationCommand => {
const canonicalCommand =
"createdAt" in command
? {
...command,
createdAt: receivedAt,
}
: command;

if (canonicalCommand.type !== "thread.turn.start" || !canonicalCommand.bootstrap?.createThread) {
return canonicalCommand;
}

return {
...canonicalCommand,
bootstrap: {
...canonicalCommand.bootstrap,
createThread: {
...canonicalCommand.bootstrap.createThread,
createdAt: receivedAt,
},
},
};
};

export const normalizeDispatchCommand = (command: ClientOrchestrationCommand) =>
Effect.gen(function* () {
const receivedAt = DateTime.formatIso(yield* DateTime.now);
const canonicalCommand = canonicalizeClientCommandTimestamps(command, receivedAt);
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const serverConfig = yield* ServerConfig;
Expand Down Expand Up @@ -47,30 +79,33 @@ export const normalizeDispatchCommand = (command: ClientOrchestrationCommand) =>
),
);

if (command.type === "project.create") {
if (canonicalCommand.type === "project.create") {
return {
...command,
...canonicalCommand,
workspaceRoot: yield* normalizeProjectWorkspaceRootForCreate(
command.workspaceRoot,
command.createWorkspaceRootIfMissing,
canonicalCommand.workspaceRoot,
canonicalCommand.createWorkspaceRootIfMissing,
),
createWorkspaceRootIfMissing: command.createWorkspaceRootIfMissing === true,
createWorkspaceRootIfMissing: canonicalCommand.createWorkspaceRootIfMissing === true,
} satisfies OrchestrationCommand;
}

if (command.type === "project.meta.update" && command.workspaceRoot !== undefined) {
if (
canonicalCommand.type === "project.meta.update" &&
canonicalCommand.workspaceRoot !== undefined
) {
return {
...command,
workspaceRoot: yield* normalizeProjectWorkspaceRoot(command.workspaceRoot),
...canonicalCommand,
workspaceRoot: yield* normalizeProjectWorkspaceRoot(canonicalCommand.workspaceRoot),
} satisfies OrchestrationCommand;
}

if (command.type !== "thread.turn.start") {
return command as OrchestrationCommand;
if (canonicalCommand.type !== "thread.turn.start") {
return canonicalCommand as OrchestrationCommand;
}

const normalizedAttachments = yield* Effect.forEach(
command.message.attachments,
canonicalCommand.message.attachments,
(attachment) =>
Effect.gen(function* () {
const parsed = parseBase64DataUrl(attachment.dataUrl);
Expand All @@ -87,7 +122,7 @@ export const normalizeDispatchCommand = (command: ClientOrchestrationCommand) =>
});
}

const attachmentId = createAttachmentId(command.threadId);
const attachmentId = createAttachmentId(canonicalCommand.threadId);
if (!attachmentId) {
return yield* new OrchestrationDispatchCommandError({
message: "Failed to create a safe attachment id.",
Expand Down Expand Up @@ -135,9 +170,9 @@ export const normalizeDispatchCommand = (command: ClientOrchestrationCommand) =>
);

return {
...command,
...canonicalCommand,
message: {
...command.message,
...canonicalCommand.message,
attachments: normalizedAttachments,
},
} satisfies OrchestrationCommand;
Expand Down
Loading