fix(agent): correct uuidv7 sequence handling
This commit is contained in:
@@ -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];
|
||||
|
||||
@@ -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
|
||||
|
||||
50
packages/agent/test/harness/session-uuid.test.ts
Normal file
50
packages/agent/test/harness/session-uuid.test.ts
Normal file
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -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("")}`;
|
||||
}
|
||||
Reference in New Issue
Block a user