From d2f8dafb0f07409758797c880fbc3d526fa7c5c6 Mon Sep 17 00:00:00 2001 From: Alexey Zaytsev Date: Sun, 19 Jul 2026 20:23:47 -0300 Subject: [PATCH] fix(ai,agent,coding-agent): share UUIDv7 and use for Codex (#6834) --- packages/agent/CHANGELOG.md | 4 ++++ .../agent/src/harness/session/jsonl-storage.ts | 2 +- .../agent/src/harness/session/memory-storage.ts | 2 +- packages/agent/src/harness/session/repo-utils.ts | 2 +- packages/agent/src/index.ts | 1 - packages/ai/CHANGELOG.md | 5 +++++ packages/ai/src/api/openai-codex-responses.ts | 10 ++-------- packages/ai/src/index.ts | 1 + .../src/harness/session => ai/src/utils}/uuid.ts | 16 +++++----------- .../test/uuid.test.ts} | 2 +- .../coding-agent/src/core/session-manager.ts | 4 ++-- 11 files changed, 23 insertions(+), 26 deletions(-) rename packages/{agent/src/harness/session => ai/src/utils}/uuid.ts (83%) rename packages/{agent/test/harness/session-uuid.test.ts => ai/test/uuid.test.ts} (96%) diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index b27954ca..eba2a62a 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Breaking Changes + +- Moved the `uuidv7` export to `@earendil-works/pi-ai`. + ## [0.80.10] - 2026-07-16 ## [0.80.9] - 2026-07-16 diff --git a/packages/agent/src/harness/session/jsonl-storage.ts b/packages/agent/src/harness/session/jsonl-storage.ts index b4111572..d57398d5 100644 --- a/packages/agent/src/harness/session/jsonl-storage.ts +++ b/packages/agent/src/harness/session/jsonl-storage.ts @@ -1,7 +1,7 @@ +import { uuidv7 } from "@earendil-works/pi-ai"; import type { FileSystem, JsonlSessionMetadata, LeafEntry, SessionStorage, SessionTreeEntry } from "../types.ts"; import { SessionError, toError } from "../types.ts"; import { getFileSystemResultOrThrow } from "./repo-utils.ts"; -import { uuidv7 } from "./uuid.ts"; type JsonlSessionStorageFileSystem = Pick; diff --git a/packages/agent/src/harness/session/memory-storage.ts b/packages/agent/src/harness/session/memory-storage.ts index fba55dae..68ec7623 100644 --- a/packages/agent/src/harness/session/memory-storage.ts +++ b/packages/agent/src/harness/session/memory-storage.ts @@ -1,3 +1,4 @@ +import { uuidv7 } from "@earendil-works/pi-ai"; import { type LeafEntry, SessionError, @@ -5,7 +6,6 @@ import { type SessionStorage, type SessionTreeEntry, } from "../types.ts"; -import { uuidv7 } from "./uuid.ts"; function updateLabelCache(labelsById: Map, entry: SessionTreeEntry): void { if (entry.type !== "label") return; diff --git a/packages/agent/src/harness/session/repo-utils.ts b/packages/agent/src/harness/session/repo-utils.ts index a25b62de..dd2e0f9a 100644 --- a/packages/agent/src/harness/session/repo-utils.ts +++ b/packages/agent/src/harness/session/repo-utils.ts @@ -1,3 +1,4 @@ +import { uuidv7 } from "@earendil-works/pi-ai"; import { type FileError, type Result, @@ -7,7 +8,6 @@ import { type SessionTreeEntry, } from "../types.ts"; import { Session } from "./session.ts"; -import { uuidv7 } from "./uuid.ts"; export function createSessionId(): string { return uuidv7(); diff --git a/packages/agent/src/index.ts b/packages/agent/src/index.ts index 89ba269a..49baf081 100644 --- a/packages/agent/src/index.ts +++ b/packages/agent/src/index.ts @@ -33,7 +33,6 @@ export * from "./harness/session/memory-repo.ts"; export * from "./harness/session/memory-storage.ts"; export * from "./harness/session/repo-utils.ts"; export * from "./harness/session/session.ts"; -export { uuidv7 } from "./harness/session/uuid.ts"; export * from "./harness/skills.ts"; export * from "./harness/system-prompt.ts"; // Harness diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 694c828b..7f9388ac 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,8 +2,13 @@ ## [Unreleased] +### Added + +- Added a shared `uuidv7` utility for time-ordered identifiers. + ### Fixed +- Fixed sessionless OpenAI Codex WebSocket requests to use UUIDv7 request IDs, enabling models that reject UUIDv4 IDs. - Fixed GitHub Copilot long-context pricing tiers in generated model metadata ([#6668](https://github.com/earendil-works/pi/issues/6668)). - Fixed Kimi Coding subscription models to report API-equivalent implied costs when models.dev reports zero pricing. - Fixed OpenAI Responses early stream endings to be classified as retryable provider errors ([#6727](https://github.com/earendil-works/pi/issues/6727)). diff --git a/packages/ai/src/api/openai-codex-responses.ts b/packages/ai/src/api/openai-codex-responses.ts index de0d1792..955964fe 100644 --- a/packages/ai/src/api/openai-codex-responses.ts +++ b/packages/ai/src/api/openai-codex-responses.ts @@ -46,6 +46,7 @@ import { formatProviderError, normalizeProviderError } from "../utils/error-body import { AssistantMessageEventStream } from "../utils/event-stream.ts"; import { headersToRecord } from "../utils/headers.ts"; import { resolveHttpProxyUrlForTarget } from "../utils/node-http-proxy.ts"; +import { uuidv7 } from "../utils/uuid.ts"; import { clampOpenAIPromptCacheKey } from "./openai-prompt-cache.ts"; import { convertResponsesMessages, convertResponsesTools, processResponsesStream } from "./openai-responses-shared.ts"; import { buildBaseOptions } from "./simple-options.ts"; @@ -259,7 +260,7 @@ export const stream: StreamFunction<"openai-codex-responses", OpenAICodexRespons body = nextBody as RequestBody; } const codexSessionId = clampOpenAIPromptCacheKey(options?.sessionId); - const websocketRequestId = codexSessionId || createCodexRequestId(); + const websocketRequestId = codexSessionId || uuidv7(); const sseHeaders = buildSSEHeaders(model.headers, options?.headers, accountId, apiKey, codexSessionId); const websocketHeaders = buildWebSocketHeaders( model.headers, @@ -1505,13 +1506,6 @@ function extractAccountId(token: string): string { } } -function createCodexRequestId(): string { - if (typeof globalThis.crypto?.randomUUID === "function") { - return globalThis.crypto.randomUUID(); - } - return `codex_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`; -} - function buildBaseCodexHeaders( initHeaders: Record | undefined, additionalHeaders: ProviderHeaders | undefined, diff --git a/packages/ai/src/index.ts b/packages/ai/src/index.ts index 1056dc5a..1ba45811 100644 --- a/packages/ai/src/index.ts +++ b/packages/ai/src/index.ts @@ -42,4 +42,5 @@ export * from "./utils/json-parse.ts"; export * from "./utils/overflow.ts"; export * from "./utils/retry.ts"; export * from "./utils/typebox-helpers.ts"; +export { uuidv7 } from "./utils/uuid.ts"; export * from "./utils/validation.ts"; diff --git a/packages/agent/src/harness/session/uuid.ts b/packages/ai/src/utils/uuid.ts similarity index 83% rename from packages/agent/src/harness/session/uuid.ts rename to packages/ai/src/utils/uuid.ts index 0c4cef8e..53d900ca 100644 --- a/packages/agent/src/harness/session/uuid.ts +++ b/packages/ai/src/utils/uuid.ts @@ -1,10 +1,9 @@ let lastTimestamp = -Infinity; let sequence = 0; -function fillRandomBytes(bytes: Uint8Array): void { - const crypto = globalThis.crypto; - if (crypto?.getRandomValues) { - crypto.getRandomValues(bytes); +function fillRandomBytes(bytes: Uint8Array): void { + if (globalThis.crypto?.getRandomValues) { + globalThis.crypto.getRandomValues(bytes); return; } for (let i = 0; i < bytes.length; i++) { @@ -12,6 +11,7 @@ function fillRandomBytes(bytes: Uint8Array): void { } } +/** Generate a time-ordered UUIDv7. */ export function uuidv7(): string { const random = new Uint8Array(16); fillRandomBytes(random); @@ -22,9 +22,7 @@ export function uuidv7(): string { lastTimestamp = timestamp; } else { sequence = (sequence + 1) >>> 0; - if (sequence === 0) { - lastTimestamp++; - } + if (sequence === 0) lastTimestamp++; } const bytes = new Uint8Array(16); @@ -45,10 +43,6 @@ export function uuidv7(): string { 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("")}`; } diff --git a/packages/agent/test/harness/session-uuid.test.ts b/packages/ai/test/uuid.test.ts similarity index 96% rename from packages/agent/test/harness/session-uuid.test.ts rename to packages/ai/test/uuid.test.ts index b8d571f4..3e542650 100644 --- a/packages/agent/test/harness/session-uuid.test.ts +++ b/packages/ai/test/uuid.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -import { uuidv7 } from "../../src/harness/session/uuid.ts"; +import { uuidv7 } from "../src/utils/uuid.ts"; 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; diff --git a/packages/coding-agent/src/core/session-manager.ts b/packages/coding-agent/src/core/session-manager.ts index f5b75c2c..0ab66595 100644 --- a/packages/coding-agent/src/core/session-manager.ts +++ b/packages/coding-agent/src/core/session-manager.ts @@ -1,5 +1,5 @@ -import { type AgentMessage, uuidv7 } from "@earendil-works/pi-agent-core"; -import type { ImageContent, Message, TextContent } from "@earendil-works/pi-ai"; +import type { AgentMessage } from "@earendil-works/pi-agent-core"; +import { type ImageContent, type Message, type TextContent, uuidv7 } from "@earendil-works/pi-ai"; import { randomUUID } from "crypto"; import { appendFileSync,