From c00f1cf1302142c90a414e26ecd09f7988d97b6f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 13 May 2026 15:53:53 +0200 Subject: [PATCH] fix(agent): correct uuidv7 sequence handling --- packages/agent/src/harness/session/uuid.ts | 6 +-- packages/agent/src/index.ts | 1 + .../agent/test/harness/session-uuid.test.ts | 50 +++++++++++++++++++ .../coding-agent/src/core/session-manager.ts | 3 +- packages/coding-agent/src/utils/uuid.ts | 44 ---------------- 5 files changed, 55 insertions(+), 49 deletions(-) create mode 100644 packages/agent/test/harness/session-uuid.test.ts delete mode 100644 packages/coding-agent/src/utils/uuid.ts diff --git a/packages/agent/src/harness/session/uuid.ts b/packages/agent/src/harness/session/uuid.ts index 27b660ae..c7e2896e 100644 --- a/packages/agent/src/harness/session/uuid.ts +++ b/packages/agent/src/harness/session/uuid.ts @@ -8,10 +8,10 @@ export function uuidv7(): string { const timestamp = Date.now(); if (timestamp > lastTimestamp) { - sequence = (random[6] << 23) | (random[7] << 16) | (random[8] << 8) | random[9]; + sequence = random[6] * 0x1000000 + random[7] * 0x10000 + random[8] * 0x100 + random[9]; lastTimestamp = timestamp; } else { - sequence = (sequence + 1) | 0; + sequence = (sequence + 1) >>> 0; if (sequence === 0) { lastTimestamp++; } @@ -28,7 +28,7 @@ export function uuidv7(): string { bytes[7] = (sequence >>> 20) & 0xff; bytes[8] = 0x80 | ((sequence >>> 14) & 0x3f); bytes[9] = (sequence >>> 6) & 0xff; - bytes[10] = ((sequence << 2) & 0xff) | (random[10] & 0x03); + bytes[10] = ((sequence & 0x3f) << 2) | (random[10] & 0x03); bytes[11] = random[11]; bytes[12] = random[12]; bytes[13] = random[13]; diff --git a/packages/agent/src/index.ts b/packages/agent/src/index.ts index 7b6018c1..293ce196 100644 --- a/packages/agent/src/index.ts +++ b/packages/agent/src/index.ts @@ -29,6 +29,7 @@ export * from "./harness/session/repo/jsonl.js"; export * from "./harness/session/repo/memory.js"; export * from "./harness/session/repo/shared.js"; export * from "./harness/session/session.js"; +export { uuidv7 } from "./harness/session/uuid.js"; export * from "./harness/skills.js"; export * from "./harness/system-prompt.js"; // Harness diff --git a/packages/agent/test/harness/session-uuid.test.ts b/packages/agent/test/harness/session-uuid.test.ts new file mode 100644 index 00000000..0c7f42e2 --- /dev/null +++ b/packages/agent/test/harness/session-uuid.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it, vi } from "vitest"; + +const randomBytesMock = vi.hoisted(() => vi.fn<(size: number) => Buffer>()); + +vi.mock("node:crypto", () => ({ + randomBytes: randomBytesMock, +})); + +import { uuidv7 } from "../../src/harness/session/uuid.js"; + +const UUID_V7_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; +const TIMESTAMP = 0x0123456789ab; + +function parseTimestamp(uuid: string): number { + return Number.parseInt(uuid.replaceAll("-", "").slice(0, 12), 16); +} + +describe("uuidv7", () => { + it("uses the RFC 9562 layout and preserves monotonic order", () => { + randomBytesMock + .mockReturnValueOnce( + Buffer.from([0, 0, 0, 0, 0, 0, 0xff, 0xff, 0xff, 0xfe, 0x01, 0x11, 0x22, 0x33, 0x44, 0x55]), + ) + .mockReturnValueOnce(Buffer.alloc(16)) + .mockReturnValueOnce(Buffer.alloc(16)); + const dateNow = vi.spyOn(Date, "now").mockReturnValue(TIMESTAMP); + + try { + const first = uuidv7(); + const second = uuidv7(); + const third = uuidv7(); + + expect(first).toBe("01234567-89ab-7fff-bfff-f91122334455"); + expect(second).toBe("01234567-89ab-7fff-bfff-fc0000000000"); + expect(third).toBe("01234567-89ac-7000-8000-000000000000"); + expect(first).toMatch(UUID_V7_RE); + expect(second).toMatch(UUID_V7_RE); + expect(third).toMatch(UUID_V7_RE); + expect(parseTimestamp(first)).toBe(TIMESTAMP); + expect(parseTimestamp(second)).toBe(TIMESTAMP); + expect(parseTimestamp(third)).toBe(TIMESTAMP + 1); + expect(first < second).toBe(true); + expect(second < third).toBe(true); + expect(randomBytesMock).toHaveBeenCalledTimes(3); + expect(randomBytesMock).toHaveBeenCalledWith(16); + } finally { + dateNow.mockRestore(); + } + }); +}); diff --git a/packages/coding-agent/src/core/session-manager.ts b/packages/coding-agent/src/core/session-manager.ts index 60b111e0..5d8971c5 100644 --- a/packages/coding-agent/src/core/session-manager.ts +++ b/packages/coding-agent/src/core/session-manager.ts @@ -1,4 +1,4 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; +import { type AgentMessage, uuidv7 } from "@earendil-works/pi-agent-core"; import type { ImageContent, Message, TextContent } from "@earendil-works/pi-ai"; import { randomUUID } from "crypto"; import { @@ -16,7 +16,6 @@ import { import { readdir, readFile, stat } from "fs/promises"; import { join, resolve } from "path"; import { getAgentDir as getDefaultAgentDir, getSessionsDir } from "../config.js"; -import { uuidv7 } from "../utils/uuid.js"; import { type BashExecutionMessage, type CustomMessage, diff --git a/packages/coding-agent/src/utils/uuid.ts b/packages/coding-agent/src/utils/uuid.ts deleted file mode 100644 index 27b660ae..00000000 --- a/packages/coding-agent/src/utils/uuid.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { randomBytes } from "node:crypto"; - -let lastTimestamp = -Infinity; -let sequence = 0; - -export function uuidv7(): string { - const random = randomBytes(16); - const timestamp = Date.now(); - - if (timestamp > lastTimestamp) { - sequence = (random[6] << 23) | (random[7] << 16) | (random[8] << 8) | random[9]; - lastTimestamp = timestamp; - } else { - sequence = (sequence + 1) | 0; - if (sequence === 0) { - lastTimestamp++; - } - } - - const bytes = new Uint8Array(16); - bytes[0] = (lastTimestamp / 0x10000000000) & 0xff; - bytes[1] = (lastTimestamp / 0x100000000) & 0xff; - bytes[2] = (lastTimestamp / 0x1000000) & 0xff; - bytes[3] = (lastTimestamp / 0x10000) & 0xff; - bytes[4] = (lastTimestamp / 0x100) & 0xff; - bytes[5] = lastTimestamp & 0xff; - bytes[6] = 0x70 | ((sequence >>> 28) & 0x0f); - bytes[7] = (sequence >>> 20) & 0xff; - bytes[8] = 0x80 | ((sequence >>> 14) & 0x3f); - bytes[9] = (sequence >>> 6) & 0xff; - bytes[10] = ((sequence << 2) & 0xff) | (random[10] & 0x03); - bytes[11] = random[11]; - bytes[12] = random[12]; - bytes[13] = random[13]; - bytes[14] = random[14]; - bytes[15] = random[15]; - - return formatUuid(bytes); -} - -function formatUuid(bytes: Uint8Array): string { - const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")); - return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10, 16).join("")}`; -}